mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
- Multiple fixes and CI setup
This commit is contained in:
298
.github/workflows/alpha.yml
vendored
298
.github/workflows/alpha.yml
vendored
@@ -2,283 +2,25 @@ name: Alpha SideStore build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# - alpha
|
||||
- rebase-2.0-wip
|
||||
- develop-alpha
|
||||
|
||||
# cancel duplicate run if from same branch
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore Alpha releases
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '16.1'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
- name: Set current build as ALPHA
|
||||
run: echo "IS_ALPHA=1" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install xcbeautify
|
||||
run: brew install xcbeautify
|
||||
|
||||
- name: Cache .alpha-build-num
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .alpha-build-num
|
||||
key: alpha-build-num
|
||||
|
||||
- name: Get version
|
||||
id: version-marketing
|
||||
run: echo "VERSION_IPA=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_ENV
|
||||
|
||||
- name: Increase alpha build number and set as version
|
||||
run: bash .github/workflows/increase-alpha-build-num.sh
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- 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
|
||||
# if: ${{ steps.pods-restore.outputs.cache-hit != 'true'}}
|
||||
id: pods-install
|
||||
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: Build SideStore
|
||||
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- 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 alpha release
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: "Alpha"
|
||||
tag: "alpha"
|
||||
prerelease: true
|
||||
files: SideStore.ipa SideStore.dSYMs.zip
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ alpha build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
Alpha builds are **extremely experimental builds only meant to be used by developers and alpha 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 Stable](https://github.com/${{ github.repository }}/releases?q=stable).
|
||||
|
||||
## 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 }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv 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/*
|
||||
|
||||
# Check if PUBLISH_ALPHA_UPDATES secret is set to non-zero
|
||||
- name: Check if PUBLISH_ALPHA_UPDATES is set
|
||||
id: check_publish
|
||||
run: |
|
||||
if [[ "${{ secrets.PUBLISH_ALPHA_UPDATES }}" != "__YES__" ]]; then
|
||||
echo "PUBLISH_ALPHA_UPDATES is not set. Skipping deployment."
|
||||
exit 1 # Exit with 1 to indicate no deployment
|
||||
else
|
||||
echo "PUBLISH_ALPHA_UPDATES is set. Proceeding with deployment."
|
||||
exit 0 # Exit with 0 to indicate deployment should proceed
|
||||
fi
|
||||
continue-on-error: true # Continue even if exit code is 1
|
||||
|
||||
- name: Get short commit hash
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
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: Get formatted date
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
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)
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
run: |
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
IPA_SIZE=$(stat -f %z SideStore-${{ steps.version.outputs.version }}.ipa)
|
||||
else
|
||||
# Linux
|
||||
IPA_SIZE=$(stat -c %s SideStore-${{ steps.version.outputs.version }}.ipa)
|
||||
fi
|
||||
echo "IPA size in bytes: $IPA_SIZE"
|
||||
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
|
||||
- name: Compute SHA-256 of IPA
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
run: |
|
||||
SHA256_HASH=$(shasum -a 256 SideStore-${{ steps.version.outputs.version }}.ipa | awk '{ print $1 }')
|
||||
echo "SHA-256 Hash: $SHA256_HASH"
|
||||
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
|
||||
|
||||
- name: Set environment variables dynamically
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
run: |
|
||||
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
|
||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
echo "BETA=true" >> $GITHUB_ENV
|
||||
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
|
||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||
echo "LOCALIZED_DESCRIPTION=This is alpha release for revision: ${{ github.sha }}" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/alpha/SideStore.ipa" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout SideStore/apps-v2.json
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
# Default: ${{ github.repository }}
|
||||
repository: 'SideStore/apps-v2.json'
|
||||
ref: 'main'
|
||||
# token: ${{ github.token }}
|
||||
token: ${{ secrets.APPS_DEPLOY_KEY }}
|
||||
path: 'SideStore/apps-v2.json'
|
||||
|
||||
- name: Publish to SideStore/apps-v2.json
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
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"
|
||||
|
||||
# Make the update script executable and run it
|
||||
python3 ../../update_apps.py "./_includes/source.json"
|
||||
|
||||
# Commit changes and push using SSH
|
||||
git add ./_includes/source.json
|
||||
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
|
||||
|
||||
git status
|
||||
git push origin HEAD:main
|
||||
popd
|
||||
Reuseable-build:
|
||||
uses: ./.github/workflows/reusable-build-workflow.yml
|
||||
with:
|
||||
bundle_id: "com.SideStore.SideStore.Alpha"
|
||||
is_beta: true
|
||||
publish: true
|
||||
is_shared_build_num: false
|
||||
release_tag: "alpha"
|
||||
release_name: "Alpha"
|
||||
upstream_tag: "nightly"
|
||||
upstream_name: "Nightly"
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
28
.github/workflows/increase-alpha-build-num.sh
vendored
28
.github/workflows/increase-alpha-build-num.sh
vendored
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure we are in root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-alpha.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > .alpha-build-num
|
||||
}
|
||||
|
||||
if [ ! -f ".alpha-build-num" ]; then
|
||||
write
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_DATE=`cat .alpha-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||
LAST_BUILD_NUM=`cat .alpha-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||
|
||||
if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||
write
|
||||
else
|
||||
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
write
|
||||
fi
|
||||
|
||||
34
.github/workflows/increase-beta-build-num.sh
vendored
Executable file
34
.github/workflows/increase-beta-build-num.sh
vendored
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure we are in root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
# Use RELEASE_CHANNEL from the environment variable or default to "beta"
|
||||
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"beta"}
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-$RELEASE_CHANNEL.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > build_number.txt
|
||||
}
|
||||
|
||||
if [ ! -f "build_number.txt" ]; then
|
||||
write
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_DATE=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||
LAST_BUILD_NUM=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||
|
||||
# if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||
# write
|
||||
# else
|
||||
# BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
# write
|
||||
# fi
|
||||
|
||||
# Build number is always incremental
|
||||
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
write
|
||||
28
.github/workflows/increase-nightly-build-num.sh
vendored
28
.github/workflows/increase-nightly-build-num.sh
vendored
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure we are in root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||
}
|
||||
|
||||
if [ ! -f ".nightly-build-num" ]; then
|
||||
write
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_DATE=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||
LAST_BUILD_NUM=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||
|
||||
if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||
write
|
||||
else
|
||||
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
write
|
||||
fi
|
||||
|
||||
206
.github/workflows/nightly.yml
vendored
206
.github/workflows/nightly.yml
vendored
@@ -1,20 +1,71 @@
|
||||
name: Nightly SideStore build
|
||||
name: Nightly SideStore Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Runs every night at midnight UTC
|
||||
workflow_dispatch: # Allows manual trigger
|
||||
|
||||
# cancel duplicate run if from same branch
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Ensure full history
|
||||
|
||||
- name: Get last successful workflow run
|
||||
id: get_last_success
|
||||
run: |
|
||||
LAST_SUCCESS=$(gh run list --workflow "Nightly SideStore Build" --json createdAt,conclusion \
|
||||
--jq '[.[] | select(.conclusion=="success")][0].createdAt' || echo "")
|
||||
echo "Last successful run: $LAST_SUCCESS"
|
||||
echo "last_success=$LAST_SUCCESS" >> $GITHUB_ENV
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for new commits since last successful build
|
||||
id: check
|
||||
run: |
|
||||
if [ -n "$LAST_SUCCESS" ]; then
|
||||
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop)
|
||||
else
|
||||
NEW_COMMITS=1
|
||||
fi
|
||||
echo "Has changes: $NEW_COMMITS"
|
||||
if [ "$NEW_COMMITS" -gt 0 ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LAST_SUCCESS: ${{ env.last_success }}
|
||||
|
||||
build:
|
||||
name: Build and upload SideStore Nightly releases
|
||||
needs: check-changes
|
||||
if: |
|
||||
always() &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: build-number-increment # serialize for build num cache access
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
- os: 'macos-15'
|
||||
version: '16.1'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -22,7 +73,6 @@ jobs:
|
||||
|
||||
- name: Set current build as BETA
|
||||
run: |
|
||||
echo "IS_BETA=1" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=beta" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
@@ -36,25 +86,60 @@ jobs:
|
||||
- name: Install xcbeautify
|
||||
run: brew install xcbeautify
|
||||
|
||||
- name: Cache .nightly-build-num
|
||||
uses: actions/cache@v4
|
||||
- name: Checkout SideStore/beta-build-num
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: .nightly-build-num
|
||||
key: nightly-build-num
|
||||
repository: 'SideStore/beta-build-num'
|
||||
# ref: 'main' # use this when you want to share the build num with other beta workflows
|
||||
ref: 'nightly'
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/beta-build-num'
|
||||
|
||||
- name: Get version
|
||||
id: version-marketing
|
||||
run: echo "VERSION_IPA=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_ENV
|
||||
- name: Copy build_number.txt to repo root
|
||||
run: |
|
||||
cp SideStore/beta-build-num/build_number.txt .
|
||||
|
||||
- name: Echo Build.xcconfig, build_number.txt
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
run: bash .github/workflows/increase-beta-build-num.sh
|
||||
|
||||
- name: Get version
|
||||
- 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 }}"
|
||||
- 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
|
||||
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
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
@@ -203,7 +288,7 @@ jobs:
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
run: cp SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -227,63 +312,68 @@ jobs:
|
||||
- name: Check if PUBLISH_BETA_UPDATES is set
|
||||
id: check_publish
|
||||
run: |
|
||||
if [[ "${{ secrets.PUBLISH_BETA_UPDATES }}" != "__YES__" ]]; then
|
||||
echo "PUBLISH_BETA_UPDATES=${{ vars.PUBLISH_BETA_UPDATES }}"
|
||||
if [[ "${{ vars.PUBLISH_BETA_UPDATES }}" == "__YES__" ]]; then
|
||||
echo "PUBLISH_BETA_UPDATES is not set. Skipping deployment."
|
||||
exit 1 # Exit with 1 to indicate no deployment
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "PUBLISH_BETA_UPDATES is set. Proceeding with deployment."
|
||||
exit 0 # Exit with 0 to indicate deployment should proceed
|
||||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true # Continue even if exit code is 1
|
||||
|
||||
- name: Get short commit hash
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
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: Get formatted date
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
if: steps.check_publish.outputs.should_deploy == 'true'
|
||||
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)
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
if: steps.check_publish.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
IPA_SIZE=$(stat -f %z SideStore-${{ steps.version.outputs.version }}.ipa)
|
||||
IPA_SIZE=$(stat -f %z SideStore.ipa)
|
||||
else
|
||||
# Linux
|
||||
IPA_SIZE=$(stat -c %s SideStore-${{ steps.version.outputs.version }}.ipa)
|
||||
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
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
if: steps.check_publish.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
SHA256_HASH=$(shasum -a 256 SideStore-${{ steps.version.outputs.version }}.ipa | awk '{ print $1 }')
|
||||
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 environment variables dynamically
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
if: steps.check_publish.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
|
||||
|
||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||
This is release for:
|
||||
- version: "${{ steps.version.outputs.version }}"
|
||||
- revision: "$SHORT_COMMIT"
|
||||
- track: "$RELEASE_CHANNEL"
|
||||
- timestamp: "${{ steps.date.outputs.date }}"
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "VERSION_IPA=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
|
||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||
echo "LOCALIZED_DESCRIPTION=This is nightly release for revision: ${{ github.sha }}" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/nightly/SideStore.ipa" >> $GITHUB_ENV
|
||||
|
||||
# multiline strings
|
||||
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
|
||||
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout SideStore/apps-v2.json
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
if: steps.check_publish.outputs.should_deploy == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
@@ -292,11 +382,11 @@ jobs:
|
||||
ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
|
||||
# ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
|
||||
# token: ${{ github.token }}
|
||||
token: ${{ secrets.APPS_DEPLOY_KEY }}
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/apps-v2.json'
|
||||
|
||||
- name: Publish to SideStore/apps-v2.json
|
||||
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||
id: publish-release
|
||||
run: |
|
||||
# Copy and execute the update script
|
||||
pushd SideStore/apps-v2.json/
|
||||
@@ -312,6 +402,32 @@ jobs:
|
||||
git add ./_includes/source.json
|
||||
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
|
||||
|
||||
git status
|
||||
git push origin HEAD:main
|
||||
git push --verbose
|
||||
popd
|
||||
|
||||
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
|
||||
# save it
|
||||
- name: Publish to SideStore/beta-build-num
|
||||
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 $RELEASE_CHANNEL - $SHORT_COMMIT deployment" || echo "No changes to commit"
|
||||
|
||||
echo "Performing git pull, to see if any extenal change has been made within our current run duration"
|
||||
git pull
|
||||
|
||||
echo "Pushing to remote repo"
|
||||
git push --verbose
|
||||
popd
|
||||
|
||||
3
.github/workflows/pr.yml
vendored
3
.github/workflows/pr.yml
vendored
@@ -1,10 +1,13 @@
|
||||
name: Pull Request SideStore build
|
||||
on:
|
||||
pull_request:
|
||||
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
407
.github/workflows/reusable-build-workflow.yml
vendored
Normal file
407
.github/workflows/reusable-build-workflow.yml
vendored
Normal file
@@ -0,0 +1,407 @@
|
||||
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
|
||||
|
||||
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: Increase build number for beta builds
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
echo "RELEASE_CHANNEL=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT
|
||||
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: 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 alpha 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.upstrea_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
|
||||
@@ -35,10 +35,6 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>BuildRevision</key>
|
||||
<string>$(BUILD_REVISION)</string>
|
||||
<key>BuildChannel</key>
|
||||
<string>$(BUILD_CHANNEL)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
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 */; };
|
||||
A888EAD52D401D8F0026F7E3 /* BuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A888EAD42D401D8A0026F7E3 /* BuildInfo.swift */; };
|
||||
A888EAD62D4020770026F7E3 /* BuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A888EAD42D401D8A0026F7E3 /* BuildInfo.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 */; };
|
||||
@@ -90,6 +88,7 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
|
||||
@@ -651,7 +650,6 @@
|
||||
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>"; };
|
||||
A888EAD42D401D8A0026F7E3 /* BuildInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildInfo.swift; 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; };
|
||||
@@ -667,6 +665,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
A8FD915B2D046EF100322782 /* ProcessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessError.swift; sourceTree = "<group>"; };
|
||||
@@ -1227,14 +1226,6 @@
|
||||
path = errors;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A888EAD32D401D7C0026F7E3 /* buildinfo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A888EAD42D401D8A0026F7E3 /* BuildInfo.swift */,
|
||||
);
|
||||
path = buildinfo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A88B8C532D35F1E800F53F9D /* operations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1299,7 +1290,6 @@
|
||||
A8C38C1C2D2068D100E83DBD /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A888EAD32D401D7C0026F7E3 /* buildinfo */,
|
||||
A8AD35572D31BEB2003A28B4 /* datastructures */,
|
||||
A8A853AD2D3050CC00995795 /* pagination */,
|
||||
A8087E712D2D291B002DB21B /* importexport */,
|
||||
@@ -1330,6 +1320,31 @@
|
||||
path = common;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8EA19602D4982E300DC6322 /* DatabaseManager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EECA2501AECA007EE018 /* DatabaseManager.swift */,
|
||||
D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */,
|
||||
);
|
||||
path = DatabaseManager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8EA19612D4982E300DC6322 /* MergePolicies */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EEC52501AECA007EE018 /* MergePolicy.swift */,
|
||||
);
|
||||
path = MergePolicies;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8EA19622D4982FC00DC6322 /* Transformers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EEC12501AECA007EE018 /* SecureValueTransformer.swift */,
|
||||
);
|
||||
path = Transformers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8F66C072D04C025009689E6 /* SideStore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1665,27 +1680,26 @@
|
||||
BF66EEAA2501AECA007EE018 /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8EA19602D4982E300DC6322 /* DatabaseManager */,
|
||||
A8EA19612D4982E300DC6322 /* MergePolicies */,
|
||||
A8EA19622D4982FC00DC6322 /* Transformers */,
|
||||
D557A4862AE88232007D0DCF /* Patreon */,
|
||||
BF66EEAC2501AECA007EE018 /* Migrations */,
|
||||
A8EA195E2D4982D600DC6322 /* BaseEntity.swift */,
|
||||
BF66EEB72501AECA007EE018 /* AltStore.xcdatamodeld */,
|
||||
BF66EEC92501AECA007EE018 /* Account.swift */,
|
||||
BF66EEC72501AECA007EE018 /* AppID.swift */,
|
||||
BF66EEC62501AECA007EE018 /* AppPermission.swift */,
|
||||
D5F9821C2AB900060045751F /* AppScreenshot.swift */,
|
||||
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */,
|
||||
BF66EECA2501AECA007EE018 /* DatabaseManager.swift */,
|
||||
D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */,
|
||||
BF66EEC02501AECA007EE018 /* InstalledApp.swift */,
|
||||
BF66EECB2501AECA007EE018 /* InstalledExtension.swift */,
|
||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */,
|
||||
BF66EEC52501AECA007EE018 /* MergePolicy.swift */,
|
||||
BF66EEBF2501AECA007EE018 /* NewsItem.swift */,
|
||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */,
|
||||
BF66EEC32501AECA007EE018 /* RefreshAttempt.swift */,
|
||||
BF66EEC12501AECA007EE018 /* SecureValueTransformer.swift */,
|
||||
BF66EEAB2501AECA007EE018 /* Source.swift */,
|
||||
BF66EEC42501AECA007EE018 /* StoreApp.swift */,
|
||||
BF66EEC22501AECA007EE018 /* Team.swift */,
|
||||
D557A4862AE88232007D0DCF /* Patreon */,
|
||||
BF66EEAC2501AECA007EE018 /* Migrations */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@@ -1713,21 +1727,21 @@
|
||||
BF66EEB02501AECA007EE018 /* Mapping Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EEB12501AECA007EE018 /* AltStoreToAltStore2.xcmappingmodel */,
|
||||
BF66EEB22501AECA007EE018 /* AltStore6ToAltStore7.xcmappingmodel */,
|
||||
BF66EEB32501AECA007EE018 /* AltStore3ToAltStore4.xcmappingmodel */,
|
||||
BF66EEB42501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel */,
|
||||
BF66EEB52501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel */,
|
||||
BF66EEB62501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel */,
|
||||
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */,
|
||||
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */,
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */,
|
||||
D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */,
|
||||
D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */,
|
||||
D5185B7F2AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel */,
|
||||
D5753A612B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel */,
|
||||
D5CE309B2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel */,
|
||||
D51E83812B8692DF0092FC61 /* AltStore16ToAltStore17.xcmappingmodel */,
|
||||
D5CE309B2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel */,
|
||||
D5753A612B279F1900090456 /* AltStore14ToAltStore15.xcmappingmodel */,
|
||||
D5185B7F2AE1E51B00646E33 /* AltStore13ToAltStore14.xcmappingmodel */,
|
||||
D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */,
|
||||
D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */,
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */,
|
||||
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */,
|
||||
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */,
|
||||
BF66EEB22501AECA007EE018 /* AltStore6ToAltStore7.xcmappingmodel */,
|
||||
BF66EEB62501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel */,
|
||||
BF66EEB42501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel */,
|
||||
BF66EEB32501AECA007EE018 /* AltStore3ToAltStore4.xcmappingmodel */,
|
||||
BF66EEB52501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel */,
|
||||
BF66EEB12501AECA007EE018 /* AltStoreToAltStore2.xcmappingmodel */,
|
||||
);
|
||||
path = "Mapping Models";
|
||||
sourceTree = "<group>";
|
||||
@@ -2234,6 +2248,7 @@
|
||||
D557A4802AE85BB0007D0DCF /* Pledge.swift */,
|
||||
D557A4842AE88227007D0DCF /* PledgeTier.swift */,
|
||||
D557A4822AE85DB7007D0DCF /* PledgeReward.swift */,
|
||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */,
|
||||
);
|
||||
path = Patreon;
|
||||
sourceTree = "<group>";
|
||||
@@ -2889,6 +2904,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */,
|
||||
A8EA195F2D4982D600DC6322 /* BaseEntity.swift in Sources */,
|
||||
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */,
|
||||
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
|
||||
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
||||
@@ -2922,7 +2938,6 @@
|
||||
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */,
|
||||
0EE7FDCB2BE8D12B00D1E390 /* ALTLocalizedError.swift in Sources */,
|
||||
BF66EEA92501AEC5007EE018 /* Tier.swift in Sources */,
|
||||
A888EAD62D4020770026F7E3 /* BuildInfo.swift in Sources */,
|
||||
BF66EEDB2501AECA007EE018 /* StoreApp.swift in Sources */,
|
||||
D5CE309C2B4C946300DB8151 /* AltStore15ToAltStore16.xcmappingmodel in Sources */,
|
||||
BF66EEDE2501AECA007EE018 /* AppID.swift in Sources */,
|
||||
@@ -3067,7 +3082,6 @@
|
||||
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
|
||||
D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */,
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||
A888EAD52D401D8F0026F7E3 /* BuildInfo.swift in Sources */,
|
||||
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
||||
BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */,
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
|
||||
|
||||
@@ -387,7 +387,8 @@ private extension AppViewController
|
||||
{
|
||||
var buttonAction: AppBannerView.AppAction?
|
||||
|
||||
if let installedApp = self.app.installedApp, let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||
// if let installedApp = self.app.installedApp, let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||
if let installedApp = self.app.installedApp, installedApp.hasUpdate
|
||||
{
|
||||
// Explicitly set button action to .update if there is an update available, even if it's not supported.
|
||||
buttonAction = .update
|
||||
@@ -537,7 +538,8 @@ extension AppViewController
|
||||
{
|
||||
if let installedApp = self.app.installedApp
|
||||
{
|
||||
if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||
// if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||
if let latestVersion = self.app.latestAvailableVersion, installedApp.hasUpdate
|
||||
{
|
||||
self.updateApp(installedApp, to: latestVersion)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
|
||||
|
||||
// Recreate Database if requested
|
||||
// NOTE: Userdefaults are local to the SideStore.app sandbox and are not shared
|
||||
if UserDefaults.standard.recreateDatabaseOnNextStart{
|
||||
// reset the state
|
||||
UserDefaults.standard.recreateDatabaseOnNextStart = false
|
||||
|
||||
// re-create database
|
||||
DatabaseManager.recreateDatabase()
|
||||
}
|
||||
|
||||
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
@@ -99,7 +109,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||
|
||||
#if DEBUG && (targetEnvironment(simulator) || BETA)
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
UserDefaults.standard.isDebugModeEnabled = true
|
||||
#endif
|
||||
|
||||
@@ -424,6 +434,8 @@ private extension AppDelegate
|
||||
|
||||
try context.save()
|
||||
|
||||
|
||||
|
||||
let updatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest()
|
||||
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
|
||||
|
||||
@@ -538,7 +538,8 @@ private extension BrowseViewController
|
||||
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
|
||||
// if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
|
||||
if let installedApp = app.installedApp, !installedApp.hasUpdate
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
@@ -563,7 +564,8 @@ private extension BrowseViewController
|
||||
}
|
||||
|
||||
Task<Void, Never>(priority: .userInitiated) { @MainActor in
|
||||
if let installedApp = app.installedApp, installedApp.isUpdateAvailable
|
||||
// if let installedApp = app.installedApp, installedApp.isUpdateAvailable
|
||||
if let installedApp = app.installedApp, installedApp.hasUpdate
|
||||
{
|
||||
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||
}
|
||||
|
||||
@@ -482,7 +482,8 @@ private extension FeaturedViewController
|
||||
|
||||
let storeApp = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
@@ -500,7 +501,8 @@ private extension FeaturedViewController
|
||||
return
|
||||
}
|
||||
|
||||
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
|
||||
{
|
||||
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||
}
|
||||
|
||||
@@ -233,7 +233,8 @@ extension AppBannerView
|
||||
{
|
||||
// App is installed
|
||||
|
||||
if installedApp.isUpdateAvailable
|
||||
// if installedApp.isUpdateAvailable
|
||||
if installedApp.hasUpdate
|
||||
{
|
||||
buttonAction = .update
|
||||
}
|
||||
|
||||
@@ -81,10 +81,6 @@
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>BuildRevision</key>
|
||||
<string>$(BUILD_REVISION)</string>
|
||||
<key>BuildChannel</key>
|
||||
<string>$(BUILD_CHANNEL)</string>
|
||||
<key>INIntentsSupported</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
|
||||
@@ -316,7 +316,12 @@ extension LaunchViewController
|
||||
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
|
||||
print("Failed to update sources on launch. \(errorDesc)")
|
||||
|
||||
let toastView = ToastView(error: error, mode: .fullError)
|
||||
var mode: ToastView.InfoMode = .fullError
|
||||
if String(describing: error).contains("The Internet connection appears to be offline"){
|
||||
mode = .localizedDescription // dont make noise!
|
||||
}
|
||||
|
||||
let toastView = ToastView(error: error, mode: mode)
|
||||
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
|
||||
}
|
||||
|
||||
@@ -1232,16 +1232,12 @@ private extension AppManager
|
||||
else
|
||||
{
|
||||
// Disable the idleTimeout
|
||||
if !UIApplication.shared.isIdleTimerDisabled { // accept only once if concurrent
|
||||
DispatchQueue.main.schedule {
|
||||
DispatchQueue.main.schedule {
|
||||
if !UIApplication.shared.isIdleTimerDisabled { // accept only once if concurrent
|
||||
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
||||
}
|
||||
}
|
||||
performAppOperations()
|
||||
// Moved to self.finish()
|
||||
// DispatchQueue.main.schedule {
|
||||
// UIApplication.shared.isIdleTimerDisabled = false
|
||||
// }
|
||||
}
|
||||
|
||||
return group
|
||||
@@ -2106,8 +2102,8 @@ private extension AppManager
|
||||
// TODO: This should disable for the last finish() request not the first though for batches
|
||||
// probably if we are in batch mode, we can count expected no of finishes() to arrive
|
||||
// and schedule disabling only on last request by matching it with count.
|
||||
if UIApplication.shared.isIdleTimerDisabled { // accept only once if concurrent
|
||||
DispatchQueue.main.schedule {
|
||||
DispatchQueue.main.schedule {
|
||||
if UIApplication.shared.isIdleTimerDisabled { // accept only once if concurrent
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
import minimuxer
|
||||
import SemanticVersion
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -241,7 +242,18 @@ private extension MyAppsViewController
|
||||
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.configure(for: app, action: .update)
|
||||
cell.bannerView.subtitleLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), latestSupportedVersion.localizedVersion)
|
||||
|
||||
var versionText = latestSupportedVersion.localizedVersion
|
||||
|
||||
// If the app is SideStore itself, remove the build number to save space
|
||||
if app.bundleIdentifier == Bundle.Info.appbundleIdentifier,
|
||||
let version = SemanticVersion(latestSupportedVersion.version)
|
||||
{
|
||||
// leave out the build so that it doesnt take up much space
|
||||
versionText = SemanticVersion(version.major, version.minor, version.patch, version.preRelease).description
|
||||
}
|
||||
|
||||
cell.bannerView.subtitleLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), versionText)
|
||||
|
||||
let appName: String
|
||||
|
||||
@@ -1044,57 +1056,6 @@ private extension MyAppsViewController
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
|
||||
|
||||
func removeAppExtensions() throws
|
||||
{
|
||||
for appExtension in application.appExtensions
|
||||
{
|
||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||
}
|
||||
|
||||
let scInfoURL = application.fileURL.appendingPathComponent("SC_Info")
|
||||
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
|
||||
|
||||
if let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL),
|
||||
let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String]
|
||||
{
|
||||
let replacementPaths = sinfReplicationPaths.filter { !$0.starts(with: "PlugIns/") } // Filter out app extension paths.
|
||||
manifestPlist["SinfReplicationPaths"] = replacementPaths
|
||||
try manifestPlist.write(to: manifestPlistURL)
|
||||
}
|
||||
}
|
||||
|
||||
let firstSentence: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
|
||||
}
|
||||
|
||||
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||
completion(.failure(OperationError.cancelled))
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||
completion(.success(()))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
let result = Result { try removeAppExtensions() }
|
||||
completion(result)
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func showHiddenUpdatesAlert(_ sender: UIButton)
|
||||
{
|
||||
guard !self.unsupportedUpdates.isEmpty else { return }
|
||||
@@ -1602,6 +1563,7 @@ private extension MyAppsViewController
|
||||
}
|
||||
catch let error as AppManager.FetchSourcesError
|
||||
{
|
||||
print(error)
|
||||
try await error.managedObjectContext?.performAsync {
|
||||
try error.managedObjectContext?.save()
|
||||
}
|
||||
@@ -1633,6 +1595,7 @@ private extension MyAppsViewController
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
print(error)
|
||||
let toastView = ToastView(error: error.withLocalizedTitle(NSLocalizedString("Unable to Check for Updates", comment: "")))
|
||||
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||
toastView.show(in: self)
|
||||
|
||||
@@ -319,7 +319,8 @@ private extension NewsViewController
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
guard let storeApp = app.storeApp else { return }
|
||||
|
||||
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
@@ -338,7 +339,8 @@ private extension NewsViewController
|
||||
}
|
||||
|
||||
Task<Void, Never>(priority: .userInitiated) { @MainActor in
|
||||
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
|
||||
{
|
||||
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
if let installedApp = storeApp.installedApp
|
||||
{
|
||||
guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
|
||||
// guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
|
||||
guard installedApp.hasUpdate else { return self.finish(.failure(error)) }
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
||||
|
||||
@@ -45,8 +45,14 @@ extension VerificationError
|
||||
VerificationError(code: .mismatchedHash, app: app, hash: hash, expectedHash: expectedHash)
|
||||
}
|
||||
|
||||
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
||||
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
|
||||
static func mismatchedVersion(version: String,
|
||||
expectedVersion: String,
|
||||
app: AppProtocol) -> VerificationError
|
||||
{
|
||||
VerificationError(code: .mismatchedVersion, app: app,
|
||||
version: version,
|
||||
expectedVersion: expectedVersion
|
||||
)
|
||||
}
|
||||
|
||||
static func mismatchedBuildVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
||||
|
||||
@@ -55,8 +55,23 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
|
||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func updateManifest() throws {
|
||||
guard let app = context.app else {
|
||||
return
|
||||
}
|
||||
|
||||
let scInfoURL = app.fileURL.appendingPathComponent("SC_Info")
|
||||
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
|
||||
|
||||
if let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL),
|
||||
let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String]
|
||||
{
|
||||
let replacementPaths = sinfReplicationPaths.filter { !$0.starts(with: "PlugIns/") } // Filter out app extension paths.
|
||||
manifestPlist["SinfReplicationPaths"] = replacementPaths
|
||||
try manifestPlist.write(to: manifestPlistURL)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeAppExtensions(from targetAppBundle: ALTApplication,
|
||||
localAppExtensions: Set<ALTApplication>?,
|
||||
@@ -127,6 +142,7 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
do {
|
||||
try Self.removeExtensions(from: targetAppBundle.appExtensions)
|
||||
try self.updateManifest()
|
||||
return self.finish(.success(()))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
|
||||
@@ -82,17 +82,16 @@ final class VerifyAppOperation: ResultOperation<Void>
|
||||
do
|
||||
{
|
||||
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
|
||||
|
||||
|
||||
// TODO: @mahee96: appVersion is instantiated source info as AppVersion incoming from source json
|
||||
// app is the instantiated ipa downloaded from the specified in the source json in temp dir
|
||||
//
|
||||
// For alpha and beta/nightly releases, the CFBundleShortVersionString which is the
|
||||
// $(MARKETING_VERSION) will be overriden with the commit id before invoking xcode build
|
||||
//
|
||||
|
||||
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
|
||||
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
|
||||
try await self.verifyPermissions(of: app, match: appVersion)
|
||||
|
||||
// process missing permissions check only if the source is V2 or later
|
||||
if let source = appVersion.app?.source,
|
||||
source.isSourceAtLeastV2
|
||||
{
|
||||
try await self.verifyPermissions(of: app, match: appVersion)
|
||||
}
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
@@ -129,24 +128,17 @@ private extension VerifyAppOperation
|
||||
{
|
||||
let (version, buildVersion) = await $appVersion.perform { ($0.version, $0.buildVersion) }
|
||||
|
||||
let downloadedIpaRevision = Bundle(url: app.fileURL)!.object(forInfoDictionaryKey: "BuildRevision") as? String ?? ""
|
||||
let sourceJsonIpaRevision = appVersion.revision
|
||||
|
||||
// if not beta but version matches, then accept it, else compare revisions between source and downloaded
|
||||
// marketplace buildVersion validation
|
||||
if let buildVersion
|
||||
{
|
||||
guard buildVersion == app.buildVersion else {
|
||||
throw VerificationError.mismatchedBuildVersion(app.buildVersion, expectedVersion: buildVersion, app: app)
|
||||
}
|
||||
}
|
||||
|
||||
if version != app.version {
|
||||
throw VerificationError.mismatchedVersion(app.version, expectedVersion: version, app: app)
|
||||
throw VerificationError.mismatchedVersion(version: app.version, expectedVersion: version, app: app)
|
||||
}
|
||||
if (appVersion.isBeta && downloadedIpaRevision != sourceJsonIpaRevision) {
|
||||
let sourceJsonIpaRevision = sourceJsonIpaRevision ?? "nil"
|
||||
throw VerificationError.mismatchedVersion(app.version + " - " + downloadedIpaRevision,
|
||||
expectedVersion: version + " - " + sourceJsonIpaRevision, app: app)
|
||||
}
|
||||
|
||||
// if let buildVersion
|
||||
// {
|
||||
// // TODO: @mahee96: requires altsign-marketplace branch release or equivalent
|
||||
// guard buildVersion == app.buildVersion else { throw VerificationError.mismatchedBuildVersion(app.buildVersion, expectedVersion: buildVersion, app: app) }
|
||||
// }
|
||||
}
|
||||
|
||||
func verifyPermissions(of app: ALTApplication, @AsyncManaged match appVersion: AppVersion) async throws
|
||||
|
||||
@@ -228,7 +228,7 @@ struct OperationsLoggingControlView: View {
|
||||
CustomToggle("1. Anisette Internal Logging", isOn: Binding(
|
||||
// enable anisette internal logging by default since it was already printing before
|
||||
get: { OperationsLoggingControl.getUpdatedFromDatabase(
|
||||
for: ANISETTE_VERBOSITY.self, defaultVal: true
|
||||
for: ANISETTE_VERBOSITY.self, defaultVal: false
|
||||
)},
|
||||
set: { value in
|
||||
self.viewModel.updateDatabase(for: ANISETTE_VERBOSITY.self, value: value)
|
||||
|
||||
@@ -76,13 +76,13 @@ final class AboutPatreonHeaderView: UICollectionReusableView
|
||||
self.textView.layer.cornerRadius = 20
|
||||
self.textView.textContainer.lineFragmentPadding = 0
|
||||
|
||||
for imageView in [self.rileyImageView!, self.shaneImageView!]
|
||||
for imageView in [self.rileyImageView, self.shaneImageView].compactMap({$0})
|
||||
{
|
||||
imageView.clipsToBounds = true
|
||||
imageView.layer.cornerRadius = imageView.bounds.midY
|
||||
}
|
||||
|
||||
for button in [self.supportButton!, self.accountButton!]
|
||||
for button in [self.supportButton, self.accountButton].compactMap({$0})
|
||||
{
|
||||
button.clipsToBounds = true
|
||||
button.layer.cornerRadius = 16
|
||||
|
||||
@@ -24,7 +24,7 @@ extension PatreonViewController
|
||||
|
||||
final class PatreonViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
// private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var patronsDataSource = self.makePatronsDataSource()
|
||||
|
||||
private var prototypeAboutHeader: AboutPatreonHeaderView!
|
||||
@@ -40,13 +40,13 @@ final class PatreonViewController: UICollectionViewController
|
||||
let aboutHeaderNib = UINib(nibName: "AboutPatreonHeaderView", bundle: nil)
|
||||
self.prototypeAboutHeader = aboutHeaderNib.instantiate(withOwner: nil, options: nil)[0] as? AboutPatreonHeaderView
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
// self.collectionView.dataSource = self.dataSource
|
||||
|
||||
self.collectionView.register(aboutHeaderNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "AboutHeader")
|
||||
self.collectionView.register(PatronsHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "PatronsHeader")
|
||||
//self.collectionView.register(PatronsFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "PatronsFooter")
|
||||
self.collectionView.register(PatronsFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "PatronsFooter")
|
||||
|
||||
//NotificationCenter.default.addObserver(self, selector: #selector(PatreonViewController.didUpdatePatrons(_:)), name: AppManager.didUpdatePatronsNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(PatreonViewController.didUpdatePatrons(_:)), name: AppManager.didUpdatePatronsNotification, object: nil)
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
@@ -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="1726.3333358764648" width="402" height="125"/>
|
||||
<rect key="frame" x="0.0" y="1877.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">
|
||||
@@ -246,8 +246,9 @@
|
||||
<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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="ksV-TB-cmH">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ksV-TB-cmH">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -286,8 +287,9 @@
|
||||
<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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wmy-RF-obD">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="wmy-RF-obD">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -463,8 +465,9 @@
|
||||
<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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="WtV-Dt-sDn">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="WtV-Dt-sDn">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -497,14 +500,15 @@
|
||||
<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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -533,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"/>
|
||||
@@ -565,23 +569,24 @@
|
||||
<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="214.66666666666663" y="15.333333333333334" width="157.33333333333337" 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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
||||
<rect key="frame" x="139.33333333333334" y="0.0" width="18" height="20.333333333333332"/>
|
||||
<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>
|
||||
</subviews>
|
||||
</stackView>
|
||||
@@ -609,23 +614,24 @@
|
||||
<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="225" y="15.333333333333334" width="147" 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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
||||
<rect key="frame" x="129" y="0.0" width="18" height="20.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>
|
||||
</stackView>
|
||||
@@ -653,23 +659,24 @@
|
||||
<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="233" y="15.333333333333334" width="139" 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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
||||
<rect key="frame" x="121" y="0.0" width="18" height="20.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>
|
||||
</stackView>
|
||||
@@ -697,14 +704,15 @@
|
||||
<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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -731,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="1156.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"/>
|
||||
@@ -743,8 +751,9 @@
|
||||
<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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -764,20 +773,21 @@
|
||||
</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="1207.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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="187.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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -800,20 +810,21 @@
|
||||
</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="1258.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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
|
||||
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -833,20 +844,21 @@
|
||||
</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="1309.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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="140" 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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -866,20 +878,21 @@
|
||||
</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="1360.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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="135.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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -898,59 +911,24 @@
|
||||
<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="1454.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" ambiguous="YES" 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" ambiguous="YES" 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="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="1545.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1451.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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Disable Response Caching" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jFh-36-AP2">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Disable Response Caching" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jFh-36-AP2">
|
||||
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="215.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="AAh-cu-qw8">
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AAh-cu-qw8">
|
||||
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleDisableResponseCaching:" destination="aMk-Xp-UL8" eventType="valueChanged" id="lCm-qi-piH"/>
|
||||
@@ -973,19 +951,19 @@
|
||||
</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="1596.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1502.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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Export Resigned Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="d5F-bf-6kB">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Export Resigned Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="d5F-bf-6kB">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="180" 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="GYP-qn-wzh">
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GYP-qn-wzh">
|
||||
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleResignedAppExport:" destination="aMk-Xp-UL8" eventType="valueChanged" id="Z1k-xh-sjD"/>
|
||||
@@ -1008,19 +986,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="1647.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1553.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"/>
|
||||
@@ -1043,26 +1021,21 @@
|
||||
</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="1698.3333377838135" width="402" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1604.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 SqLite DB" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho1-To-wve">
|
||||
<rect key="frame" x="30" y="15.333333333333334" width="137.66666666666666" height="20.333333333333329"/>
|
||||
<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"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wfX-fH-gXe">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Ho1-To-wve" firstAttribute="leading" secondItem="VTT-z5-C89" secondAttribute="leadingMargin" id="50N-ql-gna"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="wfX-fH-gXe" secondAttribute="trailing" id="9fe-Pw-SAN"/>
|
||||
<constraint firstItem="wfX-fH-gXe" firstAttribute="centerY" secondItem="VTT-z5-C89" secondAttribute="centerY" id="LPh-vG-0sK"/>
|
||||
<constraint firstItem="Ho1-To-wve" firstAttribute="centerY" secondItem="VTT-z5-C89" secondAttribute="centerY" id="eYD-QD-yYa"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
@@ -1075,21 +1048,50 @@
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</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="1655.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" 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"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="CcF-9x-Eu8" firstAttribute="leading" secondItem="Acf-xV-Isn" secondAttribute="leadingMargin" id="0IG-KN-mgk"/>
|
||||
<constraint firstItem="CcF-9x-Eu8" firstAttribute="centerY" secondItem="Acf-xV-Isn" secondAttribute="centerY" id="JeN-cS-xJV"/>
|
||||
</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>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</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="1706.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" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="zl4-ti-HTW">
|
||||
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -1109,22 +1111,22 @@
|
||||
</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="1757.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="Minimuxer Console Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZRk-8S-kBQ">
|
||||
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="225.33333333333337" height="20.333333333333329"/>
|
||||
<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">
|
||||
<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="toggleMinimuxerConsoleLogging:" destination="aMk-Xp-UL8" eventType="valueChanged" id="jOU-Ic-46O"/>
|
||||
<action selector="toggleRecreateDatabaseSwitch:" destination="aMk-Xp-UL8" eventType="valueChanged" id="vlf-Iz-kWr"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
@@ -1137,6 +1139,41 @@
|
||||
</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="9By-QW-Jw9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1808.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" 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" 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"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="os8-7F-rSm" secondAttribute="trailing" id="CF2-kM-7uy"/>
|
||||
<constraint firstItem="jW6-pb-xdP" firstAttribute="centerY" secondItem="Dzq-gE-zyT" secondAttribute="centerY" id="GXF-63-RYZ"/>
|
||||
<constraint firstItem="jW6-pb-xdP" firstAttribute="leading" secondItem="Dzq-gE-zyT" secondAttribute="leadingMargin" id="PXa-2Y-iti"/>
|
||||
<constraint firstItem="os8-7F-rSm" firstAttribute="centerY" secondItem="Dzq-gE-zyT" secondAttribute="centerY" id="ypQ-wu-K9d"/>
|
||||
</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"/>
|
||||
@@ -1157,14 +1194,14 @@
|
||||
<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="betaUpdatesSwitch" destination="e32-w4-5fk" id="Dty-Yb-eo1"/>
|
||||
<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"/>
|
||||
<outlet property="githubButton" destination="oqj-4S-I9l" id="sxB-LE-gA2"/>
|
||||
<outlet property="mastodonButton" destination="B8Q-e7-beR" id="Kbe-Og-rsg"/>
|
||||
<outlet property="minimuxerConsoleLoggingSwitch" destination="uGv-Lb-Ita" id="aTL-Md-tZ8"/>
|
||||
<outlet property="minimuxerConsoleLoggingSwitch" destination="os8-7F-rSm" id="gxg-Gx-xwO"/>
|
||||
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
|
||||
<outlet property="recreateDatabaseSwitch" destination="uGv-Lb-Ita" id="BMU-cK-0ee"/>
|
||||
<outlet property="threadsButton" destination="AWk-yE-9LI" id="SOc-ei-4gK"/>
|
||||
<outlet property="twitterButton" destination="uYZ-Vu-RzK" id="anA-jh-w4z"/>
|
||||
<outlet property="verboseOperationsLoggingSwitch" destination="Q5X-Mo-KpE" id="aVL-Md-tZ8"/>
|
||||
@@ -1674,10 +1711,10 @@ Settings by i cons from the Noun Project</string>
|
||||
<resources>
|
||||
<image name="GitHub" width="130" height="130"/>
|
||||
<image name="Mastodon" width="130" height="130"/>
|
||||
<image name="Next" width="18" height="18"/>
|
||||
<image name="Settings" width="20" height="20"/>
|
||||
<image name="Threads" width="130" height="130"/>
|
||||
<image name="Twitter" width="130" height="130"/>
|
||||
<image name="chevron.right" catalog="system" width="97" height="128"/>
|
||||
<image name="ladybug" catalog="system" width="128" height="122"/>
|
||||
<image name="terminal" catalog="system" width="128" height="93"/>
|
||||
<image name="trash" catalog="system" width="117" height="128"/>
|
||||
|
||||
@@ -13,6 +13,8 @@ import MessageUI
|
||||
import Intents
|
||||
import IntentsUI
|
||||
|
||||
import SemanticVersion
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension SettingsViewController
|
||||
@@ -73,7 +75,6 @@ extension SettingsViewController
|
||||
case refreshSideJITServer
|
||||
case resetPairingFile
|
||||
case anisetteServers
|
||||
case betaUpdates
|
||||
// case hiddenSettings
|
||||
}
|
||||
|
||||
@@ -82,8 +83,10 @@ extension SettingsViewController
|
||||
case responseCaching
|
||||
case exportResignedApp
|
||||
case verboseOperationsLogging
|
||||
case exportSqliteDB
|
||||
case exportDatabase
|
||||
case deleteDatabase
|
||||
case operationsLoggingControl
|
||||
case recreateDatabase
|
||||
case minimuxerConsoleLogging
|
||||
}
|
||||
}
|
||||
@@ -104,7 +107,6 @@ 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!
|
||||
@@ -118,12 +120,15 @@ final class SettingsViewController: UITableViewController
|
||||
@IBOutlet private var githubButton: UIButton!
|
||||
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
|
||||
@IBOutlet private var recreateDatabaseSwitch: UISwitch!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
private var exportDBInProgress = false
|
||||
private static var exportDBInProgress = false
|
||||
private static var deleteDBInProgress = false
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
@@ -207,6 +212,51 @@ final class SettingsViewController: UITableViewController
|
||||
|
||||
}
|
||||
|
||||
private class BuildInfo{
|
||||
private static let MARKETING_VERSION_TAG = "CFBundleShortVersionString"
|
||||
private static let CURRENT_PROJECT_VERSION_TAG = kCFBundleVersionKey as String
|
||||
|
||||
private static let XCODE_VERSION_TAG = "DTXcode"
|
||||
private static let XCODE_REVISION_TAG = "DTXcodeBuild"
|
||||
|
||||
let bundle: Bundle
|
||||
|
||||
public init(){
|
||||
bundle = Bundle.main
|
||||
}
|
||||
|
||||
enum BundleError: Swift.Error {
|
||||
case invalidURL
|
||||
}
|
||||
|
||||
public init(url: URL) throws {
|
||||
guard let bundle = Bundle(url: url) else {
|
||||
throw BundleError.invalidURL
|
||||
}
|
||||
self.bundle = bundle
|
||||
}
|
||||
|
||||
public lazy var project_version: String? = {
|
||||
let version = bundle.object(forInfoDictionaryKey: Self.CURRENT_PROJECT_VERSION_TAG) as? String
|
||||
return version
|
||||
}()
|
||||
|
||||
public lazy var marketing_version: String? = {
|
||||
let version = bundle.object(forInfoDictionaryKey: Self.MARKETING_VERSION_TAG) as? String
|
||||
return version
|
||||
}()
|
||||
|
||||
public lazy var xcode: String? = {
|
||||
let xcode = bundle.object(forInfoDictionaryKey: Self.XCODE_VERSION_TAG) as? String
|
||||
return xcode
|
||||
}()
|
||||
|
||||
public lazy var xcode_revision: String? = {
|
||||
let revision = bundle.object(forInfoDictionaryKey: Self.XCODE_REVISION_TAG) as? String
|
||||
return revision
|
||||
}()
|
||||
}
|
||||
|
||||
private extension SettingsViewController
|
||||
{
|
||||
|
||||
@@ -227,30 +277,23 @@ private extension SettingsViewController
|
||||
|
||||
|
||||
var versionLabel: String = ""
|
||||
|
||||
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
|
||||
let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
|
||||
// first check if there is installed app entity, if so, get version info from that
|
||||
if let installedApp
|
||||
{
|
||||
let isStableBuild = (buildInfo.channel == .stable)
|
||||
let revision = buildInfo.revision ?? ""
|
||||
|
||||
var localizedVersion = installedApp.version
|
||||
// Only show build version (and build revision) for non stable builds.
|
||||
if !isStableBuild {
|
||||
localizedVersion += buildInfo.project_version.map{ version in
|
||||
version.isEmpty ? "" : " (\(version))" + (revision.isEmpty ? "" : " - \(revision)")
|
||||
} ?? installedApp.localizedVersion
|
||||
}
|
||||
// Only show build version for non stable builds.
|
||||
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
|
||||
else if var version = buildInfo.marketing_version
|
||||
{
|
||||
var version = "SideStore \(version)"
|
||||
|
||||
version += getXcodeVersion()
|
||||
|
||||
versionLabel = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var version = "SideStore\t"
|
||||
@@ -259,7 +302,11 @@ private extension SettingsViewController
|
||||
}
|
||||
|
||||
// add xcode build version for local builds
|
||||
if buildInfo.channel == .local
|
||||
if let installedApp,
|
||||
let version = installedApp.storeApp?.latestSupportedVersion?.version,
|
||||
// if MARKETING_VERSION is set as "0.6.0-local" in CodeSigning.xcconfig as override,
|
||||
// then it is assumed it is local build and we should show xcode build information
|
||||
SemanticVersion(version)?.preRelease == "local"
|
||||
{
|
||||
versionLabel += "\n\(getXcodeVersion())"
|
||||
}
|
||||
@@ -289,7 +336,6 @@ private extension SettingsViewController
|
||||
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
|
||||
|
||||
// AdvancedSettingsRow
|
||||
self.betaUpdatesSwitch.isOn = UserDefaults.standard.isBetaUpdatesEnabled
|
||||
|
||||
// DiagnosticsRow
|
||||
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
|
||||
@@ -297,6 +343,8 @@ private extension SettingsViewController
|
||||
self.verboseOperationsLoggingSwitch.isOn = UserDefaults.standard.isVerboseOperationsLoggingEnabled
|
||||
self.minimuxerConsoleLoggingSwitch.isOn = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
||||
|
||||
self.recreateDatabaseSwitch.isOn = UserDefaults.standard.recreateDatabaseOnNextStart
|
||||
|
||||
if self.isViewLoaded
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
@@ -499,10 +547,35 @@ private extension SettingsViewController
|
||||
UserDefaults.standard.isMinimuxerConsoleLoggingEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleEnableBetaUpdates(_ sender: UISwitch) {
|
||||
// update it in database
|
||||
UserDefaults.standard.isBetaUpdatesEnabled = sender.isOn
|
||||
@IBAction func toggleRecreateDatabaseSwitch(_ sender: UISwitch) {
|
||||
// Update the setting in UserDefaults
|
||||
UserDefaults.standard.recreateDatabaseOnNextStart = sender.isOn
|
||||
|
||||
guard sender.isOn else { return }
|
||||
|
||||
DispatchQueue.global().async {
|
||||
for time in (1...3).reversed() {
|
||||
DispatchQueue.main.async {
|
||||
guard UserDefaults.standard.recreateDatabaseOnNextStart else {
|
||||
return
|
||||
}
|
||||
let toast = ToastView(text: "Database Delete Scheduled on Next Launch", detailText: "App is closing in \(time) seconds...")
|
||||
toast.tintColor = .altPrimary
|
||||
toast.preferredDuration = 1
|
||||
toast.show(in: self)
|
||||
}
|
||||
sleep(1) // Background sleep
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard UserDefaults.standard.recreateDatabaseOnNextStart else {
|
||||
return
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch)
|
||||
{
|
||||
@@ -877,6 +950,7 @@ extension SettingsViewController
|
||||
mailViewController.mailComposeDelegate = self
|
||||
mailViewController.setToRecipients(["support@sidestore.io"])
|
||||
|
||||
// TODO: MARKETING_VERSION is going to be set anyways so this needs to be fixed for beta
|
||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||
mailViewController.setSubject("SideStore Beta \(version) Feedback")
|
||||
} else {
|
||||
@@ -1064,7 +1138,7 @@ extension SettingsViewController
|
||||
// } else {
|
||||
// ELOG("UIApplication.openSettingsURLString invalid")
|
||||
// }
|
||||
case .refreshAttempts, .betaUpdates : break
|
||||
case .refreshAttempts : break
|
||||
|
||||
}
|
||||
|
||||
@@ -1072,10 +1146,19 @@ extension SettingsViewController
|
||||
let row = DiagnosticsRow.allCases[indexPath.row]
|
||||
switch row {
|
||||
|
||||
case .exportSqliteDB:
|
||||
case .deleteDatabase:
|
||||
if !Self.deleteDBInProgress {
|
||||
Self.deleteDBInProgress = true
|
||||
|
||||
_ = DatabaseManager.deleteDatabase()
|
||||
|
||||
exit(0) // exit app immediately to prevent db usage and crashes
|
||||
}
|
||||
|
||||
case .exportDatabase:
|
||||
// do not accept simulatenous export requests
|
||||
if !exportDBInProgress {
|
||||
exportDBInProgress = true
|
||||
if !Self.exportDBInProgress {
|
||||
Self.exportDBInProgress = true
|
||||
Task{
|
||||
var toastView: ToastView?
|
||||
do{
|
||||
@@ -1093,7 +1176,7 @@ extension SettingsViewController
|
||||
}
|
||||
|
||||
// update that work has finished
|
||||
exportDBInProgress = false
|
||||
Self.exportDBInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,7 +1187,7 @@ extension SettingsViewController
|
||||
let segue = UIStoryboardSegue(identifier: "operationsLoggingControl", source: self, destination: operationsLoggingController)
|
||||
self.present(segue.destination, animated: true, completion: nil)
|
||||
|
||||
case .responseCaching, .exportResignedApp, .verboseOperationsLogging, .minimuxerConsoleLogging : break
|
||||
case .responseCaching, .exportResignedApp, .verboseOperationsLogging, .minimuxerConsoleLogging, .recreateDatabase : break
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -625,7 +625,7 @@ private extension AddSourceViewController
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to load recommended source \(sourceURL.absoluteString):", error.localizedDescription)
|
||||
print("Failed to load recommended source \(sourceURL.absoluteString):", error.localizedDescription, error)
|
||||
fetchError = error
|
||||
|
||||
case .success(let source): sourcesByURL[source.sourceURL] = source
|
||||
|
||||
@@ -385,7 +385,8 @@ private extension SourceDetailContentViewController
|
||||
|
||||
let storeApp = self.dataSource.item(at: indexPath) as! StoreApp
|
||||
|
||||
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
@@ -406,7 +407,8 @@ private extension SourceDetailContentViewController
|
||||
do
|
||||
{
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
|
||||
{
|
||||
AppManager.shared.update(installedApp, presentingViewController: self) { result in
|
||||
continuation.resume(with: result.map { _ in () })
|
||||
|
||||
@@ -32,10 +32,10 @@ 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
|
||||
@NSManaged var recreateDatabaseOnNextStart: Bool
|
||||
@NSManaged var isPairingReset: Bool
|
||||
@NSManaged var isDebugModeEnabled: Bool
|
||||
@NSManaged var presentedLaunchReminderNotification: Bool
|
||||
@@ -89,7 +89,7 @@ public extension UserDefaults
|
||||
|
||||
@NSManaged var permissionCheckingDisabled: Bool
|
||||
@NSManaged var responseCachingDisabled: Bool
|
||||
|
||||
|
||||
class func registerDefaults()
|
||||
{
|
||||
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
|
||||
@@ -121,11 +121,11 @@ 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,
|
||||
#keyPath(UserDefaults.isMinimuxerConsoleLoggingEnabled): true, // minimuxer logging is enabled by default as before
|
||||
#keyPath(UserDefaults.isMinimuxerConsoleLoggingEnabled): false, // minimuxer logging is disabled by default for console loggin
|
||||
#keyPath(UserDefaults.recreateDatabaseOnNextStart): false,
|
||||
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
|
||||
#keyPath(UserDefaults.isPairingReset): true,
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>BuildRevision</key>
|
||||
<string>$(BUILD_REVISION)</string>
|
||||
<key>BuildChannel</key>
|
||||
<string>$(BUILD_CHANNEL)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -12,7 +12,7 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(Account)
|
||||
public class Account: NSManagedObject, Fetchable
|
||||
public class Account: BaseEntity
|
||||
{
|
||||
public var localizedName: String {
|
||||
var components = PersonNameComponents()
|
||||
|
||||
@@ -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="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
@@ -64,11 +64,9 @@
|
||||
<attribute name="buildVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="revision" 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"/>
|
||||
@@ -231,6 +229,7 @@
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="version" optional="YES" attributeType="Integer 64" defaultValueString="1" usesScalarValueType="NO"/>
|
||||
<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"/>
|
||||
@@ -245,28 +244,28 @@
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="category" optional="YES" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<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"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="marketplaceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="pledgeAmount" optional="YES" attributeType="Decimal"/>
|
||||
<attribute name="pledgeCurrency" optional="YES" attributeType="String"/>
|
||||
<attribute name="prefersCustomPledge" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="revision" optional="YES" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sha256" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" 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="version" optional="YES" attributeType="String"/>
|
||||
<attribute name="versionDate" optional="YES" 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"/>
|
||||
|
||||
@@ -12,7 +12,7 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(AppID)
|
||||
public class AppID: NSManagedObject, Fetchable
|
||||
public class AppID: BaseEntity
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
|
||||
@@ -12,7 +12,7 @@ import UIKit
|
||||
import AltSign
|
||||
|
||||
@objc(AppPermission) @dynamicMemberLookup
|
||||
public class AppPermission: NSManagedObject, Fetchable
|
||||
public class AppPermission: BaseEntity
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var type: ALTAppPermissionType
|
||||
|
||||
@@ -16,7 +16,7 @@ public extension AppScreenshot
|
||||
}
|
||||
|
||||
@objc(AppScreenshot)
|
||||
public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
||||
public class AppScreenshot: BaseEntity, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var imageURL: URL
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import CoreData
|
||||
|
||||
@objc(AppVersion)
|
||||
public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
public class AppVersion: BaseEntity, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var version: String
|
||||
@@ -22,11 +22,16 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
}
|
||||
@NSManaged @objc(buildVersion) public private(set) var _buildVersion: String
|
||||
|
||||
@NSManaged public var date: Date
|
||||
@NSManaged public var localizedDescription: String?
|
||||
@NSManaged public var downloadURL: URL
|
||||
@NSManaged public var size: Int64
|
||||
@NSManaged public var sha256: String?
|
||||
@NSManaged public private(set) var date: Date
|
||||
@NSManaged @objc(localizedDescription) private(set) var _localizedDescription: String?
|
||||
@NSManaged public private(set) var downloadURL: URL
|
||||
@NSManaged public private(set) var size: Int64
|
||||
@NSManaged public private(set) var sha256: String?
|
||||
|
||||
@nonobjc public var localizedDescription: String {
|
||||
return self._localizedDescription ?? app?.localizedDescription ??
|
||||
"localizedDescription not set, contact the source owner to fix this"
|
||||
}
|
||||
|
||||
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = self._minOSVersion else { return nil }
|
||||
@@ -46,11 +51,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
@NSManaged public var appBundleID: String
|
||||
@NSManaged public var sourceID: String?
|
||||
|
||||
// TODO: @mahee96: retire isBeta and use a string type to decode and store values as enum
|
||||
@NSManaged public var isBeta: Bool
|
||||
@NSManaged public var revision: String?
|
||||
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var app: StoreApp?
|
||||
@NSManaged @objc(latestVersionApp) public internal(set) var latestSupportedVersionApp: StoreApp?
|
||||
@@ -71,8 +72,6 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
case sha256
|
||||
case minOSVersion
|
||||
case maxOSVersion
|
||||
case isBeta = "beta"
|
||||
case revision = "commitID"
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
@@ -89,7 +88,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
self.buildVersion = try container.decodeIfPresent(String.self, forKey: .buildVersion)
|
||||
|
||||
self.date = try container.decode(Date.self, forKey: .date)
|
||||
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
|
||||
self._localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
|
||||
|
||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
self.size = try container.decode(Int64.self, forKey: .size)
|
||||
@@ -97,9 +96,6 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
self.sha256 = try container.decodeIfPresent(String.self, forKey: .sha256)?.lowercased()
|
||||
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
|
||||
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
|
||||
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
self.revision = try container.decodeIfPresent(String.self, forKey: .revision)
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -140,6 +136,8 @@ public extension AppVersion
|
||||
return NSFetchRequest<AppVersion>(entityName: "AppVersion")
|
||||
}
|
||||
|
||||
// this creates an entry into context(for each instantiation), so don't invoke unnessarily for temp things
|
||||
// create once and use mutateForData() to update it if required
|
||||
class func makeAppVersion(
|
||||
version: String,
|
||||
buildVersion: String?,
|
||||
@@ -147,6 +145,7 @@ public extension AppVersion
|
||||
localizedDescription: String? = nil,
|
||||
downloadURL: URL,
|
||||
size: Int64,
|
||||
sha256: String? = nil,
|
||||
appBundleID: String,
|
||||
sourceID: String? = nil,
|
||||
in context: NSManagedObjectContext) -> AppVersion
|
||||
@@ -155,9 +154,10 @@ public extension AppVersion
|
||||
appVersion.version = version
|
||||
appVersion.buildVersion = buildVersion
|
||||
appVersion.date = date
|
||||
appVersion.localizedDescription = localizedDescription
|
||||
appVersion._localizedDescription = localizedDescription
|
||||
appVersion.downloadURL = downloadURL
|
||||
appVersion.size = size
|
||||
appVersion.sha256 = sha256
|
||||
appVersion.appBundleID = appBundleID
|
||||
appVersion.sourceID = sourceID
|
||||
|
||||
|
||||
24
AltStoreCore/Model/BaseEntity.swift
Normal file
24
AltStoreCore/Model/BaseEntity.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// BaseEntity.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 28/01/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
public class BaseEntity: NSManagedObject, Fetchable
|
||||
{
|
||||
@nonobjc class func fetchRequest<T>() -> NSFetchRequest<T>
|
||||
{
|
||||
fatalError("method not implemented, subclass needs to provide an implementation")
|
||||
}
|
||||
|
||||
internal override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
|
||||
// print("\(BaseEntity.self):\(type(of: self)): Inserting: \(entity.name ?? "nil") into context: \(String(describing: context))")
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ fileprivate class PersistentContainer: RSTPersistentContainer
|
||||
|
||||
public class DatabaseManager
|
||||
{
|
||||
public static let shared = DatabaseManager()
|
||||
public static private(set) var shared = DatabaseManager()
|
||||
|
||||
public let persistentContainer: RSTPersistentContainer
|
||||
|
||||
@@ -64,10 +64,95 @@ public class DatabaseManager
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
private class func loadPersistentStoresSync() {
|
||||
let container = Self.shared.persistentContainer
|
||||
let semaphore = DispatchSemaphore(value: 0) // Semaphore to wait for async completion
|
||||
|
||||
container.loadPersistentStores { description, error in
|
||||
if let error = error {
|
||||
print("Failed to load store: \(error)")
|
||||
} else {
|
||||
print("Store URL: \(description.url ?? URL(string: "unknown")!)")
|
||||
}
|
||||
|
||||
semaphore.signal() // Signal the semaphore to unblock the thread
|
||||
}
|
||||
|
||||
semaphore.wait() // Wait for the semaphore signal to unblock the thread
|
||||
print("Persistent store loading complete.")
|
||||
}
|
||||
|
||||
class func deleteDatabase() -> Bool
|
||||
{
|
||||
// delete existing database and start fresh if required
|
||||
do {
|
||||
let container = Self.shared.persistentContainer
|
||||
|
||||
var databaseStore = container.persistentStoreCoordinator.persistentStores.first
|
||||
if databaseStore == nil{
|
||||
// perform a load before acquiring the databaseStoreURL
|
||||
Self.loadPersistentStoresSync()
|
||||
databaseStore = container.persistentStoreCoordinator.persistentStores.first
|
||||
}
|
||||
|
||||
|
||||
guard let databaseStore else
|
||||
{
|
||||
print("\nDatabase Delete request FAILED: databaseStore = nil\n")
|
||||
return false
|
||||
}
|
||||
|
||||
guard let databaseStoreURL = databaseStore.url else
|
||||
{
|
||||
print("\nDatabase Delete request FAILED: databaseStoreURL = nil\n")
|
||||
return false
|
||||
}
|
||||
|
||||
// Reset the managed object context
|
||||
Self.shared.persistentContainer.viewContext.reset()
|
||||
|
||||
// Remove all existing persistent stores
|
||||
for store in Self.shared.persistentContainer.persistentStoreCoordinator.persistentStores {
|
||||
try? Self.shared.persistentContainer.persistentStoreCoordinator.remove(store)
|
||||
}
|
||||
|
||||
// Now destroy the persistent store
|
||||
try Self.shared.persistentContainer.persistentStoreCoordinator.destroyPersistentStore(
|
||||
at: databaseStoreURL,
|
||||
ofType: NSSQLiteStoreType,
|
||||
options: nil
|
||||
)
|
||||
|
||||
// just be sure
|
||||
try? FileManager.default.removeItem(at: databaseStoreURL)
|
||||
|
||||
print("\nDatabase Delete: SUCCEEDED\n")
|
||||
|
||||
return true
|
||||
}catch{
|
||||
print("\nDatabase Delete request FAILED: \(error)\n")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class func recreateDatabase() {
|
||||
// Try to perform delete if one exists
|
||||
_ = Self.deleteDatabase()
|
||||
|
||||
// create new instance and load persistence store
|
||||
Self.shared = DatabaseManager()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
func start(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
|
||||
func finish(_ error: Error?)
|
||||
{
|
||||
self.dispatchQueue.async {
|
||||
@@ -42,7 +42,7 @@ public protocol InstalledAppProtocol: Fetchable
|
||||
}
|
||||
|
||||
@objc(InstalledApp)
|
||||
public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
public class InstalledApp: BaseEntity, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
@@ -76,45 +76,93 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
return self.storeApp == nil
|
||||
}
|
||||
|
||||
|
||||
// TODO: integrate the following into the hasUpdate such that altstore sources also work with SideStore, ex: pledge check etc for updates
|
||||
/*
|
||||
|
||||
|
||||
|
||||
|
||||
// let predicateFormat = [
|
||||
// // isActive && storeApp != nil && latestSupportedVersion != nil
|
||||
// "%K == YES AND %K != nil AND %K != nil",
|
||||
//
|
||||
// "AND",
|
||||
//
|
||||
// // latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion
|
||||
// //
|
||||
// // We have to also check !(latestSupportedVersion.buildVersion == '' && installedApp.storeBuildVersion == nil)
|
||||
// // because latestSupportedVersion.buildVersion stores an empty string for nil, while installedApp.storeBuildVersion uses NULL.
|
||||
// "(%K != %K OR (%K != %K AND NOT (%K == '' AND %K == nil)))",
|
||||
//
|
||||
// "AND",
|
||||
//
|
||||
// // !isPledgeRequired || isPledged
|
||||
// "(%K == NO OR %K == YES)"
|
||||
// ].joined(separator: " ")
|
||||
//
|
||||
// fetchRequest.predicate = NSPredicate(format: predicateFormat,
|
||||
// #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
// #keyPath(InstalledApp.storeApp.isPledgeRequired), #keyPath(InstalledApp.storeApp.isPledged))
|
||||
//
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@objc public var hasUpdate: Bool {
|
||||
guard let storeApp = self.storeApp,
|
||||
let latestSupportedVersion = storeApp.latestSupportedVersion?.version else {
|
||||
// Basic validation
|
||||
guard isActive,
|
||||
let storeApp = self.storeApp,
|
||||
let latestVersion = storeApp.latestSupportedVersion else
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
let currentVersion = SemanticVersion(self.version)
|
||||
let latestVersion = SemanticVersion(latestSupportedVersion)
|
||||
|
||||
if currentVersion == nil || latestVersion == nil {
|
||||
return self.version < latestSupportedVersion
|
||||
// Check pledge requirements
|
||||
guard !storeApp.isPledgeRequired || storeApp.isPledged else
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
let isBeta = storeApp.isBeta
|
||||
// Get current semantic versions
|
||||
let currentSemVer = SemanticVersion(self.version)
|
||||
let latestSemVer = SemanticVersion(latestVersion.version)
|
||||
|
||||
// compare semantic version updates
|
||||
// - for stable releases "beta" shouldn't be true
|
||||
if !isBeta && (currentVersion! < latestVersion!) {
|
||||
return true
|
||||
// If semantic versions can't be parsed, fall back to string comparison
|
||||
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)")
|
||||
|
||||
if UserDefaults.standard.isBetaUpdatesEnabled {
|
||||
// NOTE: beta builds will always need commit ID suffix
|
||||
// so it doesn't matter if semantic version was bumped, because commit ID won't be same
|
||||
// and we will accept this update
|
||||
|
||||
// storeApp.revision is set in sources.json deployed at apps.json for the respective source
|
||||
let revision = storeApp.revision ?? ""
|
||||
if(isBeta && !revision.isEmpty){
|
||||
let SHORT_COMMIT_LEN = 7
|
||||
let isRevisionValid = (revision.count == SHORT_COMMIT_LEN)
|
||||
let installedAppRevision = Bundle.main.object(forInfoDictionaryKey: "BuildRevision") as? String ?? ""
|
||||
// when installing beta build over stable build installedAppRevision will be empty!
|
||||
let isBetaUpdateAvailable = (installedAppRevision != revision)
|
||||
return isRevisionValid && isBetaUpdateAvailable
|
||||
}
|
||||
}
|
||||
return false
|
||||
// // Compare by major.minor.patch
|
||||
// if latestVer! > latestVer! {
|
||||
// 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
|
||||
return latestSemVer! > currentSemVer!
|
||||
}
|
||||
|
||||
|
||||
public var appIDCount: Int {
|
||||
return 1 + self.appExtensions.count
|
||||
@@ -165,8 +213,8 @@ public extension InstalledApp
|
||||
|
||||
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
|
||||
self.version = resignedApp.version
|
||||
// TODO: @mahee96: requires altsign-marketplace branch release or equivalent
|
||||
// self.buildVersion = resignedApp.buildVersion
|
||||
|
||||
self.buildVersion = resignedApp.buildVersion
|
||||
self.storeBuildVersion = storeBuildVersion
|
||||
|
||||
self.certificateSerialNumber = certificateSerialNumber
|
||||
@@ -239,33 +287,7 @@ public extension InstalledApp
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
|
||||
// let predicateFormat = [
|
||||
// // isActive && storeApp != nil && latestSupportedVersion != nil
|
||||
// "%K == YES AND %K != nil AND %K != nil",
|
||||
|
||||
// "AND",
|
||||
|
||||
// // latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion
|
||||
// //
|
||||
// // We have to also check !(latestSupportedVersion.buildVersion == '' && installedApp.storeBuildVersion == nil)
|
||||
// // because latestSupportedVersion.buildVersion stores an empty string for nil, while installedApp.storeBuildVersion uses NULL.
|
||||
// "(%K != %K OR (%K != %K AND NOT (%K == '' AND %K == nil)))",
|
||||
|
||||
// "AND",
|
||||
|
||||
// // !isPledgeRequired || isPledged
|
||||
// "(%K == NO OR %K == YES)"
|
||||
// ].joined(separator: " ")
|
||||
|
||||
// fetchRequest.predicate = NSPredicate(format: predicateFormat,
|
||||
// #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
// #keyPath(InstalledApp.storeApp.isPledgeRequired), #keyPath(InstalledApp.storeApp.isPledged))
|
||||
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES",
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate))
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.hasUpdate))
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
@@ -383,13 +405,13 @@ public extension InstalledApp
|
||||
return openAppURL
|
||||
}
|
||||
|
||||
var isUpdateAvailable: Bool {
|
||||
guard let storeApp = self.storeApp, let latestVersion = storeApp.latestSupportedVersion else { return false }
|
||||
guard !storeApp.isPledgeRequired || storeApp.isPledged else { return false }
|
||||
// var isUpdateAvailable: Bool {
|
||||
// guard let storeApp = self.storeApp, let latestVersion = storeApp.latestSupportedVersion else { return false }
|
||||
// guard !storeApp.isPledgeRequired || storeApp.isPledged else { return false }
|
||||
|
||||
let isUpdateAvailable = !self.matches(latestVersion)
|
||||
return isUpdateAvailable
|
||||
}
|
||||
// let isUpdateAvailable = !self.matches(latestVersion)
|
||||
// return isUpdateAvailable
|
||||
// }
|
||||
}
|
||||
|
||||
public extension InstalledApp
|
||||
|
||||
@@ -12,7 +12,7 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(InstalledExtension)
|
||||
public class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
public class InstalledExtension: BaseEntity, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
|
||||
@@ -24,7 +24,7 @@ extension LoggedError
|
||||
}
|
||||
|
||||
@objc(LoggedError)
|
||||
public class LoggedError: NSManagedObject, Fetchable
|
||||
public class LoggedError: BaseEntity
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var date: Date
|
||||
|
||||
@@ -129,77 +129,107 @@ private extension Error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
{
|
||||
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var sortedScreenshotIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var featuredAppIDsBySourceID = [String: [String]]()
|
||||
|
||||
|
||||
// MARK: - Actual Constraint conflict resolution takes place here!
|
||||
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
||||
{
|
||||
|
||||
// When conflict.databaseObject is unavailable, it means this is the first time insertion
|
||||
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
||||
for conflict in conflicts
|
||||
{
|
||||
switch conflict.conflictingObjects.first
|
||||
{
|
||||
case is StoreApp where conflict.conflictingObjects.count == 2:
|
||||
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
||||
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
||||
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
||||
|
||||
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
||||
{
|
||||
// Delete previous permissions (different than below).
|
||||
for case let permission as AppPermission in previousApp._permissions where permission.app == nil
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
// Delete previous versions (different than below).
|
||||
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil
|
||||
{
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
|
||||
// Delete previous screenshots (different than below).
|
||||
for case let appScreenshot as AppScreenshot in previousApp._screenshots where appScreenshot.app == nil
|
||||
{
|
||||
appScreenshot.managedObjectContext?.delete(appScreenshot)
|
||||
}
|
||||
}
|
||||
|
||||
case is AppVersion where conflict.conflictingObjects.count == 2:
|
||||
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
|
||||
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
||||
|
||||
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
||||
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
|
||||
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
|
||||
{
|
||||
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
||||
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown context-level conflict.
|
||||
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||
}
|
||||
}
|
||||
|
||||
try self.resolveWhenDatabaseObjectUnavailable(conflicts)
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var sortedScreenshotIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
permissionsByGlobalAppID.removeAll()
|
||||
sortedVersionIDsByGlobalAppID.removeAll()
|
||||
sortedScreenshotIDsByGlobalAppID.removeAll()
|
||||
featuredAppIDsBySourceID.removeAll()
|
||||
|
||||
var featuredAppIDsBySourceID = [String: [String]]()
|
||||
// When conflict.databaseObject is available, it means this is replace (delete + insert) or update
|
||||
try self.resolveWhenDatabaseObjectAvailable(conflicts)
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
|
||||
try self.performPostMergeValidationAndCorrections(for: conflicts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MergePolicy{
|
||||
|
||||
// When conflict.databaseObject is unavailable, the conflicts exist only in context level and they must be new insertions
|
||||
private func resolveWhenDatabaseObjectUnavailable(_ conflicts: [NSConstraintConflict]) throws{
|
||||
|
||||
for conflict in conflicts
|
||||
{
|
||||
switch conflict.conflictingObjects.first
|
||||
{
|
||||
case is StoreApp where conflict.conflictingObjects.count == 2:
|
||||
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
||||
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
||||
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
||||
|
||||
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
||||
{
|
||||
// Delete previous permissions (different than below).
|
||||
for case let permission as AppPermission in previousApp._permissions where permission.app == nil
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
// Delete previous versions (different than below).
|
||||
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil
|
||||
{
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
|
||||
// Delete previous screenshots (different than below).
|
||||
for case let appScreenshot as AppScreenshot in previousApp._screenshots where appScreenshot.app == nil
|
||||
{
|
||||
appScreenshot.managedObjectContext?.delete(appScreenshot)
|
||||
}
|
||||
}
|
||||
|
||||
case is AppVersion where conflict.conflictingObjects.count == 2:
|
||||
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
|
||||
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
||||
|
||||
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
||||
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
|
||||
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
|
||||
{
|
||||
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
||||
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown context-level conflict.
|
||||
// assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||
assertionFailure("Context Conflict Detected: is there ambigious data in your incoming sources?\nConflict:\(conflict)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When conflict.databaseObject is available, it means this is replace (delete + insert) or update
|
||||
private func resolveWhenDatabaseObjectAvailable(_ conflicts: [NSConstraintConflict]) throws {
|
||||
|
||||
for conflict in conflicts
|
||||
{
|
||||
switch conflict.databaseObject
|
||||
{
|
||||
case let databaseObject as StoreApp:
|
||||
guard let contextApp = conflict.conflictingObjects.first as? StoreApp else { break }
|
||||
|
||||
|
||||
// Permissions
|
||||
let contextPermissions = Set(contextApp._permissions.lazy.compactMap { $0 as? AppPermission }.map { AnyHashable($0.permission) })
|
||||
for case let databasePermission as AppPermission in databaseObject._permissions /* where !contextPermissions.contains(AnyHashable(databasePermission.permission)) */ // Compiler error as of Xcode 15
|
||||
@@ -308,7 +338,13 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
}
|
||||
}
|
||||
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MergePolicy{
|
||||
|
||||
func performPostMergeValidationAndCorrections(for conflicts: [NSConstraintConflict]) throws{
|
||||
|
||||
for conflict in conflicts
|
||||
{
|
||||
@@ -318,23 +354,23 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
do
|
||||
{
|
||||
var appVersions = databaseObject.versions
|
||||
|
||||
|
||||
if let globallyUniqueID = databaseObject.globallyUniqueID
|
||||
{
|
||||
// Permissions
|
||||
if let appPermissions = permissionsByGlobalAppID[globallyUniqueID],
|
||||
case let databasePermissions = Set(databaseObject.permissions.map({ AnyHashable($0.permission) })),
|
||||
databasePermissions != appPermissions
|
||||
case let databasePermissions = Set(databaseObject.permissions.map({ AnyHashable($0.permission) })),
|
||||
databasePermissions != appPermissions
|
||||
{
|
||||
// Sorting order doesn't matter, but elements themselves don't match so throw error.
|
||||
throw MergeError.incorrectPermissions(for: databaseObject)
|
||||
}
|
||||
|
||||
|
||||
// App versions
|
||||
if let sortedAppVersionIDs = sortedVersionIDsByGlobalAppID[globallyUniqueID],
|
||||
let sortedAppVersionsIDsArray = sortedAppVersionIDs.array as? [String],
|
||||
let sortedAppVersionsIDsArray = sortedAppVersionIDs.array as? [String],
|
||||
case let databaseVersionIDs = databaseObject.versions.map({ $0.versionID }),
|
||||
databaseVersionIDs != sortedAppVersionsIDsArray
|
||||
databaseVersionIDs != sortedAppVersionsIDsArray
|
||||
{
|
||||
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
|
||||
|
||||
@@ -355,9 +391,9 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
|
||||
// Screenshots
|
||||
if let sortedScreenshotIDs = sortedScreenshotIDsByGlobalAppID[globallyUniqueID],
|
||||
let sortedScreenshotIDsArray = sortedScreenshotIDs.array as? [String],
|
||||
case let databaseScreenshotIDs = databaseObject.screenshots.map({ $0.screenshotID }),
|
||||
databaseScreenshotIDs != sortedScreenshotIDsArray
|
||||
let sortedScreenshotIDsArray = sortedScreenshotIDs.array as? [String],
|
||||
case let databaseScreenshotIDs = databaseObject.screenshots.map({ $0.screenshotID }),
|
||||
databaseScreenshotIDs != sortedScreenshotIDsArray
|
||||
{
|
||||
// Screenshot order is incorrect, so attempt to fix by re-sorting.
|
||||
let fixedScreenshots = databaseObject.screenshots.sorted { (screenshotA, screenshotB) in
|
||||
@@ -421,3 +457,9 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MergePolicy{
|
||||
class func getHeader(_ obj: AnyObject) -> String {
|
||||
return obj.debugDescription.components(separatedBy: "; data:").first ?? ""
|
||||
}
|
||||
}
|
||||
@@ -22,27 +22,27 @@ fileprivate extension NSManagedObject
|
||||
}
|
||||
|
||||
var storeAppVersion: String? {
|
||||
let version = self.value(forKey: #keyPath(StoreApp._version)) as? String
|
||||
let version = self.value(forKey: #keyPath(StoreApp.version)) as? String
|
||||
return version
|
||||
}
|
||||
|
||||
var storeAppVersionDate: Date? {
|
||||
let versionDate = self.value(forKey: #keyPath(StoreApp._versionDate)) as? Date
|
||||
let versionDate = self.value(forKey: #keyPath(StoreApp.versionDate)) as? Date
|
||||
return versionDate
|
||||
}
|
||||
|
||||
var storeAppVersionDescription: String? {
|
||||
let versionDescription = self.value(forKey: #keyPath(StoreApp._versionDescription)) as? String
|
||||
let versionDescription = self.value(forKey: #keyPath(StoreApp.versionDescription)) as? String
|
||||
return versionDescription
|
||||
}
|
||||
|
||||
var storeAppSize: NSNumber? {
|
||||
let size = self.value(forKey: #keyPath(StoreApp._size)) as? NSNumber
|
||||
let size = self.value(forKey: #keyPath(StoreApp.size)) as? NSNumber
|
||||
return size
|
||||
}
|
||||
|
||||
var storeAppDownloadURL: URL? {
|
||||
let downloadURL = self.value(forKey: #keyPath(StoreApp._downloadURL)) as? URL
|
||||
let downloadURL = self.value(forKey: #keyPath(StoreApp.downloadURL)) as? URL
|
||||
return downloadURL
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ fileprivate extension NSManagedObject
|
||||
let appVersion = NSEntityDescription.insertNewObject(forEntityName: AppVersion.entity().name!, into: context)
|
||||
appVersion.setValue(version, forKey: #keyPath(AppVersion.version))
|
||||
appVersion.setValue(date, forKey: #keyPath(AppVersion.date))
|
||||
appVersion.setValue(localizedDescription, forKey: #keyPath(AppVersion.localizedDescription))
|
||||
appVersion.setValue(localizedDescription, forKey: #keyPath(AppVersion._localizedDescription))
|
||||
appVersion.setValue(downloadURL, forKey: #keyPath(AppVersion.downloadURL))
|
||||
appVersion.setValue(size, forKey: #keyPath(AppVersion.size))
|
||||
appVersion.setValue(appBundleID, forKey: #keyPath(AppVersion.appBundleID))
|
||||
|
||||
@@ -10,7 +10,7 @@ import UIKit
|
||||
import CoreData
|
||||
|
||||
@objc(NewsItem)
|
||||
public class NewsItem: NSManagedObject, Decodable, Fetchable
|
||||
public class NewsItem: BaseEntity, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import CoreData
|
||||
|
||||
@objc(ManagedPatron)
|
||||
public class ManagedPatron: NSManagedObject, Fetchable
|
||||
public class ManagedPatron: BaseEntity
|
||||
{
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
@@ -18,7 +18,7 @@ public class ManagedPatron: NSManagedObject, Fetchable
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
|
||||
public init?(patron: PatreonAPI.Patron, context: NSManagedObjectContext)
|
||||
{
|
||||
// Only cache Patrons with non-nil names.
|
||||
@@ -9,7 +9,7 @@
|
||||
import CoreData
|
||||
|
||||
@objc(RefreshAttempt)
|
||||
public class RefreshAttempt: NSManagedObject, Fetchable
|
||||
public class RefreshAttempt: BaseEntity
|
||||
{
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var date: Date
|
||||
|
||||
@@ -52,6 +52,8 @@ public struct AppVersionFeed: Codable {
|
||||
|
||||
let downloadURL: URL
|
||||
let size: Int64
|
||||
// added in 0.6.0
|
||||
let sha256: String? // sha 256 of the uploaded IPA
|
||||
|
||||
enum CodingKeys: String, CodingKey
|
||||
{
|
||||
@@ -60,6 +62,8 @@ public struct AppVersionFeed: Codable {
|
||||
case localizedDescription
|
||||
case downloadURL
|
||||
case size
|
||||
// added in 0.6.0
|
||||
case sha256
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +103,7 @@ public struct StoreAppFeed: Codable {
|
||||
let isBeta: Bool
|
||||
|
||||
// let source: Source?
|
||||
let appPermission: [AppPermissionFeed]
|
||||
let appPermissions: [AppPermissionFeed]
|
||||
let versions: [AppVersionFeed]
|
||||
|
||||
enum CodingKeys: String, CodingKey
|
||||
@@ -111,7 +115,7 @@ public struct StoreAppFeed: Codable {
|
||||
case isBeta = "beta"
|
||||
case localizedDescription
|
||||
case name
|
||||
case appPermission = "permissions"
|
||||
case appPermissions
|
||||
case platformURLs
|
||||
case screenshotURLs
|
||||
case size
|
||||
@@ -154,6 +158,7 @@ public struct NewsItemFeed: Codable {
|
||||
|
||||
|
||||
public struct SourceJSON: Codable {
|
||||
let version: Int?
|
||||
let name: String
|
||||
let identifier: String
|
||||
let sourceURL: URL
|
||||
@@ -163,6 +168,7 @@ public struct SourceJSON: Codable {
|
||||
|
||||
enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case version
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
@@ -195,9 +201,10 @@ public extension Source
|
||||
}
|
||||
|
||||
@objc(Source)
|
||||
public class Source: NSManagedObject, Fetchable, Decodable
|
||||
public class Source: BaseEntity, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var version: Int
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public private(set) var identifier: String
|
||||
@NSManaged public var sourceURL: URL
|
||||
@@ -246,6 +253,12 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var isSourceAtLeastV2: Bool {
|
||||
return self.version >= 2
|
||||
}
|
||||
|
||||
|
||||
// `internal` to prevent accidentally using instead of `effectiveFeaturedApps`
|
||||
@nonobjc internal var featuredApps: [StoreApp]? {
|
||||
return self._hasFeaturedApps ? self._featuredApps.array as? [StoreApp] : nil
|
||||
@@ -253,6 +266,7 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case version
|
||||
case name
|
||||
case sourceURL
|
||||
case subtitle
|
||||
@@ -287,6 +301,10 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
|
||||
// Optional Values
|
||||
|
||||
// use sourceversion = 1 by default if not specified in source json
|
||||
self.version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1
|
||||
|
||||
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
self.websiteURL = try container.decodeIfPresent(URL.self, forKey: .websiteURL)
|
||||
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
|
||||
|
||||
@@ -123,8 +123,15 @@ private struct PatreonParameters: Decodable
|
||||
var hidden: Bool?
|
||||
}
|
||||
|
||||
// added for v0.6.0
|
||||
extension StoreApp {
|
||||
//MARK: - properties
|
||||
@NSManaged public private(set) var sha256: String?
|
||||
}
|
||||
|
||||
|
||||
@objc(StoreApp)
|
||||
public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
public class StoreApp: BaseEntity, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var name: String
|
||||
@@ -132,8 +139,8 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@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
|
||||
@NSManaged public private(set) var localizedDescription: String?
|
||||
@NSManaged public private(set) var size: Int64
|
||||
|
||||
@nonobjc public var category: StoreCategory? {
|
||||
guard let _category else { return nil }
|
||||
@@ -146,14 +153,13 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged public private(set) var iconURL: URL
|
||||
@NSManaged public private(set) var screenshotURLs: [URL]
|
||||
|
||||
@NSManaged @objc(downloadURL) internal var _downloadURL: URL
|
||||
@NSManaged public private(set) var downloadURL: URL?
|
||||
@NSManaged public private(set) var platformURLs: PlatformURLs?
|
||||
|
||||
@NSManaged public private(set) var tintColor: UIColor?
|
||||
|
||||
// TODO: @mahee96: retire isBeta and use a string type to decode and store values as enum
|
||||
@NSManaged public private(set) var isBeta: Bool
|
||||
@NSManaged public private(set) var revision: String?
|
||||
|
||||
// Required for Marketplace apps.
|
||||
@NSManaged public private(set) var marketplaceID: String?
|
||||
@@ -202,9 +208,9 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged private var primitiveSourceIdentifier: String?
|
||||
|
||||
// Legacy (kept for backwards compatibility)
|
||||
@NSManaged @objc(version) internal private(set) var _version: String
|
||||
@NSManaged @objc(versionDate) internal private(set) var _versionDate: Date
|
||||
@NSManaged @objc(versionDescription) internal private(set) var _versionDescription: String?
|
||||
@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?
|
||||
@@ -243,30 +249,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
return self._versions.array as! [AppVersion]
|
||||
}
|
||||
|
||||
@nonobjc public var size: Int64? {
|
||||
guard let version = self.latestSupportedVersion else { return nil }
|
||||
return version.size
|
||||
}
|
||||
|
||||
@nonobjc public var version: String? {
|
||||
guard let version = self.latestSupportedVersion else { return nil }
|
||||
return version.version
|
||||
}
|
||||
|
||||
@nonobjc public var versionDescription: String? {
|
||||
guard let version = self.latestSupportedVersion else { return nil }
|
||||
return version.localizedDescription
|
||||
}
|
||||
|
||||
@nonobjc public var versionDate: Date? {
|
||||
guard let version = self.latestSupportedVersion else { return nil }
|
||||
return version.date
|
||||
}
|
||||
|
||||
@nonobjc public var downloadURL: URL? {
|
||||
guard let version = self.latestSupportedVersion else { return nil }
|
||||
return version.downloadURL
|
||||
}
|
||||
@nonobjc public var screenshots: [AppScreenshot] {
|
||||
return self._screenshots.array as! [AppScreenshot]
|
||||
}
|
||||
@@ -292,7 +274,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case permissions = "appPermissions"
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
case revision = "commitID"
|
||||
case versions
|
||||
case patreon
|
||||
case category
|
||||
@@ -303,6 +284,9 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case versionDate
|
||||
case downloadURL
|
||||
case screenshotURLs
|
||||
|
||||
// new for v0.6.0
|
||||
case sha256
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
@@ -316,50 +300,16 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.sha256 = try container.decodeIfPresent(String.self, forKey: .sha256)
|
||||
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
|
||||
self.developerName = try container.decode(String.self, forKey: .developerName)
|
||||
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
||||
self.localizedDescription = try container.decodeIfPresent(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
|
||||
self.revision = try container.decodeIfPresent(String.self, forKey: .revision)
|
||||
|
||||
var downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
|
||||
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
|
||||
if let platformURLs = platformURLs {
|
||||
self.platformURLs = platformURLs
|
||||
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
|
||||
if let first = platformURLs.sorted().first {
|
||||
self._downloadURL = first.downloadURL
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
|
||||
|
||||
}
|
||||
|
||||
} else if let downloadURL = downloadURL {
|
||||
self._downloadURL = downloadURL
|
||||
} else {
|
||||
let version = try container.decode(String.self, forKey: .version)
|
||||
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions){
|
||||
for ver in versions {
|
||||
if ver.version == version {
|
||||
self._downloadURL = ver.downloadURL
|
||||
downloadURL = ver.downloadURL // not sure if this is needed
|
||||
}
|
||||
}
|
||||
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
||||
}
|
||||
// Required for Marketplace apps, but we'll verify later.
|
||||
self.marketplaceID = try container.decodeIfPresent(String.self, forKey: .marketplaceID)
|
||||
|
||||
// else {
|
||||
// throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
||||
// }
|
||||
}
|
||||
|
||||
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
||||
{
|
||||
@@ -383,13 +333,21 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
}
|
||||
else if let screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs)
|
||||
{
|
||||
// Update to iPhone 13 screen size
|
||||
let modernAspectRatio = CGSize(width: 1170, height: 2532)
|
||||
// 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: modernAspectRatio, deviceType: .iphone, context: context)
|
||||
let screenshot = AppScreenshot(imageURL: imageURL, size: legacyAspectRatio, deviceType: .iphone, context: context)
|
||||
return screenshot
|
||||
}
|
||||
|
||||
// // Update to iPhone 13 screen size
|
||||
// let modernAspectRatio = CGSize(width: 1170, height: 2532)
|
||||
|
||||
// appScreenshots = screenshotURLs.map { imageURL in
|
||||
// let screenshot = AppScreenshot(imageURL: imageURL, size: modernAspectRatio, deviceType: .iphone, context: context)
|
||||
// return screenshot
|
||||
// }
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -418,6 +376,9 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
self._permissions = NSSet()
|
||||
}
|
||||
|
||||
// Required for Marketplace apps, but we'll verify later.
|
||||
self.marketplaceID = try container.decodeIfPresent(String.self, forKey: .marketplaceID)
|
||||
|
||||
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
||||
{
|
||||
//TODO: Throw error if there isn't at least one version.
|
||||
@@ -479,6 +440,28 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
try self.setVersions([appVersion])
|
||||
}
|
||||
|
||||
// latestSupportedVersion is set by this point if one was available
|
||||
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
|
||||
if let platformURLs = platformURLs {
|
||||
self.platformURLs = platformURLs
|
||||
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
|
||||
if let first = platformURLs.sorted().first {
|
||||
self.downloadURL = first.downloadURL
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
|
||||
|
||||
}
|
||||
} else if let downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL) {
|
||||
self.downloadURL = downloadURL
|
||||
} else {
|
||||
// capture it first coz field might still be faulted by coredata
|
||||
guard let _ = self.downloadURL else
|
||||
{
|
||||
let error = DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Must _explicitly_ set to false to ensure it updates cached database value.
|
||||
self.isPledged = false
|
||||
self.prefersCustomPledge = false
|
||||
@@ -560,11 +543,13 @@ internal extension StoreApp
|
||||
}
|
||||
|
||||
// Preserve backwards compatibility by assigning legacy property values.
|
||||
self._version = latestVersion.version
|
||||
self._versionDate = latestVersion.date
|
||||
self._versionDescription = latestVersion.localizedDescription
|
||||
self._downloadURL = latestVersion.downloadURL
|
||||
self._size = Int32(latestVersion.size)
|
||||
self.version = latestVersion.version
|
||||
self.versionDate = latestVersion.date
|
||||
self.versionDescription = latestVersion.localizedDescription
|
||||
self.downloadURL = latestVersion.downloadURL
|
||||
self.size = latestVersion.size
|
||||
self.localizedDescription = latestVersion.localizedDescription
|
||||
self.sha256 = latestVersion.sha256
|
||||
}
|
||||
|
||||
func setPermissions(_ permissions: Set<AppPermission>)
|
||||
@@ -674,24 +659,63 @@ public extension StoreApp
|
||||
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
||||
}
|
||||
|
||||
//MARK: - override in subclasses if required
|
||||
@objc func placeholderAppVersion(appVersion: AppVersion, in context: NSManagedObjectContext) -> AppVersion{
|
||||
return appVersion
|
||||
}
|
||||
//MARK: - override in subclasses if required
|
||||
@objc class func createStoreApp(in context: NSManagedObjectContext) -> StoreApp{
|
||||
return StoreApp(context: context)
|
||||
}
|
||||
|
||||
|
||||
class func isPlaceHolderVersion(_ version: AppVersion) -> Bool{
|
||||
return version.version == "0.0.0" && version.date == Date.distantPast && version.appBundleID == StoreApp.altstoreAppID
|
||||
}
|
||||
|
||||
class func isPlaceHolderStoreApp(_ app: StoreApp) -> Bool{
|
||||
return app.version == "0.0.0" && app.versionDate == Date.distantPast && app.bundleIdentifier == StoreApp.altstoreAppID
|
||||
}
|
||||
|
||||
|
||||
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 placeholderDownloadURL = URL(string: "https://sidestore.io")!
|
||||
let placeholderSourceID = Source.altStoreIdentifier
|
||||
|
||||
let app = StoreApp(context: context)
|
||||
app.name = "SideStore"
|
||||
app.bundleIdentifier = StoreApp.altstoreAppID
|
||||
app.bundleIdentifier = placeholderAppID
|
||||
app.developerName = "Side Team"
|
||||
app.localizedDescription = "SideStore is an alternative App Store."
|
||||
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
|
||||
app.iconURL = Self.sideStoreAppIconURL
|
||||
app.screenshotURLs = []
|
||||
app.sourceIdentifier = Source.altStoreIdentifier
|
||||
app.sourceIdentifier = placeholderSourceID
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: version,
|
||||
buildVersion: buildVersion,
|
||||
date: Date(),
|
||||
downloadURL: URL(string: "http://rileytestut.com")!,
|
||||
downloadURL: placeholderDownloadURL,
|
||||
size: 0,
|
||||
appBundleID: app.bundleIdentifier,
|
||||
sourceID: Source.altStoreIdentifier,
|
||||
sourceID: app.sourceIdentifier,
|
||||
in: context)
|
||||
try? app.setVersions([appVersion])
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public extension Team
|
||||
}
|
||||
|
||||
@objc(Team)
|
||||
public class Team: NSManagedObject, Fetchable
|
||||
public class Team: BaseEntity
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var name: String
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>BuildRevision</key>
|
||||
<string>$(BUILD_REVISION)</string>
|
||||
<key>BuildChannel</key>
|
||||
<string>$(BUILD_CHANNEL)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
MARKETING_VERSION = 0.6.0
|
||||
CURRENT_PROJECT_VERSION = 6000
|
||||
BUILD_CHANNEL = stable
|
||||
|
||||
// Vars to be overwritten by `CodeSigning.xcconfig` if exists
|
||||
DEVELOPMENT_TEAM = S32Z3HMYVQ
|
||||
|
||||
@@ -47,10 +47,16 @@ minimuxer and em_proxy use prebuilt static library binaries built by GitHub Acti
|
||||
[`SideStore/fetch-prebuilt.sh`](./SideStore/fetch-prebuilt.sh) will be run before each build by Xcode, and it will check if the downloaded binaries are up-to-date once every 6 hours. If you want
|
||||
to force it to check for new binaries, run `bash ./SideStore/fetch-prebuilt.sh force`.
|
||||
|
||||
## Building with Xcode
|
||||
|
||||
Install cocoapods if required using: `brew install cocoapods`
|
||||
Now using commandline on the repository workspace root, perform Pod-Install using: `pod install` command to install the cocoapod dependencies.
|
||||
After this you can do regular builds within Xcode.
|
||||
|
||||
## Building an IPA for distribution
|
||||
|
||||
Install cocoapods if required using: `brew install cocoapods`
|
||||
Now perform Pod-Install using: `pod install` command to install the dependencies.
|
||||
Install cocoapods if required using: `brew install cocoapods`
|
||||
Now using commandline on the repository workspace root, perform Pod-Install using: `pod install` command to install the cocoapod dependencies.
|
||||
|
||||
You can then use the Makefile command: `make build fakesign ipa` in the root directory.
|
||||
By default the config for build is: `Release`
|
||||
|
||||
@@ -15,10 +15,6 @@ ORG_IDENTIFIER = com.myuniquename
|
||||
// we don't want to do this for release since those builds will most likely be installed via SideServer, which adds the team ID
|
||||
BUNDLE_ID_SUFFIX = .$(DEVELOPMENT_TEAM)
|
||||
|
||||
// Comment this out to use default 'stable' channel defined in Build.xcconfig
|
||||
// For local xcode or commandline builds we can use local channel for local build specific logic
|
||||
BUILD_CHANNEL = local
|
||||
|
||||
// Set to YES if you have a valid paid Apple Developer account
|
||||
DEVELOPER_ACCOUNT_PAID = NO
|
||||
|
||||
|
||||
51
Makefile
51
Makefile
@@ -157,57 +157,19 @@ test:
|
||||
bundle exec fastlane test
|
||||
|
||||
## -- Building --
|
||||
|
||||
IS_ALPHA_TRUE := $(filter true TRUE 1, $(IS_ALPHA))
|
||||
IS_BETA_TRUE := $(filter true TRUE 1, $(IS_BETA))
|
||||
|
||||
# set build types to embed into Info.plist for key: BuildChannel
|
||||
BUILD_CHANNEL := stable
|
||||
BUILD_CHANNEL := $(if $(IS_ALPHA_TRUE),alpha,$(BUILD_CHANNEL))
|
||||
BUILD_CHANNEL := $(if $(IS_BETA_TRUE),beta,$(BUILD_CHANNEL))
|
||||
|
||||
# Fetch the latest commit ID for ALPHA or BETA builds
|
||||
COMMIT_ID := $(if $(or $(IS_ALPHA_TRUE),$(IS_BETA_TRUE)),$(shell git rev-parse --short HEAD),)
|
||||
|
||||
# Print release type based on the value of IS_ALPHA or IS_BETA
|
||||
print_release_type:
|
||||
@echo ""
|
||||
@if [ $(IS_ALPHA_TRUE) ]; then \
|
||||
echo "'IS_ALPHA' is set to true. Fetched the latest commit ID from HEAD..."; \
|
||||
echo " Commit ID: $(COMMIT_ID)"; \
|
||||
echo ""; \
|
||||
echo ">>>>>>>> This is now an ALPHA release for COMMIT_ID = '$(COMMIT_ID)' <<<<<<<<<"; \
|
||||
echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
|
||||
elif [ $(IS_BETA_TRUE) ]; then \
|
||||
echo "'IS_BETA' is set to true. Fetched the latest commit ID from HEAD..."; \
|
||||
echo " Commit ID: $(COMMIT_ID)"; \
|
||||
echo ""; \
|
||||
echo ">>>>>>>> This is now a BETA release for COMMIT_ID = '$(COMMIT_ID)' <<<<<<<<<"; \
|
||||
echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
|
||||
else \
|
||||
echo "'IS_ALPHA' and 'IS_BETA' are not set to true. Skipping commit ID fetch."; \
|
||||
echo ""; \
|
||||
echo ">>>>>>>> This is now a STABLE release because neither IS_ALPHA nor IS_BETA was true <<<<<<<<<"; \
|
||||
echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
|
||||
echo ""; \
|
||||
fi
|
||||
@echo ""
|
||||
|
||||
# Build target with the print_commit_id dependency
|
||||
|
||||
# NOTE: The build config was implicitly 'release' since it was set in AltStore.project
|
||||
# under "use "Release" configuration for commandline builds" setting
|
||||
# so I had just defined it explicitly.
|
||||
#
|
||||
# However the scheme used is Debug Scheme, so it was deliberately
|
||||
# using scheme = Debug and config = Release (so I have kept it as-is)
|
||||
# BUILD_CONFIG := "Debug" # switched to debug build-config to diagnose issue since debugger won't resolve breakpoints in release
|
||||
# BUILD_CONFIG := "Release"
|
||||
# BUILD_CONFIG ?= Debug # switched to debug build-config to diagnose issue since debugger won't resolve breakpoints in release
|
||||
|
||||
# switched back to release build as default config, unless specified by the incoming environment vars
|
||||
# Overrides (will inherit from env if set already)
|
||||
BUILD_CONFIG ?= Release
|
||||
build: print_release_type
|
||||
@echo ">>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<"
|
||||
MARKETING_VERSION ?=
|
||||
build:
|
||||
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||
@echo ""
|
||||
@xcodebuild -workspace AltStore.xcworkspace \
|
||||
-scheme SideStore \
|
||||
@@ -219,8 +181,7 @@ build: print_release_type
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||
ORG_IDENTIFIER=com.SideStore \
|
||||
BUILD_REVISION=$(COMMIT_ID) \
|
||||
BUILD_CHANNEL=$(BUILD_CHANNEL)
|
||||
MARKETING_VERSION=$(MARKETING_VERSION) \
|
||||
BUNDLE_ID_SUFFIX=
|
||||
# DWARF_DSYM_FOLDER_PATH="."
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// BuildInfo.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 21/01/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
public class BuildInfo{
|
||||
private static let BUILD_REVISION_TAG = "BuildRevision" // commit ID for now (but could be any, set by build env vars
|
||||
private static let BUILD_CHANNEL_TAG = "BuildChannel" // set by build env, ex CI will set it via env vars, for xcode builds this is empty
|
||||
|
||||
private static let MARKETING_VERSION_TAG = "CFBundleShortVersionString"
|
||||
private static let CURRENT_PROJECT_VERSION_TAG = kCFBundleVersionKey as String
|
||||
|
||||
private static let XCODE_VERSION_TAG = "DTXcode"
|
||||
private static let XCODE_REVISION_TAG = "DTXcodeBuild"
|
||||
|
||||
public enum Channel: String {
|
||||
case unknown
|
||||
case local // xcodebuilds can use this by setting BUILD_CHANNEL in CodeSigning.xcconfig
|
||||
|
||||
case alpha
|
||||
case beta
|
||||
case stable
|
||||
}
|
||||
|
||||
public lazy var channel: Channel = {
|
||||
let channel = Bundle.main.object(forInfoDictionaryKey: Self.BUILD_CHANNEL_TAG) as? String
|
||||
return Channel(rawValue: channel ?? "") ?? .unknown
|
||||
}()
|
||||
|
||||
public lazy var revision: String? = {
|
||||
let revision = Bundle.main.object(forInfoDictionaryKey: Self.BUILD_REVISION_TAG) as? String
|
||||
return revision
|
||||
}()
|
||||
|
||||
public lazy var project_version: String? = {
|
||||
let revision = Bundle.main.object(forInfoDictionaryKey: Self.CURRENT_PROJECT_VERSION_TAG) as? String
|
||||
return revision
|
||||
}()
|
||||
|
||||
public lazy var marketing_version: String? = {
|
||||
let revision = Bundle.main.object(forInfoDictionaryKey: Self.MARKETING_VERSION_TAG) as? String
|
||||
return revision
|
||||
}()
|
||||
|
||||
public lazy var xcode: String? = {
|
||||
let xcode = Bundle.main.object(forInfoDictionaryKey: Self.XCODE_VERSION_TAG) as? String
|
||||
return xcode
|
||||
}()
|
||||
|
||||
public lazy var xcode_revision: String? = {
|
||||
let revision = Bundle.main.object(forInfoDictionaryKey: Self.XCODE_REVISION_TAG) as? String
|
||||
return revision
|
||||
}()
|
||||
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import Foundation
|
||||
import CoreData
|
||||
import System
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class CoreDataHelper{
|
||||
|
||||
private static let STORE_XCMODELD_NAME = "AltStore"
|
||||
@@ -36,23 +38,25 @@ class CoreDataHelper{
|
||||
}
|
||||
|
||||
// let container = NSPersistentContainer(name: STORE_XCMODELD_NAME)
|
||||
let container = NSPersistentContainer(name: STORE_XCMODELD_NAME, managedObjectModel: model)
|
||||
|
||||
// let container = NSPersistentContainer(name: STORE_XCMODELD_NAME, managedObjectModel: model)
|
||||
let container = DatabaseManager.shared.persistentContainer
|
||||
|
||||
// bridge callback into async-await pattern
|
||||
return try await withCheckedThrowingContinuation{ (continuation: CheckedContinuation<URL, Error>) in
|
||||
// return try await withCheckedThrowingContinuation{ (continuation: CheckedContinuation<URL, Error>) in
|
||||
|
||||
// async callback processing
|
||||
container.loadPersistentStores { description, error in
|
||||
// container.loadPersistentStores { description, error in
|
||||
// perform actual backup in sync manner
|
||||
do{
|
||||
let exportedURL = try backupCoreDataStore(container: container, loadError: error)
|
||||
continuation.resume(returning: exportedURL)
|
||||
}catch{
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
// do{
|
||||
// let exportedURL = try backupCoreDataStore(container: container, loadError: error)
|
||||
// let exportedURL = try backupCoreDataStore(container: container)
|
||||
return try backupCoreDataStore(container: container)
|
||||
// continuation.resume(returning: exportedURL)
|
||||
// }catch{
|
||||
// continuation.resume(throwing: error)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private static func lockSQLiteFile(at url: URL) -> FileDescriptor? {
|
||||
@@ -86,7 +90,8 @@ class CoreDataHelper{
|
||||
}
|
||||
|
||||
|
||||
private static func backupCoreDataStore(container: NSPersistentContainer, loadError: Error?) throws -> URL {
|
||||
private static func backupCoreDataStore(container: NSPersistentContainer, loadError: Error? = nil) throws -> URL
|
||||
{
|
||||
|
||||
// Check for load errors
|
||||
if let error = loadError {
|
||||
@@ -119,25 +124,29 @@ class CoreDataHelper{
|
||||
let currentDateTime = Date()
|
||||
let currentTimeStamp = DateTimeUtil.getDateInTimeStamp(date: currentDateTime)
|
||||
|
||||
let fileNamePrefix = storeURL.deletingPathExtension().lastPathComponent
|
||||
let fileExtension = storeURL.pathExtension
|
||||
let fileName = DateTimeUtil.getTimeStampSuffixedFileName(
|
||||
fileName: fileNamePrefix,
|
||||
timestamp: currentTimeStamp,
|
||||
extn: "." + fileExtension
|
||||
)
|
||||
func getFileName(extn fileExtension: String) -> String {
|
||||
let fileNamePrefix = storeURL.deletingPathExtension().lastPathComponent
|
||||
let fileName = DateTimeUtil.getTimeStampSuffixedFileName(
|
||||
fileName: fileNamePrefix,
|
||||
timestamp: currentTimeStamp,
|
||||
extn: "." + fileExtension
|
||||
)
|
||||
return fileName
|
||||
}
|
||||
|
||||
let fileName = getFileName(extn: storeURL.pathExtension)
|
||||
let destinationURL = exportedDir.appendingPathComponent(fileName)
|
||||
|
||||
let directoryURL = storeURL.deletingLastPathComponent()
|
||||
if let files = try? FileManager.default.contentsOfDirectory(atPath: directoryURL.path) {
|
||||
print("Files in Application Support: \(files)")
|
||||
print("Files in Database Dir: \(directoryURL), \(files)")
|
||||
} else {
|
||||
print("Failed to list directory contents.")
|
||||
}
|
||||
|
||||
let parentDirectory = destinationURL.deletingLastPathComponent()
|
||||
|
||||
// TODO: CLOSE Store such that WAL and SHM are flushed and take backup of single sqlite store
|
||||
|
||||
do {
|
||||
// create intermediate dirs as required
|
||||
@@ -147,18 +156,19 @@ class CoreDataHelper{
|
||||
|
||||
// Copy main SQLite file
|
||||
try fileManager.copyItem(at: storeURL, to: destinationURL)
|
||||
|
||||
print("Core Data store exported to: \(destinationURL.path)")
|
||||
|
||||
// Copy -shm and -wal files if they exist
|
||||
let additionalFiles = ["-shm", "-wal"].compactMap {
|
||||
destinationURL.deletingPathExtension().appendingPathExtension(destinationURL.pathExtension + $0)
|
||||
storeURL.deletingPathExtension().appendingPathExtension(destinationURL.pathExtension + $0)
|
||||
}
|
||||
|
||||
for file in additionalFiles where fileManager.fileExists(atPath: file.path) {
|
||||
let destination = documentsURL.appendingPathComponent(file.lastPathComponent)
|
||||
let destination = destinationURL.deletingPathExtension() .appendingPathExtension(file.pathExtension)
|
||||
try fileManager.copyItem(at: file, to: destination)
|
||||
print("Core Data store exported to: \(destination.path)")
|
||||
}
|
||||
|
||||
print("Core Data store exported to: \(destinationURL.path)")
|
||||
return destinationURL
|
||||
|
||||
} catch {
|
||||
|
||||
117
update_apps.py
117
update_apps.py
@@ -4,24 +4,21 @@ import os
|
||||
import json
|
||||
import sys
|
||||
|
||||
SIDESTORE_BUNDLE_ID = "com.SideStore.SideStore"
|
||||
# SIDESTORE_BUNDLE_ID = "com.SideStore.SideStore"
|
||||
|
||||
# Set environment variables with default values
|
||||
VERSION_IPA = os.getenv("VERSION_IPA")
|
||||
VERSION_DATE = os.getenv("VERSION_DATE")
|
||||
RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL")
|
||||
COMMIT_ID = os.getenv("COMMIT_ID")
|
||||
IS_BETA = os.getenv("IS_BETA")
|
||||
SIZE = os.getenv("SIZE")
|
||||
SHA256 = os.getenv("SHA256")
|
||||
LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION")
|
||||
DOWNLOAD_URL = os.getenv("DOWNLOAD_URL")
|
||||
BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER", SIDESTORE_BUNDLE_ID)
|
||||
BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER")
|
||||
|
||||
# Uncomment to debug/test by simulating dummy input locally
|
||||
# VERSION_IPA = os.getenv("VERSION_IPA", "0.0.0")
|
||||
# VERSION_DATE = os.getenv("VERSION_DATE", "2000-12-18T00:00:00Z")
|
||||
# RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL", "alpha")
|
||||
# COMMIT_ID = os.getenv("COMMIT_ID", "1234567")
|
||||
# SIZE = int(os.getenv("SIZE", "0")) # Convert to integer
|
||||
# SHA256 = os.getenv("SHA256", "")
|
||||
# LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION", "Invalid Update")
|
||||
@@ -37,15 +34,22 @@ print(f"Input File: {input_file}")
|
||||
|
||||
# Debugging the environment variables
|
||||
print(" ====> Required parameter list <====")
|
||||
print("Bundle Identifier:", BUNDLE_IDENTIFIER)
|
||||
print("Version:", VERSION_IPA)
|
||||
print("Version Date:", VERSION_DATE)
|
||||
print("ReleaseChannel:", RELEASE_CHANNEL)
|
||||
print("Commit ID:", COMMIT_ID)
|
||||
print("IsBeta:", IS_BETA)
|
||||
print("Size:", SIZE)
|
||||
print("Sha256:", SHA256)
|
||||
print("Localized Description:", LOCALIZED_DESCRIPTION)
|
||||
print("Download URL:", DOWNLOAD_URL)
|
||||
|
||||
if IS_BETA is None:
|
||||
print("Setting IS_BETA = False since no value was provided")
|
||||
IS_BETA = False
|
||||
|
||||
if str(IS_BETA).lower() in ["true", "1", "yes"]:
|
||||
IS_BETA = True
|
||||
|
||||
# Read the input JSON file
|
||||
try:
|
||||
with open(input_file, "r") as file:
|
||||
@@ -54,77 +58,56 @@ except Exception as e:
|
||||
print(f"Error reading the input file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if (VERSION_IPA == None or \
|
||||
VERSION_DATE == None or \
|
||||
RELEASE_CHANNEL == None or \
|
||||
SIZE == None or \
|
||||
SHA256 == None or \
|
||||
LOCALIZED_DESCRIPTION == None or \
|
||||
DOWNLOAD_URL == None):
|
||||
if (not BUNDLE_IDENTIFIER or
|
||||
not VERSION_IPA or
|
||||
not VERSION_DATE or
|
||||
not SIZE or
|
||||
not SHA256 or
|
||||
not LOCALIZED_DESCRIPTION or
|
||||
not DOWNLOAD_URL):
|
||||
print("One or more required parameter(s) were not defined as environment variable(s)")
|
||||
sys.exit(1)
|
||||
|
||||
# make it lowecase
|
||||
RELEASE_CHANNEL = RELEASE_CHANNEL.lower()
|
||||
# Convert to integer
|
||||
SIZE = int(SIZE)
|
||||
|
||||
if RELEASE_CHANNEL != 'stable' and COMMIT_ID is None:
|
||||
print("Commit ID cannot be empty when ReleaseChannel is not 'stable' ")
|
||||
sys.exit(1)
|
||||
|
||||
version = data.get("version")
|
||||
if int(version) < 2:
|
||||
print("Only v2 and above are supported for direct updates to sources.json on push")
|
||||
sys.exit(1)
|
||||
|
||||
# Process the JSON data
|
||||
updated = False
|
||||
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,
|
||||
}
|
||||
# add commit ID if release is not stable
|
||||
if RELEASE_CHANNEL != 'stable':
|
||||
new_version["commitID"] = COMMIT_ID
|
||||
|
||||
if not channels.get(RELEASE_CHANNEL):
|
||||
# there was no entries in this release channel so create one
|
||||
channels[RELEASE_CHANNEL] = [new_version]
|
||||
else:
|
||||
# Update the existing TOP version object entry
|
||||
channels[RELEASE_CHANNEL][0] = new_version
|
||||
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
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,
|
||||
}
|
||||
versions.insert(0, new_version)
|
||||
|
||||
# Save the updated JSON to the input file
|
||||
try:
|
||||
print("\nUpdated Sources File:\n")
|
||||
|
||||
Reference in New Issue
Block a user