- Multiple fixes and CI setup

This commit is contained in:
Magesh K
2025-02-08 04:45:22 +05:30
parent e608211f32
commit bf766c1b84
61 changed files with 1631 additions and 1154 deletions

View File

@@ -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 }}

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View 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

View File

@@ -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>

View File

@@ -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 */,

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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(_:))
}

View File

@@ -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(_:))
}

View File

@@ -233,7 +233,8 @@ extension AppBannerView
{
// App is installed
if installedApp.isUpdateAvailable
// if installedApp.isUpdateAvailable
if installedApp.hasUpdate
{
buttonAction = .update
}

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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(_:))
}

View File

@@ -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: "")

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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"/>

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 () })

View File

@@ -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,

View File

@@ -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>

View File

@@ -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()

View File

@@ -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"/>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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))")
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 ?? ""
}
}

View File

@@ -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))

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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])

View File

@@ -31,7 +31,7 @@ public extension Team
}
@objc(Team)
public class Team: NSManagedObject, Fetchable
public class Team: BaseEntity
{
/* Properties */
@NSManaged public var name: String

View File

@@ -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>

View File

@@ -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

View File

@@ -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`

View File

@@ -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

View File

@@ -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="."

View File

@@ -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
}()
}

View File

@@ -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 {

View File

@@ -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")