mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Compare commits
50 Commits
release-tr
...
0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aca1dfa43 | ||
|
|
c8127fb3b9 | ||
|
|
729fca9100 | ||
|
|
c6703d66c1 | ||
|
|
2197161d55 | ||
|
|
cfaf79f878 | ||
|
|
2bea980d1f | ||
|
|
f11e27c712 | ||
|
|
b316e84f0d | ||
|
|
4668f8499b | ||
|
|
f9aedaba04 | ||
|
|
8cb3de9ab5 | ||
|
|
ca57d58219 | ||
|
|
6a56fbd206 | ||
|
|
cec3825de0 | ||
|
|
b3e99d1ae3 | ||
|
|
7243d79646 | ||
|
|
e50da6603c | ||
|
|
136f07e4b9 | ||
|
|
f4d367b857 | ||
|
|
3e96583525 | ||
|
|
84bb1f7c08 | ||
|
|
a5aec978bb | ||
|
|
d677292bd3 | ||
|
|
722f67d3c7 | ||
|
|
07e0aea24f | ||
|
|
673f2ba693 | ||
|
|
0070519736 | ||
|
|
359b38609b | ||
|
|
348a24d885 | ||
|
|
ebdd0d4cb4 | ||
|
|
614ab4cd33 | ||
|
|
ca38008328 | ||
|
|
e5713fa3a9 | ||
|
|
35e3cf1e14 | ||
|
|
ca8c394ae0 | ||
|
|
5323fdadcf | ||
|
|
e43bff5f8f | ||
|
|
4659d617f8 | ||
|
|
87fe360927 | ||
|
|
71212130c5 | ||
|
|
6370105c85 | ||
|
|
15f4ae7b5a | ||
|
|
08e11eece4 | ||
|
|
1a43ad4aa3 | ||
|
|
a5ec12e3df | ||
|
|
c0400446bc | ||
|
|
13c3d0c1e9 | ||
|
|
92edd4b800 | ||
|
|
eb0e1326b9 |
1007
.github/.obsolete/reusable-build-workflow.yml
vendored
Normal file
1007
.github/.obsolete/reusable-build-workflow.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
63
.github/maintenance/cache.py
vendored
Normal file
63
.github/maintenance/cache.py
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Your GitHub Personal Access Token
|
||||
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
||||
|
||||
# Repository details
|
||||
REPO_OWNER = "SideStore"
|
||||
REPO_NAME = "SideStore"
|
||||
|
||||
|
||||
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
|
||||
|
||||
# Common headers for GitHub API calls
|
||||
HEADERS = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {GITHUB_TOKEN}"
|
||||
}
|
||||
|
||||
def list_caches():
|
||||
response = requests.get(API_URL, headers=HEADERS)
|
||||
if response.status_code != 200:
|
||||
print(f"Failed to list caches. HTTP {response.status_code}")
|
||||
print("Response:", response.text)
|
||||
sys.exit(1)
|
||||
data = response.json()
|
||||
return data.get("actions_caches", [])
|
||||
|
||||
def delete_cache(cache_id):
|
||||
delete_url = f"{API_URL}/{cache_id}"
|
||||
response = requests.delete(delete_url, headers=HEADERS)
|
||||
return response.status_code
|
||||
|
||||
def main():
|
||||
caches = list_caches()
|
||||
if not caches:
|
||||
print("No caches found.")
|
||||
return
|
||||
|
||||
print("Found caches:")
|
||||
for cache in caches:
|
||||
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
|
||||
|
||||
print("\nDeleting caches...")
|
||||
for cache in caches:
|
||||
cache_id = cache.get("id")
|
||||
status = delete_cache(cache_id)
|
||||
if status == 204:
|
||||
print(f"Successfully deleted cache with ID: {cache_id}")
|
||||
else:
|
||||
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
|
||||
|
||||
print("All caches processed.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
### How to use
|
||||
'''
|
||||
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
|
||||
'''
|
||||
11
.github/workflows/alpha.yml
vendored
11
.github/workflows/alpha.yml
vendored
@@ -10,13 +10,14 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Reuseable-build:
|
||||
uses: ./.github/workflows/reusable-build-workflow.yml
|
||||
Reusable-build:
|
||||
uses: ./.github/workflows/reusable-sidestore-build.yml
|
||||
with:
|
||||
bundle_id: "com.SideStore.SideStore.Alpha"
|
||||
bundle_id_suffix: ".Alpha"
|
||||
# bundle_id: "com.SideStore.SideStore.Alpha"
|
||||
bundle_id: "com.SideStore.SideStore"
|
||||
# bundle_id_suffix: ".Alpha"
|
||||
is_beta: true
|
||||
publish: true
|
||||
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
|
||||
is_shared_build_num: false
|
||||
release_tag: "alpha"
|
||||
release_name: "Alpha"
|
||||
|
||||
11
.github/workflows/nightly.yml
vendored
11
.github/workflows/nightly.yml
vendored
@@ -58,18 +58,19 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LAST_SUCCESS: ${{ env.last_success }}
|
||||
|
||||
Reuseable-build:
|
||||
Reusable-build:
|
||||
if: |
|
||||
always() &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
|
||||
needs: check-changes
|
||||
uses: ./.github/workflows/reusable-build-workflow.yml
|
||||
uses: ./.github/workflows/reusable-sidestore-build.yml
|
||||
with:
|
||||
bundle_id: "com.SideStore.SideStore.Nightly"
|
||||
bundle_id_suffix: ".Nightly"
|
||||
# bundle_id: "com.SideStore.SideStore.Nightly"
|
||||
bundle_id: "com.SideStore.SideStore"
|
||||
# bundle_id_suffix: ".Nightly"
|
||||
is_beta: true
|
||||
publish: true
|
||||
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
|
||||
is_shared_build_num: false
|
||||
release_tag: "nightly"
|
||||
release_name: "Nightly"
|
||||
|
||||
419
.github/workflows/reusable-build-workflow.yml
vendored
419
.github/workflows/reusable-build-workflow.yml
vendored
@@ -1,419 +0,0 @@
|
||||
name: Reusable SideStore Build
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
publish:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
is_shared_build_num:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
release_name:
|
||||
required: true
|
||||
type: string
|
||||
release_tag:
|
||||
required: true
|
||||
type: string
|
||||
upstream_tag:
|
||||
required: true
|
||||
type: string
|
||||
upstream_name:
|
||||
required: true
|
||||
type: string
|
||||
bundle_id:
|
||||
default: com.SideStore.SideStore
|
||||
required: true
|
||||
type: string
|
||||
bundle_id_suffix:
|
||||
default: ''
|
||||
required: false
|
||||
type: string
|
||||
|
||||
secrets:
|
||||
# GITHUB_TOKEN:
|
||||
# required: true
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore ${{ inputs.release_tag }} releases
|
||||
concurrency:
|
||||
group: build-number-increment # serialize for build num cache access
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.1'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
- name: Set beta status
|
||||
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: brew install ldid xcbeautify
|
||||
|
||||
- name: Set ref based on is_shared_build_num
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: set_ref
|
||||
run: |
|
||||
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
|
||||
echo "ref=main" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Checkout SideStore/beta-build-num repo
|
||||
if: ${{ inputs.is_beta }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/beta-build-num'
|
||||
ref: ${{ env.ref }}
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/beta-build-num'
|
||||
|
||||
- name: Copy build_number.txt to repo root
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cp SideStore/beta-build-num/build_number.txt .
|
||||
echo "cat build_number.txt"
|
||||
cat build_number.txt
|
||||
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
|
||||
- name: Set Release Channel info for build number bumper
|
||||
run: |
|
||||
echo "RELEASE_CHANNEL=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
||||
|
||||
|
||||
- name: Increase build number for beta builds
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
bash .github/workflows/increase-beta-build-num.sh
|
||||
|
||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||
id: version
|
||||
run: |
|
||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "version=$version"
|
||||
|
||||
- name: Get short commit hash
|
||||
run: |
|
||||
# SHORT_COMMIT="${{ github.sha }}"
|
||||
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
||||
echo "Short commit hash: $SHORT_COMMIT"
|
||||
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV
|
||||
|
||||
- name: Set MARKETING_VERSION
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
# Extract version number (e.g., "0.6.0")
|
||||
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
# Extract date (YYYYMMDD) (e.g., "20250205")
|
||||
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
|
||||
# Extract build number (e.g., "2")
|
||||
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||
|
||||
# Combine them into the final output
|
||||
MARKETING_VERSION="${version}-${date}.${build_num}+${SHORT_COMMIT}"
|
||||
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
||||
|
||||
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata-
|
||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
||||
swiftpm-cache-restore-keys: |
|
||||
xcode-cache-sourcedata-
|
||||
|
||||
- name: Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||
# pods-cache-
|
||||
|
||||
- name: Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-
|
||||
|
||||
|
||||
- name: Install CocoaPods
|
||||
run: pod install
|
||||
|
||||
- name: Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: List Files and derived data
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
|
||||
- name: Set BundleID Suffix for Sidestore build
|
||||
run: |
|
||||
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build SideStore
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make build 2>&1 | tee build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign | tee -a build.log
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa | tee -a build.log
|
||||
|
||||
- name: Encrypt build.log generated from SideStore build for upload
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
if [ ! -f build.log ]; then
|
||||
echo "Warning: build.log is missing, creating a dummy log..."
|
||||
echo "Error: build.log was missing, This is a dummy placeholder file..." > build.log
|
||||
fi
|
||||
|
||||
zip -e -P "$BUILD_LOG_ZIP_PASSWORD" encrypted-build_log.zip build.log
|
||||
|
||||
- name: List Files after SideStore build
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create dSYMs zip
|
||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs/*
|
||||
|
||||
- name: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: ${{ inputs.release_name }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
prerelease: ${{ inputs.is_beta }}
|
||||
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build_log.zip
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
# save it
|
||||
- name: Publish to SideStore/beta-build-num
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
rm SideStore/beta-build-num/build_number.txt
|
||||
mv build_number.txt SideStore/beta-build-num/build_number.txt
|
||||
pushd SideStore/beta-build-num/
|
||||
|
||||
echo "Configure Git user (committer details)"
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
echo "Adding files to commit"
|
||||
git add --verbose build_number.txt
|
||||
git commit -m " - updated for ${{ inputs.release_tag }} - $SHORT_COMMIT deployment" || echo "No changes to commit"
|
||||
|
||||
echo "Pushing to remote repo"
|
||||
git push --verbose
|
||||
popd
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: cp SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./SideStore.xcarchive/dSYMs/*
|
||||
|
||||
- name: Upload encrypted-build_log.zip
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-build_log.zip
|
||||
path: encrypted-build_log.zip
|
||||
|
||||
- name: Get formatted date
|
||||
run: |
|
||||
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "Formatted date: $FORMATTED_DATE"
|
||||
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
|
||||
- name: Get size of IPA in bytes (macOS/Linux)
|
||||
run: |
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
IPA_SIZE=$(stat -f %z SideStore.ipa)
|
||||
else
|
||||
# Linux
|
||||
IPA_SIZE=$(stat -c %s SideStore.ipa)
|
||||
fi
|
||||
echo "IPA size in bytes: $IPA_SIZE"
|
||||
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
|
||||
- name: Compute SHA-256 of IPA
|
||||
run: |
|
||||
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
|
||||
echo "SHA-256 Hash: $SHA256_HASH"
|
||||
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
|
||||
|
||||
- name: Set Release Info variables
|
||||
run: |
|
||||
# Format localized description
|
||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||
This is release for:
|
||||
- version: "${{ steps.version.outputs.version }}"
|
||||
- revision: "$SHORT_COMMIT"
|
||||
- timestamp: "${{ steps.date.outputs.date }}"
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
||||
echo "VERSION_IPA=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
||||
|
||||
# multiline strings
|
||||
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
|
||||
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if Publish updates is set
|
||||
id: check_publish
|
||||
run: |
|
||||
echo "Publish updates to source.json = ${{ inputs.publish }}"
|
||||
|
||||
- name: Checkout SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/apps-v2.json'
|
||||
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/apps-v2.json'
|
||||
|
||||
# for stable builds, let the user manually edit the source.json
|
||||
- name: Publish to SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
id: publish-release
|
||||
run: |
|
||||
# Copy and execute the update script
|
||||
pushd SideStore/apps-v2.json/
|
||||
|
||||
# Configure Git user (committer details)
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
# update the source.json
|
||||
python3 ../../update_apps.py "./_includes/source.json"
|
||||
|
||||
# Commit changes and push using SSH
|
||||
git add --verbose ./_includes/source.json
|
||||
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
|
||||
|
||||
git push --verbose
|
||||
popd
|
||||
104
.github/workflows/reusable-sidestore-build.yml
vendored
Normal file
104
.github/workflows/reusable-sidestore-build.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Reusable SideStore Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
publish:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
is_shared_build_num:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
release_name:
|
||||
required: true
|
||||
type: string
|
||||
release_tag:
|
||||
required: true
|
||||
type: string
|
||||
upstream_tag:
|
||||
required: true
|
||||
type: string
|
||||
upstream_name:
|
||||
required: true
|
||||
type: string
|
||||
bundle_id:
|
||||
default: com.SideStore.SideStore
|
||||
required: true
|
||||
type: string
|
||||
bundle_id_suffix:
|
||||
default: ''
|
||||
required: false
|
||||
type: string
|
||||
|
||||
secrets:
|
||||
# GITHUB_TOKEN:
|
||||
# required: true
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
|
||||
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
|
||||
# so we serialize on the entire workflow
|
||||
concurrency:
|
||||
group: serialize-workflow
|
||||
|
||||
jobs:
|
||||
shared:
|
||||
uses: ./.github/workflows/sidestore-shared.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
needs: shared
|
||||
uses: ./.github/workflows/sidestore-build.yml
|
||||
with:
|
||||
is_beta: ${{ inputs.is_beta }}
|
||||
is_shared_build_num: ${{ inputs.is_shared_build_num }}
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
bundle_id: ${{ inputs.bundle_id }}
|
||||
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
|
||||
secrets: inherit
|
||||
|
||||
tests-build:
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||
needs: shared
|
||||
uses: ./.github/workflows/sidestore-tests-build.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
secrets: inherit
|
||||
|
||||
tests-run:
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
needs: [shared, tests-build]
|
||||
uses: ./.github/workflows/sidestore-tests-run.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
secrets: inherit
|
||||
|
||||
deploy:
|
||||
needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
|
||||
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
|
||||
uses: ./.github/workflows/sidestore-deploy.yml
|
||||
with:
|
||||
is_beta: ${{ inputs.is_beta }}
|
||||
publish: ${{ inputs.publish }}
|
||||
release_name: ${{ inputs.release_name }}
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
upstream_tag: ${{ inputs.upstream_tag }}
|
||||
upstream_name: ${{ inputs.upstream_name }}
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
release_channel: ${{ needs.build.outputs.release-channel }}
|
||||
marketing_version: ${{ needs.build.outputs.marketing-version }}
|
||||
bundle_id: ${{ inputs.bundle_id }}
|
||||
secrets: inherit
|
||||
401
.github/workflows/sidestore-build.yml
vendored
Normal file
401
.github/workflows/sidestore-build.yml
vendored
Normal file
@@ -0,0 +1,401 @@
|
||||
name: SideStore Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
type: boolean
|
||||
is_shared_build_num:
|
||||
type: boolean
|
||||
release_tag:
|
||||
type: string
|
||||
bundle_id:
|
||||
type: string
|
||||
bundle_id_suffix:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
outputs:
|
||||
version:
|
||||
value: ${{ jobs.build.outputs.version }}
|
||||
marketing-version:
|
||||
value: ${{ jobs.build.outputs.marketing-version }}
|
||||
release-channel:
|
||||
value: ${{ jobs.build.outputs.release-channel }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build SideStore - ${{ inputs.release_tag }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
|
||||
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
|
||||
|
||||
steps:
|
||||
- name: Set beta status
|
||||
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: |
|
||||
brew install ldid xcbeautify
|
||||
|
||||
- name: Set ref based on is_shared_build_num
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: set_ref
|
||||
run: |
|
||||
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
|
||||
echo "ref=main" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Checkout SideStore/beta-build-num repo
|
||||
if: ${{ inputs.is_beta }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/beta-build-num'
|
||||
ref: ${{ env.ref }}
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/beta-build-num'
|
||||
|
||||
- name: Copy build_number.txt to repo root
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cp SideStore/beta-build-num/build_number.txt .
|
||||
echo "cat build_number.txt"
|
||||
cat build_number.txt
|
||||
shell: bash
|
||||
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Channel info for build number bumper
|
||||
id: release-channel
|
||||
run: |
|
||||
RELEASE_CHANNEL="${{ inputs.release_tag }}"
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
||||
shell: bash
|
||||
|
||||
- name: Increase build number for beta builds
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
bash .github/workflows/increase-beta-build-num.sh
|
||||
shell: bash
|
||||
|
||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||
id: version
|
||||
run: |
|
||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "version=$version"
|
||||
shell: bash
|
||||
|
||||
- name: Set MARKETING_VERSION
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: marketing-version
|
||||
run: |
|
||||
# Extract version number (e.g., "0.6.0")
|
||||
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
# Extract date (YYYYMMDD) (e.g., "20250205")
|
||||
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
|
||||
# Extract build number (e.g., "2")
|
||||
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||
|
||||
# Combine them into the final output
|
||||
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
|
||||
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
||||
shell: bash
|
||||
|
||||
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-
|
||||
|
||||
# - name: (Build) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-restore-keys: |
|
||||
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||
# pods-cache-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-
|
||||
|
||||
|
||||
- name: (Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Clean previous build artifacts
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Build) List Files and derived data
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Set BundleID Suffix for Sidestore build
|
||||
run: |
|
||||
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
|
||||
- name: Build SideStore.xcarchive
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Build) List Files and Build artifacts
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt build-logs for upload
|
||||
id: encrypt-build-log
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-build-logs.zip
|
||||
id: attach-encrypted-build-log
|
||||
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||
path: encrypted-build-logs.zip
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Zip dSYMs
|
||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||
shell: bash
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||
path: SideStore.dSYMs.zip
|
||||
|
||||
- name: Keep rolling the build numbers for each successful build
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
pushd SideStore/beta-build-num/
|
||||
|
||||
echo "Configure Git user (committer details)"
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
echo "Adding files to commit"
|
||||
git add --verbose build_number.txt
|
||||
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||
|
||||
echo "Pushing to remote repo"
|
||||
git push --verbose
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Get last successful commit
|
||||
id: get_last_commit
|
||||
run: |
|
||||
# Try to get the last successful workflow run commit
|
||||
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
|
||||
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
|
||||
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
|
||||
- name: Create release notes
|
||||
run: |
|
||||
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
|
||||
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
|
||||
|
||||
FROM_COMMIT=$LAST_SUCCESS_SHA
|
||||
# Check if we got a valid SHA
|
||||
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
|
||||
echo "No successful run found, using initial commit of branch"
|
||||
# Get the first commit of the branch (initial commit)
|
||||
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
|
||||
fi
|
||||
|
||||
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
|
||||
# cat release-notes.md
|
||||
shell: bash
|
||||
|
||||
- name: Upload release-notes.md
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-notes-${{ inputs.short_commit }}.md
|
||||
path: release-notes.md
|
||||
|
||||
- name: Upload update_release_notes.py
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||
path: update_release_notes.py
|
||||
|
||||
- name: Upload update_apps.py
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: update_apps-${{ inputs.short_commit }}.py
|
||||
path: update_apps.py
|
||||
235
.github/workflows/sidestore-deploy.yml
vendored
Normal file
235
.github/workflows/sidestore-deploy.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
name: SideStore Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
type: boolean
|
||||
publish:
|
||||
type: boolean
|
||||
release_name:
|
||||
type: string
|
||||
release_tag:
|
||||
type: string
|
||||
upstream_tag:
|
||||
type: string
|
||||
upstream_name:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
marketing_version:
|
||||
type: string
|
||||
release_channel:
|
||||
type: string
|
||||
bundle_id:
|
||||
type: string
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
# GITHUB_TOKEN:
|
||||
# required: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy SideStore - ${{ inputs.release_tag }}
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Download IPA artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ inputs.version }}.ipa
|
||||
|
||||
- name: Download dSYM artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ inputs.version }}-dSYMs.zip
|
||||
|
||||
- name: Download encrypted-build-logs artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ inputs.version }}.zip
|
||||
|
||||
- name: Download encrypted-tests-build-logs artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||
|
||||
- name: Download encrypted-tests-run-logs artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||
|
||||
- name: Download tests-recording artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||
|
||||
- name: Download test-results artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ inputs.short_commit }}.zip
|
||||
|
||||
- name: Download release-notes.md
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-notes-${{ inputs.short_commit }}.md
|
||||
|
||||
- name: Download update_release_notes.py
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||
|
||||
- name: Download update_apps.py
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: update_apps-${{ inputs.short_commit }}.py
|
||||
|
||||
- name: Read release notes
|
||||
id: release_notes
|
||||
run: |
|
||||
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: List files before upload
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: ${{ inputs.release_name }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
prerelease: ${{ inputs.is_beta }}
|
||||
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip encrypted-tests-build-logs.zip encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ inputs.version }}`
|
||||
|
||||
${{ steps.release_notes.outputs.content }}
|
||||
|
||||
- name: Get formatted date
|
||||
run: |
|
||||
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "Formatted date: $FORMATTED_DATE"
|
||||
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Get size of IPA in bytes (macOS/Linux)
|
||||
run: |
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
IPA_SIZE=$(stat -f %z SideStore.ipa)
|
||||
else
|
||||
# Linux
|
||||
IPA_SIZE=$(stat -c %s SideStore.ipa)
|
||||
fi
|
||||
echo "IPA size in bytes: $IPA_SIZE"
|
||||
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Compute SHA-256 of IPA
|
||||
run: |
|
||||
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
|
||||
echo "SHA-256 Hash: $SHA256_HASH"
|
||||
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Info variables
|
||||
run: |
|
||||
# Format localized description
|
||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||
This is release for:
|
||||
- version: "${{ inputs.version }}"
|
||||
- revision: "${{ inputs.short_commit }}"
|
||||
- timestamp: "${{ steps.date.outputs.date }}"
|
||||
|
||||
Release Notes:
|
||||
${{ steps.release_notes.outputs.content }}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
||||
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
|
||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
|
||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
||||
|
||||
# multiline strings
|
||||
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
|
||||
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Check if Publish updates is set
|
||||
id: check_publish
|
||||
run: |
|
||||
echo "Publish updates to source.json = ${{ inputs.publish }}"
|
||||
shell: bash
|
||||
|
||||
- name: Checkout SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/apps-v2.json'
|
||||
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/apps-v2.json'
|
||||
|
||||
# for stable builds, let the user manually edit the source.json
|
||||
- name: Publish to SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
id: publish-release
|
||||
shell: bash
|
||||
run: |
|
||||
# Copy and execute the update script
|
||||
pushd SideStore/apps-v2.json/
|
||||
|
||||
# Configure Git user (committer details)
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
# update the source.json
|
||||
python3 ../../update_apps.py "./_includes/source.json"
|
||||
|
||||
# Commit changes and push using SSH
|
||||
git add --verbose ./_includes/source.json
|
||||
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||
|
||||
git push --verbose
|
||||
popd
|
||||
24
.github/workflows/sidestore-shared.yml
vendored
Normal file
24
.github/workflows/sidestore-shared.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: SideStore Shared
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
short-commit:
|
||||
value: ${{ jobs.shared.outputs.short-commit }}
|
||||
|
||||
jobs:
|
||||
shared:
|
||||
name: Shared Steps
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: 'macos-15'
|
||||
steps:
|
||||
- name: Set short commit hash
|
||||
id: commit-id
|
||||
run: |
|
||||
# SHORT_COMMIT="${{ github.sha }}"
|
||||
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
||||
echo "Short commit hash: $SHORT_COMMIT"
|
||||
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}
|
||||
204
.github/workflows/sidestore-tests-build.yml
vendored
Normal file
204
.github/workflows/sidestore-tests-build.yml
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
name: SideStore Tests Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
secrets:
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
tests-build:
|
||||
name: Tests-Build SideStore - ${{ inputs.release_tag }}
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies - xcbeautify
|
||||
run: |
|
||||
brew install xcbeautify
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
|
||||
# - name: (Tests-Build) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# # tests shouldn't restore cache unless it is same build
|
||||
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-restore-keys: |
|
||||
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
|
||||
# delete-used-deriveddata-cache: true
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) Save Pods to Cache
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: Clean Derived Data (if required)
|
||||
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
|
||||
run: |
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/
|
||||
make clean
|
||||
xcodebuild clean
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) List Files and derived data
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Build SideStore Tests
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
shell: bash
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: (Tests-Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) List Files and Build artifacts
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
|
||||
echo ""
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
|
||||
path: tests-build-deriveddata.txt
|
||||
|
||||
- name: Encrypt tests-build-logs for upload
|
||||
id: encrypt-test-log
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
|
||||
- name: Upload encrypted-tests-build-logs.zip
|
||||
id: attach-encrypted-test-log
|
||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||
path: encrypted-tests-build-logs.zip
|
||||
235
.github/workflows/sidestore-tests-run.yml
vendored
Normal file
235
.github/workflows/sidestore-tests-run.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
name: SideStore Tests Run
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
secrets:
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
tests-run:
|
||||
name: Tests-Run SideStore - ${{ inputs.release_tag }}
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Boot Simulator async(nohup) for testing
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
|
||||
# - name: (Tests-Run) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# # This comes from
|
||||
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Run) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Run) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Run) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) Save Pods to Cache
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Run) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) List Files and derived data
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
|
||||
echo ""
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
|
||||
path: tests-run-deriveddata.txt
|
||||
|
||||
# we expect simulator to have been booted by now, so exit otherwise
|
||||
- name: Simulator Boot Check
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
make -B sim-boot-check | tee -a build/logs/tests-run.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
|
||||
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
|
||||
run: |
|
||||
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
|
||||
RECORD_PID=$!
|
||||
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Run SideStore Tests
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
|
||||
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Stop Recording tests
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
kill -INT ${{ env.RECORD_PID }}
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) List Files and Build artifacts
|
||||
if: always()
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt tests-run-logs for upload
|
||||
id: encrypt-test-log
|
||||
if: always()
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-tests-run-logs.zip
|
||||
id: attach-encrypted-test-log
|
||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||
path: encrypted-tests-run-logs.zip
|
||||
|
||||
- name: Print tests-recording.log contents (if exists)
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
if [ -f tests-recording.log ]; then
|
||||
echo "tests-recording.log found. Its contents:"
|
||||
cat tests-recording.log
|
||||
else
|
||||
echo "tests-recording.log not found."
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Check for tests-recording.mp4 presence
|
||||
id: check-recording
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
if [ -f tests-recording.mp4 ]; then
|
||||
echo "::set-output name=found::true"
|
||||
echo "tests-recording.mp4 found."
|
||||
else
|
||||
echo "tests-recording.mp4 not found, skipping upload."
|
||||
echo "::set-output name=found::false"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload tests-recording.mp4
|
||||
id: upload-recording
|
||||
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||
path: tests-recording.mp4
|
||||
|
||||
- name: Zip test-results
|
||||
run: zip -r -9 ./test-results.zip ./build/tests
|
||||
shell: bash
|
||||
|
||||
- name: Upload Test Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ inputs.short_commit }}.zip
|
||||
path: test-results.zip
|
||||
290
.github/workflows/stable.yml
vendored
290
.github/workflows/stable.yml
vendored
@@ -7,97 +7,277 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore
|
||||
name: Build SideStore - stable (on tag push)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '15.4'
|
||||
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
|
||||
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
- name: Echo Updated Build.xcconfig
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "version=$version"
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
|
||||
echo "MARKETING_VERSION=$version"
|
||||
|
||||
shell: bash
|
||||
|
||||
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
|
||||
run: |
|
||||
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
|
||||
echo 'Version mismatch: $tag != $marketing_version ... '
|
||||
echo " expected-tag : $MARKETING_VERSION"
|
||||
echo " pushed-tag : ${{ github.ref_name }}"
|
||||
exit 1
|
||||
fi
|
||||
echo 'Version matches: $tag == $marketing_version ... '
|
||||
echo " expected-tag : $MARKETING_VERSION"
|
||||
echo " pushed-tag : ${{ github.ref_name }}"
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: |
|
||||
brew install ldid xcbeautify
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata-
|
||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
||||
swiftpm-cache-restore-keys: |
|
||||
xcode-cache-sourcedata-
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-stable-${{ github.sha }}
|
||||
|
||||
- name: Build SideStore
|
||||
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-stable-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-
|
||||
|
||||
|
||||
- name: (Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Build) List Files and derived data
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Build SideStore.xcarchive
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
run: make fakesign | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
run: make ipa | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new stable release
|
||||
uses: softprops/action-gh-release@v1
|
||||
- name: (Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-stable-${{ github.sha }}
|
||||
|
||||
- name: (Build) List Files and Build artifacts
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt build-logs for upload
|
||||
id: encrypt-build-log
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-build-logs.zip
|
||||
id: attach-encrypted-build-log
|
||||
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||
path: encrypted-build-logs.zip
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Zip dSYMs
|
||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||
shell: bash
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||
path: SideStore.dSYMs.zip
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
release: ${{ github.ref_name }} # name
|
||||
tag: ${{ github.ref_name }}
|
||||
# stick with what the user pushed, do not use latest commit or anything,
|
||||
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
|
||||
# in this case we do not want the tag to be auto-updated to latest
|
||||
updateTag: false
|
||||
prerelease: false
|
||||
files: >
|
||||
SideStore.ipa
|
||||
SideStore.dSYMs.zip
|
||||
encrypted-build-logs.zip
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -63,4 +63,10 @@ SideStore/.skip-prebuilt-fetch-em_proxy
|
||||
# Never check-in this package.resolved file
|
||||
# coz SPM then resolves packages using the stale entries in this file
|
||||
*.xcodeproj/**/Package.resolved
|
||||
*.xcworkspace/**/Package.resolved
|
||||
*.xcworkspace/**/Package.resolved
|
||||
|
||||
# some more commandline build artifacts
|
||||
test-recording.mp4
|
||||
test-recording.log
|
||||
altstore-sources.md
|
||||
local-build.sh
|
||||
@@ -56,15 +56,29 @@
|
||||
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
|
||||
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
|
||||
A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; };
|
||||
A80D60D32D3DD85100CEF65D /* ReleaseTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */; };
|
||||
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
|
||||
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
|
||||
A81A8CB92D68B30B0086C96F /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
||||
A81A8CBA2D68B3110086C96F /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB02D68B0320086C96F /* TreeMap.swift */; };
|
||||
A81A8CBD2D68B43F0086C96F /* LinkedHashMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */; };
|
||||
A81A8CC82D68BA610086C96F /* DataStructuresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CC72D68BA610086C96F /* DataStructuresTests.swift */; };
|
||||
A81A8CCE2D68BA8D0086C96F /* LinkedHashMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */; };
|
||||
A81A8CCF2D68BA8D0086C96F /* TreeMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB42D68B2180086C96F /* TreeMapTests.swift */; };
|
||||
A81A8CD02D68BA9B0086C96F /* LinkedHashMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */; };
|
||||
A81A8CD12D68BA9B0086C96F /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB02D68B0320086C96F /* TreeMap.swift */; };
|
||||
A81A8CD22D68BAA30086C96F /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
||||
A81A8CD42D68BAFF0086C96F /* DataStructureTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = A81A8CD32D68BAFF0086C96F /* DataStructureTests.xctestplan */; };
|
||||
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
|
||||
A8228B5B2D6E2C0C00F7CE0E /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
A8228B5D2D6E361F00F7CE0E /* (null) in Sources */ = {isa = PBXBuildFile; };
|
||||
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
||||
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; };
|
||||
A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
||||
A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; };
|
||||
A881E7C72D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */; };
|
||||
A881E7CB2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */; };
|
||||
A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; };
|
||||
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; };
|
||||
A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; };
|
||||
@@ -73,6 +87,9 @@
|
||||
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
|
||||
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
|
||||
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
|
||||
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */; };
|
||||
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B645FE2D70C1AD00125819 /* MarkdownKit */; };
|
||||
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B646002D70C23E00125819 /* MarkdownKit */; };
|
||||
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
|
||||
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
|
||||
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
|
||||
@@ -88,6 +105,9 @@
|
||||
A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; };
|
||||
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */; };
|
||||
A8E2DB312D684E2A009E5D31 /* UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E2DB2E2D684E2A009E5D31 /* UITests.swift */; };
|
||||
A8E2DB322D684E2A009E5D31 /* UITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E2DB2F2D684E2A009E5D31 /* UITestsLaunchTests.swift */; };
|
||||
A8E2DB342D68507F009E5D31 /* SideStoreTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */; };
|
||||
A8EA195F2D4982D600DC6322 /* BaseEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8EA195E2D4982D600DC6322 /* BaseEntity.swift */; };
|
||||
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
|
||||
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
|
||||
@@ -343,11 +363,8 @@
|
||||
D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */; };
|
||||
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */; };
|
||||
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE52A90391900C96F28 /* View+AltWidget.swift */; };
|
||||
D5177B0D2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */; };
|
||||
D5185B802AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5185B7F2AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel */; };
|
||||
D5185B822AE1E71D00646E33 /* Source13To14MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5185B812AE1E71D00646E33 /* Source13To14MigrationPolicy.swift */; };
|
||||
D5185B822AE1E71D00646E33 /* Source11To17MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5185B812AE1E71D00646E33 /* Source11To17MigrationPolicy.swift */; };
|
||||
D519AD46292D665B004B12F9 /* Managed.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB39B5B252BC10E00D1BE50 /* Managed.swift */; };
|
||||
D51E83822B8692DF0092FC61 /* AltStore16ToAltStore17.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D51E83812B8692DF0092FC61 /* AltStore16ToAltStore17.xcmappingmodel */; };
|
||||
D52A2F972ACB40F700BDF8E3 /* Logger+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */; };
|
||||
D52B4ABF2AF183F0005991C3 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52B4ABE2AF183F0005991C3 /* WebViewController.swift */; };
|
||||
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
|
||||
@@ -376,7 +393,6 @@
|
||||
D56D21422B7D9C41007641C5 /* AltIcons.plist in Resources */ = {isa = PBXBuildFile; fileRef = D56D21412B7D9C41007641C5 /* AltIcons.plist */; };
|
||||
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; };
|
||||
D5728CA72A0D79D30014E73C /* OptionalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */; };
|
||||
D5753A622B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5753A612B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel */; };
|
||||
D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */; };
|
||||
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */; };
|
||||
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57968CA29CB99EF00539069 /* VibrantButton.swift */; };
|
||||
@@ -390,7 +406,6 @@
|
||||
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */; };
|
||||
D59162AD29BA616A005CBF47 /* SourceHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */; };
|
||||
D5927D6629DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */; };
|
||||
D5927D6929DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */; };
|
||||
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5935AEC29C39DE300C157EF /* SourceComponents.swift */; };
|
||||
D5935AEF29C3B23600C157EF /* Sources.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5935AEE29C3B23600C157EF /* Sources.storyboard */; };
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
|
||||
@@ -406,7 +421,6 @@
|
||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
|
||||
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */; };
|
||||
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */; };
|
||||
D5CE309C2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CE309B2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel */; };
|
||||
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
||||
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB81632B0410BC003F5F8B /* AppSorting.swift */; };
|
||||
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */; };
|
||||
@@ -503,6 +517,13 @@
|
||||
remoteGlobalIDString = BF58047A246A28F7008AE704;
|
||||
remoteInfo = AltBackup;
|
||||
};
|
||||
A8E2DB272D684CBD009E5D31 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BFD247622284B9A500981D42 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = BFD247692284B9A500981D42;
|
||||
remoteInfo = SideStore;
|
||||
};
|
||||
BF66EE832501AE50007EE018 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BFD247622284B9A500981D42 /* Project object */;
|
||||
@@ -636,8 +657,16 @@
|
||||
A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "minimuxer-helpers.swift"; sourceTree = "<group>"; };
|
||||
A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = "<group>"; };
|
||||
A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = "<group>"; };
|
||||
A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseTrack.swift; sourceTree = "<group>"; };
|
||||
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = "<group>"; };
|
||||
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = "<group>"; };
|
||||
A81A8CB02D68B0320086C96F /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
|
||||
A81A8CB42D68B2180086C96F /* TreeMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMapTests.swift; sourceTree = "<group>"; };
|
||||
A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedHashMap.swift; sourceTree = "<group>"; };
|
||||
A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedHashMapTests.swift; sourceTree = "<group>"; };
|
||||
A81A8CC52D68BA610086C96F /* DataStructureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DataStructureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A81A8CC72D68BA610086C96F /* DataStructuresTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStructuresTests.swift; sourceTree = "<group>"; };
|
||||
A81A8CD32D68BAFF0086C96F /* DataStructureTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DataStructureTests.xctestplan; sourceTree = "<group>"; };
|
||||
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; };
|
||||
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
|
||||
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -650,6 +679,9 @@
|
||||
A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorProcessing.swift; sourceTree = "<group>"; };
|
||||
A868CFE32D319988002F1201 /* SingletonGenericMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonGenericMap.swift; sourceTree = "<group>"; };
|
||||
A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppExtensionsOperation.swift; sourceTree = "<group>"; };
|
||||
A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore17.xcmappingmodel; sourceTree = "<group>"; };
|
||||
A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp11To17MigrationPolicy.swift; sourceTree = "<group>"; };
|
||||
A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DataStructureTests.xcconfig; sourceTree = "<group>"; };
|
||||
A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingContolView.swift; sourceTree = "<group>"; };
|
||||
A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingControl.swift; sourceTree = "<group>"; };
|
||||
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -657,6 +689,7 @@
|
||||
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = "<group>"; };
|
||||
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
|
||||
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
|
||||
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsingMarkdownView.swift; sourceTree = "<group>"; };
|
||||
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
|
||||
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
|
||||
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
|
||||
@@ -665,6 +698,11 @@
|
||||
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = "<group>"; };
|
||||
A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; };
|
||||
A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+AltStore.swift"; sourceTree = "<group>"; };
|
||||
A8E2DB212D684CBD009E5D31 /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = "<group>"; };
|
||||
A8E2DB2E2D684E2A009E5D31 /* UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITests.swift; sourceTree = "<group>"; };
|
||||
A8E2DB2F2D684E2A009E5D31 /* UITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SideStoreTests.xctestplan; sourceTree = "<group>"; };
|
||||
A8EA195E2D4982D600DC6322 /* BaseEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEntity.swift; sourceTree = "<group>"; };
|
||||
A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = "<group>"; };
|
||||
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = "<group>"; };
|
||||
@@ -939,12 +977,8 @@
|
||||
D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsIntent.swift; sourceTree = "<group>"; };
|
||||
D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsWidgetIntent.swift; sourceTree = "<group>"; };
|
||||
D5151BE52A90391900C96F28 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; };
|
||||
D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore12ToAltStore13.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5185B7E2AE1E35200646E33 /* AltStore 14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 14.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D5185B7F2AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore13ToAltStore14.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5185B812AE1E71D00646E33 /* Source13To14MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source13To14MigrationPolicy.swift; sourceTree = "<group>"; };
|
||||
D5185B812AE1E71D00646E33 /* Source11To17MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source11To17MigrationPolicy.swift; sourceTree = "<group>"; };
|
||||
D51E83802B86926B0092FC61 /* AltStore 17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 17.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D51E83812B8692DF0092FC61 /* AltStore16ToAltStore17.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore16ToAltStore17.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+AltStore.swift"; sourceTree = "<group>"; };
|
||||
D52B4ABE2AF183F0005991C3 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
|
||||
@@ -979,8 +1013,6 @@
|
||||
D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = "<group>"; };
|
||||
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
|
||||
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = "<group>"; };
|
||||
D5753A602B279D1400090456 /* AltStore 15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 15.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D5753A612B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore14ToAltStore15.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsTimelineProvider.swift; sourceTree = "<group>"; };
|
||||
D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailWidget.swift; sourceTree = "<group>"; };
|
||||
D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = "<group>"; };
|
||||
@@ -990,7 +1022,6 @@
|
||||
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = "<group>"; };
|
||||
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = "<group>"; };
|
||||
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogViewController.swift; sourceTree = "<group>"; };
|
||||
D581822C2A218A140087965B /* AltStore 13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 13.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D586D39828EF58B0000E101F /* AltTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D586D39A28EF58B0000E101F /* AltTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltTests.swift; sourceTree = "<group>"; };
|
||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; };
|
||||
@@ -999,8 +1030,6 @@
|
||||
D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceHeaderView.swift; sourceTree = "<group>"; };
|
||||
D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SourceHeaderView.xib; sourceTree = "<group>"; };
|
||||
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBarAppearance+TintColor.swift"; sourceTree = "<group>"; };
|
||||
D5927D6729DCE1FE00D6898E /* AltStore 12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 12.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore12.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5935AEC29C39DE300C157EF /* SourceComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceComponents.swift; sourceTree = "<group>"; };
|
||||
D5935AEE29C3B23600C157EF /* Sources.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Sources.storyboard; sourceTree = "<group>"; };
|
||||
D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; };
|
||||
@@ -1019,8 +1048,6 @@
|
||||
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContentViewController.swift; sourceTree = "<group>"; };
|
||||
D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailViewController.swift; sourceTree = "<group>"; };
|
||||
D5CE309A2B4C93BE00DB8151 /* AltStore 16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 16.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D5CE309B2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore15ToAltStore16.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5CF56812A0D83F9006D93E2 /* VerificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationError.swift; sourceTree = "<group>"; };
|
||||
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
|
||||
D5DB81632B0410BC003F5F8B /* AppSorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSorting.swift; sourceTree = "<group>"; };
|
||||
@@ -1064,6 +1091,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A81A8CC22D68BA610086C96F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A8E2DB1E2D684CBD009E5D31 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF580478246A28F7008AE704 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1100,6 +1141,7 @@
|
||||
files = (
|
||||
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */,
|
||||
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */,
|
||||
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */,
|
||||
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */,
|
||||
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */,
|
||||
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */,
|
||||
@@ -1107,6 +1149,7 @@
|
||||
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
|
||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
|
||||
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
||||
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */,
|
||||
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1203,6 +1246,24 @@
|
||||
path = Intents;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A81A8CB22D68B2030086C96F /* UnitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A81A8CB32D68B20F0086C96F /* datastructures */,
|
||||
);
|
||||
path = UnitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A81A8CB32D68B20F0086C96F /* datastructures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */,
|
||||
A81A8CB42D68B2180086C96F /* TreeMapTests.swift */,
|
||||
A81A8CC72D68BA610086C96F /* DataStructuresTests.swift */,
|
||||
);
|
||||
path = datastructures;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1214,6 +1275,8 @@
|
||||
A85ACB902D1F31C400AA3DE7 /* AltStore.release.xcconfig */,
|
||||
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */,
|
||||
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */,
|
||||
A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */,
|
||||
A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */,
|
||||
);
|
||||
path = xcconfigs;
|
||||
sourceTree = "<group>";
|
||||
@@ -1264,6 +1327,8 @@
|
||||
A8AD35572D31BEB2003A28B4 /* datastructures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */,
|
||||
A81A8CB02D68B0320086C96F /* TreeMap.swift */,
|
||||
A868CFE32D319988002F1201 /* SingletonGenericMap.swift */,
|
||||
);
|
||||
path = datastructures;
|
||||
@@ -1287,6 +1352,22 @@
|
||||
path = database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8B645F82D70C0DD00125819 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8B645FA2D70C0F600125819 /* UIKit */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8B645FA2D70C0F600125819 /* UIKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */,
|
||||
);
|
||||
path = UIKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8C38C1C2D2068D100E83DBD /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1320,6 +1401,26 @@
|
||||
path = common;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8E2DB302D684E2A009E5D31 /* UITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8E2DB2E2D684E2A009E5D31 /* UITests.swift */,
|
||||
A8E2DB2F2D684E2A009E5D31 /* UITestsLaunchTests.swift */,
|
||||
);
|
||||
path = UITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8E2DB352D6850A9009E5D31 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A81A8CB22D68B2030086C96F /* UnitTests */,
|
||||
A8E2DB302D684E2A009E5D31 /* UITests */,
|
||||
A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */,
|
||||
A81A8CD32D68BAFF0086C96F /* DataStructureTests.xctestplan */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8EA19602D4982E300DC6322 /* DatabaseManager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1348,6 +1449,8 @@
|
||||
A8F66C072D04C025009689E6 /* SideStore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8B645F82D70C0DD00125819 /* Views */,
|
||||
A8E2DB352D6850A9009E5D31 /* Tests */,
|
||||
A8F66C5C2D04D433009689E6 /* minimuxer */,
|
||||
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
|
||||
A8F66C412D04D433009689E6 /* em_proxy */,
|
||||
@@ -1685,8 +1788,8 @@
|
||||
A8EA19622D4982FC00DC6322 /* Transformers */,
|
||||
D557A4862AE88232007D0DCF /* Patreon */,
|
||||
BF66EEAC2501AECA007EE018 /* Migrations */,
|
||||
A8EA195E2D4982D600DC6322 /* BaseEntity.swift */,
|
||||
BF66EEB72501AECA007EE018 /* AltStore.xcdatamodeld */,
|
||||
A8EA195E2D4982D600DC6322 /* BaseEntity.swift */,
|
||||
BF66EEC92501AECA007EE018 /* Account.swift */,
|
||||
BF66EEC72501AECA007EE018 /* AppID.swift */,
|
||||
BF66EEC62501AECA007EE018 /* AppPermission.swift */,
|
||||
@@ -1697,6 +1800,7 @@
|
||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */,
|
||||
BF66EEBF2501AECA007EE018 /* NewsItem.swift */,
|
||||
BF66EEC32501AECA007EE018 /* RefreshAttempt.swift */,
|
||||
A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */,
|
||||
BF66EEAB2501AECA007EE018 /* Source.swift */,
|
||||
BF66EEC42501AECA007EE018 /* StoreApp.swift */,
|
||||
BF66EEC22501AECA007EE018 /* Team.swift */,
|
||||
@@ -1716,10 +1820,11 @@
|
||||
BF66EEAD2501AECA007EE018 /* Policies */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EEAF2501AECA007EE018 /* InstalledAppPolicy.swift */,
|
||||
D5185B812AE1E71D00646E33 /* Source11To17MigrationPolicy.swift */,
|
||||
BF66EEAE2501AECA007EE018 /* StoreAppPolicy.swift */,
|
||||
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */,
|
||||
BF66EEAF2501AECA007EE018 /* InstalledAppPolicy.swift */,
|
||||
D5185B812AE1E71D00646E33 /* Source13To14MigrationPolicy.swift */,
|
||||
A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */,
|
||||
);
|
||||
path = Policies;
|
||||
sourceTree = "<group>";
|
||||
@@ -1727,12 +1832,7 @@
|
||||
BF66EEB02501AECA007EE018 /* Mapping Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D51E83812B8692DF0092FC61 /* AltStore16ToAltStore17.xcmappingmodel */,
|
||||
D5CE309B2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel */,
|
||||
D5753A612B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel */,
|
||||
D5185B7F2AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel */,
|
||||
D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */,
|
||||
D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */,
|
||||
A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */,
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */,
|
||||
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */,
|
||||
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */,
|
||||
@@ -1916,6 +2016,8 @@
|
||||
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */,
|
||||
191E5FAB290A5D92001A3B7C /* libminimuxer.a */,
|
||||
D586D39828EF58B0000E101F /* AltTests.xctest */,
|
||||
A8E2DB212D684CBD009E5D31 /* UITests.xctest */,
|
||||
A81A8CC52D68BA610086C96F /* DataStructureTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -2396,6 +2498,45 @@
|
||||
productReference = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */;
|
||||
productType = "com.apple.product-type.library.static";
|
||||
};
|
||||
A81A8CC42D68BA610086C96F /* DataStructureTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A81A8CC92D68BA610086C96F /* Build configuration list for PBXNativeTarget "DataStructureTests" */;
|
||||
buildPhases = (
|
||||
A81A8CC12D68BA610086C96F /* Sources */,
|
||||
A81A8CC22D68BA610086C96F /* Frameworks */,
|
||||
A81A8CC32D68BA610086C96F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = DataStructureTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = DataStructuresTests;
|
||||
productReference = A81A8CC52D68BA610086C96F /* DataStructureTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
A8E2DB202D684CBD009E5D31 /* UITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A8E2DB292D684CBD009E5D31 /* Build configuration list for PBXNativeTarget "UITests" */;
|
||||
buildPhases = (
|
||||
A8E2DB1D2D684CBD009E5D31 /* Sources */,
|
||||
A8E2DB1E2D684CBD009E5D31 /* Frameworks */,
|
||||
A8E2DB1F2D684CBD009E5D31 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
A8E2DB282D684CBD009E5D31 /* PBXTargetDependency */,
|
||||
);
|
||||
name = UITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = UITests;
|
||||
productReference = A8E2DB212D684CBD009E5D31 /* UITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
BF45872A2298D31600BD7491 /* libimobiledevice */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BF4587332298D31600BD7491 /* Build configuration list for PBXNativeTarget "libimobiledevice" */;
|
||||
@@ -2507,7 +2648,7 @@
|
||||
BFD247622284B9A500981D42 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1400;
|
||||
LastSwiftUpdateCheck = 1620;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = SideStore;
|
||||
TargetAttributes = {
|
||||
@@ -2517,6 +2658,13 @@
|
||||
191E5FAA290A5D92001A3B7C = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
};
|
||||
A81A8CC42D68BA610086C96F = {
|
||||
CreatedOnToolsVersion = 16.2;
|
||||
};
|
||||
A8E2DB202D684CBD009E5D31 = {
|
||||
CreatedOnToolsVersion = 16.2;
|
||||
TestTargetID = BFD247692284B9A500981D42;
|
||||
};
|
||||
BF45872A2298D31600BD7491 = {
|
||||
CreatedOnToolsVersion = 10.2.1;
|
||||
};
|
||||
@@ -2557,6 +2705,7 @@
|
||||
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */,
|
||||
D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
|
||||
A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
|
||||
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */,
|
||||
);
|
||||
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -2583,6 +2732,8 @@
|
||||
BF989166250AABF3002ACF50 /* AltWidgetExtension */,
|
||||
19104DB12909C06C00C49C7B /* EmotionalDamage */,
|
||||
191E5FAA290A5D92001A3B7C /* minimuxer */,
|
||||
A8E2DB202D684CBD009E5D31 /* UITests */,
|
||||
A81A8CC42D68BA610086C96F /* DataStructureTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -2640,6 +2791,22 @@
|
||||
/* End PBXReferenceProxy section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
A81A8CC32D68BA610086C96F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A8E2DB1F2D684CBD009E5D31 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A81A8CD42D68BAFF0086C96F /* DataStructureTests.xctestplan in Resources */,
|
||||
A8E2DB342D68507F009E5D31 /* SideStoreTests.xctestplan in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF580479246A28F7008AE704 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -2811,6 +2978,28 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A81A8CC12D68BA610086C96F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A81A8CC82D68BA610086C96F /* DataStructuresTests.swift in Sources */,
|
||||
A81A8CD02D68BA9B0086C96F /* LinkedHashMap.swift in Sources */,
|
||||
A81A8CD22D68BAA30086C96F /* SingletonGenericMap.swift in Sources */,
|
||||
A81A8CD12D68BA9B0086C96F /* TreeMap.swift in Sources */,
|
||||
A81A8CCE2D68BA8D0086C96F /* LinkedHashMapTests.swift in Sources */,
|
||||
A81A8CCF2D68BA8D0086C96F /* TreeMapTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A8E2DB1D2D684CBD009E5D31 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A8E2DB312D684E2A009E5D31 /* UITests.swift in Sources */,
|
||||
A8E2DB322D684E2A009E5D31 /* UITestsLaunchTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF4587282298D31600BD7491 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -2903,6 +3092,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A8228B5D2D6E361F00F7CE0E /* (null) in Sources */,
|
||||
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */,
|
||||
A8EA195F2D4982D600DC6322 /* BaseEntity.swift in Sources */,
|
||||
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */,
|
||||
@@ -2918,7 +3108,6 @@
|
||||
BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */,
|
||||
D552EB062AF453F900A3AB4D /* URL+Normalized.swift in Sources */,
|
||||
BFAECC522501B0A400528F27 /* CodableError.swift in Sources */,
|
||||
D51E83822B8692DF0092FC61 /* AltStore16ToAltStore17.xcmappingmodel in Sources */,
|
||||
A8FD915F2D046F5200322782 /* UserInfoValue.swift in Sources */,
|
||||
D5F9821D2AB900060045751F /* AppScreenshot.swift in Sources */,
|
||||
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */,
|
||||
@@ -2938,8 +3127,8 @@
|
||||
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */,
|
||||
0EE7FDCB2BE8D12B00D1E390 /* ALTLocalizedError.swift in Sources */,
|
||||
BF66EEA92501AEC5007EE018 /* Tier.swift in Sources */,
|
||||
A8228B5B2D6E2C0C00F7CE0E /* (null) in Sources */,
|
||||
BF66EEDB2501AECA007EE018 /* StoreApp.swift in Sources */,
|
||||
D5CE309C2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel in Sources */,
|
||||
BF66EEDE2501AECA007EE018 /* AppID.swift in Sources */,
|
||||
BF66EECF2501AECA007EE018 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||
BF66EEA82501AEC5007EE018 /* Patron.swift in Sources */,
|
||||
@@ -2955,14 +3144,12 @@
|
||||
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */,
|
||||
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */,
|
||||
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
|
||||
D5753A622B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel in Sources */,
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
|
||||
BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */,
|
||||
D5B6F6A92AD75D01007EED5A /* ProcessInfo+Previews.swift in Sources */,
|
||||
BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */,
|
||||
D5F48B4829CCF21B002B52A4 /* AltStore+Async.swift in Sources */,
|
||||
BF66EED92501AECA007EE018 /* Team.swift in Sources */,
|
||||
D5185B802AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel in Sources */,
|
||||
D5728CA72A0D79D30014E73C /* OptionalProtocol.swift in Sources */,
|
||||
BF66EED12501AECA007EE018 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
|
||||
D5F48B4C29CD0C48002B52A4 /* AsyncManaged.swift in Sources */,
|
||||
@@ -2973,10 +3160,11 @@
|
||||
BF66EED52501AECA007EE018 /* AltStore.xcdatamodeld in Sources */,
|
||||
BFAECC582501B0A400528F27 /* ALTConstants.m in Sources */,
|
||||
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
|
||||
D5177B0D2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel in Sources */,
|
||||
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
|
||||
A80D60D32D3DD85100CEF65D /* ReleaseTrack.swift in Sources */,
|
||||
D56915072AD5E91B00A2B747 /* Regex+Permissions.swift in Sources */,
|
||||
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
|
||||
A881E7CB2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift in Sources */,
|
||||
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */,
|
||||
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */,
|
||||
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
|
||||
@@ -2987,14 +3175,14 @@
|
||||
BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */,
|
||||
0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */,
|
||||
D5A645232AF5B5C50047D980 /* PatreonAPI+Responses.swift in Sources */,
|
||||
D5185B822AE1E71D00646E33 /* Source13To14MigrationPolicy.swift in Sources */,
|
||||
D5185B822AE1E71D00646E33 /* Source11To17MigrationPolicy.swift in Sources */,
|
||||
BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */,
|
||||
BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
|
||||
BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */,
|
||||
A881E7C72D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel in Sources */,
|
||||
BF66EED02501AECA007EE018 /* AltStore6ToAltStore7.xcmappingmodel in Sources */,
|
||||
BF66EEDC2501AECA007EE018 /* MergePolicy.swift in Sources */,
|
||||
BF66EEE22501AECA007EE018 /* InstalledExtension.swift in Sources */,
|
||||
D5927D6929DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel in Sources */,
|
||||
BF66EED62501AECA007EE018 /* NewsItem.swift in Sources */,
|
||||
BF66EEA72501AEC5007EE018 /* Campaign.swift in Sources */,
|
||||
D52C8F032AFC56F000CA0BDD /* StoreCategory.swift in Sources */,
|
||||
@@ -3011,6 +3199,7 @@
|
||||
files = (
|
||||
D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */,
|
||||
D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */,
|
||||
A81A8CB92D68B30B0086C96F /* SingletonGenericMap.swift in Sources */,
|
||||
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
|
||||
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
|
||||
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
||||
@@ -3019,7 +3208,6 @@
|
||||
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
|
||||
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */,
|
||||
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
|
||||
A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */,
|
||||
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */,
|
||||
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
||||
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
|
||||
@@ -3053,6 +3241,7 @@
|
||||
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
||||
A81A8CBD2D68B43F0086C96F /* LinkedHashMap.swift in Sources */,
|
||||
D5390C3C2AC3A43900D17E62 /* AddSourceViewController.swift in Sources */,
|
||||
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */,
|
||||
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */,
|
||||
@@ -3127,6 +3316,7 @@
|
||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
|
||||
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
|
||||
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */,
|
||||
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
|
||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
@@ -3135,6 +3325,7 @@
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
|
||||
A81A8CBA2D68B3110086C96F /* TreeMap.swift in Sources */,
|
||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
||||
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
|
||||
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
||||
@@ -3198,6 +3389,11 @@
|
||||
target = BF58047A246A28F7008AE704 /* AltBackup */;
|
||||
targetProxy = A8E00D3D2D0C95B5000DD2C7 /* PBXContainerItemProxy */;
|
||||
};
|
||||
A8E2DB282D684CBD009E5D31 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BFD247692284B9A500981D42 /* SideStore */;
|
||||
targetProxy = A8E2DB272D684CBD009E5D31 /* PBXContainerItemProxy */;
|
||||
};
|
||||
BF66EE842501AE50007EE018 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BF66EE7D2501AE50007EE018 /* AltStoreCore */;
|
||||
@@ -3361,6 +3557,115 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A81A8CCA2D68BA610086C96F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
A81A8CCB2D68BA610086C96F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A8E2DB2A2D684CBD009E5D31 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SideStore;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
A8E2DB2B2D684CBD009E5D31 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SideStore;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BF4587342298D31600BD7491 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -3896,6 +4201,24 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
A81A8CC92D68BA610086C96F /* Build configuration list for PBXNativeTarget "DataStructureTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A81A8CCA2D68BA610086C96F /* Debug */,
|
||||
A81A8CCB2D68BA610086C96F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
A8E2DB292D684CBD009E5D31 /* Build configuration list for PBXNativeTarget "UITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A8E2DB2A2D684CBD009E5D31 /* Debug */,
|
||||
A8E2DB2B2D684CBD009E5D31 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BF4587332298D31600BD7491 /* Build configuration list for PBXNativeTarget "libimobiledevice" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@@ -3961,6 +4284,14 @@
|
||||
minimumVersion = 0.4.0;
|
||||
};
|
||||
};
|
||||
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.7.1;
|
||||
};
|
||||
};
|
||||
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin.git";
|
||||
@@ -3985,6 +4316,16 @@
|
||||
package = A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */;
|
||||
productName = SemanticVersion;
|
||||
};
|
||||
A8B645FE2D70C1AD00125819 /* MarkdownKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
|
||||
productName = MarkdownKit;
|
||||
};
|
||||
A8B646002D70C23E00125819 /* MarkdownKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
|
||||
productName = MarkdownKit;
|
||||
};
|
||||
A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = "AltSign-Static";
|
||||
@@ -4000,11 +4341,6 @@
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
D51E83802B86926B0092FC61 /* AltStore 17.xcdatamodel */,
|
||||
D5CE309A2B4C93BE00DB8151 /* AltStore 16.xcdatamodel */,
|
||||
D5753A602B279D1400090456 /* AltStore 15.xcdatamodel */,
|
||||
D5185B7E2AE1E35200646E33 /* AltStore 14.xcdatamodel */,
|
||||
D581822C2A218A140087965B /* AltStore 13.xcdatamodel */,
|
||||
D5927D6729DCE1FE00D6898E /* AltStore 12.xcdatamodel */,
|
||||
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */,
|
||||
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */,
|
||||
BFBF33142526754700B7B8C9 /* AltStore 9.xcdatamodel */,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
|
||||
BuildableName = "DataStructureTests.xctest"
|
||||
BlueprintName = "DataStructureTests"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -28,18 +28,27 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<!-- shouldAutocreateTestPlan = "YES"> -->
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:SideStore/Tests/SideStoreTests.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D586D39728EF58B0000E101F"
|
||||
BuildableName = "AltTests.xctest"
|
||||
BlueprintName = "AltTests"
|
||||
BlueprintIdentifier = "A8E2DB202D684CBD009E5D31"
|
||||
BuildableName = "UITests.xctest"
|
||||
BlueprintName = "UITests"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SelectedTests>
|
||||
<Test
|
||||
Identifier = "UITests/testExample()">
|
||||
</Test>
|
||||
</SelectedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
|
||||
@@ -43,8 +43,10 @@ final class AppContentViewController: UITableViewController
|
||||
}()
|
||||
|
||||
@IBOutlet private var subtitleLabel: UILabel!
|
||||
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||
// @IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
|
||||
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
@IBOutlet private var versionDateLabel: UILabel!
|
||||
@IBOutlet private var sizeLabel: UILabel!
|
||||
@@ -55,35 +57,32 @@ final class AppContentViewController: UITableViewController
|
||||
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
|
||||
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.contentInset.bottom = 20
|
||||
|
||||
self.subtitleLabel.text = self.app.subtitle
|
||||
self.descriptionTextView.text = self.app.localizedDescription
|
||||
let desc = self.app.localizedDescription
|
||||
self.descriptionTextView.text = desc
|
||||
|
||||
if let version = self.app.latestAvailableVersion
|
||||
{
|
||||
self.versionDescriptionTextView.text = version.localizedDescription
|
||||
if let version = self.app.latestAvailableVersion {
|
||||
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.versionDescriptionTextView.text = nil
|
||||
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
|
||||
} else {
|
||||
self.versionDescriptionTextView.text = "nil"
|
||||
self.versionLabel.text = nil
|
||||
self.versionDateLabel.text = nil
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
||||
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
||||
}
|
||||
|
||||
self.descriptionTextView.maximumNumberOfLines = 5
|
||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
self.versionDescriptionTextView.maximumNumberOfLines = 5
|
||||
|
||||
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
||||
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
@@ -162,8 +161,12 @@ private extension AppContentViewController
|
||||
|
||||
switch sender
|
||||
{
|
||||
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
case self.descriptionTextView.toggleButton:
|
||||
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
|
||||
case self.versionDescriptionTextView.toggleButton:
|
||||
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
|
||||
default: return
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="20" width="335" height="34"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
@@ -353,7 +353,7 @@
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
|
||||
@@ -138,11 +138,12 @@ extension AppBannerView
|
||||
init(app: AppProtocol)
|
||||
{
|
||||
self.name = app.name
|
||||
|
||||
|
||||
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
|
||||
self.developerName = storeApp.developerName
|
||||
|
||||
if storeApp.isBeta
|
||||
|
||||
if let track = storeApp.latestSupportedVersion?.channel,
|
||||
ReleaseTracks.betaTracks.contains(track)
|
||||
{
|
||||
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
||||
self.isBeta = true
|
||||
|
||||
@@ -13,21 +13,18 @@ final class CollapsingTextView: UITextView
|
||||
var isCollapsed = true {
|
||||
didSet {
|
||||
guard self.isCollapsed != oldValue else { return }
|
||||
self.shouldResetLayout = true
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var maximumNumberOfLines = 2 {
|
||||
didSet {
|
||||
self.shouldResetLayout = true
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var lineSpacing: Double = 2 {
|
||||
didSet {
|
||||
self.shouldResetLayout = true
|
||||
|
||||
if #available(iOS 16, *)
|
||||
{
|
||||
@@ -42,7 +39,6 @@ final class CollapsingTextView: UITextView
|
||||
|
||||
override var text: String! {
|
||||
didSet {
|
||||
self.shouldResetLayout = true
|
||||
|
||||
guard #available(iOS 16, *) else { return }
|
||||
self.updateText()
|
||||
@@ -51,9 +47,6 @@ final class CollapsingTextView: UITextView
|
||||
|
||||
let moreButton = UIButton(type: .system)
|
||||
|
||||
private var shouldResetLayout: Bool = false
|
||||
private var previousSize: CGSize?
|
||||
|
||||
override init(frame: CGRect, textContainer: NSTextContainer?)
|
||||
{
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
@@ -115,45 +108,39 @@ final class CollapsingTextView: UITextView
|
||||
height: font.lineHeight)
|
||||
self.moreButton.frame = moreButtonFrame
|
||||
|
||||
if self.shouldResetLayout || self.previousSize != self.bounds.size
|
||||
if self.isCollapsed
|
||||
{
|
||||
if self.isCollapsed
|
||||
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
|
||||
|
||||
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||
{
|
||||
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
|
||||
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||
|
||||
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||
{
|
||||
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||
|
||||
var exclusionFrame = moreButtonFrame
|
||||
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
|
||||
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
||||
|
||||
self.moreButton.isHidden = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
|
||||
self.textContainer.exclusionPaths = []
|
||||
|
||||
self.moreButton.isHidden = true
|
||||
}
|
||||
var exclusionFrame = moreButtonFrame
|
||||
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
|
||||
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
||||
|
||||
self.moreButton.isHidden = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.textContainer.maximumNumberOfLines = 0
|
||||
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
|
||||
self.textContainer.exclusionPaths = []
|
||||
|
||||
self.moreButton.isHidden = true
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.textContainer.maximumNumberOfLines = 0
|
||||
self.textContainer.exclusionPaths = []
|
||||
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.moreButton.isHidden = true
|
||||
}
|
||||
|
||||
self.shouldResetLayout = false
|
||||
self.previousSize = self.bounds.size
|
||||
self.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -235,7 +235,8 @@ private extension MyAppsViewController
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.tintColor = app.tintColor ?? .altPrimary
|
||||
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
|
||||
cell.versionDescriptionTextView.maximumNumberOfLines = 2
|
||||
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
|
||||
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
@@ -257,7 +258,7 @@ private extension MyAppsViewController
|
||||
|
||||
let appName: String
|
||||
|
||||
if app.isBeta
|
||||
if ReleaseTracks.betaTracks.contains(latestSupportedVersion.channel)
|
||||
{
|
||||
appName = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
||||
}
|
||||
@@ -281,12 +282,9 @@ private extension MyAppsViewController
|
||||
cell.mode = .collapsed
|
||||
}
|
||||
|
||||
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
|
||||
|
||||
cell.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
|
||||
|
||||
cell.setNeedsLayout()
|
||||
|
||||
// Below lines are necessary to avoid "more" button layout issues.
|
||||
cell.versionDescriptionTextView.setNeedsLayout()
|
||||
cell.layoutIfNeeded()
|
||||
}
|
||||
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
||||
@@ -367,8 +365,12 @@ private extension MyAppsViewController
|
||||
|
||||
formatter.maximumUnitCount = 1
|
||||
|
||||
|
||||
let timeInterval = formatter.string(from: currentDate, to: installedApp.expirationDate)
|
||||
var timeInterval: String? = "expired"
|
||||
let expirationDate = installedApp.expirationDate
|
||||
let isExpired = currentDate > expirationDate
|
||||
if(!isExpired) {
|
||||
timeInterval = formatter.string(from: currentDate, to: expirationDate)
|
||||
}
|
||||
cell.bannerView.button.setTitle(timeInterval?.uppercased(), for: .normal)
|
||||
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
@@ -376,7 +378,7 @@ private extension MyAppsViewController
|
||||
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
cell.bannerView.buttonLabel.isHidden = false
|
||||
cell.bannerView.buttonLabel.isHidden = isExpired
|
||||
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||
|
||||
cell.bannerView.button.removeTarget(self, action: nil, for: .primaryActionTriggered)
|
||||
@@ -720,22 +722,29 @@ private extension MyAppsViewController
|
||||
|
||||
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
|
||||
|
||||
// Toggle the state
|
||||
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
|
||||
{
|
||||
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
|
||||
// Set collapsed mode on the cell
|
||||
cell?.mode = .collapsed
|
||||
}
|
||||
else
|
||||
{
|
||||
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
|
||||
// Set expanded mode on the cell
|
||||
cell?.mode = .expanded
|
||||
}
|
||||
|
||||
// Clear cached size so it's recalculated
|
||||
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
|
||||
|
||||
self.collectionView.performBatchUpdates({
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}, completion: nil)
|
||||
// Animate the change smoothly with a duration
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.collectionView.performBatchUpdates({
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func refreshApp(_ sender: UIButton)
|
||||
|
||||
@@ -21,12 +21,19 @@ extension UpdateCollectionViewCell
|
||||
{
|
||||
var mode: Mode = .expanded {
|
||||
didSet {
|
||||
self.update()
|
||||
switch self.mode {
|
||||
case .collapsed:
|
||||
self.versionDescriptionTextView.isCollapsed = true
|
||||
case .expanded:
|
||||
self.versionDescriptionTextView.isCollapsed = false
|
||||
}
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||
// @IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet var versionDescriptionTextView: CollapsingMarkdownView!
|
||||
|
||||
@IBOutlet private var blurView: UIVisualEffectView!
|
||||
|
||||
@@ -85,16 +92,16 @@ extension UpdateCollectionViewCell
|
||||
}
|
||||
}
|
||||
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||
{
|
||||
// Ensure cell is laid out so it will report correct size.
|
||||
self.versionDescriptionTextView.setNeedsLayout()
|
||||
self.versionDescriptionTextView.layoutIfNeeded()
|
||||
|
||||
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||
|
||||
return size
|
||||
}
|
||||
// override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||
// {
|
||||
// // Ensure cell is laid out so it will report correct size.
|
||||
// self.versionDescriptionTextView.setNeedsLayout()
|
||||
// self.versionDescriptionTextView.layoutIfNeeded()
|
||||
//
|
||||
// let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||
//
|
||||
// return size
|
||||
// }
|
||||
}
|
||||
|
||||
private extension UpdateCollectionViewCell
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -11,7 +11,7 @@
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
@@ -30,7 +30,7 @@
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
|
||||
@@ -39,7 +39,7 @@
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
|
||||
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
@@ -91,7 +91,7 @@
|
||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<stackView key="tableFooterView" opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalCentering" alignment="center" spacing="15" id="48g-cT-stR">
|
||||
<rect key="frame" x="0.0" y="1790.6666698455811" width="402" height="125"/>
|
||||
<rect key="frame" x="0.0" y="1986.3333377838135" width="402" height="125"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Follow SideStore for updates" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XFa-MY-7cV">
|
||||
@@ -500,13 +500,13 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vH6-7i-tCE">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vH6-7i-tCE">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="119" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
|
||||
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
|
||||
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
|
||||
</imageView>
|
||||
@@ -537,7 +537,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Clear Cache…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-Mz-DlL">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Clear Cache…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-Mz-DlL">
|
||||
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="114.33333333333331" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -569,22 +569,22 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="86" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
|
||||
<rect key="frame" x="217" y="15.333333333333334" width="155" height="20.333333333333329"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
|
||||
<rect key="frame" x="217" y="15.333333333333336" width="155" height="20.333333333333329"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="125.33333333333333" height="20.333333333333332"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
||||
<rect key="frame" x="139.33333333333331" y="-1" width="15.666666666666657" height="22.333333333333332"/>
|
||||
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
|
||||
</imageView>
|
||||
@@ -614,23 +614,23 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="89" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
||||
<rect key="frame" x="227.33333333333337" y="15.333333333333334" width="144.66666666666663" height="20.333333333333329"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
||||
<rect key="frame" x="227.33333333333337" y="15.333333333333336" width="144.66666666666663" height="20.333333333333329"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="115" height="20.333333333333332"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
||||
<rect key="frame" x="128.99999999999997" y="-1" width="15.666666666666657" height="22.333333333333332"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
||||
<rect key="frame" x="129" y="-1" width="15.666666666666657" height="22.333333333333332"/>
|
||||
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
@@ -659,23 +659,23 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
|
||||
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
||||
<rect key="frame" x="235.33333333333337" y="15.333333333333334" width="136.66666666666663" height="20.333333333333329"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
||||
<rect key="frame" x="235.33333333333337" y="15.333333333333336" width="136.66666666666663" height="20.333333333333329"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="107" height="20.333333333333332"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
||||
<rect key="frame" x="120.99999999999999" y="-1" width="15.666666666666671" height="22.333333333333332"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
||||
<rect key="frame" x="121" y="-1" width="15.666666666666629" height="22.333333333333332"/>
|
||||
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
@@ -698,19 +698,19 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1107.6666698455811" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1071.6666698455811" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="67.333333333333329" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
||||
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
|
||||
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
|
||||
</imageView>
|
||||
@@ -739,7 +739,7 @@
|
||||
<tableViewSection headerTitle="" id="OMa-EK-hRI">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1199.0000038146973" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1163.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -773,7 +773,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1250.0000038146973" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1214.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -810,7 +810,7 @@
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1301.0000038146973" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1265.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VrV-qI-zXF" id="w1r-uY-4pD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -844,7 +844,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1352.0000038146973" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1316.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -878,7 +878,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1403.0000038146973" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1367.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -906,17 +906,89 @@
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="3"/>
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-nXH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1418.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XW5-Zc-nXH" id="AtM-bL-8pS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable Beta Updates" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2px-HD-0UT">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="169" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e32-w4-5fk">
|
||||
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleEnableBetaUpdates:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uxG-df-7GK"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="2px-HD-0UT" firstAttribute="centerY" secondItem="AtM-bL-8pS" secondAttribute="centerY" id="07r-jt-3rz"/>
|
||||
<constraint firstItem="2px-HD-0UT" firstAttribute="leading" secondItem="AtM-bL-8pS" secondAttribute="leadingMargin" id="K2i-9G-bG8"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="e32-w4-5fk" secondAttribute="trailing" id="Wa7-m6-lcl"/>
|
||||
<constraint firstItem="e32-w4-5fk" firstAttribute="centerY" secondItem="AtM-bL-8pS" secondAttribute="centerY" id="n7R-av-FBX"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="qbY-8c-LYT" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1469.0000038146973" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qbY-8c-LYT" id="NxK-qB-w7Q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Beta Updates Track" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5J9-vR-vhX" userLabel="Beta Track Label">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="159.66666666666666" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" changesSelectionAsPrimaryAction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Oct-iT-NwP" userLabel="Beta Track Drop Down Button">
|
||||
<rect key="frame" x="301.66666666666669" y="8.3333333333333321" width="70.333333333333314" height="34.333333333333343"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="50" id="new-width-constraint"/>
|
||||
</constraints>
|
||||
<buttonConfiguration key="configuration" style="plain" title="stable" titleAlignment="trailing"/>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="5J9-vR-vhX" firstAttribute="centerY" secondItem="NxK-qB-w7Q" secondAttribute="centerY" id="5gk-uR-bun"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="Oct-iT-NwP" secondAttribute="trailing" id="Wa8-m6-lcl"/>
|
||||
<constraint firstItem="5J9-vR-vhX" firstAttribute="leading" secondItem="NxK-qB-w7Q" secondAttribute="leadingMargin" id="af8-XD-RSn"/>
|
||||
<constraint firstItem="Oct-iT-NwP" firstAttribute="centerY" secondItem="NxK-qB-w7Q" secondAttribute="centerY" id="n7t-av-FBX"/>
|
||||
<constraint firstItem="Oct-iT-NwP" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="5J9-vR-vhX" secondAttribute="trailing" constant="16" id="new-spacing-constraint"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="3"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="" id="lLQ-K0-XSb">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="daQ-mk-yqC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1494.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1560.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="daQ-mk-yqC" id="ZkW-ZR-twy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -951,7 +1023,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="hRP-jU-2hd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1545.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1611.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hRP-jU-2hd" id="JhE-O4-pRg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
@@ -986,19 +1058,19 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="JoN-Aj-XtZ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1596.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1662.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JoN-Aj-XtZ" id="v8Q-VQ-Q1h">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Enable Verbose Ops Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7bz-tI-tLY">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable Verbose Ops Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7bz-tI-tLY">
|
||||
<rect key="frame" x="29.999999999999986" y="15.333333333333334" width="232.66666666666663" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q5X-Mo-KpE">
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q5X-Mo-KpE">
|
||||
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleVerboseOperationsLogging:" destination="aMk-Xp-UL8" eventType="valueChanged" id="n9N-Gt-OY2"/>
|
||||
@@ -1021,13 +1093,13 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="QOO-bO-4M5" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1647.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1713.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QOO-bO-4M5" id="VTT-z5-C89">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Export Database..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho1-To-wve" userLabel="Export Database">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Export Database..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho1-To-wve" userLabel="Export Database">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="151.66666666666666" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -1049,13 +1121,13 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="ToB-H7-2lR" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1698.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1764.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ToB-H7-2lR" id="Acf-xV-Isn">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Delete Database..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CcF-9x-Eu8" userLabel="Delete Database Label">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Delete Database..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CcF-9x-Eu8" userLabel="Delete Database Label">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="150.33333333333334" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -1077,19 +1149,19 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="xtI-eU-LFb" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1749.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1815.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="xtI-eU-LFb" id="bc9-41-6mE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Operations Logging Control" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LW3-gm-lj5">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Operations Logging Control" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LW3-gm-lj5">
|
||||
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="224.33333333333337" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zl4-ti-HTW">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zl4-ti-HTW">
|
||||
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
|
||||
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
|
||||
</imageView>
|
||||
@@ -1111,19 +1183,19 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="pvu-IV-Poa" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1800.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1866.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pvu-IV-Poa" id="zck-an-8cK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Recreate Database on Next Start" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZRk-8S-kBQ" userLabel="Recreate Database Label">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Recreate Database on Next Start" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZRk-8S-kBQ" userLabel="Recreate Database Label">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="265.33333333333331" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uGv-Lb-Ita" userLabel="Recreate DB switch">
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uGv-Lb-Ita" userLabel="Recreate DB switch">
|
||||
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleRecreateDatabaseSwitch:" destination="aMk-Xp-UL8" eventType="valueChanged" id="vlf-Iz-kWr"/>
|
||||
@@ -1146,19 +1218,19 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="9By-QW-Jw9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1851.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1917.3333377838135" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="9By-QW-Jw9" id="Dzq-gE-zyT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Minimuxer Console Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jW6-pb-xdP">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Minimuxer Console Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jW6-pb-xdP">
|
||||
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="225.33333333333337" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="os8-7F-rSm" userLabel="Minimuxer logging Switch">
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="os8-7F-rSm" userLabel="Minimuxer logging Switch">
|
||||
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleMinimuxerConsoleLogging:" destination="aMk-Xp-UL8" eventType="valueChanged" id="d0C-kx-aFV"/>
|
||||
@@ -1194,6 +1266,9 @@
|
||||
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
|
||||
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
|
||||
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
|
||||
<outlet property="betaTrackLabel" destination="5J9-vR-vhX" id="x2k-X7-pUr"/>
|
||||
<outlet property="betaTrackPopupButton" destination="Oct-iT-NwP" id="sOR-cc-IWC"/>
|
||||
<outlet property="betaUpdatesSwitch" destination="e32-w4-5fk" id="kdn-ZR-cNU"/>
|
||||
<outlet property="disableAppLimitSwitch" destination="1aa-og-ZXD" id="oVL-Md-yZ8"/>
|
||||
<outlet property="disableResponseCachingSwitch" destination="AAh-cu-qw8" id="aVT-Md-yZ8"/>
|
||||
<outlet property="exportResignedAppsSwitch" destination="GYP-qn-wzh" id="aVL-Md-yZ8"/>
|
||||
|
||||
@@ -75,6 +75,8 @@ extension SettingsViewController
|
||||
case refreshSideJITServer
|
||||
case resetPairingFile
|
||||
case anisetteServers
|
||||
case betaUpdates
|
||||
case betaTrack
|
||||
// case hiddenSettings
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ extension SettingsViewController
|
||||
case exportDatabase
|
||||
case deleteDatabase
|
||||
case operationsLoggingControl
|
||||
case recreateDatabase
|
||||
case recreateDatabase
|
||||
case minimuxerConsoleLogging
|
||||
}
|
||||
}
|
||||
@@ -97,6 +99,10 @@ final class SettingsViewController: UITableViewController
|
||||
|
||||
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
|
||||
|
||||
// Add outlet
|
||||
@IBOutlet private var betaTrackLabel: UILabel!
|
||||
@IBOutlet private var betaTrackPopupButton: UIButton!
|
||||
|
||||
private var debugGestureCounter = 0
|
||||
private weak var debugGestureTimer: Timer?
|
||||
|
||||
@@ -107,6 +113,7 @@ final class SettingsViewController: UITableViewController
|
||||
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
||||
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
|
||||
@IBOutlet private var disableAppLimitSwitch: UISwitch!
|
||||
@IBOutlet private var betaUpdatesSwitch: UISwitch!
|
||||
@IBOutlet private var exportResignedAppsSwitch: UISwitch!
|
||||
@IBOutlet private var verboseOperationsLoggingSwitch: UISwitch!
|
||||
@IBOutlet private var minimuxerConsoleLoggingSwitch: UISwitch!
|
||||
@@ -120,7 +127,7 @@ final class SettingsViewController: UITableViewController
|
||||
@IBOutlet private var githubButton: UIButton!
|
||||
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
|
||||
|
||||
@IBOutlet private var recreateDatabaseSwitch: UISwitch!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
@@ -128,7 +135,7 @@ final class SettingsViewController: UITableViewController
|
||||
}
|
||||
|
||||
private static var exportDBInProgress = false
|
||||
private static var deleteDBInProgress = false
|
||||
private static var deleteDBInProgress = false
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
@@ -138,6 +145,48 @@ private static var deleteDBInProgress = false
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
|
||||
}
|
||||
|
||||
|
||||
private func handleReleaseChannelSelection(_ channel: String) {
|
||||
// Update your model/preferences
|
||||
UserDefaults.standard.betaUdpatesTrack = channel
|
||||
updateReleaseChannelButtonTitle()
|
||||
}
|
||||
|
||||
private func updateReleaseChannelButtonTitle() {
|
||||
let channel = UserDefaults.standard.betaUdpatesTrack ?? UserDefaults.defaultBetaUpdatesTrack
|
||||
betaTrackPopupButton.setTitle(channel, for: .normal)
|
||||
}
|
||||
|
||||
private func configureReleaseChannelButton() {
|
||||
let currentTrack = UserDefaults.standard.betaUdpatesTrack
|
||||
|
||||
// get all tracks as string available except .stable and .unknown
|
||||
var trackOptions: [String] = ReleaseTracks.betaTracks.map {$0.rawValue}
|
||||
|
||||
if let currentTrack{
|
||||
// prepend currently selected beta track from the user defaults
|
||||
trackOptions = [currentTrack] + trackOptions.filter { $0 != currentTrack }
|
||||
}
|
||||
|
||||
// Create menu items with proper styling
|
||||
let items = trackOptions.map{ channel in
|
||||
UIAction(title: channel, handler: { [weak self] _ in
|
||||
self?.handleReleaseChannelSelection(channel)
|
||||
})
|
||||
}
|
||||
|
||||
// Create menu with proper styling
|
||||
let menu = UIMenu(title: "",
|
||||
options: [.singleSelection, .displayInline], // Add displayInline
|
||||
children: items
|
||||
)
|
||||
betaTrackPopupButton.menu = menu
|
||||
|
||||
// Set initial state
|
||||
updateReleaseChannelButtonTitle()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
@@ -185,6 +234,8 @@ private static var deleteDBInProgress = false
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
configureReleaseChannelButton()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
@@ -286,7 +337,7 @@ private extension SettingsViewController
|
||||
localizedVersion += buildInfo.project_version.map{ version in
|
||||
version.isEmpty ? "" : " (\(version))"
|
||||
} ?? installedApp.localizedVersion
|
||||
|
||||
|
||||
versionLabel = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
|
||||
}
|
||||
else if let version = buildInfo.marketing_version
|
||||
@@ -306,7 +357,7 @@ private extension SettingsViewController
|
||||
{
|
||||
versionLabel += "\n\(getXcodeVersion())"
|
||||
}
|
||||
|
||||
|
||||
return versionLabel
|
||||
}
|
||||
|
||||
@@ -332,6 +383,9 @@ private extension SettingsViewController
|
||||
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
|
||||
|
||||
// AdvancedSettingsRow
|
||||
self.betaUpdatesSwitch.isOn = UserDefaults.standard.isBetaUpdatesEnabled
|
||||
self.betaTrackLabel.isEnabled = UserDefaults.standard.isBetaUpdatesEnabled
|
||||
self.betaTrackPopupButton.isEnabled = UserDefaults.standard.isBetaUpdatesEnabled
|
||||
|
||||
// DiagnosticsRow
|
||||
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
|
||||
@@ -573,6 +627,13 @@ private extension SettingsViewController
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleEnableBetaUpdates(_ sender: UISwitch) {
|
||||
betaTrackLabel.isEnabled = sender.isOn
|
||||
betaTrackPopupButton.isEnabled = sender.isOn
|
||||
// update it in database
|
||||
UserDefaults.standard.isBetaUpdatesEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch)
|
||||
{
|
||||
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
|
||||
@@ -1134,7 +1195,7 @@ extension SettingsViewController
|
||||
// } else {
|
||||
// ELOG("UIApplication.openSettingsURLString invalid")
|
||||
// }
|
||||
case .refreshAttempts : break
|
||||
case .refreshAttempts, .betaUpdates, .betaTrack: break
|
||||
|
||||
}
|
||||
|
||||
@@ -1177,6 +1238,7 @@ extension SettingsViewController
|
||||
}
|
||||
|
||||
case .operationsLoggingControl:
|
||||
|
||||
// Instantiate SwiftUI View inside UIHostingController
|
||||
let operationsLoggingControlView = OperationsLoggingControlView()
|
||||
let operationsLoggingController = UIHostingController(rootView: operationsLoggingControlView)
|
||||
@@ -1190,6 +1252,10 @@ extension SettingsViewController
|
||||
// case .account, .patreon, .display, .instructions, .macDirtyCow: break
|
||||
case .account, .patreon, .display, .instructions: break
|
||||
}
|
||||
|
||||
|
||||
// deselect the row before returning (so that it doesn't look like stuck selected)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ extension AddSourceViewController
|
||||
var sourceAddress: String = ""
|
||||
|
||||
@Published
|
||||
var sourceURL: URL?
|
||||
var sourceURLs: [URL] = []
|
||||
|
||||
@Published
|
||||
var sourcePreviewResult: SourcePreviewResult?
|
||||
var sourcePreviewResults: [SourcePreviewResult] = []
|
||||
|
||||
|
||||
/* State */
|
||||
@@ -60,6 +60,8 @@ extension AddSourceViewController
|
||||
|
||||
class AddSourceViewController: UICollectionViewController
|
||||
{
|
||||
private var stagedForAdd: LinkedHashMap<Source, Bool> = LinkedHashMap()
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var addSourceDataSource = self.makeAddSourceDataSource()
|
||||
private lazy var sourcePreviewDataSource = self.makeSourcePreviewDataSource()
|
||||
@@ -117,6 +119,7 @@ private extension AddSourceViewController
|
||||
layoutConfig.contentInsetsReference = .safeArea
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
|
||||
guard let self, let section = Section(rawValue: sectionIndex) else { return nil }
|
||||
switch section
|
||||
{
|
||||
@@ -140,14 +143,19 @@ private extension AddSourceViewController
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .clear
|
||||
|
||||
if self.viewModel.sourceURL != nil && self.viewModel.isShowingPreviewStatus
|
||||
if !self.viewModel.sourceURLs.isEmpty && self.viewModel.isShowingPreviewStatus
|
||||
{
|
||||
switch self.viewModel.sourcePreviewResult
|
||||
for result in self.viewModel.sourcePreviewResults
|
||||
{
|
||||
case (_, .success)?: configuration.footerMode = .none
|
||||
case (_, .failure)?: configuration.footerMode = .supplementary
|
||||
case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
||||
default: configuration.footerMode = .none
|
||||
switch result
|
||||
{
|
||||
case (_, .success): configuration.footerMode = .none
|
||||
case (_, .failure): configuration.footerMode = .supplementary
|
||||
break
|
||||
// case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
||||
// break
|
||||
// default: configuration.footerMode = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -303,50 +311,71 @@ private extension AddSourceViewController
|
||||
{
|
||||
/* Pipeline */
|
||||
|
||||
// Map UITextField text -> URL
|
||||
// Map UITextField text -> URLs
|
||||
self.viewModel.$sourceAddress
|
||||
.map { [weak self] in self?.sourceURL(from: $0) }
|
||||
.assign(to: &self.viewModel.$sourceURL)
|
||||
|
||||
.map { [weak self] in
|
||||
guard let self else { return [] }
|
||||
|
||||
// Preserve order of parsed URLs
|
||||
let lines = $0.split(whereSeparator: { $0.isWhitespace })
|
||||
.map(String.init)
|
||||
.compactMap(self.sourceURL)
|
||||
|
||||
return NSOrderedSet(array: lines).array as! [URL] // de-duplicate while preserving order
|
||||
}
|
||||
.assign(to: &self.viewModel.$sourceURLs)
|
||||
|
||||
let showPreviewStatusPublisher = self.viewModel.$isShowingPreviewStatus
|
||||
.filter { $0 == true }
|
||||
|
||||
let sourceURLPublisher = self.viewModel.$sourceURL
|
||||
let sourceURLsPublisher = self.viewModel.$sourceURLs
|
||||
.removeDuplicates()
|
||||
.debounce(for: 0.2, scheduler: RunLoop.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.map { [weak self] sourceURL in
|
||||
.map { [weak self] sourceURLs in
|
||||
// Only set sourcePreviewResult to nil if sourceURL actually changes.
|
||||
self?.viewModel.sourcePreviewResult = nil
|
||||
return sourceURL
|
||||
self?.viewModel.sourcePreviewResults = []
|
||||
return sourceURLs
|
||||
}
|
||||
|
||||
// Map URL -> Source Preview
|
||||
Publishers.CombineLatest(sourceURLPublisher, showPreviewStatusPublisher.prepend(false))
|
||||
Publishers.CombineLatest(sourceURLsPublisher, showPreviewStatusPublisher.prepend(false))
|
||||
.receive(on: RunLoop.main)
|
||||
.map { $0.0 }
|
||||
.compactMap { [weak self] (sourceURL: URL?) -> AnyPublisher<SourcePreviewResult?, Never>? in
|
||||
guard let self else { return nil }
|
||||
|
||||
guard let sourceURL else {
|
||||
// Unlike above guard, this continues the pipeline with nil value.
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
.flatMap { [weak self] (sourceURLs: [URL]) -> AnyPublisher<[SourcePreviewResult?], Never> in
|
||||
guard let self else { return Just([]).eraseToAnyPublisher() }
|
||||
|
||||
self.viewModel.isLoadingPreview = true
|
||||
return self.fetchSourcePreview(sourceURL: sourceURL).eraseToAnyPublisher()
|
||||
|
||||
// Create publishers maintaining order
|
||||
let publishers = sourceURLs.enumerated().map { index, sourceURL in
|
||||
self.fetchSourcePreview(sourceURL: sourceURL)
|
||||
.map { result in
|
||||
// Add index to maintain order
|
||||
(index: index, result: result)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// since network requests are concurrent, we sort the values when they are received
|
||||
return publishers.isEmpty
|
||||
? Just([]).eraseToAnyPublisher()
|
||||
: Publishers.MergeMany(publishers)
|
||||
.collect() // await all publishers to emit the results
|
||||
.map { results in // perform sorting of the collected results
|
||||
// Sort by original index before returning
|
||||
results.sorted { $0.index < $1.index }
|
||||
.map { $0.result }
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest() // Cancels previous publisher
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] sourcePreviewResult in
|
||||
.sink { [weak self] sourcePreviewResults in
|
||||
self?.viewModel.isLoadingPreview = false
|
||||
self?.viewModel.sourcePreviewResult = sourcePreviewResult
|
||||
self?.viewModel.sourcePreviewResults = sourcePreviewResults.compactMap{$0}
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
|
||||
/* Update UI */
|
||||
|
||||
Publishers.CombineLatest(self.viewModel.$isLoadingPreview.removeDuplicates(),
|
||||
self.viewModel.$isShowingPreviewStatus.removeDuplicates())
|
||||
.sink { [weak self] _ in
|
||||
@@ -359,7 +388,7 @@ private extension AddSourceViewController
|
||||
|
||||
if let footerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath) as? PlaceholderCollectionReusableView
|
||||
{
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResults)
|
||||
}
|
||||
|
||||
let context = UICollectionViewLayoutInvalidationContext()
|
||||
@@ -370,27 +399,38 @@ private extension AddSourceViewController
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
self.viewModel.$sourcePreviewResult
|
||||
.map { $0?.1 }
|
||||
.map { result -> Managed<Source>? in
|
||||
switch result
|
||||
{
|
||||
case .success(let source): return source
|
||||
case .failure, nil: return nil
|
||||
self.viewModel.$sourcePreviewResults
|
||||
.map { sourcePreviewResults -> [Source] in
|
||||
// Maintain order based on original sourceURLs array
|
||||
let orderedSources = self.viewModel.sourceURLs.compactMap { sourceURL -> Source? in
|
||||
// Find the preview result matching this URL
|
||||
guard let previewResult = sourcePreviewResults.first(where: { $0.sourceURL == sourceURL }),
|
||||
case .success(let managedSource) = previewResult.result
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return managedSource.wrappedValue
|
||||
}
|
||||
}
|
||||
.removeDuplicates { (sourceA: Managed<Source>?, sourceB: Managed<Source>?) in
|
||||
sourceA?.identifier == sourceB?.identifier
|
||||
|
||||
return orderedSources
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] source in
|
||||
self?.updateSourcePreview(for: source?.wrappedValue)
|
||||
.sink { [weak self] sources in
|
||||
self?.updateSourcesPreview(for: sources)
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
|
||||
let addPublisher = NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification)
|
||||
let removePublisher = NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
||||
Publishers.Merge(addPublisher, removePublisher)
|
||||
let mergedNotificationPublisher = Publishers.Merge(
|
||||
NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification),
|
||||
NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.share() // Shares the upstream publisher with multiple subscribers
|
||||
|
||||
// Update recommended sources section when sources are added/removed
|
||||
mergedNotificationPublisher
|
||||
.compactMap { notification -> String? in
|
||||
guard let source = notification.object as? Source,
|
||||
let context = source.managedObjectContext
|
||||
@@ -399,7 +439,6 @@ private extension AddSourceViewController
|
||||
let sourceID = context.performAndWait { source.identifier }
|
||||
return sourceID
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.compactMap { [dataSource = recommendedSourcesDataSource] sourceID -> IndexPath? in
|
||||
guard let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID }) else { return nil }
|
||||
|
||||
@@ -411,6 +450,32 @@ private extension AddSourceViewController
|
||||
self?.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
// Update previews section when sources are added/removed
|
||||
// mergedNotificationPublisher
|
||||
// .sink { [weak self] _ in
|
||||
// // reload the entire of previews section to get latest state
|
||||
// self?.collectionView.reloadSections(IndexSet(integer: Section.preview.rawValue))
|
||||
// }
|
||||
// .store(in: &self.cancellables)
|
||||
|
||||
mergedNotificationPublisher
|
||||
.compactMap { notification -> String? in
|
||||
guard let source = notification.object as? Source,
|
||||
let context = source.managedObjectContext
|
||||
else { return nil }
|
||||
return context.performAndWait { source.identifier }
|
||||
}
|
||||
.compactMap { [weak self] sourceID -> IndexPath? in
|
||||
guard let dataSource = self?.sourcePreviewDataSource,
|
||||
let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID })
|
||||
else { return nil }
|
||||
return IndexPath(item: index, section: Section.preview.rawValue)
|
||||
}
|
||||
.sink { [weak self] indexPath in
|
||||
self?.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
||||
func sourceURL(from address: String) -> URL?
|
||||
@@ -458,35 +523,51 @@ private extension AddSourceViewController
|
||||
})
|
||||
}
|
||||
|
||||
func updateSourcePreview(for source: Source?)
|
||||
{
|
||||
let items = [source].compactMap { $0 }
|
||||
func updateSourcesPreview(for sources: [Source]) {
|
||||
// Calculate changes needed to go from current items to new items
|
||||
let currentItemCount = self.sourcePreviewDataSource.items.count
|
||||
let newItemCount = sources.count
|
||||
|
||||
// Have to provide changes in terms of sourcePreviewDataSource.
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
var changes: [RSTCellContentChange] = []
|
||||
|
||||
if !items.isEmpty && self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: indexPath)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
else if items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .delete, currentIndexPath: indexPath, destinationIndexPath: nil)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
else if !items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .update, currentIndexPath: indexPath, destinationIndexPath: indexPath)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
if currentItemCount == 0 && newItemCount > 0 {
|
||||
// Insert all items if we currently have none
|
||||
for i in 0..<newItemCount {
|
||||
let indexPath = IndexPath(row: i, section: 0)
|
||||
let change = RSTCellContentChange(type: .insert,
|
||||
currentIndexPath: nil,
|
||||
destinationIndexPath: indexPath)
|
||||
changes.append(change)
|
||||
}
|
||||
} else if currentItemCount > 0 && newItemCount == 0 {
|
||||
// Delete all items if we're going to have none
|
||||
for i in 0..<currentItemCount {
|
||||
let indexPath = IndexPath(row: i, section: 0)
|
||||
let change = RSTCellContentChange(type: .delete,
|
||||
currentIndexPath: indexPath,
|
||||
destinationIndexPath: nil)
|
||||
changes.append(change)
|
||||
}
|
||||
} else if currentItemCount != newItemCount {
|
||||
// If counts differ, do a section update
|
||||
let change = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||
changes = [change]
|
||||
} else {
|
||||
// Update existing items in place
|
||||
for i in 0..<newItemCount {
|
||||
let indexPath = IndexPath(row: i, section: 0)
|
||||
let change = RSTCellContentChange(type: .update,
|
||||
currentIndexPath: indexPath,
|
||||
destinationIndexPath: indexPath)
|
||||
changes.append(change)
|
||||
}
|
||||
}
|
||||
|
||||
if source == nil
|
||||
{
|
||||
self.sourcePreviewDataSource.setItems(sources, with: changes)
|
||||
|
||||
if sources.isEmpty {
|
||||
self.collectionView.reloadSections([Section.preview.rawValue])
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
@@ -510,9 +591,6 @@ private extension AddSourceViewController
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
let config = UIImage.SymbolConfiguration(scale: .medium)
|
||||
let image = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
cell.bannerView.button.setImage(image, for: .normal)
|
||||
cell.bannerView.button.setImage(image, for: .highlighted)
|
||||
cell.bannerView.button.setTitle(nil, for: .normal)
|
||||
cell.bannerView.button.imageView?.contentMode = .scaleAspectFit
|
||||
cell.bannerView.button.contentHorizontalAlignment = .fill // Fill entire button with imageView
|
||||
@@ -521,28 +599,54 @@ private extension AddSourceViewController
|
||||
cell.bannerView.button.tintColor = .clear
|
||||
cell.bannerView.button.isHidden = false
|
||||
|
||||
// mark the button with label (useful for accessibility and for UITests)
|
||||
cell.bannerView.button.accessibilityIdentifier = "add"
|
||||
|
||||
func setButtonIcon()
|
||||
{
|
||||
Task<Void, Never>(priority: .userInitiated) { [weak cell] in
|
||||
guard let cell else { return }
|
||||
|
||||
var isSourceAlreadyPersisted = false
|
||||
do
|
||||
{
|
||||
isSourceAlreadyPersisted = try await source.isAdded
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to determine if source is added.", error)
|
||||
}
|
||||
|
||||
// use the plus icon by default
|
||||
var buttonIcon = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
|
||||
// if the source is already added/staged for adding, use the checkmark icon
|
||||
let isStagedForAdd = self.stagedForAdd[source] == true
|
||||
if isStagedForAdd || isSourceAlreadyPersisted
|
||||
{
|
||||
buttonIcon = UIImage(systemName: "checkmark.circle.fill", withConfiguration: config)?
|
||||
.withTintColor(isSourceAlreadyPersisted ? .green : .white, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
cell.bannerView.button.setImage(buttonIcon, for: .normal)
|
||||
cell.bannerView.button.isEnabled = !isSourceAlreadyPersisted
|
||||
}
|
||||
}
|
||||
|
||||
// set the icon
|
||||
setButtonIcon()
|
||||
|
||||
let action = UIAction(identifier: .addSource) { [weak self] _ in
|
||||
self?.add(source)
|
||||
guard let self else { return }
|
||||
|
||||
self.stagedForAdd[source, default: false].toggle()
|
||||
|
||||
// update the button icon
|
||||
setButtonIcon()
|
||||
}
|
||||
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
||||
|
||||
Task<Void, Never>(priority: .userInitiated) {
|
||||
do
|
||||
{
|
||||
let isAdded = try await source.isAdded
|
||||
if isAdded
|
||||
{
|
||||
cell.bannerView.button.isHidden = true
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to determine if source is added.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResult: SourcePreviewResult?)
|
||||
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResults: [SourcePreviewResult?])
|
||||
{
|
||||
footerView.placeholderView.stackView.isLayoutMarginsRelativeArrangement = false
|
||||
|
||||
@@ -552,23 +656,33 @@ private extension AddSourceViewController
|
||||
|
||||
footerView.placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
switch sourcePreviewResult
|
||||
var errorText: String? = nil
|
||||
var isError: Bool = false
|
||||
for result in sourcePreviewResults
|
||||
{
|
||||
case (let sourceURL, .failure(let previewError))? where self.viewModel.sourceURL == sourceURL && !self.viewModel.isLoadingPreview:
|
||||
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
||||
|
||||
footerView.placeholderView.textLabel.text = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
||||
footerView.placeholderView.textLabel.isHidden = false
|
||||
|
||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||
|
||||
default:
|
||||
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
||||
|
||||
footerView.placeholderView.textLabel.text = nil
|
||||
footerView.placeholderView.textLabel.isHidden = true
|
||||
|
||||
switch result
|
||||
{
|
||||
case (let sourceURL, .failure(let previewError))? where (self.viewModel.sourceURLs.contains(sourceURL) && !self.viewModel.isLoadingPreview):
|
||||
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
||||
|
||||
errorText = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
||||
footerView.placeholderView.textLabel.text = errorText
|
||||
footerView.placeholderView.textLabel.isHidden = false
|
||||
|
||||
isError = true
|
||||
|
||||
default:
|
||||
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
||||
errorText = nil
|
||||
footerView.placeholderView.textLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
footerView.placeholderView.textLabel.text = errorText
|
||||
|
||||
if !isError{
|
||||
footerView.placeholderView.activityIndicatorView.startAnimating()
|
||||
} else{
|
||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,30 +766,60 @@ private extension AddSourceViewController
|
||||
}
|
||||
}
|
||||
|
||||
func add(@AsyncManaged _ source: Source)
|
||||
@IBAction func commitChanges(_ sender: UIBarButtonItem)
|
||||
{
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
let isRecommended = await $source.isRecommended
|
||||
if isRecommended
|
||||
{
|
||||
try await AppManager.shared.add(source, message: nil, presentingViewController: self)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use default message
|
||||
try await AppManager.shared.add(source, presentingViewController: self)
|
||||
}
|
||||
|
||||
self.dismiss()
|
||||
|
||||
struct StagedSource: Hashable {
|
||||
@AsyncManaged var source: Source
|
||||
|
||||
// Conformance for Equatable/Hashable by comparing the underlying source
|
||||
static func == (lhs: StagedSource, rhs: StagedSource) -> Bool {
|
||||
return lhs.source.identifier == rhs.source.identifier
|
||||
}
|
||||
catch is CancellationError {}
|
||||
catch
|
||||
{
|
||||
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(source)
|
||||
}
|
||||
}
|
||||
|
||||
Task<Void, Never> {
|
||||
var isCancelled = false
|
||||
// OK: COMMIT the staged changes now
|
||||
// Convert the stagedForAdd dictionary into an array of StagedSource
|
||||
let stagedSources: [StagedSource] = self.stagedForAdd.filter { $0.value }
|
||||
.map { StagedSource(source: $0.key) }
|
||||
|
||||
for staged in stagedSources {
|
||||
do
|
||||
{
|
||||
// Use the projected value to safely access isRecommended asynchronously
|
||||
let isRecommended = await staged.$source.isRecommended
|
||||
if isRecommended
|
||||
{
|
||||
try await AppManager.shared.add(staged.source, message: nil, presentingViewController: self)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use default message
|
||||
try await AppManager.shared.add(staged.source, presentingViewController: self)
|
||||
}
|
||||
|
||||
// remove this kv pair
|
||||
_ = self.stagedForAdd.removeValue(forKey: staged.source)
|
||||
}
|
||||
catch is CancellationError {
|
||||
isCancelled = true
|
||||
break
|
||||
}
|
||||
catch
|
||||
{
|
||||
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
if !isCancelled {
|
||||
// finally dismiss the sheet/viewcontroller
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -737,7 +881,7 @@ extension AddSourceViewController: UICollectionViewDelegateFlowLayout
|
||||
case (.preview, UICollectionView.elementKindSectionFooter):
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseID.placeholderFooter.rawValue, for: indexPath) as! PlaceholderCollectionReusableView
|
||||
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResults)
|
||||
|
||||
return footerView
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class AddSourceTextFieldCell: UICollectionViewCell
|
||||
self.textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textField.placeholder = "apps.sidestore.io"
|
||||
self.textField.textContentType = .URL
|
||||
self.textField.keyboardType = .URL
|
||||
// self.textField.keyboardType = .URL // we can add multiple sources now delimited by spaces/newline so we use normal keyboard not url keyboard
|
||||
self.textField.returnKeyType = .done
|
||||
self.textField.autocapitalizationType = .none
|
||||
self.textField.autocorrectionType = .no
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
||||
@@ -224,6 +224,11 @@
|
||||
<segue destination="qr8-ss-Ghz" kind="unwind" unwindAction="unwindFromAddSource:" id="Pba-Kh-qfH"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="done" id="oza-rj-JhC">
|
||||
<connections>
|
||||
<action selector="commitChanges:" destination="bbz-wy-kaK" id="4FB-Sj-E14"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="LO9-iP-zZC"/>
|
||||
|
||||
@@ -32,6 +32,7 @@ public extension UserDefaults
|
||||
@NSManaged var isBackgroundRefreshEnabled: Bool
|
||||
@NSManaged var isIdleTimeoutDisableEnabled: Bool
|
||||
@NSManaged var isAppLimitDisabled: Bool
|
||||
@NSManaged var isBetaUpdatesEnabled: Bool
|
||||
@NSManaged var isExportResignedAppEnabled: Bool
|
||||
@NSManaged var isVerboseOperationsLoggingEnabled: Bool
|
||||
@NSManaged var isMinimuxerConsoleLoggingEnabled: Bool
|
||||
@@ -55,6 +56,8 @@ public extension UserDefaults
|
||||
@NSManaged var trustedServerURL: String?
|
||||
@NSManaged var skipPatreonDownloads: Bool
|
||||
|
||||
@NSManaged var betaUdpatesTrack: String?
|
||||
|
||||
@nonobjc var preferredAppSorting: AppSorting {
|
||||
get {
|
||||
let sorting = _preferredAppSorting.flatMap { AppSorting(rawValue: $0) } ?? .default
|
||||
@@ -89,7 +92,10 @@ public extension UserDefaults
|
||||
|
||||
@NSManaged var permissionCheckingDisabled: Bool
|
||||
@NSManaged var responseCachingDisabled: Bool
|
||||
|
||||
|
||||
// Default track for beta updates when beta-updates are enabled
|
||||
static let defaultBetaUpdatesTrack: String = ReleaseTracks.nightly.rawValue
|
||||
|
||||
class func registerDefaults()
|
||||
{
|
||||
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
|
||||
@@ -121,6 +127,7 @@ public extension UserDefaults
|
||||
|
||||
let defaults = [
|
||||
#keyPath(UserDefaults.isAppLimitDisabled): false,
|
||||
#keyPath(UserDefaults.isBetaUpdatesEnabled): false,
|
||||
#keyPath(UserDefaults.isExportResignedAppEnabled): false,
|
||||
#keyPath(UserDefaults.isDebugModeEnabled): false,
|
||||
#keyPath(UserDefaults.isVerboseOperationsLoggingEnabled): false,
|
||||
@@ -138,6 +145,7 @@ public extension UserDefaults
|
||||
#keyPath(UserDefaults.isCowExploitSupported): isMacDirtyCowSupported,
|
||||
#keyPath(UserDefaults.permissionCheckingDisabled): permissionCheckingDisabled,
|
||||
#keyPath(UserDefaults._preferredAppSorting): preferredAppSorting.rawValue,
|
||||
#keyPath(UserDefaults.betaUdpatesTrack): defaultBetaUpdatesTrack,
|
||||
] as [String: Any]
|
||||
|
||||
UserDefaults.standard.register(defaults: defaults)
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="version"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hasUpdate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="appName" attributeType="String"/>
|
||||
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="operation" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,234 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="appBundleID" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="permission" attributeType="String"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="permission"/>
|
||||
<constraint value="type"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="sha256" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="version"/>
|
||||
<constraint value="buildVersion"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hasUpdate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="storeBuildVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="appName" attributeType="String"/>
|
||||
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="operation" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,252 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="appBundleID" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="permission" attributeType="String"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="permission"/>
|
||||
<constraint value="type"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="deviceType" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<attribute name="imageURL" attributeType="URI"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="width" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="screenshots" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="imageURL"/>
|
||||
<constraint value="deviceType"/>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="sha256" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="version"/>
|
||||
<constraint value="buildVersion"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hasUpdate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="storeBuildVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="appName" attributeType="String"/>
|
||||
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="operation" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="screenshots" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppScreenshot" inverseName="app" inverseEntity="AppScreenshot"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,296 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="appBundleID" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="permission" attributeType="String"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="permission"/>
|
||||
<constraint value="type"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="deviceType" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<attribute name="imageURL" attributeType="URI"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="width" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="screenshots" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="imageURL"/>
|
||||
<constraint value="deviceType"/>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="sha256" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="version"/>
|
||||
<constraint value="buildVersion"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hasUpdate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="storeBuildVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="appName" attributeType="String"/>
|
||||
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="operation" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledges" toMany="YES" deletionRule="Cascade" destinationEntity="Pledge" inverseName="account" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Pledge" representedClassName="Pledge" syncable="YES">
|
||||
<attribute name="amount" attributeType="Decimal" defaultValueString="0"/>
|
||||
<attribute name="campaignURL" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PatreonAccount" inverseName="pledges" inverseEntity="PatreonAccount"/>
|
||||
<relationship name="rewards" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeReward" inverseName="pledge" inverseEntity="PledgeReward"/>
|
||||
<relationship name="tiers" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeTier" inverseName="pledge" inverseEntity="PledgeTier"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PledgeReward" representedClassName="PledgeReward" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="rewards" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PledgeTier" representedClassName="PledgeTier" syncable="YES">
|
||||
<attribute name="amount" attributeType="Decimal" defaultValueString="0.0"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="tiers" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="patreonURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="category" optional="YES" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isPledged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isPledgeRequired" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="pledgeAmount" optional="YES" attributeType="Decimal"/>
|
||||
<attribute name="pledgeCurrency" optional="YES" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="screenshots" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppScreenshot" inverseName="app" inverseEntity="AppScreenshot"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,296 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="appBundleID" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="permission" attributeType="String"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="permission"/>
|
||||
<constraint value="type"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="deviceType" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<attribute name="imageURL" attributeType="URI"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="width" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="screenshots" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="imageURL"/>
|
||||
<constraint value="deviceType"/>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="sha256" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="version"/>
|
||||
<constraint value="buildVersion"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hasUpdate" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="storeBuildVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="appName" attributeType="String"/>
|
||||
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="operation" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledges" toMany="YES" deletionRule="Cascade" destinationEntity="Pledge" inverseName="account" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Pledge" representedClassName="Pledge" syncable="YES">
|
||||
<attribute name="amount" attributeType="Decimal" defaultValueString="0"/>
|
||||
<attribute name="campaignURL" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PatreonAccount" inverseName="pledges" inverseEntity="PatreonAccount"/>
|
||||
<relationship name="rewards" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeReward" inverseName="pledge" inverseEntity="PledgeReward"/>
|
||||
<relationship name="tiers" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeTier" inverseName="pledge" inverseEntity="PledgeTier"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PledgeReward" representedClassName="PledgeReward" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="rewards" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PledgeTier" representedClassName="PledgeTier" syncable="YES">
|
||||
<attribute name="amount" attributeType="Decimal" defaultValueString="0.0"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="tiers" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="patreonURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="category" optional="YES" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isPledged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isPledgeRequired" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="pledgeAmount" optional="YES" attributeType="Decimal"/>
|
||||
<attribute name="pledgeCurrency" optional="YES" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="screenshots" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppScreenshot" inverseName="app" inverseEntity="AppScreenshot"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D70" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="v17">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
@@ -62,6 +62,7 @@
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="buildVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
@@ -73,6 +74,7 @@
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<relationship name="releaseTrack" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ReleaseTrack" inverseName="releases" inverseEntity="ReleaseTrack"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
@@ -216,6 +218,16 @@
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ReleaseTrack" representedClassName="ReleaseTrack" syncable="YES">
|
||||
<attribute name="track" optional="YES" attributeType="String"/>
|
||||
<relationship name="releases" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="releaseTrack" inverseEntity="AppVersion"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="releaseTracks" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="track"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||
@@ -247,7 +259,6 @@
|
||||
<attribute name="downloadURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isPledged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isPledgeRequired" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
@@ -272,6 +283,7 @@
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="releaseTracks" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="ReleaseTrack" inverseName="storeApp" inverseEntity="ReleaseTrack"/>
|
||||
<relationship name="screenshots" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppScreenshot" inverseName="app" inverseEntity="AppScreenshot"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
|
||||
@@ -27,7 +27,8 @@ public class AppVersion: BaseEntity, Decodable
|
||||
@NSManaged public private(set) var downloadURL: URL
|
||||
@NSManaged public private(set) var size: Int64
|
||||
@NSManaged public private(set) var sha256: String?
|
||||
|
||||
@NSManaged @objc(channel) public private(set) var _channel: String?
|
||||
|
||||
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = self._minOSVersion else { return nil }
|
||||
|
||||
@@ -45,11 +46,21 @@ public class AppVersion: BaseEntity, Decodable
|
||||
@NSManaged @objc(maxOSVersion) private var _maxOSVersion: String?
|
||||
|
||||
@NSManaged public var appBundleID: String
|
||||
@NSManaged public var sourceID: String?
|
||||
@NSManaged public var sourceID: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var app: StoreApp?
|
||||
@NSManaged @objc(app) public private(set) var _app: StoreApp?
|
||||
@NSManaged @objc(latestVersionApp) public internal(set) var latestSupportedVersionApp: StoreApp?
|
||||
@NSManaged public var releaseTrack: ReleaseTrack?
|
||||
|
||||
// public accessors
|
||||
public var app: StoreApp? {
|
||||
return releaseTrack?.storeApp ?? _app
|
||||
}
|
||||
|
||||
public var channel: ReleaseTracks {
|
||||
ReleaseTracks(stringValue: releaseTrack?.track ?? _channel ?? "") ?? .unknown
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
@@ -136,6 +147,7 @@ public extension AppVersion
|
||||
class func makeAppVersion(
|
||||
version: String,
|
||||
buildVersion: String?,
|
||||
channel: String? = nil,
|
||||
date: Date,
|
||||
localizedDescription: String? = nil,
|
||||
downloadURL: URL,
|
||||
@@ -148,6 +160,7 @@ public extension AppVersion
|
||||
let appVersion = AppVersion(context: context)
|
||||
appVersion.version = version
|
||||
appVersion.buildVersion = buildVersion
|
||||
appVersion._channel = channel
|
||||
appVersion.date = date
|
||||
appVersion.localizedDescription = localizedDescription
|
||||
appVersion.downloadURL = downloadURL
|
||||
@@ -159,6 +172,49 @@ public extension AppVersion
|
||||
return appVersion
|
||||
}
|
||||
|
||||
// update with new values
|
||||
func mutateForData(
|
||||
version: String? = nil,
|
||||
channel: String? = nil,
|
||||
buildVersion: String? = nil,
|
||||
date: Date? = nil,
|
||||
localizedDescription: String? = nil,
|
||||
downloadURL: URL? = nil,
|
||||
size: Int64? = nil,
|
||||
sha256: String? = nil,
|
||||
appBundleID: String? = nil,
|
||||
sourceID: String? = nil) -> AppVersion
|
||||
{
|
||||
// use overriding incoming params if present else retain existing
|
||||
|
||||
// non-optionals
|
||||
if let version {
|
||||
self.version = version
|
||||
}
|
||||
if let date {
|
||||
self.date = date
|
||||
}
|
||||
if let downloadURL{
|
||||
self.downloadURL = downloadURL
|
||||
}
|
||||
if let size{
|
||||
self.size = size
|
||||
}
|
||||
if let appBundleID{
|
||||
self.appBundleID = appBundleID
|
||||
}
|
||||
|
||||
// optionals
|
||||
self.localizedDescription = localizedDescription ?? self.localizedDescription
|
||||
self._channel = channel ?? self._channel
|
||||
self.buildVersion = buildVersion ?? self.buildVersion
|
||||
self.sha256 = sha256 ?? self.sha256
|
||||
self.sourceID = sourceID ?? self.sourceID
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
var isSupported: Bool {
|
||||
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion)
|
||||
{
|
||||
|
||||
@@ -122,7 +122,7 @@ public class InstalledApp: BaseEntity, InstalledAppProtocol
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Check pledge requirements
|
||||
guard !storeApp.isPledgeRequired || storeApp.isPledged else
|
||||
{
|
||||
@@ -137,26 +137,26 @@ public class InstalledApp: BaseEntity, InstalledAppProtocol
|
||||
if currentSemVer == nil || latestSemVer == nil {
|
||||
return !matches(latestVersion)
|
||||
}
|
||||
// let currentVer = SemanticVersion("\(currentSemVer!.major).\(currentSemVer!.minor).\(currentSemVer!.patch)")
|
||||
// let latestVer = SemanticVersion("\(latestSemVer!.major).\(latestSemVer!.minor).\(latestSemVer!.patch)")
|
||||
let currentVer = SemanticVersion("\(currentSemVer!.major).\(currentSemVer!.minor).\(currentSemVer!.patch)")
|
||||
let latestVer = SemanticVersion("\(latestSemVer!.major).\(latestSemVer!.minor).\(latestSemVer!.patch)")
|
||||
|
||||
// // Compare by major.minor.patch
|
||||
// if latestVer! > latestVer! {
|
||||
// return true
|
||||
// }
|
||||
// Compare by major.minor.patch
|
||||
if latestVer! > currentVer! {
|
||||
return true
|
||||
}
|
||||
|
||||
// // Check beta updates if enabled
|
||||
// if UserDefaults.standard.isBetaUpdatesEnabled,
|
||||
// ReleaseTracks.betaTracks.contains(latestVersion.channel),
|
||||
// latestVer == currentVer, // major.minor.patch are matching
|
||||
// // now compare by preRelease and build to break the tie
|
||||
// // TODO: since multiple tracks can be independent, when a different version is available on selected track than installed
|
||||
// // we accept it, now ex: if the setup is consistent for upstream merge lets say from alpha to nightly and alpha can never fall behind nightly,
|
||||
// // then the preRelease+build combo will always be incremental and our below not-equals check will still work.
|
||||
// (latestSemVer!.build != currentSemVer!.build) || (latestSemVer!.preRelease != currentSemVer!.preRelease)
|
||||
// {
|
||||
// return true
|
||||
// }
|
||||
// Check beta updates if enabled
|
||||
if UserDefaults.standard.isBetaUpdatesEnabled,
|
||||
ReleaseTracks.betaTracks.contains(latestVersion.channel),
|
||||
latestVer == currentVer, // major.minor.patch are matching
|
||||
// now compare by preRelease and build to break the tie
|
||||
// TODO: since multiple tracks can be independent, when a different version is available on selected track than installed
|
||||
// we accept it, now ex: if the setup is consistent for upstream merge lets say from alpha to nightly and alpha can never fall behind nightly,
|
||||
// then the preRelease+build combo will always be incremental and our below not-equals check will still work.
|
||||
(latestSemVer!.build != currentSemVer!.build) || (latestSemVer!.preRelease != currentSemVer!.preRelease)
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// else include everything as-is when doing lexicographic comparison
|
||||
// NOTE: stable x.y.z is always > x.y.z-abcd+1234
|
||||
@@ -268,8 +268,8 @@ public extension InstalledApp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func matches(_ appVersion: AppVersion) -> Bool
|
||||
|
||||
func matches(_ appVersion: AppVersion) -> Bool
|
||||
{
|
||||
let matchesAppVersion = (self.version == appVersion.version && self.storeBuildVersion == appVersion.buildVersion)
|
||||
return matchesAppVersion
|
||||
@@ -283,12 +283,12 @@ public extension InstalledApp
|
||||
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
|
||||
}
|
||||
|
||||
class func supportedUpdatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||
class func supportedUpdatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.hasUpdate))
|
||||
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -77,17 +77,24 @@ fileprivate extension NSManagedObject
|
||||
}
|
||||
}
|
||||
|
||||
@objc(Source13To14MigrationPolicy)
|
||||
class Source13To14MigrationPolicy: NSEntityMigrationPolicy
|
||||
@objc(Source11To17MigrationPolicy)
|
||||
class Source11To17MigrationPolicy: NSEntityMigrationPolicy
|
||||
{
|
||||
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
|
||||
{
|
||||
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
|
||||
|
||||
guard let sourceURL = dInstance.sourceSourceURL else { return }
|
||||
guard var sourceURL = dInstance.sourceSourceURL else { return }
|
||||
|
||||
// Copied from Source.setSourceURL()
|
||||
|
||||
// sidestore official soruce has been moved to sidestore.io/apps-v2.json
|
||||
// if we don't switch, users will end up with 2 offical sources
|
||||
if sourceURL.absoluteString.contains("apps.sidestore.io") // if using old source
|
||||
{
|
||||
sourceURL = Source.altStoreSourceURL // switch to latest
|
||||
}
|
||||
|
||||
let sourceID = try Source.sourceID(from: sourceURL)
|
||||
dInstance.setSourceSourceID(sourceID)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// StoreAppMigration11To17Policy.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 25/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(StoreApp11To17MigrationPolicy)
|
||||
class StoreApp11To17MigrationPolicy: NSEntityMigrationPolicy {
|
||||
|
||||
let defaultChannel = "stable"
|
||||
|
||||
|
||||
// override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
|
||||
// // First, let the default implementation create the basic destination StoreApp
|
||||
// try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
|
||||
//
|
||||
// // Get the destination StoreApp instance that was created
|
||||
// guard let destinationStoreApp = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first else {
|
||||
// print("Failed to locate destination StoreApp instance")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Get the source versions array
|
||||
// guard let sourceVersions = sInstance.value(forKey: #keyPath(StoreApp._versions)) as? NSOrderedSet else {
|
||||
// print("Source has no versions")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Create a new ReleaseTrack entity
|
||||
// let context = destinationStoreApp.managedObjectContext!
|
||||
// let releaseTrack = NSEntityDescription.insertNewObject(forEntityName: ReleaseTrack.entity().name!, into: context)
|
||||
// releaseTrack.setValue(defaultChannel, forKey: #keyPath(ReleaseTrack._track))
|
||||
//
|
||||
// // Connect the releaseTrack to the destination StoreApp
|
||||
// releaseTrack.setValue(destinationStoreApp, forKey: #keyPath(ReleaseTrack.storeApp))
|
||||
//
|
||||
// // Add it to the releaseTracks of the destination StoreApp
|
||||
// let releaseTracks = NSMutableOrderedSet()
|
||||
// releaseTracks.add(releaseTrack)
|
||||
// destinationStoreApp.setValue(releaseTracks, forKey: #keyPath(StoreApp._releaseTracks))
|
||||
//
|
||||
// // Find the entity mapping for AppVersion
|
||||
// let appVersionMappingName = findEntityMappingName(for: AppVersion.entity().name!, in: manager)
|
||||
//
|
||||
// // Now for each source version, find its corresponding migrated version and add to the releaseTrack
|
||||
// let versions = NSMutableOrderedSet()
|
||||
// for sourceVersion in sourceVersions.array {
|
||||
// guard let sourceVersion = sourceVersion as? NSManagedObject else { continue }
|
||||
//
|
||||
// let destinationVersions = manager.destinationInstances(forEntityMappingName: appVersionMappingName, sourceInstances: [sourceVersion])
|
||||
// if let destinationVersion = destinationVersions.first {
|
||||
//
|
||||
// // update channel info
|
||||
// if let appVersion = destinationVersion as? AppVersion {
|
||||
// _ = appVersion.mutateForData(channel: defaultChannel)
|
||||
// }
|
||||
//
|
||||
// versions.add(destinationVersion)
|
||||
//
|
||||
// // Connect in the other direction too
|
||||
// destinationVersion.setValue(releaseTrack, forKey: #keyPath(AppVersion.releaseTrack))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Set the releases relationship on the releaseTrack
|
||||
// releaseTrack.setValue(versions, forKey: #keyPath(ReleaseTrack._releases))
|
||||
// }
|
||||
|
||||
|
||||
// Helper function to find the entity mapping name for a given entity
|
||||
private func findEntityMappingName(for entityName: String, in manager: NSMigrationManager) -> String {
|
||||
let mappingModel = manager.mappingModel
|
||||
|
||||
for entityMapping in mappingModel.entityMappings {
|
||||
if entityMapping.sourceEntityName == entityName {
|
||||
return entityMapping.name
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, return a default (you might want to handle this differently)
|
||||
print("Warning: Could not find mapping for entity: \(entityName)")
|
||||
return "\(entityName)To\(entityName)"
|
||||
}
|
||||
|
||||
override func createRelationships(
|
||||
forDestination dInstance: NSManagedObject,
|
||||
in mapping: NSEntityMapping,
|
||||
manager: NSMigrationManager
|
||||
) throws {
|
||||
// Retrieve the corresponding source instance for the destination StoreApp
|
||||
let sourceInstances = manager.sourceInstances(forEntityMappingName: mapping.name, destinationInstances: [dInstance])
|
||||
guard let sInstance = sourceInstances.first else {
|
||||
print("No source instance found for destination: \(dInstance)")
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the source versions from the source StoreApp
|
||||
guard let sourceVersions = sInstance.value(forKey: #keyPath(StoreApp._versions)) as? NSOrderedSet else {
|
||||
print("Source store app has no versions")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new ReleaseTrack entity
|
||||
let context = dInstance.managedObjectContext!
|
||||
let releaseTrack = NSEntityDescription.insertNewObject(forEntityName: ReleaseTrack.entity().name!, into: context)
|
||||
releaseTrack.setValue(defaultChannel, forKey: #keyPath(ReleaseTrack._track))
|
||||
|
||||
// Connect the releaseTrack to the destination StoreApp
|
||||
releaseTrack.setValue(dInstance, forKey: #keyPath(ReleaseTrack.storeApp))
|
||||
|
||||
|
||||
// Find the mapping name for AppVersion (make sure this exactly matches your mapping model)
|
||||
let appVersionMappingName = findEntityMappingName(for: AppVersion.entity().name!, in: manager)
|
||||
|
||||
// Create a mutable ordered set for the destination AppVersion objects
|
||||
let destinationVersionsSet = NSMutableOrderedSet()
|
||||
for sourceVersion in sourceVersions.array {
|
||||
guard let sourceVersion = sourceVersion as? NSManagedObject else { continue }
|
||||
|
||||
// Retrieve the corresponding destination AppVersion instance
|
||||
let destVersions = manager.destinationInstances(forEntityMappingName: appVersionMappingName, sourceInstances: [sourceVersion])
|
||||
if let destVersion = destVersions.first {
|
||||
// update channel info
|
||||
destinationVersionsSet.add(destVersion)
|
||||
|
||||
// Optionally update properties or establish the inverse relationship
|
||||
destVersion.setValue(releaseTrack, forKey: #keyPath(AppVersion.releaseTrack))
|
||||
destVersion.setValue(defaultChannel, forKey: #keyPath(AppVersion._channel))
|
||||
destVersion.setValue("", forKey: #keyPath(AppVersion._buildVersion))
|
||||
} else {
|
||||
print("Destination AppVersion not found for source version: \(sourceVersion)")
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, link the destination AppVersion objects to the ReleaseTrack's relationship
|
||||
releaseTrack.setValue(destinationVersionsSet, forKey: #keyPath(ReleaseTrack._releases))
|
||||
|
||||
// clear the versions field
|
||||
// dInstance.setValue(NSOrderedSet(), forKey: #keyPath(StoreApp._versions))
|
||||
dInstance.setValue(nil, forKey: #keyPath(StoreApp._versions))
|
||||
}
|
||||
|
||||
}
|
||||
114
AltStoreCore/Model/ReleaseTrack.swift
Normal file
114
AltStoreCore/Model/ReleaseTrack.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// AppreleaseTrack.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 19/01/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
// created for 0.6.0
|
||||
@objc(ReleaseTrack)
|
||||
public class ReleaseTrack: BaseEntity, Decodable
|
||||
{
|
||||
// attributes
|
||||
@NSManaged @objc(track) public private(set) var _track: String?
|
||||
|
||||
// RelationShips
|
||||
@NSManaged @objc(releases) public private(set) var _releases: NSOrderedSet?
|
||||
@NSManaged public private(set) var storeApp: StoreApp?
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case track
|
||||
case releases
|
||||
}
|
||||
|
||||
public var track: String? {
|
||||
return _track?.isEmpty == false ? _track : nil
|
||||
}
|
||||
|
||||
public var releases:[AppVersion]? {
|
||||
return _releases?.array as? [AppVersion]
|
||||
}
|
||||
|
||||
// Required initializer for Core Data (context saves)
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws{
|
||||
guard let context = decoder.managedObjectContext else {
|
||||
preconditionFailure("Decoder must have non-nil NSManagedObjectContext.")
|
||||
}
|
||||
|
||||
// Must initialize with context in order for child context saves to work correctly.
|
||||
super.init(entity: ReleaseTrack.entity(), insertInto: context)
|
||||
|
||||
do
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self._track = try container.decode(String.self, forKey: .track)
|
||||
|
||||
let releases = try container.decode([AppVersion].self, forKey: .releases)
|
||||
guard releases.count > 0 else
|
||||
{
|
||||
throw DecodingError.dataCorruptedError(forKey: .releases, in: container, debugDescription: "At least one version is required in key: releases")
|
||||
}
|
||||
self._releases = NSOrderedSet(array: releases)
|
||||
}
|
||||
catch
|
||||
{
|
||||
if let context = self.managedObjectContext
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ReleaseTrack{
|
||||
|
||||
/// Warning:
|
||||
/// - Special handling required for deleted objects:
|
||||
/// - CoreData sets all properties to nil during deletion
|
||||
/// - This triggers KVO and could cause "mutating removed object" errors
|
||||
/// - We guard against this by checking deletion state before updates
|
||||
///
|
||||
internal func updateVersions(for storeApp: StoreApp?) {
|
||||
guard let storeApp = storeApp else { return }
|
||||
|
||||
releases?.forEach { version in
|
||||
// never mutate objects that are being deleted or is already deleted
|
||||
guard let context = version.managedObjectContext,
|
||||
!version.isDeleted, !context.deletedObjects.contains(version) else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
// update it into the appVersion
|
||||
_ = version.mutateForData(channel: track, appBundleID: storeApp.bundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defer updates to fields that require storeApp inverse relationship to be set, which is not available in init(),
|
||||
/// by observing changes to the prop and update the data later
|
||||
///
|
||||
/// NOTE: We use KVO here only coz, ReleaseTrack already has an inverse relationship to StoreAppV2
|
||||
/// So coredata will actually set the storeApp but only issue is that it happens after init() is complete
|
||||
/// hence we are using KVO so that one doesn't need to manually set the value via a setter method
|
||||
///
|
||||
/// However this caused an issue when an object is marked deleted during merge policy conflict resolution, all its props are set to nil by coredata.
|
||||
/// this causes this KVO observer to be triggered and mutating the deleted entity causing a "coredata error: Mutating removed object"
|
||||
/// which is now handled by checking if context.deletedObjects doesn't contain it and version.isDeleted is not true yet
|
||||
///
|
||||
override func didChangeValue(forKey key: String) {
|
||||
super.didChangeValue(forKey: key)
|
||||
if key == NSExpression(forKeyPath: #keyPath(ReleaseTrack.storeApp)).keyPath
|
||||
{
|
||||
updateVersions(for: storeApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,32 @@ import CoreData
|
||||
import Roxas
|
||||
import AltSign
|
||||
|
||||
import SemanticVersion
|
||||
|
||||
public enum ReleaseTracks: String, CodingKey, CaseIterable
|
||||
{
|
||||
case unknown
|
||||
case local
|
||||
|
||||
case alpha
|
||||
case nightly = "nightly"
|
||||
case stable
|
||||
|
||||
|
||||
public static var betaTracks: [ReleaseTracks] {
|
||||
ReleaseTracks.allCases.filter(isBetaTrack)
|
||||
}
|
||||
|
||||
public static var nonBetaTracks: [ReleaseTracks] {
|
||||
ReleaseTracks.allCases.filter { !isBetaTrack($0) }
|
||||
}
|
||||
|
||||
private static func isBetaTrack(_ key: ReleaseTracks) -> Bool {
|
||||
key == .alpha || key == .nightly
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public extension StoreApp
|
||||
{
|
||||
#if ALPHA
|
||||
@@ -21,7 +47,7 @@ public extension StoreApp
|
||||
#else
|
||||
static let altstoreAppID = Bundle.Info.appbundleIdentifier
|
||||
#endif
|
||||
|
||||
|
||||
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
||||
}
|
||||
|
||||
@@ -37,22 +63,22 @@ public final class PlatformURL: NSManagedObject, Decodable {
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var platform: Platform
|
||||
@NSManaged public private(set) var downloadURL: URL
|
||||
|
||||
|
||||
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case platform
|
||||
case downloadURL
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
|
||||
// Must initialize with context in order for child context saves to work correctly.
|
||||
super.init(entity: PlatformURL.entity(), insertInto: context)
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
@@ -65,7 +91,7 @@ public final class PlatformURL: NSManagedObject, Decodable {
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -75,15 +101,15 @@ extension PlatformURL: Comparable {
|
||||
public static func < (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
return lhs.platform.rawValue < rhs.platform.rawValue
|
||||
}
|
||||
|
||||
|
||||
public static func > (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
return lhs.platform.rawValue > rhs.platform.rawValue
|
||||
}
|
||||
|
||||
|
||||
public static func <= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
return lhs.platform.rawValue <= rhs.platform.rawValue
|
||||
}
|
||||
|
||||
|
||||
public static func >= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
||||
return lhs.platform.rawValue >= rhs.platform.rawValue
|
||||
}
|
||||
@@ -97,11 +123,11 @@ private struct PatreonParameters: Decodable
|
||||
{
|
||||
var amount: Decimal
|
||||
var isCustom: Bool
|
||||
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
|
||||
if let stringValue = try? container.decode(String.self), stringValue == "custom"
|
||||
{
|
||||
self.amount = 0 // Use 0 as amount internally to simplify logic.
|
||||
@@ -115,7 +141,7 @@ private struct PatreonParameters: Decodable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var pledge: Pledge?
|
||||
var currency: String?
|
||||
var tiers: Set<String>?
|
||||
@@ -124,6 +150,51 @@ private struct PatreonParameters: Decodable
|
||||
}
|
||||
|
||||
|
||||
extension StoreApp {
|
||||
|
||||
//MARK: - relationships
|
||||
@NSManaged @objc(releaseTracks) public private(set) var _releaseTracks: NSOrderedSet?
|
||||
|
||||
private var releaseTracks: [ReleaseTrack]?{
|
||||
return _releaseTracks?.array as? [ReleaseTrack]
|
||||
}
|
||||
|
||||
private func releaseTrackFor(track: String) -> ReleaseTrack? {
|
||||
return releaseTracks?.first(where: { $0.track == track })
|
||||
}
|
||||
|
||||
private var stableTrack: ReleaseTrack? {
|
||||
releaseTrackFor(track: ReleaseTracks.stable.stringValue)
|
||||
}
|
||||
|
||||
private var betaReleases: [AppVersion]? {
|
||||
// If beta track is selected, use it instead
|
||||
if UserDefaults.standard.isBetaUpdatesEnabled,
|
||||
let betaTrack = UserDefaults.standard.betaUdpatesTrack {
|
||||
|
||||
// Filter and flatten beta and stable releases
|
||||
let betaReleases = releaseTrackFor(track: betaTrack)?.releases?.compactMap { $0 }
|
||||
|
||||
// Ensure both beta and stable releases are found and supported
|
||||
if let latestBeta = betaReleases?.first(where: { $0.isSupported }),
|
||||
let latestStable = stableTrack?.releases?.first(where: { $0.isSupported }),
|
||||
let stableSemVer = SemanticVersion(latestStable.version),
|
||||
let betaSemVer = SemanticVersion(latestBeta.version),
|
||||
betaSemVer >= stableSemVer
|
||||
{
|
||||
return betaReleases
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getReleases(default releases: ReleaseTrack?) -> [AppVersion]?
|
||||
{
|
||||
return betaReleases ?? releases?.releases?.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc(StoreApp)
|
||||
public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
@@ -131,27 +202,26 @@ public class StoreApp: BaseEntity, Decodable
|
||||
@NSManaged public private(set) var name: String
|
||||
@NSManaged public private(set) var bundleIdentifier: String
|
||||
@NSManaged public private(set) var subtitle: String?
|
||||
|
||||
|
||||
@NSManaged public private(set) var developerName: String
|
||||
@NSManaged public private(set) var localizedDescription: String
|
||||
@NSManaged @objc(size) internal var _size: Int32
|
||||
|
||||
|
||||
@nonobjc public var category: StoreCategory? {
|
||||
guard let _category else { return nil }
|
||||
|
||||
|
||||
let category = StoreCategory(rawValue: _category)
|
||||
return category
|
||||
}
|
||||
@NSManaged @objc(category) public private(set) var _category: String?
|
||||
|
||||
|
||||
@NSManaged public private(set) var iconURL: URL
|
||||
@NSManaged public private(set) var screenshotURLs: [URL]
|
||||
|
||||
|
||||
@NSManaged public private(set) var downloadURL: URL?
|
||||
@NSManaged public private(set) var platformURLs: PlatformURLs?
|
||||
|
||||
@NSManaged public private(set) var tintColor: UIColor?
|
||||
@NSManaged public private(set) var isBeta: Bool
|
||||
|
||||
// Required for Marketplace apps.
|
||||
@NSManaged public private(set) var marketplaceID: String?
|
||||
@@ -161,18 +231,18 @@ public class StoreApp: BaseEntity, Decodable
|
||||
@NSManaged public private(set) var isHiddenWithoutPledge: Bool
|
||||
@NSManaged public private(set) var pledgeCurrency: String?
|
||||
@NSManaged public private(set) var prefersCustomPledge: Bool
|
||||
|
||||
|
||||
@nonobjc public var pledgeAmount: Decimal? { _pledgeAmount as? Decimal }
|
||||
@NSManaged @objc(pledgeAmount) private var _pledgeAmount: NSDecimalNumber?
|
||||
|
||||
|
||||
@NSManaged public var sortIndex: Int32
|
||||
@NSManaged public var featuredSortID: String?
|
||||
|
||||
|
||||
@objc public internal(set) var sourceIdentifier: String? {
|
||||
get {
|
||||
self.willAccessValue(forKey: #keyPath(sourceIdentifier))
|
||||
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
|
||||
|
||||
|
||||
let sourceIdentifier = self.primitiveSourceIdentifier
|
||||
return sourceIdentifier
|
||||
}
|
||||
@@ -180,17 +250,17 @@ public class StoreApp: BaseEntity, Decodable
|
||||
self.willChangeValue(forKey: #keyPath(sourceIdentifier))
|
||||
self.primitiveSourceIdentifier = newValue
|
||||
self.didChangeValue(forKey: #keyPath(sourceIdentifier))
|
||||
|
||||
|
||||
for version in self.versions
|
||||
{
|
||||
version.sourceID = newValue
|
||||
}
|
||||
|
||||
|
||||
for permission in self.permissions
|
||||
{
|
||||
permission.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
|
||||
|
||||
for screenshot in self.allScreenshots
|
||||
{
|
||||
screenshot.sourceID = self.sourceIdentifier ?? ""
|
||||
@@ -198,26 +268,26 @@ public class StoreApp: BaseEntity, Decodable
|
||||
}
|
||||
}
|
||||
@NSManaged private var primitiveSourceIdentifier: String?
|
||||
|
||||
|
||||
// Legacy (kept for backwards compatibility)
|
||||
@NSManaged public private(set) var version: String?
|
||||
@NSManaged public private(set) var versionDate: Date?
|
||||
@NSManaged public private(set) var versionDescription: String?
|
||||
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public var installedApp: InstalledApp?
|
||||
@NSManaged public var newsItems: Set<NewsItem>
|
||||
|
||||
|
||||
@NSManaged @objc(source) public var _source: Source?
|
||||
@NSManaged public internal(set) var featuringSource: Source?
|
||||
|
||||
|
||||
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
|
||||
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
||||
|
||||
|
||||
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
||||
|
||||
|
||||
/* Non-Core Data Properties */
|
||||
|
||||
|
||||
// Used to set isPledged after fetching source.
|
||||
public var _tierIDs: Set<String>?
|
||||
public var _rewardID: String?
|
||||
@@ -236,11 +306,11 @@ public class StoreApp: BaseEntity, Decodable
|
||||
return self._permissions as! Set<AppPermission>
|
||||
}
|
||||
@NSManaged @objc(permissions) internal private(set) var _permissions: NSSet // Use NSSet to avoid eagerly fetching values.
|
||||
|
||||
|
||||
@nonobjc public var versions: [AppVersion] {
|
||||
return self._versions.array as! [AppVersion]
|
||||
}
|
||||
|
||||
|
||||
@nonobjc public var allScreenshots: [AppScreenshot] {
|
||||
return self._screenshots.array as! [AppScreenshot]
|
||||
}
|
||||
@@ -250,7 +320,7 @@ public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case name
|
||||
@@ -265,19 +335,22 @@ public class StoreApp: BaseEntity, Decodable
|
||||
case subtitle
|
||||
case permissions = "appPermissions"
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
case isBeta = "beta" // backward compatibility for altstore source format
|
||||
case versions
|
||||
case patreon
|
||||
case category
|
||||
|
||||
|
||||
// Legacy
|
||||
case version
|
||||
case versionDescription
|
||||
case versionDate
|
||||
case downloadURL
|
||||
case screenshotURLs
|
||||
}
|
||||
|
||||
// v2 source format
|
||||
case releaseTracks = "releaseChannels"
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
@@ -293,10 +366,9 @@ public class StoreApp: BaseEntity, Decodable
|
||||
self.developerName = try container.decode(String.self, forKey: .developerName)
|
||||
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
||||
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||
|
||||
|
||||
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
|
||||
// Required for Marketplace apps, but we'll verify later.
|
||||
self.marketplaceID = try container.decodeIfPresent(String.self, forKey: .marketplaceID)
|
||||
|
||||
@@ -305,17 +377,17 @@ public class StoreApp: BaseEntity, Decodable
|
||||
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||
}
|
||||
|
||||
|
||||
self.tintColor = tintColor
|
||||
}
|
||||
|
||||
|
||||
if let rawCategory = try container.decodeIfPresent(String.self, forKey: .category)
|
||||
{
|
||||
self._category = rawCategory.lowercased() // Store raw (lowercased) category value.
|
||||
}
|
||||
|
||||
|
||||
let appScreenshots: [AppScreenshot]
|
||||
|
||||
|
||||
if let screenshots = try container.decodeIfPresent(AppScreenshots.self, forKey: .screenshots)
|
||||
{
|
||||
appScreenshots = screenshots.screenshots
|
||||
@@ -324,7 +396,7 @@ public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
// Assume 9:16 iPhone 8 screen dimensions for legacy screenshotURLs.
|
||||
let legacyAspectRatio = CGSize(width: 750, height: 1334)
|
||||
|
||||
|
||||
appScreenshots = screenshotURLs.map { imageURL in
|
||||
let screenshot = AppScreenshot(imageURL: imageURL, size: legacyAspectRatio, deviceType: .iphone, context: context)
|
||||
return screenshot
|
||||
@@ -342,14 +414,14 @@ public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
appScreenshots = []
|
||||
}
|
||||
|
||||
|
||||
for screenshot in appScreenshots
|
||||
{
|
||||
screenshot.appBundleID = self.bundleIdentifier
|
||||
}
|
||||
|
||||
|
||||
self.setScreenshots(appScreenshots)
|
||||
|
||||
|
||||
if let appPermissions = try container.decodeIfPresent(AppPermissions.self, forKey: .permissions)
|
||||
{
|
||||
let allPermissions = appPermissions.entitlements + appPermissions.privacy
|
||||
@@ -357,7 +429,7 @@ public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
permission.appBundleID = self.bundleIdentifier
|
||||
}
|
||||
|
||||
|
||||
self._permissions = NSSet(array: allPermissions)
|
||||
}
|
||||
else
|
||||
@@ -365,66 +437,8 @@ public class StoreApp: BaseEntity, Decodable
|
||||
self._permissions = NSSet()
|
||||
}
|
||||
|
||||
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
||||
{
|
||||
if (versions.count == 0){
|
||||
throw DecodingError.dataCorruptedError(forKey: .versions, in: container, debugDescription: "At least one version is required in key: versions")
|
||||
}
|
||||
|
||||
for (index, version) in zip(0..., versions)
|
||||
{
|
||||
version.appBundleID = self.bundleIdentifier
|
||||
|
||||
if self.marketplaceID != nil
|
||||
{
|
||||
struct IndexCodingKey: CodingKey
|
||||
{
|
||||
var stringValue: String { self.intValue?.description ?? "" }
|
||||
var intValue: Int?
|
||||
|
||||
init?(stringValue: String)
|
||||
{
|
||||
fatalError()
|
||||
}
|
||||
|
||||
init(intValue: Int)
|
||||
{
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
|
||||
// Marketplace apps must provide build version.
|
||||
guard version.buildVersion != nil else {
|
||||
let codingPath = container.codingPath + [CodingKeys.versions as CodingKey] + [IndexCodingKey(intValue: index) as CodingKey]
|
||||
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Notarized apps must provide a build version.")
|
||||
throw DecodingError.keyNotFound(AppVersion.CodingKeys.buildVersion, context)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try self.setVersions(versions)
|
||||
}
|
||||
else
|
||||
{
|
||||
let version = try container.decode(String.self, forKey: .version)
|
||||
let versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
let size = try container.decode(Int32.self, forKey: .size)
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: version,
|
||||
buildVersion: nil,
|
||||
date: versionDate,
|
||||
localizedDescription: versionDescription,
|
||||
downloadURL: downloadURL,
|
||||
size: Int64(size),
|
||||
appBundleID: self.bundleIdentifier,
|
||||
in: context)
|
||||
try self.setVersions([appVersion])
|
||||
}
|
||||
|
||||
try self.decodeVersions(from: decoder) // pre-req for downloadURL procesing
|
||||
|
||||
// latestSupportedVersion is set by this point if one was available
|
||||
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
|
||||
if let platformURLs = platformURLs {
|
||||
@@ -450,12 +464,12 @@ public class StoreApp: BaseEntity, Decodable
|
||||
// Must _explicitly_ set to false to ensure it updates cached database value.
|
||||
self.isPledged = false
|
||||
self.prefersCustomPledge = false
|
||||
|
||||
|
||||
if let patreon = try container.decodeIfPresent(PatreonParameters.self, forKey: .patreon)
|
||||
{
|
||||
self.isPledgeRequired = true
|
||||
self.isHiddenWithoutPledge = patreon.hidden ?? false // Default to showing Patreon apps
|
||||
|
||||
|
||||
if let pledge = patreon.pledge
|
||||
{
|
||||
self._pledgeAmount = pledge.amount as NSDecimalNumber
|
||||
@@ -467,7 +481,7 @@ public class StoreApp: BaseEntity, Decodable
|
||||
// No conditions, so default to pledgeAmount of 0 to simplify logic.
|
||||
self._pledgeAmount = 0 as NSDecimalNumber
|
||||
}
|
||||
|
||||
|
||||
self._tierIDs = patreon.tiers
|
||||
self._rewardID = patreon.benefit
|
||||
}
|
||||
@@ -477,7 +491,7 @@ public class StoreApp: BaseEntity, Decodable
|
||||
self.isHiddenWithoutPledge = false
|
||||
self._pledgeAmount = nil
|
||||
self.pledgeCurrency = nil
|
||||
|
||||
|
||||
self._tierIDs = nil
|
||||
self._rewardID = nil
|
||||
}
|
||||
@@ -488,23 +502,118 @@ public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeVersions(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let releaseTracks = try container.decodeIfPresent([ReleaseTrack].self, forKey: .releaseTracks){
|
||||
self._releaseTracks = NSOrderedSet(array: releaseTracks)
|
||||
}
|
||||
|
||||
// get channel info if present, else default to stable
|
||||
var channel = ReleaseTracks.stable.stringValue
|
||||
|
||||
var versions = getReleases(default: stableTrack) ?? []
|
||||
if versions.isEmpty {
|
||||
if let appVersions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
||||
{
|
||||
versions = appVersions
|
||||
}
|
||||
else
|
||||
{
|
||||
if try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
{
|
||||
channel = ReleaseTracks.nightly.stringValue
|
||||
}
|
||||
|
||||
// create one from the storeApp description and use it as current
|
||||
let newRelease = try createNewAppVersion(decoder: decoder)
|
||||
.mutateForData(
|
||||
channel: channel,
|
||||
appBundleID: self.bundleIdentifier
|
||||
)
|
||||
|
||||
versions = [newRelease]
|
||||
}
|
||||
}
|
||||
|
||||
for (index, version) in zip(0..., versions)
|
||||
{
|
||||
version.appBundleID = self.bundleIdentifier
|
||||
|
||||
// ignore setting, if it was already updated by ReleaseTracks in V2 sources
|
||||
if version.channel == .unknown {
|
||||
_ = version.mutateForData(channel: channel)
|
||||
}
|
||||
|
||||
if self.marketplaceID != nil
|
||||
{
|
||||
struct IndexCodingKey: CodingKey
|
||||
{
|
||||
var stringValue: String { self.intValue?.description ?? "" }
|
||||
var intValue: Int?
|
||||
|
||||
init?(stringValue: String)
|
||||
{
|
||||
fatalError()
|
||||
}
|
||||
|
||||
init(intValue: Int)
|
||||
{
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
|
||||
// Marketplace apps must provide build version.
|
||||
guard version.buildVersion != nil else {
|
||||
let codingPath = container.codingPath + [CodingKeys.versions as CodingKey] + [IndexCodingKey(intValue: index) as CodingKey]
|
||||
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Notarized apps must provide a build version.")
|
||||
throw DecodingError.keyNotFound(AppVersion.CodingKeys.buildVersion, context)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try self.setVersions(versions)
|
||||
}
|
||||
|
||||
func createNewAppVersion(decoder: Decoder) throws -> AppVersion {
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
//
|
||||
let version = try container.decode(String.self, forKey: .version)
|
||||
let versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
let size = try container.decode(Int32.self, forKey: .size)
|
||||
|
||||
return AppVersion.makeAppVersion(version: version,
|
||||
buildVersion: nil,
|
||||
date: versionDate,
|
||||
localizedDescription: versionDescription,
|
||||
downloadURL: downloadURL,
|
||||
size: Int64(size),
|
||||
appBundleID: self.bundleIdentifier,
|
||||
in: context)
|
||||
}
|
||||
|
||||
public override func awakeFromInsert()
|
||||
{
|
||||
super.awakeFromInsert()
|
||||
|
||||
|
||||
self.featuredSortID = UUID().uuidString
|
||||
}
|
||||
}
|
||||
|
||||
internal extension StoreApp
|
||||
{
|
||||
func setVersions(_ versions: [AppVersion]) throws
|
||||
{
|
||||
func setVersions(_ versions: [AppVersion]) throws
|
||||
{
|
||||
guard let latestVersion = versions.first else {
|
||||
throw MergeError.noVersions(for: self)
|
||||
}
|
||||
@@ -513,7 +622,7 @@ internal extension StoreApp
|
||||
|
||||
let latestSupportedVersion = versions.first(where: { $0.isSupported })
|
||||
self.latestSupportedVersion = latestSupportedVersion
|
||||
|
||||
|
||||
for case let version as AppVersion in self._versions
|
||||
{
|
||||
if version == latestSupportedVersion
|
||||
@@ -534,7 +643,7 @@ internal extension StoreApp
|
||||
self.downloadURL = latestVersion.downloadURL
|
||||
self._size = Int32(latestVersion.size)
|
||||
}
|
||||
|
||||
|
||||
func setPermissions(_ permissions: Set<AppPermission>)
|
||||
{
|
||||
for case let permission as AppPermission in self._permissions
|
||||
@@ -548,10 +657,10 @@ internal extension StoreApp
|
||||
permission.app = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self._permissions = permissions as NSSet
|
||||
}
|
||||
|
||||
|
||||
func setScreenshots(_ screenshots: [AppScreenshot])
|
||||
{
|
||||
for case let screenshot as AppScreenshot in self._screenshots
|
||||
@@ -565,9 +674,9 @@ internal extension StoreApp
|
||||
screenshot.app = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self._screenshots = NSOrderedSet(array: screenshots)
|
||||
|
||||
|
||||
// Backwards compatibility
|
||||
self.screenshotURLs = screenshots.map { $0.imageURL }
|
||||
}
|
||||
@@ -581,11 +690,11 @@ public extension StoreApp
|
||||
let filteredScreenshots = self.allScreenshots.filter { $0.deviceType == deviceType }
|
||||
return filteredScreenshots
|
||||
}
|
||||
|
||||
|
||||
func preferredScreenshots() -> [AppScreenshot]
|
||||
{
|
||||
let deviceType: ALTDeviceType
|
||||
|
||||
|
||||
if UIDevice.current.model.contains("iPad")
|
||||
{
|
||||
deviceType = .ipad
|
||||
@@ -594,13 +703,13 @@ public extension StoreApp
|
||||
{
|
||||
deviceType = .iphone
|
||||
}
|
||||
|
||||
|
||||
let preferredScreenshots = self.screenshots(for: deviceType)
|
||||
guard !preferredScreenshots.isEmpty else {
|
||||
// There are no screenshots for deviceType, so return _all_ screenshots instead.
|
||||
return self.allScreenshots
|
||||
}
|
||||
|
||||
|
||||
return preferredScreenshots
|
||||
}
|
||||
}
|
||||
@@ -610,10 +719,10 @@ public extension StoreApp
|
||||
var latestAvailableVersion: AppVersion? {
|
||||
return self._versions.firstObject as? AppVersion
|
||||
}
|
||||
|
||||
|
||||
var globallyUniqueID: String? {
|
||||
guard let sourceIdentifier = self.sourceIdentifier else { return nil }
|
||||
|
||||
|
||||
let globallyUniqueID = self.bundleIdentifier + "|" + sourceIdentifier
|
||||
return globallyUniqueID
|
||||
}
|
||||
@@ -629,63 +738,71 @@ public extension StoreApp
|
||||
#keyPath(StoreApp.isPledged))
|
||||
return predicate
|
||||
}
|
||||
|
||||
|
||||
class var otherCategoryPredicate: NSPredicate {
|
||||
let knownCategories = StoreCategory.allCases.lazy.filter { $0 != .other }.map { $0.rawValue }
|
||||
|
||||
|
||||
let predicate = NSPredicate(format: "%K == nil OR NOT (%K IN %@)", #keyPath(StoreApp._category), #keyPath(StoreApp._category), Array(knownCategories))
|
||||
return predicate
|
||||
}
|
||||
|
||||
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
||||
{
|
||||
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
||||
}
|
||||
|
||||
|
||||
private static var sideStoreAppIconURL: URL {
|
||||
let iconNames = [
|
||||
"AppIcon76x76@2x~ipad",
|
||||
"AppIcon60x60@2x",
|
||||
"AppIcon"
|
||||
]
|
||||
|
||||
|
||||
for iconName in iconNames {
|
||||
if let path = Bundle.main.path(forResource: iconName, ofType: "png") {
|
||||
return URL(fileURLWithPath: path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return URL(string: "https://sidestore.io/apps-v2.json/apps/sidestore/icon.png")!
|
||||
}
|
||||
|
||||
|
||||
class func makeAltStoreApp(version: String, buildVersion: String?, in context: NSManagedObjectContext) -> StoreApp
|
||||
{
|
||||
let placeholderAppID = StoreApp.altstoreAppID
|
||||
let placeholderBundleId = StoreApp.altstoreAppID
|
||||
let placeholderDownloadURL = URL(string: "https://sidestore.io")!
|
||||
let placeholderSourceID = Source.altStoreIdentifier
|
||||
|
||||
let placeholderVersion = "0.0.0"
|
||||
let placeholderDate = Date.distantPast
|
||||
// let placeholderDate = Date(timeIntervalSinceReferenceDate: 0)
|
||||
// let placeholderDate = Date(timeIntervalSince1970: 0)
|
||||
var placeholderChannel = ReleaseTracks.stable.stringValue // placeholder is always assumed to be from stable channel
|
||||
let placeholderSize: Int32 = 0
|
||||
|
||||
#if BETA
|
||||
placeholderChannel = ReleaseTracks.nightly.stringValue
|
||||
#endif
|
||||
|
||||
let app = StoreApp(context: context)
|
||||
app.name = "SideStore"
|
||||
app.bundleIdentifier = placeholderAppID
|
||||
app.bundleIdentifier = placeholderBundleId
|
||||
app.developerName = "Side Team"
|
||||
app.localizedDescription = "SideStore is an alternative App Store."
|
||||
app.iconURL = Self.sideStoreAppIconURL
|
||||
app.iconURL = sideStoreAppIconURL
|
||||
app.screenshotURLs = []
|
||||
app.sourceIdentifier = placeholderSourceID
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: version,
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: placeholderVersion,
|
||||
buildVersion: buildVersion,
|
||||
date: Date(),
|
||||
channel: placeholderChannel,
|
||||
date: placeholderDate,
|
||||
downloadURL: placeholderDownloadURL,
|
||||
size: 0,
|
||||
size: Int64(app._size),
|
||||
appBundleID: app.bundleIdentifier,
|
||||
sourceID: app.sourceIdentifier,
|
||||
in: context)
|
||||
try? app.setVersions([appVersion])
|
||||
|
||||
#if BETA
|
||||
app.isBeta = true
|
||||
#endif
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
DEVELOPMENT_TEAM = XYZ0123456
|
||||
|
||||
// Set this for dev-local xcode builds
|
||||
MARKETING_VERSION_SUFFIX = -local
|
||||
//MARKETING_VERSION_SUFFIX = -local
|
||||
|
||||
// Prefix of unique bundle IDs registered to you in Apple Developer Portal.
|
||||
// You need to register:
|
||||
|
||||
98
Makefile
98
Makefile
@@ -167,22 +167,87 @@ test:
|
||||
BUILD_CONFIG ?= Release
|
||||
MARKETING_VERSION ?=
|
||||
BUNDLE_ID_SUFFIX ?=
|
||||
# Common build settings for xcodebuild
|
||||
COMMON_BUILD_SETTINGS = \
|
||||
-workspace AltStore.xcworkspace \
|
||||
-scheme SideStore \
|
||||
-sdk iphoneos \
|
||||
-configuration $(BUILD_CONFIG) \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore
|
||||
|
||||
# Append MARKETING_VERSION if it’s not empty (coz otherwise the blank entry becomes override)
|
||||
ifneq ($(strip $(MARKETING_VERSION)),)
|
||||
COMMON_BUILD_SETTINGS += MARKETING_VERSION=$(MARKETING_VERSION)
|
||||
endif
|
||||
|
||||
# Append BUNDLE_ID_SUFFIX if it’s not empty (coz otherwise the blank entry becomes override)
|
||||
ifneq ($(strip $(BUNDLE_ID_SUFFIX)),)
|
||||
COMMON_BUILD_SETTINGS += BUNDLE_ID_SUFFIX=$(BUNDLE_ID_SUFFIX)
|
||||
endif
|
||||
|
||||
build:
|
||||
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||
@echo ""
|
||||
@xcodebuild -workspace AltStore.xcworkspace \
|
||||
-scheme SideStore \
|
||||
-sdk iphoneos \
|
||||
-configuration $(BUILD_CONFIG) \
|
||||
archive -archivePath ./SideStore \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore \
|
||||
MARKETING_VERSION=$(MARKETING_VERSION) \
|
||||
BUNDLE_ID_SUFFIX=$(BUNDLE_ID_SUFFIX)
|
||||
# DWARF_DSYM_FOLDER_PATH="."
|
||||
@xcodebuild archive -archivePath ./SideStore \
|
||||
$(COMMON_BUILD_SETTINGS)
|
||||
|
||||
build-and-test:
|
||||
@rm -rf build/tests/test-results.xcresult
|
||||
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||
@echo ""
|
||||
@echo "Performing a build and running tests..."
|
||||
@xcodebuild test \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
||||
-resultBundlePath build/tests/test-results.xcresult \
|
||||
-enableCodeCoverage YES \
|
||||
$(COMMON_BUILD_SETTINGS)
|
||||
|
||||
build-tests:
|
||||
@rm -rf build/tests/test-results.xcresult
|
||||
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building Tests for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||
@echo ""
|
||||
@echo "Performing a build-for-testing..."
|
||||
@xcodebuild build-for-testing \
|
||||
-enableCodeCoverage YES \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
||||
$(COMMON_BUILD_SETTINGS)
|
||||
|
||||
run-tests:
|
||||
@rm -rf build/tests/test-results.xcresult
|
||||
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Testing for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||
@echo ""
|
||||
@echo "Performing a test-without-building..."
|
||||
@xcodebuild test-without-building \
|
||||
-enableCodeCoverage YES \
|
||||
-resultBundlePath build/tests/test-results.xcresult \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
||||
$(COMMON_BUILD_SETTINGS)
|
||||
|
||||
boot-sim-async:
|
||||
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
|
||||
echo "Simulator 'iPhone 16 Pro' is already booted."; \
|
||||
else \
|
||||
echo "Booting simulator 'iPhone 16 Pro' asynchronously..."; \
|
||||
xcrun simctl boot "iPhone 16 Pro" & \
|
||||
echo "Simulator boot command dispatched."; \
|
||||
fi
|
||||
|
||||
sim-boot-check:
|
||||
@echo "Checking simulator boot status..."
|
||||
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
|
||||
echo "Simulator 'iPhone 16 Pro' is booted."; \
|
||||
else \
|
||||
echo "Simulator bootup failed or is not booted yet."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
clean-build:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@xcodebuild clean -workspace AltStore.xcworkspace -scheme SideStore
|
||||
|
||||
fakesign-apps:
|
||||
rm -rf SideStore.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
||||
@@ -308,7 +373,7 @@ ipa-altbackup: checkPaths copy-altbackup
|
||||
@mkdir -p "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
||||
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
|
||||
@cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
||||
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload && popd
|
||||
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload || popd
|
||||
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources
|
||||
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
|
||||
|
||||
@@ -317,11 +382,8 @@ clean-altbackup:
|
||||
@echo "====> Cleaning up AltBackup related artifacts <===="
|
||||
@rm -rf build/altbackup.xcarchive/
|
||||
@rm -f build/AltBackup.ipa
|
||||
@rm -f AltStore/Resources/AltBackup.ipa
|
||||
#@rm -f AltStore/Resources/AltBackup.ipa
|
||||
|
||||
clean: clean-altbackup
|
||||
@rm -rf *.xcarchive/
|
||||
@rm -rf *.dSYM/
|
||||
@rm -rf SideStore.ipa
|
||||
@rm -rf build/
|
||||
@rm -rf Payload/
|
||||
|
||||
35
SideStore/Tests/DataStructureTests.xctestplan
Normal file
35
SideStore/Tests/DataStructureTests.xctestplan
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "93E5E265-DC67-47F3-A214-8082A3421288",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"targetForVariableExpansion" : {
|
||||
"containerPath" : "container:AltStore.xcodeproj",
|
||||
"identifier" : "BFD247692284B9A500981D42",
|
||||
"name" : "SideStore"
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"skippedTests" : {
|
||||
"suites" : [
|
||||
{
|
||||
"name" : "DataStructuresTests"
|
||||
}
|
||||
]
|
||||
},
|
||||
"target" : {
|
||||
"containerPath" : "container:AltStore.xcodeproj",
|
||||
"identifier" : "A81A8CC42D68BA610086C96F",
|
||||
"name" : "DataStructureTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
33
SideStore/Tests/SideStoreTests.xctestplan
Normal file
33
SideStore/Tests/SideStoreTests.xctestplan
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "93E5E265-DC67-47F3-A214-8082A3421288",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"targetForVariableExpansion" : {
|
||||
"containerPath" : "container:AltStore.xcodeproj",
|
||||
"identifier" : "BFD247692284B9A500981D42",
|
||||
"name" : "SideStore"
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"skippedTests" : [
|
||||
"UITests\/testLaunchPerformance()",
|
||||
"UITestsLaunchTests",
|
||||
"UITestsLaunchTests\/testLaunch()"
|
||||
],
|
||||
"target" : {
|
||||
"containerPath" : "container:AltStore.xcodeproj",
|
||||
"identifier" : "A8E2DB202D684CBD009E5D31",
|
||||
"name" : "UITests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
459
SideStore/Tests/UITests/UITests.swift
Normal file
459
SideStore/Tests/UITests/UITests.swift
Normal file
@@ -0,0 +1,459 @@
|
||||
//
|
||||
// UITests.swift
|
||||
// UITests
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import Foundation
|
||||
|
||||
final class UITests: XCTestCase {
|
||||
|
||||
// Handle to the homescreen UI
|
||||
private static let springboard_app = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
private static let spotlight_app = XCUIApplication(bundleIdentifier: "com.apple.Spotlight")
|
||||
|
||||
private static let searchBar = spotlight_app.textFields["SpotlightSearchField"]
|
||||
|
||||
private static let APP_NAME = "SideStore"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
// Self.dismissSpotlight()
|
||||
// Self.deleteMyApp()
|
||||
Self.deleteMyApp2()
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
|
||||
// @MainActor // Xcode 16.2 bug: UITest Record Button Disabled with @MainActor, see: https://stackoverflow.com/a/79445950/11971304
|
||||
func testBulkAddRecommendedSources() throws {
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||
|
||||
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||
|
||||
// Do the actual validation
|
||||
try performBulkAddingRecommendedSources(for: app)
|
||||
}
|
||||
|
||||
func testBulkAddInputSources() throws {
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||
|
||||
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||
|
||||
// Do the actual validation
|
||||
try performBulkAddingInputSources(for: app)
|
||||
}
|
||||
|
||||
func testRepeatabilityForStagingInputSources() throws {
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||
|
||||
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||
|
||||
// Do the actual validation
|
||||
try performRepeatabilityForStagingInputSources(for: app)
|
||||
}
|
||||
|
||||
func testRepeatabilityForStagingRecommendedSources() throws {
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||
|
||||
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||
|
||||
// Do the actual validation
|
||||
try performRepeatabilityForStagingRecommendedSources(for: app)
|
||||
}
|
||||
|
||||
|
||||
// @MainActor
|
||||
// func testLaunchPerformance() throws {
|
||||
// if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// // This measures how long it takes to launch your application.
|
||||
// measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
// XCUIApplication().launch()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// Helpers
|
||||
private extension UITests {
|
||||
|
||||
class func dismissSpotlight(){
|
||||
// ignore spotlight if it was shown
|
||||
if searchBar.exists {
|
||||
let clearButton = searchBar.buttons["Clear text"]
|
||||
if clearButton.exists{
|
||||
clearButton.tap()
|
||||
}
|
||||
}
|
||||
springboard_app.tap()
|
||||
}
|
||||
|
||||
class func deleteMyApp() {
|
||||
XCUIApplication().terminate()
|
||||
dismissSpringboardAlerts()
|
||||
|
||||
// XCUIDevice.shared.press(.home)
|
||||
springboard_app.swipeDown()
|
||||
|
||||
let searchBar = Self.searchBar
|
||||
_ = searchBar.exists || searchBar.waitForExistence(timeout: 5)
|
||||
searchBar.typeText(APP_NAME)
|
||||
|
||||
// Rest of the deletion flow...
|
||||
let appIcon = spotlight_app.icons[APP_NAME]
|
||||
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||
if appIcon.exists || appIcon.waitForExistence(timeout: 5) {
|
||||
appIcon.press(forDuration: 1)
|
||||
|
||||
let deleteAppButton = spotlight_app.buttons["Delete App"]
|
||||
_ = deleteAppButton.exists || deleteAppButton.waitForExistence(timeout: 5)
|
||||
deleteAppButton.tap()
|
||||
|
||||
let confirmDeleteButton = springboard_app.alerts["Delete “\(APP_NAME)”?"]
|
||||
_ = confirmDeleteButton.exists || confirmDeleteButton.waitForExistence(timeout: 5)
|
||||
confirmDeleteButton.scrollViews.otherElements.buttons["Delete"].tap()
|
||||
}
|
||||
|
||||
let clearButton = searchBar.buttons["Clear text"]
|
||||
_ = clearButton.exists || clearButton.waitForExistence(timeout: 5)
|
||||
clearButton.tap()
|
||||
|
||||
springboard_app.tap()
|
||||
}
|
||||
|
||||
class func deleteMyApp2() {
|
||||
XCUIApplication().terminate()
|
||||
dismissSpringboardAlerts()
|
||||
|
||||
// Rest of the deletion flow...
|
||||
let appIcon = springboard_app.icons[APP_NAME]
|
||||
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||
if appIcon.exists || appIcon.waitForExistence(timeout: 5) {
|
||||
appIcon.press(forDuration: 1)
|
||||
|
||||
do {
|
||||
let button = springboard_app.buttons["Remove App"]
|
||||
_ = button.exists || button.waitForExistence(timeout: 5)
|
||||
button.tap()
|
||||
}
|
||||
do {
|
||||
let button = springboard_app.buttons["Delete App"]
|
||||
_ = button.waitForExistence(timeout: 0.3)
|
||||
button.tap()
|
||||
}
|
||||
do {
|
||||
let button = springboard_app.buttons["Delete"]
|
||||
_ = button.waitForExistence(timeout: 0.3)
|
||||
button.tap()
|
||||
}
|
||||
|
||||
// // Press home once to make the icons stop wiggling
|
||||
// XCUIDevice.shared.press(.home)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class func dismissSpringboardAlerts() {
|
||||
for alert in springboard_app.alerts.allElementsBoundByIndex {
|
||||
if alert.exists {
|
||||
// If there's a "Cancel" button, tap it; otherwise, tap the first button.
|
||||
if alert.buttons["Cancel"].exists {
|
||||
alert.buttons["Cancel"].tap()
|
||||
} else if let firstButton = alert.buttons.allElementsBoundByIndex.first {
|
||||
firstButton.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SeededGenerator: RandomNumberGenerator {
|
||||
var seed: UInt64
|
||||
|
||||
mutating func next() -> UInt64 {
|
||||
// A basic LCG (not cryptographically secure, but fine for testing)
|
||||
seed = 6364136223846793005 &* seed &+ 1
|
||||
return seed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Test guts (definition)
|
||||
private extension UITests {
|
||||
|
||||
|
||||
private func performBulkAdd(
|
||||
app: XCUIApplication,
|
||||
sourceMappings: [(identifier: String, alertTitle: String, requiresSwipe: Bool)],
|
||||
cellsQuery: XCUIElementQuery
|
||||
) throws {
|
||||
|
||||
// Tap on each sourceMappings source's "add" button.
|
||||
try tapAddForThesePickedSources(app: app, sourceMappings: sourceMappings, cellsQuery: cellsQuery)
|
||||
|
||||
// Commit the changes by tapping "Done".
|
||||
app.navigationBars["Add Source"].buttons["Done"].tap()
|
||||
|
||||
// Accept each source addition via alert.
|
||||
for source in sourceMappings {
|
||||
let alertIdentifier = "Would you like to add the source “\(source.alertTitle)”?"
|
||||
let addSourceButton = app.alerts[alertIdentifier]
|
||||
.scrollViews.otherElements.buttons["Add Source"]
|
||||
_ = addSourceButton.exists || addSourceButton.waitForExistence(timeout: 0.3)
|
||||
addSourceButton.tap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func performBulkAddingInputSources(for app: XCUIApplication) throws {
|
||||
|
||||
// set content into clipboard (for bulk add (paste))
|
||||
// NOTE: THIS IS AN ORDERED SEQUENCE AND MUST MATCH THE ORDER in textInputSources BELOW (Remember to take this into account when adding more entries)
|
||||
UIPasteboard.general.string = """
|
||||
https://alts.lao.sb
|
||||
https://taurine.app/altstore/taurinestore.json
|
||||
https://randomblock1.com/altstore/apps.json
|
||||
https://burritosoftware.github.io/altstore/channels/burritosource.json
|
||||
https://bit.ly/40Isul6
|
||||
https://bit.ly/wuxuslibraryplus
|
||||
https://bit.ly/Quantumsource-plus
|
||||
https://bit.ly/Altstore-complete
|
||||
https://bit.ly/Quantumsource
|
||||
""".trimmedIndentation
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||
|
||||
let collectionViewsQuery = app.collectionViews
|
||||
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
|
||||
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
|
||||
appsSidestoreIoTextField.tap()
|
||||
appsSidestoreIoTextField.tap()
|
||||
collectionViewsQuery.staticTexts["Paste"].tap()
|
||||
|
||||
// if app.keyboards.buttons["Return"].exists {
|
||||
// app.keyboards.buttons["Return"].tap()
|
||||
// } else if app.keyboards.buttons["Done"].exists {
|
||||
// app.keyboards.buttons["Done"].tap()
|
||||
// } else {
|
||||
// // if still exists try tapping outside of text field focus
|
||||
// app.tap()
|
||||
// }
|
||||
|
||||
if app.keyboards.count > 0 {
|
||||
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
|
||||
}
|
||||
|
||||
let cellsQuery = collectionViewsQuery.cells
|
||||
|
||||
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||
let textInputSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||
("Laoalts\nalts.lao.sb", "Laoalts", false),
|
||||
("Taurine\ntaurine.app/altstore/taurinestore.json", "Taurine", false),
|
||||
("RandomSource\nrandomblock1.com/altstore/apps.json", "RandomSource", false),
|
||||
("Burrito's AltStore\nburritosoftware.github.io/altstore/channels/burritosource.json", "Burrito's AltStore", true),
|
||||
("Qn_'s AltStore Repo\nbit.ly/40Isul6", "Qn_'s AltStore Repo", false),
|
||||
("WuXu's Library++\nThe Most Up-To-Date IPA Library on AltStore.", "WuXu's Library++", false),
|
||||
("Quantum Source++\nContains tweaked apps, free streaming, cracked apps, and more.", "Quantum Source++", false),
|
||||
("AltStore Complete\nContains tweaked apps, free streaming, cracked apps, and more.", "AltStore Complete", false),
|
||||
("Quantum Source\nContains all of your favorite emulators, games, jailbreaks, utilities, and more.", "Quantum Source", false),
|
||||
]
|
||||
|
||||
try performBulkAdd(app: app, sourceMappings: textInputSources, cellsQuery: cellsQuery)
|
||||
}
|
||||
|
||||
|
||||
private func performRepeatabilityForStagingInputSources(for app: XCUIApplication) throws {
|
||||
|
||||
// set content into clipboard (for bulk add (paste))
|
||||
// NOTE: THIS IS AN ORDERED SEQUENCE AND MUST MATCH THE ORDER in textInputSources BELOW (Remember to take this into account when adding more entries)
|
||||
UIPasteboard.general.string = """
|
||||
https://alts.lao.sb
|
||||
https://taurine.app/altstore/taurinestore.json
|
||||
https://randomblock1.com/altstore/apps.json
|
||||
https://burritosoftware.github.io/altstore/channels/burritosource.json
|
||||
https://bit.ly/40Isul6
|
||||
""".trimmedIndentation
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||
|
||||
let collectionViewsQuery = app.collectionViews
|
||||
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
|
||||
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
|
||||
appsSidestoreIoTextField.tap()
|
||||
appsSidestoreIoTextField.tap()
|
||||
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
|
||||
collectionViewsQuery.staticTexts["Paste"].tap()
|
||||
|
||||
if app.keyboards.count > 0 {
|
||||
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
|
||||
}
|
||||
|
||||
let cellsQuery = collectionViewsQuery.cells
|
||||
|
||||
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||
let textInputSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||
("Laoalts\nalts.lao.sb", "Laoalts", false),
|
||||
("Taurine\ntaurine.app/altstore/taurinestore.json", "Taurine", false),
|
||||
("RandomSource\nrandomblock1.com/altstore/apps.json", "RandomSource", false),
|
||||
("Burrito's AltStore\nburritosoftware.github.io/altstore/channels/burritosource.json", "Burrito's AltStore", false),
|
||||
("Qn_'s AltStore Repo\nbit.ly/40Isul6", "Qn_'s AltStore Repo", false),
|
||||
]
|
||||
|
||||
let repeatCount = 3 // number of times to run the entire sequence
|
||||
let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator)
|
||||
|
||||
try repeatabilityTest(app: app, sourceMappings: textInputSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed)
|
||||
}
|
||||
|
||||
private func repeatabilityTest(
|
||||
app: XCUIApplication,
|
||||
sourceMappings: [(identifier: String, alertTitle: String, requiresSwipe: Bool)],
|
||||
cellsQuery: XCUIElementQuery,
|
||||
repeatCount: Int = 1, // number of times to run the entire sequence
|
||||
seed: UInt64 = 42 // default = fixed seed for deterministic start of this generator
|
||||
) throws {
|
||||
let seededGenerator = SeededGenerator(seed: seed)
|
||||
|
||||
for _ in 0..<repeatCount {
|
||||
// The same fixed seeded generator will yield the same permutation if not advanced, so you might want to reinitialize or use a fresh copy for each iteration:
|
||||
var seededGenerator = seededGenerator // uncomment this for repeats to use same(shuffled once due to inital seed) order for all repeats
|
||||
|
||||
// let sourceMappings = sourceMappings.shuffled() // use this for non-deterministic shuffling
|
||||
let sourceMappings = sourceMappings.shuffled(using: &seededGenerator) // use this for deterministic shuffling based on seed
|
||||
try tapAddForThesePickedSources(app: app, sourceMappings: sourceMappings, cellsQuery: cellsQuery)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func tapAddForThesePickedSources(
|
||||
app: XCUIApplication,
|
||||
sourceMappings: [(identifier: String, alertTitle: String, requiresSwipe: Bool)],
|
||||
cellsQuery: XCUIElementQuery
|
||||
) throws {
|
||||
|
||||
// Tap on each sourceMappings source's "add" button.
|
||||
for source in sourceMappings {
|
||||
let sourceButton = cellsQuery.otherElements
|
||||
.containing(.button, identifier: source.identifier)
|
||||
.children(matching: .button)[source.identifier]
|
||||
XCTAssert(sourceButton.exists || sourceButton.waitForExistence(timeout: 10), "Source preview for id: '\(source.alertTitle)' not found in the view")
|
||||
|
||||
// let addButton = sourceButton.children(matching: .button).firstMatch
|
||||
let addButton = sourceButton.children(matching: .button)["add"]
|
||||
XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.3), " `+` button for id: '\(source.alertTitle)' not found in the preview container")
|
||||
addButton.tap()
|
||||
|
||||
if source.requiresSwipe {
|
||||
sourceButton.swipeUp(velocity: .slow) // Swipe up if needed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws {
|
||||
// Navigate to the Sources screen and open the Add Source view.
|
||||
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||
|
||||
let cellsQuery = app.collectionViews.cells
|
||||
|
||||
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
|
||||
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
|
||||
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
|
||||
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", true),
|
||||
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
|
||||
("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false),
|
||||
// ("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false), // not a stable source, sometimes becomes unreachable, so disabled
|
||||
("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", true),
|
||||
("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false),
|
||||
("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false),
|
||||
("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false)
|
||||
]
|
||||
|
||||
try performBulkAdd(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery)
|
||||
}
|
||||
|
||||
private func performRepeatabilityForStagingRecommendedSources(for app: XCUIApplication) throws {
|
||||
// Navigate to the Sources screen and open the Add Source view.
|
||||
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||
|
||||
let cellsQuery = app.collectionViews.cells
|
||||
|
||||
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
|
||||
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
|
||||
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
|
||||
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false),
|
||||
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
|
||||
]
|
||||
|
||||
let repeatCount = 3 // number of times to run the entire sequence
|
||||
let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator)
|
||||
|
||||
try repeatabilityTest(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
extension String {
|
||||
var trimmedIndentation: String {
|
||||
let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
let minIndent = lines
|
||||
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } // Ignore empty lines
|
||||
.map { $0.prefix { $0.isWhitespace }.count }
|
||||
.min() ?? 0
|
||||
|
||||
return lines.map { line in
|
||||
String(line.dropFirst(minIndent))
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
34
SideStore/Tests/UITests/UITestsLaunchTests.swift
Normal file
34
SideStore/Tests/UITests/UITestsLaunchTests.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// UITestsLaunchTests.swift
|
||||
// UITests
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class UITestsLaunchTests: XCTestCase {
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
// @MainActor
|
||||
// func testLaunch() throws {
|
||||
// let app = XCUIApplication()
|
||||
// app.launch()
|
||||
//
|
||||
// // Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// // such as logging into a test account or navigating somewhere in the app
|
||||
//
|
||||
// let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
// attachment.name = "Launch Screen"
|
||||
// attachment.lifetime = .keepAlways
|
||||
// add(attachment)
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DataStructuresTests.swift
|
||||
// DataStructuresTests
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import Testing
|
||||
|
||||
struct DataStructuresTests {
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// LinkedHashMapTests.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
// A helper class that signals when it is deallocated.
|
||||
class LeakTester {
|
||||
let id: Int
|
||||
var onDeinit: (() -> Void)?
|
||||
init(id: Int, onDeinit: (() -> Void)? = nil) {
|
||||
self.id = id
|
||||
self.onDeinit = onDeinit
|
||||
}
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
}
|
||||
|
||||
final class LinkedHashMapTests: XCTestCase {
|
||||
|
||||
// Test that insertion preserves order and that iteration returns items in insertion order.
|
||||
func testInsertionAndOrder() {
|
||||
let map = LinkedHashMap<String, Int>()
|
||||
map.put(key: "one", value: 1)
|
||||
map.put(key: "two", value: 2)
|
||||
map.put(key: "three", value: 3)
|
||||
|
||||
XCTAssertEqual(map.count, 3)
|
||||
XCTAssertEqual(map.keys, ["one", "two", "three"], "Insertion order should be preserved")
|
||||
|
||||
var iteratedKeys = [String]()
|
||||
for (key, _) in map {
|
||||
iteratedKeys.append(key)
|
||||
}
|
||||
XCTAssertEqual(iteratedKeys, ["one", "two", "three"], "Iterator should follow insertion order")
|
||||
}
|
||||
|
||||
// Test that updating a key does not change its order.
|
||||
func testUpdateDoesNotChangeOrder() {
|
||||
let map = LinkedHashMap<String, Int>()
|
||||
map.put(key: "a", value: 1)
|
||||
map.put(key: "b", value: 2)
|
||||
map.put(key: "c", value: 3)
|
||||
// Update key "b"
|
||||
map.put(key: "b", value: 20)
|
||||
XCTAssertEqual(map.get(key: "b"), 20)
|
||||
|
||||
XCTAssertEqual(map.keys, ["a", "b", "c"], "Order should not change on update")
|
||||
}
|
||||
|
||||
// Test removal functionality and behavior when removing a non-existent key.
|
||||
func testRemoval() {
|
||||
let map = LinkedHashMap<Int, String>()
|
||||
map.put(key: 1, value: "one")
|
||||
map.put(key: 2, value: "two")
|
||||
map.put(key: 3, value: "three")
|
||||
|
||||
let removed = map.remove(key: 2)
|
||||
XCTAssertEqual(removed, "two")
|
||||
XCTAssertEqual(map.count, 2)
|
||||
XCTAssertEqual(map.keys, [1, 3])
|
||||
|
||||
// Removing a key that doesn't exist should return nil.
|
||||
let removedNil = map.remove(key: 4)
|
||||
XCTAssertNil(removedNil)
|
||||
}
|
||||
|
||||
// Test clearing the map.
|
||||
func testClear() {
|
||||
let map = LinkedHashMap<String, Int>()
|
||||
map.put(key: "x", value: 100)
|
||||
map.put(key: "y", value: 200)
|
||||
XCTAssertEqual(map.count, 2)
|
||||
|
||||
map.clear()
|
||||
XCTAssertEqual(map.count, 0)
|
||||
XCTAssertTrue(map.isEmpty)
|
||||
XCTAssertEqual(map.keys, [])
|
||||
XCTAssertEqual(map.values, [])
|
||||
}
|
||||
|
||||
// Test subscript access for getting, updating, and removal.
|
||||
func testSubscript() {
|
||||
let map = LinkedHashMap<String, Int>()
|
||||
map["alpha"] = 10
|
||||
XCTAssertEqual(map["alpha"], 10)
|
||||
|
||||
map["alpha"] = 20
|
||||
XCTAssertEqual(map["alpha"], 20)
|
||||
|
||||
// Setting a key to nil should remove the mapping.
|
||||
map["alpha"] = nil
|
||||
XCTAssertNil(map["alpha"])
|
||||
}
|
||||
|
||||
// Test containsKey and containsValue.
|
||||
func testContains() {
|
||||
let map = LinkedHashMap<String, Int>()
|
||||
map.put(key: "key1", value: 1)
|
||||
map.put(key: "key2", value: 2)
|
||||
|
||||
XCTAssertTrue(map.containsKey("key1"))
|
||||
XCTAssertFalse(map.containsKey("key3"))
|
||||
|
||||
XCTAssertTrue(map.containsValue(1))
|
||||
XCTAssertFalse(map.containsValue(99))
|
||||
}
|
||||
|
||||
// Test initialization from a dictionary.
|
||||
func testInitializationFromDictionary() {
|
||||
// Note: Swift dictionaries preserve insertion order for literals.
|
||||
let dictionary: [String: Int] = ["a": 1, "b": 2, "c": 3]
|
||||
let map = LinkedHashMap(dictionary)
|
||||
XCTAssertEqual(map.count, 3)
|
||||
// Order may differ since Dictionary order is not strictly defined – here we verify membership.
|
||||
XCTAssertEqual(Set(map.keys), Set(["a", "b", "c"]))
|
||||
}
|
||||
|
||||
// Revised test that iterates over the map and compares key-value pairs element by element.
|
||||
func testIteration() {
|
||||
let map = LinkedHashMap<Int, String>()
|
||||
let pairs = [(1, "one"), (2, "two"), (3, "three")]
|
||||
for (key, value) in pairs {
|
||||
map.put(key: key, value: value)
|
||||
}
|
||||
|
||||
var iteratedPairs = [(Int, String)]()
|
||||
for (key, value) in map {
|
||||
iteratedPairs.append((key, value))
|
||||
}
|
||||
|
||||
XCTAssertEqual(iteratedPairs.count, pairs.count, "Iterated count should match inserted count")
|
||||
for (iter, expected) in zip(iteratedPairs, pairs) {
|
||||
XCTAssertEqual(iter.0, expected.0, "Keys should match in order")
|
||||
XCTAssertEqual(iter.1, expected.1, "Values should match in order")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the values stored in the map are deallocated when the map is deallocated.
|
||||
func testMemoryLeak() {
|
||||
weak var weakMap: LinkedHashMap<Int, LeakTester>?
|
||||
var deinitCalled = false
|
||||
|
||||
do {
|
||||
let map = LinkedHashMap<Int, LeakTester>()
|
||||
let tester = LeakTester(id: 1) { deinitCalled = true }
|
||||
map.put(key: 1, value: tester)
|
||||
weakMap = map
|
||||
XCTAssertNotNil(map.get(key: 1))
|
||||
}
|
||||
// At this point the map (and its stored objects) should be deallocated.
|
||||
XCTAssertNil(weakMap, "LinkedHashMap should be deallocated when out of scope")
|
||||
XCTAssertTrue(deinitCalled, "LeakTester should be deallocated, indicating no memory leak")
|
||||
}
|
||||
|
||||
// Test that removal from the map correctly frees stored objects.
|
||||
func testMemoryLeakOnRemoval() {
|
||||
var deinitCalledForTester1 = false
|
||||
var deinitCalledForTester2 = false
|
||||
|
||||
let map = LinkedHashMap<Int, LeakTester>()
|
||||
autoreleasepool {
|
||||
let tester1 = LeakTester(id: 1) { deinitCalledForTester1 = true }
|
||||
let tester2 = LeakTester(id: 2) { deinitCalledForTester2 = true }
|
||||
map.put(key: 1, value: tester1)
|
||||
map.put(key: 2, value: tester2)
|
||||
|
||||
XCTAssertNotNil(map.get(key: 1))
|
||||
XCTAssertNotNil(map.get(key: 2))
|
||||
|
||||
// Remove tester1; it should be deallocated if no retain cycle exists.
|
||||
_ = map.remove(key: 1)
|
||||
}
|
||||
// tester1 should be deallocated immediately after removal.
|
||||
XCTAssertTrue(deinitCalledForTester1, "Tester1 should be deallocated after removal")
|
||||
// tester2 is still in the map.
|
||||
XCTAssertNotNil(map.get(key: 2))
|
||||
|
||||
// Clear the map and tester2 should be deallocated.
|
||||
map.clear()
|
||||
XCTAssertTrue(deinitCalledForTester2, "Tester2 should be deallocated after clearing the map")
|
||||
}
|
||||
|
||||
func testDefaultSubscriptExtension() {
|
||||
// Create an instance of LinkedHashMap with String keys and Bool values.
|
||||
let map = LinkedHashMap<String, Bool>()
|
||||
|
||||
// Verify that accessing a non-existent key returns the default value (false).
|
||||
XCTAssertEqual(map["testKey", default: false], false)
|
||||
|
||||
// Use the default subscript setter to assign 'true' for the key.
|
||||
map["testKey", default: false] = true
|
||||
XCTAssertEqual(map["testKey", default: false], true)
|
||||
|
||||
// Simulate in-place toggle: read the value, toggle it, then write it back.
|
||||
var current = map["testKey", default: false]
|
||||
current.toggle() // now false
|
||||
map["testKey", default: false] = current
|
||||
XCTAssertEqual(map["testKey", default: false], false)
|
||||
}
|
||||
}
|
||||
154
SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift
Normal file
154
SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift
Normal file
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// TreeMapTests.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import XCTest
|
||||
|
||||
class TreeMapTests: XCTestCase {
|
||||
|
||||
func testInsertionAndRetrieval() {
|
||||
let map = TreeMap<Int, String>()
|
||||
XCTAssertNil(map[10])
|
||||
map[10] = "ten"
|
||||
XCTAssertEqual(map[10], "ten")
|
||||
|
||||
map[5] = "five"
|
||||
map[15] = "fifteen"
|
||||
XCTAssertEqual(map.count, 3)
|
||||
XCTAssertEqual(map[5], "five")
|
||||
XCTAssertEqual(map[15], "fifteen")
|
||||
}
|
||||
|
||||
func testUpdateValue() {
|
||||
let map = TreeMap<Int, String>()
|
||||
map[10] = "ten"
|
||||
let oldValue = map.insert(key: 10, value: "TEN")
|
||||
XCTAssertEqual(oldValue, "ten")
|
||||
XCTAssertEqual(map[10], "TEN")
|
||||
XCTAssertEqual(map.count, 1)
|
||||
}
|
||||
|
||||
func testDeletion() {
|
||||
let map = TreeMap<Int, String>()
|
||||
// Setup: Inserting three nodes.
|
||||
map[20] = "twenty"
|
||||
map[10] = "ten"
|
||||
map[30] = "thirty"
|
||||
|
||||
// Remove a leaf node.
|
||||
let removedLeaf = map.remove(key: 10)
|
||||
XCTAssertEqual(removedLeaf, "ten")
|
||||
XCTAssertNil(map[10])
|
||||
XCTAssertEqual(map.count, 2)
|
||||
|
||||
// Setup additional nodes to create a one-child scenario.
|
||||
map[25] = "twenty-five"
|
||||
map[27] = "twenty-seven" // Right child for 25.
|
||||
// Remove a node with one child.
|
||||
let removedOneChild = map.remove(key: 25)
|
||||
XCTAssertEqual(removedOneChild, "twenty-five")
|
||||
XCTAssertNil(map[25])
|
||||
XCTAssertEqual(map.count, 3)
|
||||
|
||||
// Setup for a node with two children.
|
||||
map[40] = "forty"
|
||||
map[35] = "thirty-five"
|
||||
map[45] = "forty-five"
|
||||
// Remove a node with two children.
|
||||
let removedTwoChildren = map.remove(key: 40)
|
||||
XCTAssertEqual(removedTwoChildren, "forty")
|
||||
XCTAssertNil(map[40])
|
||||
XCTAssertEqual(map.count, 5)
|
||||
}
|
||||
|
||||
func testDeletionOfRoot() {
|
||||
let map = TreeMap<Int, String>()
|
||||
map[50] = "fifty"
|
||||
map[30] = "thirty"
|
||||
map[70] = "seventy"
|
||||
|
||||
// Delete the root node.
|
||||
let removedRoot = map.remove(key: 50)
|
||||
XCTAssertEqual(removedRoot, "fifty")
|
||||
XCTAssertNil(map[50])
|
||||
// After deletion, remaining keys should be in sorted order.
|
||||
XCTAssertEqual(map.keys, [30, 70])
|
||||
}
|
||||
|
||||
func testSortedIteration() {
|
||||
let map = TreeMap<Int, String>()
|
||||
let keys = [20, 10, 30, 5, 15, 25, 35]
|
||||
for key in keys {
|
||||
map[key] = "\(key)"
|
||||
}
|
||||
let sortedKeys = map.keys
|
||||
XCTAssertEqual(sortedKeys, keys.sorted())
|
||||
|
||||
// Verify in-order traversal.
|
||||
var previous: Int? = nil
|
||||
for (key, value) in map {
|
||||
if let prev = previous {
|
||||
XCTAssertLessThanOrEqual(prev, key)
|
||||
}
|
||||
previous = key
|
||||
XCTAssertEqual(value, "\(key)")
|
||||
}
|
||||
}
|
||||
|
||||
func testRemoveAll() {
|
||||
let map = TreeMap<Int, String>()
|
||||
for i in 0..<100 {
|
||||
map[i] = "\(i)"
|
||||
}
|
||||
XCTAssertEqual(map.count, 100)
|
||||
map.removeAll()
|
||||
XCTAssertEqual(map.count, 0)
|
||||
XCTAssertTrue(map.isEmpty)
|
||||
}
|
||||
|
||||
func testBalancing() {
|
||||
let map = TreeMap<Int, Int>()
|
||||
// Insert elements in ascending order to challenge the balancing.
|
||||
for i in 1...1000 {
|
||||
map[i] = i
|
||||
}
|
||||
// Verify in-order traversal produces sorted order.
|
||||
var expected = 1
|
||||
for (key, value) in map {
|
||||
XCTAssertEqual(key, expected)
|
||||
XCTAssertEqual(value, expected)
|
||||
expected += 1
|
||||
}
|
||||
XCTAssertEqual(expected - 1, 1000)
|
||||
|
||||
// Remove odd keys to force rebalancing.
|
||||
for i in stride(from: 1, through: 1000, by: 2) {
|
||||
_ = map.remove(key: i)
|
||||
}
|
||||
let expectedEvenKeys = (1...1000).filter { $0 % 2 == 0 }
|
||||
XCTAssertEqual(map.keys, expectedEvenKeys)
|
||||
}
|
||||
|
||||
func testNonExistentDeletion() {
|
||||
let map = TreeMap<Int, String>()
|
||||
map[10] = "ten"
|
||||
let removed = map.remove(key: 20)
|
||||
XCTAssertNil(removed)
|
||||
XCTAssertEqual(map.count, 1)
|
||||
}
|
||||
|
||||
func testDuplicateInsertion() {
|
||||
let map = TreeMap<String, String>()
|
||||
map["a"] = "first"
|
||||
XCTAssertEqual(map["a"], "first")
|
||||
let oldValue = map.insert(key: "a", value: "second")
|
||||
XCTAssertEqual(oldValue, "first")
|
||||
XCTAssertEqual(map["a"], "second")
|
||||
XCTAssertEqual(map.count, 1)
|
||||
}
|
||||
}
|
||||
226
SideStore/Utils/datastructures/LinkedHashMap.swift
Normal file
226
SideStore/Utils/datastructures/LinkedHashMap.swift
Normal file
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// LinkedHashMap.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
/// A generic LinkedHashMap implementation in Swift.
|
||||
/// It provides constant-time lookup along with predictable (insertion) ordering.
|
||||
public final class LinkedHashMap<Key: Hashable, Value>: Sequence {
|
||||
|
||||
/// Internal doubly-linked list node
|
||||
fileprivate final class Node {
|
||||
let key: Key
|
||||
var value: Value
|
||||
var next: Node?
|
||||
weak var prev: Node? // weak to avoid strong reference cycle
|
||||
|
||||
init(key: Key, value: Value) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
/// Dictionary for fast lookup from key to node.
|
||||
private var dict: [Key: Node] = [:]
|
||||
|
||||
/// Head and tail of the doubly-linked list to maintain order.
|
||||
private var head: Node?
|
||||
private var tail: Node?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Creates an empty LinkedHashMap.
|
||||
public init() { }
|
||||
|
||||
/// Creates a LinkedHashMap from a standard dictionary.
|
||||
public init(_ dictionary: [Key: Value]) {
|
||||
for (key, value) in dictionary {
|
||||
_ = self.put(key: key, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// The number of key-value pairs in the map.
|
||||
public var count: Int {
|
||||
return dict.count
|
||||
}
|
||||
|
||||
/// A Boolean value indicating whether the map is empty.
|
||||
public var isEmpty: Bool {
|
||||
return dict.isEmpty
|
||||
}
|
||||
|
||||
/// Returns the value for the given key, or `nil` if the key is not found.
|
||||
public func get(key: Key) -> Value? {
|
||||
return dict[key]?.value
|
||||
}
|
||||
|
||||
/// Inserts or updates the value for the given key.
|
||||
/// - Returns: The previous value for the key if it existed; otherwise, `nil`.
|
||||
@discardableResult
|
||||
public func put(key: Key, value: Value) -> Value? {
|
||||
if let node = dict[key] {
|
||||
let oldValue = node.value
|
||||
node.value = value
|
||||
return oldValue
|
||||
} else {
|
||||
let newNode = Node(key: key, value: value)
|
||||
dict[key] = newNode
|
||||
appendNode(newNode)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the value for the given key.
|
||||
/// - Returns: The removed value if it existed; otherwise, `nil`.
|
||||
@discardableResult
|
||||
public func remove(key: Key) -> Value? {
|
||||
guard let node = dict.removeValue(forKey: key) else { return nil }
|
||||
removeNode(node)
|
||||
return node.value
|
||||
}
|
||||
|
||||
/// Removes all key-value pairs from the map.
|
||||
public func clear() {
|
||||
dict.removeAll()
|
||||
head = nil
|
||||
tail = nil
|
||||
}
|
||||
|
||||
/// Determines whether the map contains the given key.
|
||||
public func containsKey(_ key: Key) -> Bool {
|
||||
return dict[key] != nil
|
||||
}
|
||||
|
||||
/// Determines whether the map contains the given value.
|
||||
/// Note: This method requires that Value conforms to Equatable.
|
||||
public func containsValue(_ value: Value) -> Bool where Value: Equatable {
|
||||
var current = head
|
||||
while let node = current {
|
||||
if node.value == value {
|
||||
return true
|
||||
}
|
||||
current = node.next
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns all keys in insertion order.
|
||||
public var keys: [Key] {
|
||||
var result = [Key]()
|
||||
var current = head
|
||||
while let node = current {
|
||||
result.append(node.key)
|
||||
current = node.next
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Returns all values in insertion order.
|
||||
public var values: [Value] {
|
||||
var result = [Value]()
|
||||
var current = head
|
||||
while let node = current {
|
||||
result.append(node.value)
|
||||
current = node.next
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Subscript for getting and setting values.
|
||||
public subscript(key: Key) -> Value? {
|
||||
get {
|
||||
return get(key: key)
|
||||
}
|
||||
set {
|
||||
if let newValue = newValue {
|
||||
_ = put(key: key, value: newValue)
|
||||
} else {
|
||||
_ = remove(key: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sequence Conformance
|
||||
|
||||
/// Iterator that yields key-value pairs in insertion order.
|
||||
public struct Iterator: IteratorProtocol {
|
||||
private var current: Node?
|
||||
|
||||
fileprivate init(start: Node?) {
|
||||
self.current = start
|
||||
}
|
||||
|
||||
public mutating func next() -> (key: Key, value: Value)? {
|
||||
guard let node = current else { return nil }
|
||||
current = node.next
|
||||
return (node.key, node.value)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeIterator() -> Iterator {
|
||||
return Iterator(start: head)
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Appends a new node to the end of the linked list.
|
||||
private func appendNode(_ node: Node) {
|
||||
if let tailNode = tail {
|
||||
tailNode.next = node
|
||||
node.prev = tailNode
|
||||
tail = node
|
||||
} else {
|
||||
head = node
|
||||
tail = node
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the given node from the linked list.
|
||||
private func removeNode(_ node: Node) {
|
||||
let prevNode = node.prev
|
||||
let nextNode = node.next
|
||||
|
||||
if let prevNode = prevNode {
|
||||
prevNode.next = nextNode
|
||||
} else {
|
||||
head = nextNode
|
||||
}
|
||||
|
||||
if let nextNode = nextNode {
|
||||
nextNode.prev = prevNode
|
||||
} else {
|
||||
tail = prevNode
|
||||
}
|
||||
|
||||
// Disconnect node's pointers.
|
||||
node.prev = nil
|
||||
node.next = nil
|
||||
}
|
||||
|
||||
public func removeValue(forKey key: Key) -> Value? {
|
||||
return remove(key: key)
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkedHashMap {
|
||||
public subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
|
||||
get {
|
||||
if let value = self[key] {
|
||||
return value
|
||||
} else {
|
||||
return defaultValue()
|
||||
}
|
||||
}
|
||||
set {
|
||||
self[key] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
397
SideStore/Utils/datastructures/TreeMap.swift
Normal file
397
SideStore/Utils/datastructures/TreeMap.swift
Normal file
@@ -0,0 +1,397 @@
|
||||
//
|
||||
// TreeMap.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Magesh K on 21/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
public class TreeMap<Key: Comparable, Value>: Sequence {
|
||||
|
||||
// MARK: - Node and Color Definitions
|
||||
|
||||
fileprivate enum Color {
|
||||
case red
|
||||
case black
|
||||
}
|
||||
|
||||
fileprivate class Node {
|
||||
var key: Key
|
||||
var value: Value
|
||||
var left: Node?
|
||||
var right: Node?
|
||||
weak var parent: Node?
|
||||
var color: Color
|
||||
|
||||
init(key: Key, value: Value, color: Color = .red, parent: Node? = nil) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.color = color
|
||||
self.parent = parent
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TreeMap Properties and Initializer
|
||||
|
||||
private var root: Node?
|
||||
public private(set) var count: Int = 0
|
||||
|
||||
public init() {}
|
||||
|
||||
// MARK: - Public Dictionary-like API
|
||||
|
||||
/// Subscript: Get or set value for a given key.
|
||||
public subscript(key: Key) -> Value? {
|
||||
get { return get(key: key) }
|
||||
set {
|
||||
if let newValue = newValue {
|
||||
_ = insert(key: key, value: newValue)
|
||||
} else {
|
||||
_ = remove(key: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value associated with the given key.
|
||||
public func get(key: Key) -> Value? {
|
||||
guard let node = getNode(forKey: key) else { return nil }
|
||||
return node.value
|
||||
}
|
||||
|
||||
/// Inserts (or updates) the key with the given value.
|
||||
/// Returns the old value if the key was already present.
|
||||
@discardableResult
|
||||
public func insert(key: Key, value: Value) -> Value? {
|
||||
if let node = getNode(forKey: key) {
|
||||
let oldValue = node.value
|
||||
node.value = value
|
||||
return oldValue
|
||||
}
|
||||
// Create new node
|
||||
let newNode = Node(key: key, value: value)
|
||||
var parent: Node? = nil
|
||||
var current = root
|
||||
while let cur = current {
|
||||
parent = cur
|
||||
if newNode.key < cur.key {
|
||||
current = cur.left
|
||||
} else {
|
||||
current = cur.right
|
||||
}
|
||||
}
|
||||
newNode.parent = parent
|
||||
if parent == nil {
|
||||
root = newNode
|
||||
} else if newNode.key < parent!.key {
|
||||
parent!.left = newNode
|
||||
} else {
|
||||
parent!.right = newNode
|
||||
}
|
||||
count += 1
|
||||
fixAfterInsertion(newNode)
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Removes the node with the given key.
|
||||
/// Returns the removed value if it existed.
|
||||
@discardableResult
|
||||
public func remove(key: Key) -> Value? {
|
||||
guard let node = getNode(forKey: key) else { return nil }
|
||||
let removedValue = node.value
|
||||
deleteNode(node)
|
||||
count -= 1
|
||||
return removedValue
|
||||
}
|
||||
|
||||
/// Returns true if the map is empty.
|
||||
public var isEmpty: Bool {
|
||||
return count == 0
|
||||
}
|
||||
|
||||
/// Returns all keys in sorted order.
|
||||
public var keys: [Key] {
|
||||
var result = [Key]()
|
||||
for (k, _) in self { result.append(k) }
|
||||
return result
|
||||
}
|
||||
|
||||
/// Returns all values in order of their keys.
|
||||
public var values: [Value] {
|
||||
var result = [Value]()
|
||||
for (_, v) in self { result.append(v) }
|
||||
return result
|
||||
}
|
||||
|
||||
/// Removes all entries.
|
||||
public func removeAll() {
|
||||
root = nil
|
||||
count = 0
|
||||
}
|
||||
|
||||
// MARK: - Internal Helper Methods
|
||||
|
||||
/// Standard BST search for a node matching the key.
|
||||
private func getNode(forKey key: Key) -> Node? {
|
||||
var current = root
|
||||
while let node = current {
|
||||
if key == node.key {
|
||||
return node
|
||||
} else if key < node.key {
|
||||
current = node.left
|
||||
} else {
|
||||
current = node.right
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns the minimum node in the subtree rooted at `node`.
|
||||
private func minimum(_ node: Node) -> Node {
|
||||
var current = node
|
||||
while let next = current.left {
|
||||
current = next
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// MARK: - Rotation Methods
|
||||
|
||||
private func rotateLeft(_ x: Node) {
|
||||
guard let y = x.right else { return }
|
||||
x.right = y.left
|
||||
if let leftChild = y.left {
|
||||
leftChild.parent = x
|
||||
}
|
||||
y.parent = x.parent
|
||||
if x.parent == nil {
|
||||
root = y
|
||||
} else if x === x.parent?.left {
|
||||
x.parent?.left = y
|
||||
} else {
|
||||
x.parent?.right = y
|
||||
}
|
||||
y.left = x
|
||||
x.parent = y
|
||||
}
|
||||
|
||||
private func rotateRight(_ x: Node) {
|
||||
guard let y = x.left else { return }
|
||||
x.left = y.right
|
||||
if let rightChild = y.right {
|
||||
rightChild.parent = x
|
||||
}
|
||||
y.parent = x.parent
|
||||
if x.parent == nil {
|
||||
root = y
|
||||
} else if x === x.parent?.right {
|
||||
x.parent?.right = y
|
||||
} else {
|
||||
x.parent?.left = y
|
||||
}
|
||||
y.right = x
|
||||
x.parent = y
|
||||
}
|
||||
|
||||
// MARK: - Insertion Fix-Up
|
||||
|
||||
/// Restores red–black properties after insertion.
|
||||
private func fixAfterInsertion(_ x: Node) {
|
||||
var node = x
|
||||
node.color = .red
|
||||
while node !== root, let parent = node.parent, parent.color == .red {
|
||||
if parent === parent.parent?.left {
|
||||
if let uncle = parent.parent?.right, uncle.color == .red {
|
||||
parent.color = .black
|
||||
uncle.color = .black
|
||||
parent.parent?.color = .red
|
||||
if let grandparent = parent.parent {
|
||||
node = grandparent
|
||||
}
|
||||
} else {
|
||||
if node === parent.right {
|
||||
node = parent
|
||||
rotateLeft(node)
|
||||
}
|
||||
node.parent?.color = .black
|
||||
node.parent?.parent?.color = .red
|
||||
if let grandparent = node.parent?.parent {
|
||||
rotateRight(grandparent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let uncle = parent.parent?.left, uncle.color == .red {
|
||||
parent.color = .black
|
||||
uncle.color = .black
|
||||
parent.parent?.color = .red
|
||||
if let grandparent = parent.parent {
|
||||
node = grandparent
|
||||
}
|
||||
} else {
|
||||
if node === parent.left {
|
||||
node = parent
|
||||
rotateRight(node)
|
||||
}
|
||||
node.parent?.color = .black
|
||||
node.parent?.parent?.color = .red
|
||||
if let grandparent = node.parent?.parent {
|
||||
rotateLeft(grandparent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
root?.color = .black
|
||||
}
|
||||
|
||||
// MARK: - Deletion Helpers
|
||||
|
||||
/// Replaces subtree rooted at u with subtree rooted at v.
|
||||
private func transplant(_ u: Node, _ v: Node?) {
|
||||
if u.parent == nil {
|
||||
root = v
|
||||
} else if u === u.parent?.left {
|
||||
u.parent?.left = v
|
||||
} else {
|
||||
u.parent?.right = v
|
||||
}
|
||||
if let vNode = v {
|
||||
vNode.parent = u.parent
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes node z and fixes red–black properties.
|
||||
private func deleteNode(_ z: Node) {
|
||||
var y = z
|
||||
let originalColor = y.color
|
||||
var x: Node?
|
||||
|
||||
if z.left == nil {
|
||||
x = z.right
|
||||
transplant(z, z.right)
|
||||
} else if z.right == nil {
|
||||
x = z.left
|
||||
transplant(z, z.left)
|
||||
} else {
|
||||
y = minimum(z.right!)
|
||||
let yOriginalColor = y.color
|
||||
x = y.right
|
||||
if y.parent === z {
|
||||
if x != nil { x!.parent = y }
|
||||
} else {
|
||||
transplant(y, y.right)
|
||||
y.right = z.right
|
||||
y.right?.parent = y
|
||||
}
|
||||
transplant(z, y)
|
||||
y.left = z.left
|
||||
y.left?.parent = y
|
||||
y.color = z.color
|
||||
if yOriginalColor == .black {
|
||||
fixAfterDeletion(x, parent: y.parent)
|
||||
}
|
||||
return
|
||||
}
|
||||
if originalColor == .black {
|
||||
fixAfterDeletion(x, parent: z.parent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores red–black properties after deletion.
|
||||
private func fixAfterDeletion(_ x: Node?, parent: Node?) {
|
||||
var x = x
|
||||
var parent = parent
|
||||
while (x == nil || x!.color == .black) && (x !== root) {
|
||||
if x === parent?.left {
|
||||
var w = parent?.right
|
||||
if w?.color == .red {
|
||||
w?.color = .black
|
||||
parent?.color = .red
|
||||
rotateLeft(parent!)
|
||||
w = parent?.right
|
||||
}
|
||||
if (w?.left == nil || w?.left?.color == .black) &&
|
||||
(w?.right == nil || w?.right?.color == .black) {
|
||||
w?.color = .red
|
||||
x = parent
|
||||
parent = x?.parent
|
||||
} else {
|
||||
if w?.right == nil || w?.right?.color == .black {
|
||||
w?.left?.color = .black
|
||||
w?.color = .red
|
||||
if let wUnwrapped = w { rotateRight(wUnwrapped) }
|
||||
w = parent?.right
|
||||
}
|
||||
w?.color = parent?.color ?? .black
|
||||
parent?.color = .black
|
||||
w?.right?.color = .black
|
||||
rotateLeft(parent!)
|
||||
x = root
|
||||
parent = nil
|
||||
}
|
||||
} else {
|
||||
var w = parent?.left
|
||||
if w?.color == .red {
|
||||
w?.color = .black
|
||||
parent?.color = .red
|
||||
rotateRight(parent!)
|
||||
w = parent?.left
|
||||
}
|
||||
if (w?.left == nil || w?.left?.color == .black) &&
|
||||
(w?.right == nil || w?.right?.color == .black) {
|
||||
w?.color = .red
|
||||
x = parent
|
||||
parent = x?.parent
|
||||
} else {
|
||||
if w?.left == nil || w?.left?.color == .black {
|
||||
w?.right?.color = .black
|
||||
w?.color = .red
|
||||
if let wUnwrapped = w { rotateLeft(wUnwrapped) }
|
||||
w = parent?.left
|
||||
}
|
||||
w?.color = parent?.color ?? .black
|
||||
parent?.color = .black
|
||||
w?.left?.color = .black
|
||||
rotateRight(parent!)
|
||||
x = root
|
||||
parent = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
x?.color = .black
|
||||
}
|
||||
|
||||
// Convenience overload if parent is not separately tracked.
|
||||
private func fixAfterDeletion(_ x: Node?) {
|
||||
fixAfterDeletion(x, parent: x?.parent)
|
||||
}
|
||||
|
||||
// MARK: - Sequence Conformance (In-Order Traversal)
|
||||
|
||||
public struct Iterator: IteratorProtocol {
|
||||
private var stack: [Node] = []
|
||||
|
||||
// Marked as private because Node is a private type.
|
||||
fileprivate init(root: Node?) {
|
||||
var current = root
|
||||
while let node = current {
|
||||
stack.append(node)
|
||||
current = node.left
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func next() -> (Key, Value)? {
|
||||
if stack.isEmpty { return nil }
|
||||
let node = stack.removeLast()
|
||||
let result = (node.key, node.value)
|
||||
var current = node.right
|
||||
while let n = current {
|
||||
stack.append(n)
|
||||
current = n.left
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public func makeIterator() -> Iterator {
|
||||
return Iterator(root: root)
|
||||
}
|
||||
}
|
||||
571
SideStore/Utils/misc/xcmapping-diff-reporter/xcmapping-diff.py
Normal file
571
SideStore/Utils/misc/xcmapping-diff-reporter/xcmapping-diff.py
Normal file
@@ -0,0 +1,571 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Usage:
|
||||
python xml_diff.py old.xml new.xml
|
||||
|
||||
This script recursively compares two XML files and captures differences using
|
||||
a “node path” notation. It categorizes errors into types such as "mismatch",
|
||||
"missing node", and "extra node". For mismatches, the category is set to
|
||||
"mismatching in old.xml and new.xml". At the end, the script prints all errors
|
||||
grouped by error type.
|
||||
|
||||
For repeated nodes (for example, <object> elements), a specialized key is computed
|
||||
(ignoring internal idrefs) so that the report uses a human‐friendly description.
|
||||
It also ignores comparing any attribute named "sourcemodeldata" (or "destinationmodeldata").
|
||||
"""
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
|
||||
# Constants for ignore lists, categories, and error types
|
||||
IGNORED_ATTRIBUTES = {"sourcemodeldata", "destinationmodeldata", "id", "idrefs", "mappingnumber"}
|
||||
CATEGORY_EXTRA = "extra in new.xml"
|
||||
CATEGORY_MISSING = "missing in new.xml while present in old.xml"
|
||||
CATEGORY_MISMATCH = "mismatching in old.xml and new.xml"
|
||||
TYPE_MISMATCH = "mismatch"
|
||||
TYPE_MISSING = "missing node"
|
||||
TYPE_EXTRA = "extra node"
|
||||
|
||||
|
||||
def append_diff(diffs, diff_type, diff_info):
|
||||
"""
|
||||
Central method to add a difference to the diffs list.
|
||||
This allows setting a single breakpoint to catch all diff additions.
|
||||
"""
|
||||
diffs.append((diff_type, diff_info))
|
||||
# You can add a breakpoint here to inspect all diffs as they're recorded
|
||||
return diffs
|
||||
|
||||
|
||||
def get_object_key(elem):
|
||||
"""Create a unique key for an <object> node based on its type and attributes."""
|
||||
typ = elem.get("type")
|
||||
|
||||
if typ == "XDDEVENTITYMAPPING":
|
||||
return create_event_entity_mapping_key(elem)
|
||||
elif typ in ("XDDEVATTRIBUTEMAPPING", "XDDEVRELATIONSHIPMAPPING"):
|
||||
return create_attribute_or_relationship_mapping_key(elem)
|
||||
elif typ == "XDDEVMAPPINGMODEL":
|
||||
return create_mapping_model_key(elem)
|
||||
|
||||
return (elem.tag,)
|
||||
|
||||
|
||||
def create_event_entity_mapping_key(elem):
|
||||
"""Create a key for XDDEVENTITYMAPPING objects."""
|
||||
sourcename = elem.find("attribute[@name='sourcename']")
|
||||
destinationname = elem.find("attribute[@name='destinationname']")
|
||||
mappingtypename = elem.find("attribute[@name='mappingtypename']")
|
||||
|
||||
return (
|
||||
"XDDEVENTITYMAPPING",
|
||||
sourcename.text.strip() if sourcename is not None else "",
|
||||
destinationname.text.strip() if destinationname is not None else "",
|
||||
mappingtypename.text.strip() if mappingtypename is not None else "",
|
||||
)
|
||||
|
||||
|
||||
def create_attribute_or_relationship_mapping_key(elem):
|
||||
"""Create a key for XDDEVATTRIBUTEMAPPING or XDDEVRELATIONSHIPMAPPING objects."""
|
||||
name_elem = elem.find("attribute[@name='name']")
|
||||
name_val = name_elem.text.strip() if name_elem is not None else ""
|
||||
return (elem.get("type"), name_val)
|
||||
|
||||
|
||||
def create_mapping_model_key(elem):
|
||||
"""Create a key for XDDEVMAPPINGMODEL objects."""
|
||||
sourcemodelpath = elem.find("attribute[@name='sourcemodelpath']")
|
||||
path_val = sourcemodelpath.text.strip() if sourcemodelpath is not None else ""
|
||||
return ("XDDEVMAPPINGMODEL", path_val)
|
||||
|
||||
|
||||
def format_object_key(key):
|
||||
"""Format an object key into a human-readable string."""
|
||||
if key[0] == "XDDEVENTITYMAPPING":
|
||||
return f"destination name: {key[2]}, mappingTypename: {key[3]}"
|
||||
elif key[0] in ("XDDEVATTRIBUTEMAPPING", "XDDEVRELATIONSHIPMAPPING"):
|
||||
return f"name: {key[1]}"
|
||||
elif key[0] == "XDDEVMAPPINGMODEL":
|
||||
return f"sourcemodelpath: {key[1]}"
|
||||
|
||||
return str(key)
|
||||
|
||||
|
||||
def format_element(elem):
|
||||
"""Format an element as a human-readable string."""
|
||||
if elem.tag == "object":
|
||||
typ = elem.get("type")
|
||||
if typ == "XDDEVENTITYMAPPING":
|
||||
return format_event_entity_mapping(elem)
|
||||
|
||||
key = get_object_key(elem)
|
||||
return f"[{format_object_key(key)}]"
|
||||
|
||||
text = (elem.text or "").strip()
|
||||
return f"{elem.tag}: '{text}'" if text else elem.tag
|
||||
|
||||
|
||||
def format_event_entity_mapping(elem):
|
||||
"""Format an XDDEVENTITYMAPPING element."""
|
||||
dest = elem.find("attribute[@name='destinationname']")
|
||||
mappingtypename = elem.find("attribute[@name='mappingtypename']")
|
||||
dest_val = dest.text.strip() if dest is not None else ""
|
||||
mappingtypename_val = mappingtypename.text.strip() if mappingtypename is not None else ""
|
||||
return f"{{ destination name: {dest_val}, mappingTypename: {mappingtypename_val} }}"
|
||||
|
||||
|
||||
def get_element_value(elem):
|
||||
"""Get the text value of an element if it has no children."""
|
||||
return (elem.text or "").strip() if len(list(elem)) == 0 else ""
|
||||
|
||||
|
||||
def check_missing_nodes(old_elem, new_elem, path):
|
||||
"""Check for missing or extra nodes between old and new XML."""
|
||||
diffs = []
|
||||
|
||||
if old_elem is None and new_elem is None:
|
||||
return diffs, False
|
||||
|
||||
# Handle extra node (in new but not in old)
|
||||
if old_elem is None:
|
||||
formatted_elem = format_element(new_elem)
|
||||
value = get_element_value(new_elem)
|
||||
diff_info = {
|
||||
"category": CATEGORY_EXTRA,
|
||||
"path": path,
|
||||
"new": formatted_elem
|
||||
}
|
||||
if value:
|
||||
diff_info["value"] = value
|
||||
append_diff(diffs, TYPE_EXTRA, diff_info)
|
||||
return diffs, False
|
||||
|
||||
# Handle missing node (in old but not in new)
|
||||
if new_elem is None:
|
||||
formatted_elem = format_element(old_elem)
|
||||
value = get_element_value(old_elem)
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISSING,
|
||||
"path": path,
|
||||
"old": formatted_elem
|
||||
}
|
||||
if value:
|
||||
diff_info["value"] = value
|
||||
append_diff(diffs, TYPE_MISSING, diff_info)
|
||||
return diffs, False
|
||||
|
||||
return diffs, True
|
||||
|
||||
|
||||
def compare_tags(old_elem, new_elem, path):
|
||||
"""Compare tags of two elements."""
|
||||
diffs = []
|
||||
if old_elem.tag != new_elem.tag:
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISMATCH,
|
||||
"path": f"{path} tag",
|
||||
"old": old_elem.tag,
|
||||
"new": new_elem.tag,
|
||||
"type": "tag"
|
||||
}
|
||||
append_diff(diffs, TYPE_MISMATCH, diff_info)
|
||||
return diffs
|
||||
|
||||
|
||||
def compare_text(old_elem, new_elem, path):
|
||||
"""Compare text content of two elements."""
|
||||
diffs = []
|
||||
|
||||
# Skip comparisons for mappingnumber attributes
|
||||
if old_elem.tag == "attribute" and old_elem.get("name") == "mappingnumber":
|
||||
return diffs
|
||||
|
||||
text_old = (old_elem.text or "").strip()
|
||||
text_new = (new_elem.text or "").strip()
|
||||
|
||||
if text_old != text_new:
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISMATCH,
|
||||
"path": f"{path} text",
|
||||
"old": text_old,
|
||||
"new": text_new,
|
||||
"type": "text"
|
||||
}
|
||||
append_diff(diffs, TYPE_MISMATCH, diff_info)
|
||||
return diffs
|
||||
|
||||
|
||||
def compare_attributes(old_elem, new_elem, path):
|
||||
"""Compare attributes of two elements, ignoring specified attributes."""
|
||||
diffs = []
|
||||
all_attrs = set(old_elem.attrib.keys()).union(new_elem.attrib.keys())
|
||||
|
||||
for attr in [x for x in all_attrs if x not in IGNORED_ATTRIBUTES]:
|
||||
val_old = old_elem.attrib.get(attr)
|
||||
val_new = new_elem.attrib.get(attr)
|
||||
|
||||
if val_old != val_new:
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISMATCH,
|
||||
"path": f"{path} attribute '{attr}'",
|
||||
"old": val_old,
|
||||
"new": val_new,
|
||||
"type": "attribute"
|
||||
}
|
||||
append_diff(diffs, TYPE_MISMATCH, diff_info)
|
||||
return diffs
|
||||
|
||||
|
||||
def handle_plist_keys(old_list, new_list, path, tag):
|
||||
"""Handle special comparison for <key> elements in a plist dict."""
|
||||
diffs = []
|
||||
old_keys = [(child.text or "").strip() for child in old_list]
|
||||
new_keys = [(child.text or "").strip() for child in new_list]
|
||||
|
||||
if set(old_keys) == set(new_keys):
|
||||
return diffs
|
||||
|
||||
missing = [k for k in old_keys if k not in new_keys]
|
||||
extra = [k for k in new_keys if k not in old_keys]
|
||||
|
||||
# Handle case where exactly one key is changed
|
||||
if len(missing) == 1 and len(extra) == 1:
|
||||
index = old_keys.index(missing[0])
|
||||
subpath = f"{path}.{tag}[{index}]"
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISMATCH,
|
||||
"path": f"{subpath} text",
|
||||
"old": missing[0],
|
||||
"new": extra[0],
|
||||
"type": "text"
|
||||
}
|
||||
append_diff(diffs, TYPE_MISMATCH, diff_info)
|
||||
else:
|
||||
# Handle missing keys
|
||||
for k in missing:
|
||||
index = old_keys.index(k)
|
||||
subpath = f"{path}.{tag}[{index}]"
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISSING,
|
||||
"path": f"{subpath} text",
|
||||
"old": k,
|
||||
"value": k
|
||||
}
|
||||
append_diff(diffs, TYPE_MISSING, diff_info)
|
||||
|
||||
# Handle extra keys
|
||||
for k in extra:
|
||||
index = new_keys.index(k)
|
||||
subpath = f"{path}.{tag}[{index}]"
|
||||
diff_info = {
|
||||
"category": CATEGORY_EXTRA,
|
||||
"path": f"{subpath} text",
|
||||
"new": k,
|
||||
"value": k
|
||||
}
|
||||
append_diff(diffs, TYPE_EXTRA, diff_info)
|
||||
|
||||
return diffs
|
||||
|
||||
|
||||
def handle_object_nodes(old_list, new_list, path):
|
||||
"""Compare <object> nodes by their computed key."""
|
||||
diffs = []
|
||||
dict_old = {get_object_key(child): child for child in old_list}
|
||||
dict_new = {get_object_key(child): child for child in new_list}
|
||||
|
||||
for key in set(dict_old.keys()).union(dict_new.keys()):
|
||||
subpath = f"{path}.object[{format_object_key(key)}]"
|
||||
child_old = dict_old.get(key)
|
||||
child_new = dict_new.get(key)
|
||||
|
||||
if child_old is None:
|
||||
# Object exists in new but not in old
|
||||
diff_info = {
|
||||
"category": CATEGORY_EXTRA,
|
||||
"path": subpath,
|
||||
"new": format_element(child_new)
|
||||
}
|
||||
append_diff(diffs, TYPE_EXTRA, diff_info)
|
||||
elif child_new is None:
|
||||
# Object exists in old but not in new
|
||||
diff_info = {
|
||||
"category": CATEGORY_MISSING,
|
||||
"path": subpath,
|
||||
"old": format_element(child_old)
|
||||
}
|
||||
append_diff(diffs, TYPE_MISSING, diff_info)
|
||||
else:
|
||||
# Object exists in both, compare them recursively
|
||||
diffs.extend(diff_elements(child_old, child_new, subpath))
|
||||
|
||||
return diffs
|
||||
|
||||
|
||||
def handle_default_children(old_list, new_list, path, tag):
|
||||
"""Default comparison of children (by order)."""
|
||||
diffs = []
|
||||
max_len = max(len(old_list), len(new_list))
|
||||
|
||||
for i in range(max_len):
|
||||
# Use a simpler path if there's only one child
|
||||
subpath = f"{path}.{tag}" if max_len == 1 else f"{path}.{tag}[{i}]"
|
||||
child_old = old_list[i] if i < len(old_list) else None
|
||||
child_new = new_list[i] if i < len(new_list) else None
|
||||
|
||||
diffs.extend(diff_elements(child_old, child_new, subpath))
|
||||
|
||||
return diffs
|
||||
|
||||
|
||||
def filter_children(children):
|
||||
"""Filter out attribute nodes that should be ignored."""
|
||||
ignored_names = {"sourcemodeldata", "destinationmodeldata", "mappingnumber"}
|
||||
return [
|
||||
child for child in children
|
||||
if not (child.tag == "attribute" and child.get("name") in ignored_names)
|
||||
]
|
||||
|
||||
|
||||
def diff_children(old_children, new_children, path):
|
||||
"""Compare lists of child elements by grouping them by tag."""
|
||||
diffs = []
|
||||
|
||||
# Filter out children that should be ignored
|
||||
old_children = filter_children(old_children)
|
||||
new_children = filter_children(new_children)
|
||||
|
||||
# Group children by tag
|
||||
groups_old = group_by_tag(old_children)
|
||||
groups_new = group_by_tag(new_children)
|
||||
|
||||
# Compare each group of tags
|
||||
for tag in set(groups_old.keys()).union(groups_new.keys()):
|
||||
old_list = groups_old.get(tag, [])
|
||||
new_list = groups_new.get(tag, [])
|
||||
|
||||
# Use special handling for certain cases
|
||||
if tag == "key" and ".plist.dict" in path:
|
||||
diffs.extend(handle_plist_keys(old_list, new_list, path, tag))
|
||||
elif tag == "object":
|
||||
diffs.extend(handle_object_nodes(old_list, new_list, path))
|
||||
else:
|
||||
diffs.extend(handle_default_children(old_list, new_list, path, tag))
|
||||
|
||||
return diffs
|
||||
|
||||
|
||||
def group_by_tag(elements):
|
||||
"""Group a list of elements by their tag."""
|
||||
groups = {}
|
||||
for element in elements:
|
||||
groups.setdefault(element.tag, []).append(element)
|
||||
return groups
|
||||
|
||||
|
||||
def diff_elements(old_elem, new_elem, path):
|
||||
"""Recursively compare two elements and return a list of differences."""
|
||||
diffs = []
|
||||
|
||||
# Check for missing or extra nodes
|
||||
missing_diffs, both_present = check_missing_nodes(old_elem, new_elem, path)
|
||||
diffs.extend(missing_diffs)
|
||||
if not both_present:
|
||||
return diffs
|
||||
|
||||
# Compare tag names
|
||||
tag_diffs = compare_tags(old_elem, new_elem, path)
|
||||
if tag_diffs:
|
||||
diffs.extend(tag_diffs)
|
||||
return diffs # If tags differ, no need to go further
|
||||
|
||||
# Compare text content and attributes
|
||||
diffs.extend(compare_text(old_elem, new_elem, path))
|
||||
diffs.extend(compare_attributes(old_elem, new_elem, path))
|
||||
|
||||
# Compare child elements
|
||||
diffs.extend(diff_children(list(old_elem), list(new_elem), path))
|
||||
|
||||
return diffs
|
||||
|
||||
|
||||
def normalize_path(path):
|
||||
"""Make paths more readable by using curly braces for object keys."""
|
||||
if "object[" in path:
|
||||
path = path.replace("object[", "object.{")
|
||||
if path.endswith("]"):
|
||||
path = path[:-1] + "}"
|
||||
return path
|
||||
|
||||
|
||||
def print_differences(diffs):
|
||||
"""Print all found differences in a readable format."""
|
||||
if not diffs:
|
||||
print("No differences found.")
|
||||
return
|
||||
|
||||
# Group differences by error type
|
||||
error_dict = defaultdict(list)
|
||||
for err_type, msg in diffs:
|
||||
error_dict[err_type].append(msg)
|
||||
|
||||
print("\nDifferences found (grouped by error type):")
|
||||
|
||||
# Print each type of difference
|
||||
for err_type in sorted(error_dict.keys()):
|
||||
messages = error_dict[err_type]
|
||||
|
||||
if err_type == TYPE_MISMATCH:
|
||||
print_mismatches(messages)
|
||||
elif err_type == TYPE_MISSING:
|
||||
print_missing_nodes(messages)
|
||||
elif err_type == TYPE_EXTRA:
|
||||
print_extra_nodes(messages)
|
||||
else:
|
||||
print_other_diffs(err_type, messages)
|
||||
|
||||
|
||||
def print_mismatches(messages):
|
||||
"""Print all mismatched elements."""
|
||||
print(f"\n=== MISMATCH ({len(messages)} occurrence{'s' if len(messages) != 1 else ''}) ===")
|
||||
print("Category: mismatching in old.xml and new.xml\n")
|
||||
|
||||
for msg in messages:
|
||||
print(f"Path: {msg['path']}")
|
||||
print(f" old = {msg['old']}")
|
||||
print(f" new = {msg['new']}\n")
|
||||
|
||||
|
||||
def print_missing_nodes(messages):
|
||||
"""Print all nodes missing in the new XML."""
|
||||
print(f"\n=== MISSING NODE ({len(messages)} occurrence{'s' if len(messages) != 1 else ''}) ===")
|
||||
print(f"Category: {messages[0]['category']}\n")
|
||||
|
||||
sorted_msgs = sorted(messages, key=lambda m: normalize_path(m["path"]))
|
||||
for msg in sorted_msgs:
|
||||
print(f"Path: {normalize_path(msg['path'])}")
|
||||
if "value" in msg and msg["value"]:
|
||||
print(f" old = {msg['value']}")
|
||||
print("")
|
||||
|
||||
|
||||
def print_extra_nodes(messages):
|
||||
"""Print all extra nodes in the new XML."""
|
||||
print(f"\n=== EXTRA NODE ({len(messages)} occurrence{'s' if len(messages) != 1 else ''}) ===")
|
||||
print(f"Category: {messages[0]['category']}\n")
|
||||
|
||||
sorted_msgs = sorted(messages, key=lambda m: normalize_path(m["path"]))
|
||||
for msg in sorted_msgs:
|
||||
print(f"Path: {normalize_path(msg['path'])}")
|
||||
if "value" in msg and msg["value"]:
|
||||
print(f" new = {msg['value']}")
|
||||
print("")
|
||||
|
||||
|
||||
def print_other_diffs(err_type, messages):
|
||||
"""Print any other types of differences."""
|
||||
print(f"\n=== {err_type.upper()} ({len(messages)} occurrence{'s' if len(messages) != 1 else ''}) ===")
|
||||
for msg in messages:
|
||||
print(msg)
|
||||
print("")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to compare two XML files."""
|
||||
try:
|
||||
tree_old = ET.parse("old.xml")
|
||||
tree_new = ET.parse("new.xml")
|
||||
except Exception as e:
|
||||
sys.exit(f"Error parsing XML files: {e}")
|
||||
|
||||
root_old = tree_old.getroot()
|
||||
root_new = tree_new.getroot()
|
||||
|
||||
# Find all differences between the two XML files
|
||||
diffs = diff_elements(root_old, root_new, root_old.tag)
|
||||
|
||||
# Print the differences in a readable format
|
||||
print_differences(diffs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
||||
### what is this for:
|
||||
'''
|
||||
1. when a model version is updated without regenerating the xcmappingmodel, the associated xcmappingmodel becomes out-of-date and coredata will ignore it even if it is present in the app bundle
|
||||
2. to prevent this from happenning, one needs to regenerated the xcmappingmodel via xcode editor (File -> New -> New from template -> Mapping Model)
|
||||
3. after regenerating the mappingmodel, one can use this utility to report the diff between old and new xcmappingmodel files so that one can be assured that regeneration of the xcmappingmodel
|
||||
doesn't leave out any customizations that were present in the old xcmappingmodel ex: custom migration classes for entities etc.
|
||||
'''
|
||||
|
||||
### How to invoke or use:
|
||||
'''
|
||||
python SideStore/Utils/misc/xcmapping-diff-reporter/xcmapping-diff.py old.xcmappingmodel new.xcmappingmodel
|
||||
|
||||
|
||||
// sample output
|
||||
SideStore (develop-alpha) ✗ python SideStore/Utils/misc/xcmapping-diff-reporter/xcmapping-diff.py old.xcmappingmodel new.xcmappingmodel
|
||||
|
||||
Differences found (grouped by error type):
|
||||
|
||||
=== EXTRA NODE (16 occurrences) ===
|
||||
Category: extra in new.xml
|
||||
|
||||
new = database.databaseInfo.metadata.plist.dict.key[1] text
|
||||
new = database.databaseInfo.metadata.plist.dict.string[1]
|
||||
new = database.object.{destination name: Account, mappingTypename: Undefined, mappingnumber: 2}
|
||||
new = database.object.{destination name: AppID, mappingTypename: Undefined, mappingnumber: 1}
|
||||
new = database.object.{destination name: AppPermission, mappingTypename: Undefined, mappingnumber: 12}
|
||||
new = database.object.{destination name: AppVersion, mappingTypename: Undefined, mappingnumber: 5}
|
||||
new = database.object.{destination name: InstalledApp, mappingTypename: Undefined, mappingnumber: 8}
|
||||
new = database.object.{destination name: InstalledExtension, mappingTypename: Undefined, mappingnumber: 11}
|
||||
new = database.object.{destination name: LoggedError, mappingTypename: Undefined, mappingnumber: 14}
|
||||
new = database.object.{destination name: PatreonAccount, mappingTypename: Undefined, mappingnumber: 10}
|
||||
new = database.object.{destination name: Patron, mappingTypename: Undefined, mappingnumber: 3}
|
||||
new = database.object.{destination name: RefreshAttempt, mappingTypename: Undefined, mappingnumber: 13}
|
||||
new = database.object.{destination name: Source, mappingTypename: Undefined, mappingnumber: 9}
|
||||
new = database.object.{destination name: StoreApp, mappingTypename: Undefined, mappingnumber: 6}
|
||||
new = database.object.{destination name: Team, mappingTypename: Undefined, mappingnumber: 4}
|
||||
new = database.object.{name: hasUpdate}
|
||||
|
||||
=== MISMATCH (4 occurrences) ===
|
||||
Category: mismatching in old.xml and new.xml
|
||||
|
||||
Path: database.databaseInfo.nextObjectID text
|
||||
old = 242
|
||||
new = 243
|
||||
|
||||
Path: database.databaseInfo.UUID text
|
||||
old = 53991141-FED9-4F4C-8444-9076589DBD8B
|
||||
new = E471EA1B-4480-40F0-BA79-DA9311928124
|
||||
|
||||
Path: database.databaseInfo.metadata.plist.dict.integer[0] text
|
||||
old = 1244
|
||||
new = 1419
|
||||
|
||||
Path: database.databaseInfo.metadata.plist.dict.string[0] text
|
||||
old = +Hmc2uYZK6og+Pvx5GUJ7oW75UG4V/ksQanTjfTKUnxyGWJRMtB5tIRgVwGsrd7lz/QR57++wbvWsr6nxwyS0A==
|
||||
new = bMpud663vz0bXQE24C6Rh4MvJ5jVnzsD2sI3njZkKbc=
|
||||
|
||||
|
||||
=== MISSING NODE (13 occurrences) ===
|
||||
Category: missing in new.xml while present in old.xml
|
||||
|
||||
old = database.object.{destination name: Account, mappingTypename: Undefined, mappingnumber: 4}
|
||||
old = database.object.{destination name: AppID, mappingTypename: Undefined, mappingnumber: 11}
|
||||
old = database.object.{destination name: AppPermission, mappingTypename: Undefined, mappingnumber: 3}
|
||||
old = database.object.{destination name: AppVersion, mappingTypename: Undefined, mappingnumber: 10}
|
||||
old = database.object.{destination name: InstalledApp, mappingTypename: Undefined, mappingnumber: 2}
|
||||
old = database.object.{destination name: InstalledExtension, mappingTypename: Undefined, mappingnumber: 1}
|
||||
old = database.object.{destination name: LoggedError, mappingTypename: Undefined, mappingnumber: 8}
|
||||
old = database.object.{destination name: PatreonAccount, mappingTypename: Undefined, mappingnumber: 6}
|
||||
old = database.object.{destination name: Patron, mappingTypename: Undefined, mappingnumber: 9}
|
||||
old = database.object.{destination name: RefreshAttempt, mappingTypename: Undefined, mappingnumber: 5}
|
||||
old = database.object.{destination name: Source, mappingTypename: Undefined, mappingnumber: 12}
|
||||
old = database.object.{destination name: StoreApp, mappingTypename: Undefined, mappingnumber: 14}
|
||||
old = database.object.{destination name: Team, mappingTypename: Undefined, mappingnumber: 13}
|
||||
|
||||
'''
|
||||
301
SideStore/Views/UIKit/CollapsingMarkdownView.swift
Normal file
301
SideStore/Views/UIKit/CollapsingMarkdownView.swift
Normal file
@@ -0,0 +1,301 @@
|
||||
//
|
||||
// CollapsingMarkdownView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 27/02/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import UIKit
|
||||
import MarkdownKit
|
||||
|
||||
struct MarkdownManager
|
||||
{
|
||||
struct Fonts{
|
||||
static let body: UIFont = .systemFont(ofSize: UIFont.systemFontSize)
|
||||
// static let body: UIFont = .systemFont(ofSize: UIFont.labelFontSize)
|
||||
|
||||
static let header: UIFont = .boldSystemFont(ofSize: 14)
|
||||
static let list: UIFont = .systemFont(ofSize: 14)
|
||||
static let bold: UIFont = .boldSystemFont(ofSize: 14)
|
||||
static let italic: UIFont = .italicSystemFont(ofSize: 14)
|
||||
static let quote: UIFont = .italicSystemFont(ofSize: 14)
|
||||
}
|
||||
|
||||
struct Color{
|
||||
static let header = UIColor { traitCollection in
|
||||
traitCollection.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
|
||||
}
|
||||
static let bold = UIColor { traitCollection in
|
||||
traitCollection.userInterfaceStyle == .dark ? UIColor.lightText : UIColor.darkText
|
||||
}
|
||||
}
|
||||
|
||||
static var enabledElements: MarkdownParser.EnabledElements {
|
||||
[
|
||||
.header,
|
||||
.list,
|
||||
.quote,
|
||||
.code,
|
||||
.link,
|
||||
.bold,
|
||||
.italic,
|
||||
]
|
||||
}
|
||||
|
||||
var markdownParser: MarkdownParser {
|
||||
MarkdownParser(
|
||||
font: Self.Fonts.body,
|
||||
color: Self.Color.bold
|
||||
)
|
||||
}
|
||||
}
|
||||
final class CollapsingMarkdownView: UIView {
|
||||
/// Called when the collapse state toggles.
|
||||
var didToggleCollapse: (() -> Void)?
|
||||
|
||||
// MARK: - Properties
|
||||
var isCollapsed = true {
|
||||
didSet {
|
||||
guard self.isCollapsed != oldValue else { return }
|
||||
self.updateCollapsedState()
|
||||
}
|
||||
}
|
||||
|
||||
var maximumNumberOfLines = 3 {
|
||||
didSet {
|
||||
self.checkIfNeedsCollapsing()
|
||||
self.updateCollapsedState()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var text: String = "" {
|
||||
didSet {
|
||||
self.updateMarkdownContent()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var lineSpacing: Double = 2 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
let toggleButton = UIButton(type: .system)
|
||||
|
||||
private let textView = UITextView()
|
||||
private let markdownParser = MarkdownManager().markdownParser
|
||||
|
||||
private var previousSize: CGSize?
|
||||
private var actualLineCount: Int = 0
|
||||
private var needsCollapsing = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
initialize()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
initialize()
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
initialize()
|
||||
}
|
||||
|
||||
private func checkIfNeedsCollapsing() {
|
||||
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
|
||||
needsCollapsing = false
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate the number of lines in the text
|
||||
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
||||
let lineHeight = font.lineHeight
|
||||
|
||||
// Safely calculate actual line count
|
||||
actualLineCount = max(1, Int(ceil(textSize.height / lineHeight)))
|
||||
|
||||
// Only needs collapsing if actual lines exceed the maximum
|
||||
needsCollapsing = actualLineCount > maximumNumberOfLines
|
||||
|
||||
// Update button visibility
|
||||
toggleButton.isHidden = !needsCollapsing
|
||||
}
|
||||
|
||||
private func updateCollapsedState() {
|
||||
// Disable animations for this update
|
||||
UIView.performWithoutAnimation {
|
||||
// Update the button title
|
||||
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
|
||||
toggleButton.setTitle(title, for: .normal)
|
||||
|
||||
// Set max lines based on collapsed state
|
||||
if isCollapsed && needsCollapsing {
|
||||
textView.textContainer.maximumNumberOfLines = maximumNumberOfLines
|
||||
} else {
|
||||
textView.textContainer.maximumNumberOfLines = 0
|
||||
}
|
||||
|
||||
// Button is only visible if content needs collapsing
|
||||
toggleButton.isHidden = !needsCollapsing
|
||||
|
||||
// Force layout updates
|
||||
textView.layoutIfNeeded()
|
||||
self.layoutIfNeeded()
|
||||
self.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func initialize() {
|
||||
// Configure text view
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.textContainerInset = .zero
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.textContainer.lineBreakMode = .byTruncatingTail
|
||||
textView.backgroundColor = .clear
|
||||
|
||||
// Make textView selectable to enable link interactions
|
||||
textView.isSelectable = true
|
||||
textView.delegate = self
|
||||
|
||||
// Important: This prevents selection handles from appearing
|
||||
textView.dataDetectorTypes = .link
|
||||
|
||||
// Configure markdown parser
|
||||
configureMarkdownParser()
|
||||
|
||||
// Add subviews
|
||||
addSubview(textView)
|
||||
|
||||
// Configure toggle button
|
||||
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
|
||||
addSubview(toggleButton)
|
||||
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
private func configureMarkdownParser() {
|
||||
// Configure markdown parser with desired settings
|
||||
markdownParser.enabledElements = MarkdownManager.enabledElements
|
||||
|
||||
// You can also customize the styling if needed
|
||||
markdownParser.header.font = MarkdownManager.Fonts.header
|
||||
markdownParser.list.font = MarkdownManager.Fonts.list
|
||||
markdownParser.bold.font = MarkdownManager.Fonts.bold
|
||||
markdownParser.italic.font = MarkdownManager.Fonts.italic
|
||||
markdownParser.quote.font = MarkdownManager.Fonts.quote
|
||||
|
||||
markdownParser.header.color = MarkdownManager.Color.header
|
||||
markdownParser.bold.color = MarkdownManager.Color.bold
|
||||
markdownParser.list.color = MarkdownManager.Color.bold
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
// Calculate button height (for spacing)
|
||||
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
|
||||
|
||||
// Set textView frame to leave space for button
|
||||
textView.frame = CGRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: bounds.width,
|
||||
height: bounds.height - buttonHeight
|
||||
)
|
||||
|
||||
// Check if layout changed
|
||||
if previousSize?.width != bounds.width {
|
||||
checkIfNeedsCollapsing()
|
||||
updateCollapsedState()
|
||||
previousSize = bounds.size
|
||||
}
|
||||
|
||||
// Position toggle button at bottom right
|
||||
let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||
toggleButton.frame = CGRect(
|
||||
x: bounds.width - buttonSize.width,
|
||||
y: textView.frame.maxY,
|
||||
width: buttonSize.width,
|
||||
height: buttonHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func toggleCollapsed(_ sender: UIButton) {
|
||||
isCollapsed.toggle()
|
||||
didToggleCollapse?()
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
||||
}
|
||||
|
||||
let lineHeight = font.lineHeight
|
||||
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
|
||||
|
||||
// Always add button height to reserve space for it
|
||||
if isCollapsed && needsCollapsing {
|
||||
// When collapsed and needs collapsing, use maximumNumberOfLines
|
||||
let collapsedHeight = lineHeight * CGFloat(maximumNumberOfLines) +
|
||||
lineSpacing * CGFloat(max(0, maximumNumberOfLines - 1))
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: collapsedHeight + buttonHeight)
|
||||
} else if !needsCollapsing {
|
||||
// Text is shorter than max lines - use actual text height
|
||||
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
|
||||
} else {
|
||||
// When expanded and needs collapsing, use full text height plus button
|
||||
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Markdown Processing
|
||||
private func updateMarkdownContent() {
|
||||
let attributedString = markdownParser.parse(text)
|
||||
|
||||
// Apply line spacing
|
||||
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineSpacing = lineSpacing
|
||||
|
||||
mutableAttributedString.addAttribute(
|
||||
.paragraphStyle,
|
||||
value: paragraphStyle,
|
||||
range: NSRange(location: 0, length: mutableAttributedString.length)
|
||||
)
|
||||
|
||||
textView.attributedText = mutableAttributedString
|
||||
|
||||
// Check if content needs collapsing after setting text
|
||||
checkIfNeedsCollapsing()
|
||||
updateCollapsedState()
|
||||
}
|
||||
}
|
||||
|
||||
extension CollapsingMarkdownView: UITextViewDelegate {
|
||||
// This enables tapping on links while preventing text selection
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
// Open the URL using UIApplication
|
||||
UIApplication.shared.open(URL)
|
||||
return false // Return false to prevent the default behavior
|
||||
}
|
||||
|
||||
// This prevents text selection
|
||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
textView.selectedTextRange = nil
|
||||
}
|
||||
}
|
||||
137
update_apps.py
137
update_apps.py
@@ -10,6 +10,7 @@ import sys
|
||||
VERSION_IPA = os.getenv("VERSION_IPA")
|
||||
VERSION_DATE = os.getenv("VERSION_DATE")
|
||||
IS_BETA = os.getenv("IS_BETA")
|
||||
RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL")
|
||||
SIZE = os.getenv("SIZE")
|
||||
SHA256 = os.getenv("SHA256")
|
||||
LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION")
|
||||
@@ -21,6 +22,7 @@ BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER")
|
||||
# VERSION_IPA = os.getenv("VERSION_IPA", "0.0.0")
|
||||
# VERSION_DATE = os.getenv("VERSION_DATE", "2000-12-18T00:00:00Z")
|
||||
# IS_BETA = os.getenv("IS_BETA", True)
|
||||
# RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL", "alpha")
|
||||
# SIZE = int(os.getenv("SIZE", "0")) # Convert to integer
|
||||
# SHA256 = os.getenv("SHA256", "")
|
||||
# LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION", "Invalid Update")
|
||||
@@ -40,6 +42,7 @@ print("Bundle Identifier:", BUNDLE_IDENTIFIER)
|
||||
print("Version:", VERSION_IPA)
|
||||
print("Version Date:", VERSION_DATE)
|
||||
print("IsBeta:", IS_BETA)
|
||||
print("ReleaseChannel:", RELEASE_CHANNEL)
|
||||
print("Size:", SIZE)
|
||||
print("Sha256:", SHA256)
|
||||
print("Localized Description:", LOCALIZED_DESCRIPTION)
|
||||
@@ -63,6 +66,7 @@ except Exception as e:
|
||||
if (not BUNDLE_IDENTIFIER or
|
||||
not VERSION_IPA or
|
||||
not VERSION_DATE or
|
||||
not RELEASE_CHANNEL or
|
||||
not SIZE or
|
||||
not SHA256 or
|
||||
not LOCALIZED_DESCRIPTION or
|
||||
@@ -76,45 +80,106 @@ SIZE = int(SIZE)
|
||||
# Process the JSON data
|
||||
updated = False
|
||||
|
||||
apps = data.get("apps", [])
|
||||
appsToUpdate = [app for app in apps if app.get("bundleIdentifier") == BUNDLE_IDENTIFIER]
|
||||
if len(appsToUpdate) == 0:
|
||||
# apps = data.get("apps", [])
|
||||
# appsToUpdate = [app for app in apps if app.get("bundleIdentifier") == BUNDLE_IDENTIFIER]
|
||||
# if len(appsToUpdate) == 0:
|
||||
# print("No app with the specified bundle identifier found.")
|
||||
# sys.exit(1)
|
||||
|
||||
# if len(appsToUpdate) > 1:
|
||||
# print(f"Multiple apps with same `bundleIdentifier` = ${BUNDLE_IDENTIFIER} are not allowed!")
|
||||
# sys.exit(1)
|
||||
|
||||
# app = appsToUpdate[0]
|
||||
# # Update app-level metadata for store front page
|
||||
# app.update({
|
||||
# "beta": IS_BETA,
|
||||
# })
|
||||
|
||||
# versions = app.get("versions", [])
|
||||
|
||||
# versionIfExists = [version for version in versions if version == VERSION_IPA]
|
||||
# if versionIfExists: # current version is a duplicate, so reject it
|
||||
# print(f"`version` = ${VERSION_IPA} already exists!, new build cannot have an existing version, Aborting!")
|
||||
# sys.exit(1)
|
||||
|
||||
# # create an entry and keep ready
|
||||
# new_version = {
|
||||
# "version": VERSION_IPA,
|
||||
# "date": VERSION_DATE,
|
||||
# "localizedDescription": LOCALIZED_DESCRIPTION,
|
||||
# "downloadURL": DOWNLOAD_URL,
|
||||
# "size": SIZE,
|
||||
# "sha256": SHA256,
|
||||
# }
|
||||
|
||||
# if versions is []:
|
||||
# versions.append(new_version)
|
||||
# else:
|
||||
# # versions.insert(0, new_version) # insert at front
|
||||
# versions[0] = new_version # replace top one
|
||||
|
||||
|
||||
# make it lowecase
|
||||
RELEASE_CHANNEL = RELEASE_CHANNEL.lower()
|
||||
|
||||
version = data.get("version", 1)
|
||||
if int(version) < 2:
|
||||
print("Only v2 and above are supported for direct updates to sources.json on push")
|
||||
sys.exit(1)
|
||||
|
||||
for app in data.get("apps", []):
|
||||
if app.get("bundleIdentifier") == BUNDLE_IDENTIFIER:
|
||||
if RELEASE_CHANNEL == "stable" :
|
||||
# Update app-level metadata for store front page
|
||||
app.update({
|
||||
"version": VERSION_IPA,
|
||||
"versionDate": VERSION_DATE,
|
||||
"size": SIZE,
|
||||
"sha256": SHA256,
|
||||
"localizedDescription": LOCALIZED_DESCRIPTION,
|
||||
"downloadURL": DOWNLOAD_URL,
|
||||
})
|
||||
|
||||
# Process the versions array
|
||||
channels = app.get("releaseChannels", [])
|
||||
if not channels:
|
||||
app["releaseChannels"] = channels
|
||||
|
||||
# create an entry and keep ready
|
||||
new_version = {
|
||||
"version": VERSION_IPA,
|
||||
"date": VERSION_DATE,
|
||||
"localizedDescription": LOCALIZED_DESCRIPTION,
|
||||
"downloadURL": DOWNLOAD_URL,
|
||||
"size": SIZE,
|
||||
"sha256": SHA256,
|
||||
}
|
||||
|
||||
tracks = [track for track in channels if track.get("track") == RELEASE_CHANNEL]
|
||||
if len(tracks) > 1:
|
||||
print(f"Multiple tracks with same `track` name = ${RELEASE_CHANNEL} are not allowed!")
|
||||
sys.exit(1)
|
||||
|
||||
if not tracks:
|
||||
# there was no entries in this release channel so create one
|
||||
track = {
|
||||
"track": RELEASE_CHANNEL,
|
||||
"releases": [new_version]
|
||||
}
|
||||
channels.insert(0, track)
|
||||
else:
|
||||
track = tracks[0] # first result is the selected track
|
||||
# Update the existing TOP version object entry
|
||||
track["releases"][0] = new_version
|
||||
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
print("No app with the specified bundle identifier found.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(appsToUpdate) > 1:
|
||||
print(f"Multiple apps with same `bundleIdentifier` = ${BUNDLE_IDENTIFIER} are not allowed!")
|
||||
sys.exit(1)
|
||||
|
||||
app = appsToUpdate[0]
|
||||
# Update app-level metadata for store front page
|
||||
app.update({
|
||||
"beta": IS_BETA,
|
||||
})
|
||||
|
||||
versions = app.get("versions", [])
|
||||
|
||||
versionIfExists = [version for version in versions if version == VERSION_IPA]
|
||||
if versionIfExists: # current version is a duplicate, so reject it
|
||||
print(f"`version` = ${VERSION_IPA} already exists!, new build cannot have an existing version, Aborting!")
|
||||
sys.exit(1)
|
||||
|
||||
# create an entry and keep ready
|
||||
new_version = {
|
||||
"version": VERSION_IPA,
|
||||
"date": VERSION_DATE,
|
||||
"localizedDescription": LOCALIZED_DESCRIPTION,
|
||||
"downloadURL": DOWNLOAD_URL,
|
||||
"size": SIZE,
|
||||
"sha256": SHA256,
|
||||
}
|
||||
|
||||
if versions is []:
|
||||
versions.append(new_version)
|
||||
else:
|
||||
# versions.insert(0, new_version) # insert at front
|
||||
versions[0] = new_version # replace top one
|
||||
|
||||
# Save the updated JSON to the input file
|
||||
try:
|
||||
print("\nUpdated Sources File:\n")
|
||||
|
||||
381
update_release_notes.py
Normal file
381
update_release_notes.py
Normal file
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
IGNORED_AUTHORS = [
|
||||
|
||||
]
|
||||
|
||||
TAG_MARKER = "###"
|
||||
HEADER_MARKER = "####"
|
||||
|
||||
def run_command(cmd):
|
||||
"""Run a shell command and return its trimmed output."""
|
||||
return subprocess.check_output(cmd, shell=True, text=True).strip()
|
||||
|
||||
def get_head_commit():
|
||||
"""Return the HEAD commit SHA."""
|
||||
return run_command("git rev-parse HEAD")
|
||||
|
||||
def get_commit_messages(last_successful, current="HEAD"):
|
||||
"""Return a list of commit messages between last_successful and current."""
|
||||
cmd = f"git log {last_successful}..{current} --pretty=format:%s"
|
||||
output = run_command(cmd)
|
||||
if not output:
|
||||
return []
|
||||
return output.splitlines()
|
||||
|
||||
def get_authors_in_range(commit_range, fmt="%an"):
|
||||
"""Return a set of commit authors in the given commit range using the given format."""
|
||||
cmd = f"git log {commit_range} --pretty=format:{fmt}"
|
||||
output = run_command(cmd)
|
||||
if not output:
|
||||
return set()
|
||||
authors = set(line.strip() for line in output.splitlines() if line.strip())
|
||||
authors = set(authors) - set(IGNORED_AUTHORS)
|
||||
return authors
|
||||
|
||||
def get_first_commit_of_repo():
|
||||
"""Return the first commit in the repository (root commit)."""
|
||||
cmd = "git rev-list --max-parents=0 HEAD"
|
||||
output = run_command(cmd)
|
||||
return output.splitlines()[0]
|
||||
|
||||
def get_branch():
|
||||
"""
|
||||
Attempt to determine the branch base (the commit where the current branch diverged
|
||||
from the default remote branch). Falls back to the repo's first commit.
|
||||
"""
|
||||
try:
|
||||
default_ref = run_command("git rev-parse --abbrev-ref origin/HEAD")
|
||||
default_branch = default_ref.split('/')[-1]
|
||||
base_commit = run_command(f"git merge-base HEAD origin/{default_branch}")
|
||||
return base_commit
|
||||
except Exception:
|
||||
return get_first_commit_of_repo()
|
||||
|
||||
def get_repo_url():
|
||||
"""Extract and clean the repository URL from the remote 'origin'."""
|
||||
url = run_command("git config --get remote.origin.url")
|
||||
if url.startswith("git@"):
|
||||
url = url.replace("git@", "https://").replace(":", "/")
|
||||
if url.endswith(".git"):
|
||||
url = url[:-4]
|
||||
return url
|
||||
|
||||
def format_contributor(author):
|
||||
"""
|
||||
Convert an author name to a GitHub username or first name.
|
||||
If the author already starts with '@', return it;
|
||||
otherwise, take the first token and prepend '@'.
|
||||
"""
|
||||
if author.startswith('@'):
|
||||
return author
|
||||
return f"@{author.split()[0]}"
|
||||
|
||||
def format_commit_message(msg):
|
||||
"""Format a commit message as a bullet point for the release notes."""
|
||||
msg_clean = msg.lstrip() # remove leading spaces
|
||||
if msg_clean.startswith("-"):
|
||||
msg_clean = msg_clean[1:].strip() # remove leading '-' and spaces
|
||||
return f"- {msg_clean}"
|
||||
|
||||
# def generate_release_notes(last_successful, tag, branch):
|
||||
"""Generate release notes for the given tag."""
|
||||
current_commit = get_head_commit()
|
||||
messages = get_commit_messages(last_successful, current_commit)
|
||||
|
||||
# Start with the tag header
|
||||
new_section = f"{TAG_MARKER} {tag}\n"
|
||||
|
||||
# What's Changed section (always present)
|
||||
new_section += f"{HEADER_MARKER} What's Changed\n"
|
||||
|
||||
if not messages or last_successful == current_commit:
|
||||
new_section += "- Nothing...\n"
|
||||
else:
|
||||
for msg in messages:
|
||||
new_section += f"{format_commit_message(msg)}\n"
|
||||
|
||||
# New Contributors section (only if there are new contributors)
|
||||
all_previous_authors = get_authors_in_range(f"{branch}")
|
||||
recent_authors = get_authors_in_range(f"{last_successful}..{current_commit}")
|
||||
new_contributors = recent_authors - all_previous_authors
|
||||
|
||||
if new_contributors:
|
||||
new_section += f"\n{HEADER_MARKER} New Contributors\n"
|
||||
for author in sorted(new_contributors):
|
||||
new_section += f"- {format_contributor(author)} made their first contribution\n"
|
||||
|
||||
# Full Changelog section (only if there are changes)
|
||||
if messages and last_successful != current_commit:
|
||||
repo_url = get_repo_url()
|
||||
changelog_link = f"{repo_url}/compare/{last_successful}...{current_commit}"
|
||||
new_section += f"\n{HEADER_MARKER} Full Changelog: [{last_successful[:8]}...{current_commit[:8]}]({changelog_link})\n"
|
||||
|
||||
return new_section
|
||||
|
||||
def generate_release_notes(last_successful, tag, branch):
|
||||
"""Generate release notes for the given tag."""
|
||||
current_commit = get_head_commit()
|
||||
try:
|
||||
# Try to get commit messages using the provided last_successful commit
|
||||
messages = get_commit_messages(last_successful, current_commit)
|
||||
except subprocess.CalledProcessError:
|
||||
# If the range is invalid (e.g. force push made last_successful obsolete),
|
||||
# fall back to using the last 10 commits in the current branch.
|
||||
print("\nInvalid revision range error, using last 10 commits as fallback.\n")
|
||||
fallback_commit = run_command("git rev-parse HEAD~5")
|
||||
messages = get_commit_messages(fallback_commit, current_commit)
|
||||
last_successful = fallback_commit
|
||||
|
||||
# Start with the tag header
|
||||
new_section = f"{TAG_MARKER} {tag}\n"
|
||||
|
||||
# What's Changed section (always present)
|
||||
new_section += f"{HEADER_MARKER} What's Changed\n"
|
||||
|
||||
if not messages or last_successful == current_commit:
|
||||
new_section += "- Nothing...\n"
|
||||
else:
|
||||
for msg in messages:
|
||||
new_section += f"{format_commit_message(msg)}\n"
|
||||
|
||||
# New Contributors section (only if there are new contributors)
|
||||
all_previous_authors = get_authors_in_range(f"{branch}")
|
||||
recent_authors = get_authors_in_range(f"{last_successful}..{current_commit}")
|
||||
new_contributors = recent_authors - all_previous_authors
|
||||
|
||||
if new_contributors:
|
||||
new_section += f"\n{HEADER_MARKER} New Contributors\n"
|
||||
for author in sorted(new_contributors):
|
||||
new_section += f"- {format_contributor(author)} made their first contribution\n"
|
||||
|
||||
# Full Changelog section (only if there are changes)
|
||||
if messages and last_successful != current_commit:
|
||||
repo_url = get_repo_url()
|
||||
changelog_link = f"{repo_url}/compare/{last_successful}...{current_commit}"
|
||||
new_section += f"\n{HEADER_MARKER} Full Changelog: [{last_successful[:8]}...{current_commit[:8]}]({changelog_link})\n"
|
||||
|
||||
return new_section
|
||||
|
||||
def update_release_md(existing_content, new_section, tag):
|
||||
"""
|
||||
Update input based on rules:
|
||||
1. If tag exists, update it
|
||||
2. Special tags (alpha, beta, nightly) stay at the top in that order
|
||||
3. Numbered tags follow special tags
|
||||
4. Remove duplicate tags
|
||||
5. Insert new numbered tags at the top of the numbered section
|
||||
"""
|
||||
tag_lower = tag.lower()
|
||||
is_special_tag = tag_lower in ["alpha", "beta", "nightly"]
|
||||
|
||||
# Parse the existing content into sections
|
||||
if not existing_content:
|
||||
return new_section
|
||||
|
||||
# Split the content into sections by headers
|
||||
pattern = fr'(^{TAG_MARKER} .*$)'
|
||||
sections = re.split(pattern, existing_content, flags=re.MULTILINE)
|
||||
|
||||
# Create a list to store the processed content
|
||||
processed_sections = []
|
||||
|
||||
# Track special tag positions and whether tag was found
|
||||
special_tags_map = {"alpha": False, "beta": False, "nightly": False}
|
||||
last_special_index = -1
|
||||
tag_found = False
|
||||
numbered_tag_index = -1
|
||||
|
||||
i = 0
|
||||
while i < len(sections):
|
||||
# Check if this is a header
|
||||
if i % 2 == 1: # Headers are at odd indices
|
||||
header = sections[i]
|
||||
content = sections[i+1] if i+1 < len(sections) else ""
|
||||
current_tag = header[3:].strip().lower()
|
||||
|
||||
# Check for special tags to track their positions
|
||||
if current_tag in special_tags_map:
|
||||
special_tags_map[current_tag] = True
|
||||
last_special_index = len(processed_sections)
|
||||
|
||||
# Check if this is the first numbered tag
|
||||
elif re.match(r'^[0-9]+\.[0-9]+(\.[0-9]+)?$', current_tag) and numbered_tag_index == -1:
|
||||
numbered_tag_index = len(processed_sections)
|
||||
|
||||
# If this is the tag we're updating, mark it but don't add yet
|
||||
if current_tag == tag_lower:
|
||||
if not tag_found: # Replace the first occurrence
|
||||
tag_found = True
|
||||
i += 2 # Skip the content
|
||||
continue
|
||||
else: # Skip duplicate occurrences
|
||||
i += 2
|
||||
continue
|
||||
|
||||
# Add the current section
|
||||
processed_sections.append(sections[i])
|
||||
i += 1
|
||||
|
||||
# Determine where to insert the new section
|
||||
if tag_found:
|
||||
# We need to determine the insertion point
|
||||
if is_special_tag:
|
||||
# For special tags, insert after last special tag or at beginning
|
||||
desired_index = -1
|
||||
for pos, t in enumerate(["alpha", "beta", "nightly"]):
|
||||
if t == tag_lower:
|
||||
desired_index = pos
|
||||
|
||||
# Find position to insert
|
||||
insert_pos = 0
|
||||
for pos, t in enumerate(["alpha", "beta", "nightly"]):
|
||||
if t == tag_lower:
|
||||
break
|
||||
if special_tags_map[t]:
|
||||
insert_pos = processed_sections.index(f"{TAG_MARKER} {t}")
|
||||
insert_pos += 2 # Move past the header and content
|
||||
|
||||
# Insert at the determined position
|
||||
processed_sections.insert(insert_pos, new_section)
|
||||
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||
processed_sections.insert(insert_pos, '\n\n')
|
||||
else:
|
||||
# For numbered tags, insert after special tags but before other numbered tags
|
||||
insert_pos = 0
|
||||
|
||||
if last_special_index >= 0:
|
||||
# Insert after the last special tag
|
||||
insert_pos = last_special_index + 2 # +2 to skip header and content
|
||||
|
||||
processed_sections.insert(insert_pos, new_section)
|
||||
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||
processed_sections.insert(insert_pos, '\n\n')
|
||||
else:
|
||||
# Tag doesn't exist yet, determine insertion point
|
||||
if is_special_tag:
|
||||
# For special tags, maintain alpha, beta, nightly order
|
||||
special_tags = ["alpha", "beta", "nightly"]
|
||||
insert_pos = 0
|
||||
|
||||
for i, t in enumerate(special_tags):
|
||||
if t == tag_lower:
|
||||
# Check if preceding special tags exist
|
||||
for prev_tag in special_tags[:i]:
|
||||
if special_tags_map[prev_tag]:
|
||||
# Find the position after this tag
|
||||
prev_index = processed_sections.index(f"{TAG_MARKER} {prev_tag}")
|
||||
insert_pos = prev_index + 2 # Skip header and content
|
||||
|
||||
processed_sections.insert(insert_pos, new_section)
|
||||
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||
processed_sections.insert(insert_pos, '\n\n')
|
||||
else:
|
||||
# For numbered tags, insert after special tags but before other numbered tags
|
||||
insert_pos = 0
|
||||
|
||||
if last_special_index >= 0:
|
||||
# Insert after the last special tag
|
||||
insert_pos = last_special_index + 2 # +2 to skip header and content
|
||||
|
||||
processed_sections.insert(insert_pos, new_section)
|
||||
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||
processed_sections.insert(insert_pos, '\n\n')
|
||||
|
||||
# Combine sections ensuring proper spacing
|
||||
result = ""
|
||||
for i, section in enumerate(processed_sections):
|
||||
if i > 0 and section.startswith(f"{TAG_MARKER} "):
|
||||
# Ensure single blank line before headers
|
||||
if not result.endswith("\n\n"):
|
||||
result = result.rstrip("\n") + "\n\n"
|
||||
result += section
|
||||
|
||||
return result.rstrip() + "\n"
|
||||
|
||||
|
||||
def retrieve_tag_content(tag, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
return ""
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Create a pattern for the tag header (case-insensitive)
|
||||
pattern = re.compile(fr'^{TAG_MARKER} ' + re.escape(tag) + r'$', re.MULTILINE | re.IGNORECASE)
|
||||
|
||||
# Find the tag header
|
||||
match = pattern.search(content)
|
||||
if not match:
|
||||
return ""
|
||||
|
||||
# Start after the tag line
|
||||
start_pos = match.end()
|
||||
|
||||
# Skip a newline if present
|
||||
if start_pos < len(content) and content[start_pos] == "\n":
|
||||
start_pos += 1
|
||||
|
||||
# Find the next tag header after the current tag's content
|
||||
next_tag_match = re.search(fr'^{TAG_MARKER} ', content[start_pos:], re.MULTILINE)
|
||||
|
||||
if next_tag_match:
|
||||
end_pos = start_pos + next_tag_match.start()
|
||||
return content[start_pos:end_pos].strip()
|
||||
else:
|
||||
# Return until the end of the file if this is the last tag
|
||||
return content[start_pos:].strip()
|
||||
|
||||
def main():
|
||||
# Update input file
|
||||
release_file = "release-notes.md"
|
||||
|
||||
# Usage: python release.py <last_successful_commit> [tag] [branch]
|
||||
# Or: python release.py --retrieve <tagname>
|
||||
args = sys.argv[1:]
|
||||
|
||||
if len(args) < 1:
|
||||
print("Usage: python release.py <last_successful_commit> [tag] [branch]")
|
||||
print(" or: python release.py --retrieve <tagname>")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if we're retrieving a tag
|
||||
if args[0] == "--retrieve":
|
||||
if len(args) < 2:
|
||||
print("Error: Missing tag name after --retrieve")
|
||||
sys.exit(1)
|
||||
|
||||
tag_content = retrieve_tag_content(args[1], file_path=release_file)
|
||||
if tag_content:
|
||||
print(tag_content)
|
||||
else:
|
||||
print(f"Tag '{args[1]}' not found in '{release_file}'")
|
||||
return
|
||||
|
||||
# Original functionality for generating release notes
|
||||
last_successful = args[0]
|
||||
tag = args[1] if len(args) > 1 else get_head_commit()
|
||||
branch = args[2] if len(args) > 2 else (os.environ.get("GITHUB_REF") or get_branch())
|
||||
|
||||
# Generate release notes
|
||||
new_section = generate_release_notes(last_successful, tag, branch)
|
||||
|
||||
existing_content = ""
|
||||
if os.path.exists(release_file):
|
||||
with open(release_file, "r") as f:
|
||||
existing_content = f.read()
|
||||
|
||||
updated_content = update_release_md(existing_content, new_section, tag)
|
||||
|
||||
with open(release_file, "w") as f:
|
||||
f.write(updated_content)
|
||||
|
||||
# Output the new section for display
|
||||
print(new_section)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
xcconfigs/DataStructureTests.xcconfig
Normal file
3
xcconfigs/DataStructureTests.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "../Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).DataStructureTests
|
||||
3
xcconfigs/UITests.xcconfig
Normal file
3
xcconfigs/UITests.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "../Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).UITests
|
||||
Reference in New Issue
Block a user