mirror of
https://github.com/SideStore/SideStore.git
synced 2026-05-14 05:15:38 +02:00
Merge branch 'develop' into users/junepark678/altsign-fixes
This commit is contained in:
102
.github/workflows/nightly.yml
vendored
102
.github/workflows/nightly.yml
vendored
@@ -21,7 +21,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Set current build as BETA
|
- name: Set current build as BETA
|
||||||
run: echo "IS_BETA=1" >> $GITHUB_ENV
|
run: |
|
||||||
|
echo "IS_BETA=1" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_CHANNEL=beta" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -133,14 +135,40 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Build SideStore
|
||||||
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
# 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
|
- name: Fakesign app
|
||||||
run: make fakesign
|
run: make fakesign | tee -a build.log
|
||||||
|
|
||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make 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
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
@@ -159,7 +187,7 @@ jobs:
|
|||||||
release: "Nightly"
|
release: "Nightly"
|
||||||
tag: "nightly"
|
tag: "nightly"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: SideStore.ipa SideStore.dSYMs.zip
|
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build_log.zip
|
||||||
body: |
|
body: |
|
||||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||||
|
|
||||||
@@ -189,6 +217,12 @@ jobs:
|
|||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./SideStore.xcarchive/dSYMs/*
|
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
|
||||||
|
|
||||||
# Check if PUBLISH_BETA_UPDATES secret is set to non-zero
|
# Check if PUBLISH_BETA_UPDATES secret is set to non-zero
|
||||||
- name: Check if PUBLISH_BETA_UPDATES is set
|
- name: Check if PUBLISH_BETA_UPDATES is set
|
||||||
id: check_publish
|
id: check_publish
|
||||||
@@ -242,44 +276,42 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
|
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
|
||||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||||
echo "BETA=true" >> $GITHUB_ENV
|
|
||||||
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
|
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
|
||||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||||
echo "LOCALIZED_DESCRIPTION=This is nightly release for revision: ${{ github.sha }}" >> $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
|
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/nightly/SideStore.ipa" >> $GITHUB_ENV
|
||||||
|
|
||||||
# - name: Checkout SideStore/apps-v2.json
|
- name: Checkout SideStore/apps-v2.json
|
||||||
# if: ${{ steps.check_publish.outcome == 'success' }}
|
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||||
# uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
# with:
|
with:
|
||||||
# # Repository name with owner. For example, actions/checkout
|
# Repository name with owner. For example, actions/checkout
|
||||||
# # Default: ${{ github.repository }}
|
# Default: ${{ github.repository }}
|
||||||
# repository: 'SideStore/apps-v2.json'
|
repository: 'SideStore/apps-v2.json'
|
||||||
# # ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
|
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?
|
# ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
|
||||||
# # token: ${{ github.token }}
|
# token: ${{ github.token }}
|
||||||
# token: ${{ secrets.APPS_DEPLOY_KEY }}
|
token: ${{ secrets.APPS_DEPLOY_KEY }}
|
||||||
# path: 'SideStore/apps-v2.json'
|
path: 'SideStore/apps-v2.json'
|
||||||
|
|
||||||
# - name: Publish to SideStore/apps-v2.json
|
- name: Publish to SideStore/apps-v2.json
|
||||||
# if: ${{ steps.check_publish.outcome == 'success' }}
|
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||||
# run: |
|
run: |
|
||||||
# # Copy and execute the update script
|
# Copy and execute the update script
|
||||||
# pushd SideStore/apps-v2.json/
|
pushd SideStore/apps-v2.json/
|
||||||
|
|
||||||
# # Configure Git user (committer details)
|
# Configure Git user (committer details)
|
||||||
# git config user.name "GitHub Actions"
|
git config user.name "GitHub Actions"
|
||||||
# git config user.email "github-actions@github.com"
|
git config user.email "github-actions@github.com"
|
||||||
|
|
||||||
# # Make the update script executable and run it
|
# update the source.json
|
||||||
# python3 ../../update_apps.py "./_includes/source.json"
|
python3 ../../update_apps.py "./_includes/source.json"
|
||||||
|
|
||||||
# # Commit changes and push using SSH
|
# Commit changes and push using SSH
|
||||||
# git add ./_includes/source.json
|
git add ./_includes/source.json
|
||||||
# git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
|
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
# git status
|
git status
|
||||||
# # git push origin HEAD:main
|
git push origin HEAD:main
|
||||||
# git push origin HEAD:nightly
|
popd
|
||||||
# popd
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
# macOS
|
# macOS
|
||||||
#
|
#
|
||||||
*.DS_Store
|
**/*.DS_Store
|
||||||
|
|
||||||
# Xcode
|
# Xcode
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class ViewController: UIViewController
|
|||||||
self.activityIndicatorView.color = .altstoreText
|
self.activityIndicatorView.color = .altstoreText
|
||||||
self.activityIndicatorView.startAnimating()
|
self.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
// TODO: @mahee96: Disabled this buttons which were present for debugging purpose.
|
// TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose.
|
||||||
// Can find something useful for these later, but these are not required by this backup/restore app
|
// Can find something useful for these later, but these are not required by this backup/restore app
|
||||||
// #if DEBUG
|
// #if DEBUG
|
||||||
// let button1 = UIButton(type: .system)
|
// let button1 = UIButton(type: .system)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
|
03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
|
||||||
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E05025B2BEC947000879B5C /* String+SideStore.swift */; };
|
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E05025B2BEC947000879B5C /* String+SideStore.swift */; };
|
||||||
0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */; };
|
|
||||||
0E1A1F912AE36A9700364CAD /* bytearray.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E1A1F902AE36A9600364CAD /* bytearray.c */; };
|
0E1A1F912AE36A9700364CAD /* bytearray.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E1A1F902AE36A9600364CAD /* bytearray.c */; };
|
||||||
0EA1665B2ADFE0D2003015C1 /* out-limd.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166472ADFE0D1003015C1 /* out-limd.c */; };
|
0EA1665B2ADFE0D2003015C1 /* out-limd.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166472ADFE0D1003015C1 /* out-limd.c */; };
|
||||||
0EA1665C2ADFE0D2003015C1 /* out-default.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166522ADFE0D2003015C1 /* out-default.c */; };
|
0EA1665C2ADFE0D2003015C1 /* out-default.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166522ADFE0D2003015C1 /* out-default.c */; };
|
||||||
@@ -50,17 +49,37 @@
|
|||||||
551A15E55999499418AC1022 /* Pods_AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 707E746318F0B6F1A44935D3 /* Pods_AltStoreCore.framework */; };
|
551A15E55999499418AC1022 /* Pods_AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 707E746318F0B6F1A44935D3 /* Pods_AltStoreCore.framework */; };
|
||||||
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; };
|
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; };
|
||||||
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; };
|
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; };
|
||||||
|
A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8087E742D2D2958002DB21B /* ImportExport.swift */; };
|
||||||
|
A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */; };
|
||||||
|
A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */; };
|
||||||
A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; };
|
A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; };
|
||||||
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
|
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
|
||||||
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
|
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
|
||||||
A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; };
|
A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; };
|
||||||
|
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
|
||||||
|
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
|
||||||
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||||
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
|
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
|
||||||
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
||||||
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
|
A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; };
|
||||||
|
A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
||||||
|
A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; };
|
||||||
|
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 */; };
|
A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; };
|
||||||
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; };
|
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; };
|
||||||
|
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */; };
|
||||||
|
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
|
||||||
|
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
|
||||||
|
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
|
||||||
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
|
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
|
||||||
|
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
|
||||||
|
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
|
||||||
|
A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C282D206AC100E83DBD /* OutputStream.swift */; };
|
||||||
|
A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */; };
|
||||||
|
A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C312D206B2500E83DBD /* FileOutputStream.swift */; };
|
||||||
|
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */; };
|
||||||
A8C6D50C2D1EE87600DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */; };
|
A8C6D50C2D1EE87600DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */; };
|
||||||
A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; };
|
A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; };
|
||||||
A8C6D5132D1EE8D700DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
A8C6D5132D1EE8D700DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
||||||
@@ -68,6 +87,7 @@
|
|||||||
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
||||||
A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
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 */; };
|
A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; };
|
||||||
|
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */; };
|
||||||
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
|
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
|
||||||
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
|
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
|
||||||
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
|
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
|
||||||
@@ -555,9 +575,7 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
|
||||||
0E05025B2BEC947000879B5C /* String+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SideStore.swift"; sourceTree = "<group>"; };
|
0E05025B2BEC947000879B5C /* String+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SideStore.swift"; sourceTree = "<group>"; };
|
||||||
0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+SideStore.swift"; sourceTree = "<group>"; };
|
|
||||||
0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = "<group>"; };
|
0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = "<group>"; };
|
||||||
0EA166412ADFE0D1003015C1 /* jplist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jplist.c; path = Dependencies/libplist/src/jplist.c; sourceTree = SOURCE_ROOT; };
|
0EA166412ADFE0D1003015C1 /* jplist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jplist.c; path = Dependencies/libplist/src/jplist.c; sourceTree = SOURCE_ROOT; };
|
||||||
0EA166422ADFE0D1003015C1 /* Date.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Date.cpp; path = Dependencies/libplist/src/Date.cpp; sourceTree = SOURCE_ROOT; };
|
0EA166422ADFE0D1003015C1 /* Date.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Date.cpp; path = Dependencies/libplist/src/Date.cpp; sourceTree = SOURCE_ROOT; };
|
||||||
@@ -608,12 +626,17 @@
|
|||||||
7935E4499B2FC11DA8BAB2CC /* Pods-AltStoreCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStoreCore.release.xcconfig"; path = "Target Support Files/Pods-AltStoreCore/Pods-AltStoreCore.release.xcconfig"; sourceTree = "<group>"; };
|
7935E4499B2FC11DA8BAB2CC /* Pods-AltStoreCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStoreCore.release.xcconfig"; path = "Target Support Files/Pods-AltStoreCore/Pods-AltStoreCore.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; };
|
A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; };
|
||||||
A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A8087E742D2D2958002DB21B /* ImportExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExport.swift; sourceTree = "<group>"; };
|
||||||
|
A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUpdateIntent.swift; sourceTree = "<group>"; };
|
||||||
|
A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveAppsTimelineProvider+Simulator.swift"; sourceTree = "<group>"; };
|
||||||
A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = "<group>"; };
|
A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = "<group>"; };
|
||||||
A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = "<group>"; };
|
A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = "<group>"; };
|
||||||
A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = "<group>"; };
|
A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "minimuxer-helpers.swift"; sourceTree = "<group>"; };
|
A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "minimuxer-helpers.swift"; sourceTree = "<group>"; };
|
||||||
A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = "<group>"; };
|
A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = "<group>"; };
|
||||||
A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = "<group>"; };
|
A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = "<group>"; };
|
||||||
|
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = "<group>"; };
|
||||||
|
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = "<group>"; };
|
||||||
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; };
|
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; };
|
||||||
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
|
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
|
||||||
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
|
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -623,8 +646,24 @@
|
|||||||
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltWidgetExtension.xcconfig; sourceTree = "<group>"; };
|
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltWidgetExtension.xcconfig; sourceTree = "<group>"; };
|
||||||
A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = "<group>"; };
|
A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = "<group>"; };
|
||||||
A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = "<group>"; };
|
A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
|
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; };
|
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsTimelineProvider.swift; sourceTree = "<group>"; };
|
||||||
|
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = "<group>"; };
|
||||||
|
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
|
||||||
|
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
|
||||||
|
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
|
||||||
|
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
|
||||||
|
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
|
||||||
|
A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractClassError.swift; sourceTree = "<group>"; };
|
||||||
|
A8C38C312D206B2500E83DBD /* FileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputStream.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
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>"; };
|
||||||
A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; 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>"; };
|
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>"; };
|
A8FD915B2D046EF100322782 /* ProcessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessError.swift; sourceTree = "<group>"; };
|
||||||
@@ -1118,6 +1157,24 @@
|
|||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A8087E712D2D291B002DB21B /* importexport */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8087E742D2D2958002DB21B /* ImportExport.swift */,
|
||||||
|
);
|
||||||
|
path = importexport;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8096D1D2D30ADD5000C39C6 /* Providers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */,
|
||||||
|
A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */,
|
||||||
|
A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */,
|
||||||
|
);
|
||||||
|
path = Providers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A809F68A2D04D71200F0F0F3 /* Products */ = {
|
A809F68A2D04D71200F0F0F3 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1135,6 +1192,15 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A80D790B2D2F209700A40F40 /* Intents */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */,
|
||||||
|
A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */,
|
||||||
|
);
|
||||||
|
path = Intents;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = {
|
A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1150,6 +1216,22 @@
|
|||||||
path = xcconfigs;
|
path = xcconfigs;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A86315DD2D3EB2BD0048FA40 /* errors */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */,
|
||||||
|
);
|
||||||
|
path = errors;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A88B8C532D35F1E800F53F9D /* operations */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */,
|
||||||
|
);
|
||||||
|
path = operations;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A8A543222D04F0C100D72399 /* Products */ = {
|
A8A543222D04F0C100D72399 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1161,6 +1243,81 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A8A853AD2D3050CC00995795 /* pagination */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */,
|
||||||
|
);
|
||||||
|
path = pagination;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8AD35562D31BE8F003A28B4 /* Manager */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */,
|
||||||
|
);
|
||||||
|
path = Manager;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8AD35572D31BEB2003A28B4 /* datastructures */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A868CFE32D319988002F1201 /* SingletonGenericMap.swift */,
|
||||||
|
);
|
||||||
|
path = datastructures;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8B516DE2D2666900047047C /* dignostics */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A86315DD2D3EB2BD0048FA40 /* errors */,
|
||||||
|
A88B8C532D35F1E800F53F9D /* operations */,
|
||||||
|
A8B516DF2D2666A00047047C /* database */,
|
||||||
|
);
|
||||||
|
path = dignostics;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8B516DF2D2666A00047047C /* database */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */,
|
||||||
|
);
|
||||||
|
path = database;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8C38C1C2D2068D100E83DBD /* Utils */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8AD35572D31BEB2003A28B4 /* datastructures */,
|
||||||
|
A8A853AD2D3050CC00995795 /* pagination */,
|
||||||
|
A8087E712D2D291B002DB21B /* importexport */,
|
||||||
|
A8B516DE2D2666900047047C /* dignostics */,
|
||||||
|
A8C38C272D206AA500E83DBD /* common */,
|
||||||
|
A8C38C202D206A3A00E83DBD /* iostreams */,
|
||||||
|
);
|
||||||
|
path = Utils;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8C38C202D206A3A00E83DBD /* iostreams */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */,
|
||||||
|
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */,
|
||||||
|
);
|
||||||
|
path = iostreams;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8C38C272D206AA500E83DBD /* common */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8B516E52D2668020047047C /* DateTimeUtil.swift */,
|
||||||
|
A8C38C282D206AC100E83DBD /* OutputStream.swift */,
|
||||||
|
A8C38C312D206B2500E83DBD /* FileOutputStream.swift */,
|
||||||
|
A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */,
|
||||||
|
);
|
||||||
|
path = common;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A8F66C072D04C025009689E6 /* SideStore */ = {
|
A8F66C072D04C025009689E6 /* SideStore */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1170,6 +1327,7 @@
|
|||||||
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */,
|
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */,
|
||||||
19104DB32909C06D00C49C7B /* EmotionalDamage */,
|
19104DB32909C06D00C49C7B /* EmotionalDamage */,
|
||||||
B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */,
|
B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */,
|
||||||
|
A8C38C1C2D2068D100E83DBD /* Utils */,
|
||||||
);
|
);
|
||||||
path = SideStore;
|
path = SideStore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1572,12 +1730,12 @@
|
|||||||
BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */,
|
BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */,
|
||||||
BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */,
|
BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */,
|
||||||
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */,
|
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */,
|
||||||
0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */,
|
|
||||||
0E05025B2BEC947000879B5C /* String+SideStore.swift */,
|
0E05025B2BEC947000879B5C /* String+SideStore.swift */,
|
||||||
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */,
|
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */,
|
||||||
D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */,
|
D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */,
|
||||||
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */,
|
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */,
|
||||||
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */,
|
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */,
|
||||||
|
A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */,
|
||||||
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */,
|
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */,
|
||||||
D552EB052AF453F900A3AB4D /* URL+Normalized.swift */,
|
D552EB052AF453F900A3AB4D /* URL+Normalized.swift */,
|
||||||
);
|
);
|
||||||
@@ -1619,10 +1777,12 @@
|
|||||||
BF98916C250AABF3002ACF50 /* AltWidget */ = {
|
BF98916C250AABF3002ACF50 /* AltWidget */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A8AD35562D31BE8F003A28B4 /* Manager */,
|
||||||
|
A80D790B2D2F209700A40F40 /* Intents */,
|
||||||
|
A8096D1D2D30ADD5000C39C6 /* Providers */,
|
||||||
A800F6FE2CE28DE300208744 /* Extensions */,
|
A800F6FE2CE28DE300208744 /* Extensions */,
|
||||||
BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */,
|
BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */,
|
||||||
D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */,
|
D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */,
|
||||||
D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */,
|
|
||||||
D50C29F22A8ECD71009AB488 /* Widgets */,
|
D50C29F22A8ECD71009AB488 /* Widgets */,
|
||||||
D51AF9752A97D29100471312 /* Model */,
|
D51AF9752A97D29100471312 /* Model */,
|
||||||
D577AB802A968B7E007FE952 /* Components */,
|
D577AB802A968B7E007FE952 /* Components */,
|
||||||
@@ -1846,7 +2006,6 @@
|
|||||||
BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */,
|
BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */,
|
||||||
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */,
|
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */,
|
||||||
B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */,
|
B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */,
|
||||||
0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */,
|
|
||||||
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
|
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
|
||||||
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
||||||
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
||||||
@@ -1858,6 +2017,7 @@
|
|||||||
BFDB69FB22A9A7A6007EA6D6 /* Settings */ = {
|
BFDB69FB22A9A7A6007EA6D6 /* Settings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */,
|
||||||
BFE60737231ADF49002B0E8E /* Settings.storyboard */,
|
BFE60737231ADF49002B0E8E /* Settings.storyboard */,
|
||||||
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */,
|
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */,
|
||||||
0EA426392C2230150026D7FB /* AnisetteServerList.swift */,
|
0EA426392C2230150026D7FB /* AnisetteServerList.swift */,
|
||||||
@@ -1881,29 +2041,30 @@
|
|||||||
children = (
|
children = (
|
||||||
D513F6152A12CE210061EAA1 /* Errors */,
|
D513F6152A12CE210061EAA1 /* Errors */,
|
||||||
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
|
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
|
||||||
BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */,
|
|
||||||
BF770E5322BC044E002A40FE /* OperationContexts.swift */,
|
BF770E5322BC044E002A40FE /* OperationContexts.swift */,
|
||||||
|
BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */,
|
||||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
|
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
|
||||||
|
D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */,
|
||||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
|
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
|
||||||
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
|
BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
|
||||||
|
A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */,
|
||||||
|
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
|
||||||
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */,
|
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */,
|
||||||
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */,
|
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
|
||||||
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
|
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
|
||||||
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
|
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
|
||||||
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
|
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */,
|
||||||
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
|
|
||||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
||||||
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
|
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
|
||||||
BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
|
D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */,
|
||||||
BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */,
|
|
||||||
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */,
|
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */,
|
||||||
BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */,
|
BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */,
|
||||||
|
BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */,
|
||||||
|
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
|
||||||
BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */,
|
BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */,
|
||||||
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */,
|
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */,
|
||||||
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */,
|
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */,
|
||||||
D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */,
|
|
||||||
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */,
|
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */,
|
||||||
D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */,
|
|
||||||
BF7B44062725A4B8005288A4 /* Patch App */,
|
BF7B44062725A4B8005288A4 /* Patch App */,
|
||||||
);
|
);
|
||||||
path = Operations;
|
path = Operations;
|
||||||
@@ -2095,6 +2256,7 @@
|
|||||||
D589170128C7D93500E39C8B /* Error Log */ = {
|
D589170128C7D93500E39C8B /* Error Log */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */,
|
||||||
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
|
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
|
||||||
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
|
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
|
||||||
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */,
|
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */,
|
||||||
@@ -2556,7 +2718,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "#!/bin/sh\n\necho \"Build directory: $BUILD_DIR\"\necho \"Configuration build directory: $CONFIGURATION_BUILD_DIR\"\n\n# diagnostics\n# echo \">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<\"\n# find \"$BUILD_DIR\" -maxdepth 7 -exec ls -ld {} + || true # List contents if directory exists \n# # ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists \n# echo \"\"\n\n# diagnostics\n# exit 0\n\n# Define the path to your Makefile\nMAKEFILE_PATH=\"${PROJECT_DIR}/\"\n\n# Navigate to the directory containing the Makefile\ncd \"$MAKEFILE_PATH\" || exit 1\n\n# Run the make target 'ipa-altbackup'\nmake -B copy-altbackup ipa-altbackup\n\n# Ensure that the ipa-altbackup process finishes before continuing\nif [ $? -ne 0 ]; then\n echo \"Error: ipa-altbackup failed\"\n exit 1\nelse\n echo \"ipa-altbackup completed successfully\"\nfi\n# Type a script or drag a script file from your workspace to insert its path.\n";
|
shellScript = "#!/bin/sh\n\necho \"Build directory: $BUILD_DIR\"\necho \"Configuration build directory: $CONFIGURATION_BUILD_DIR\"\n\n# diagnostics\n# echo \">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<\"\n# find \"$BUILD_DIR\" -maxdepth 7 -exec ls -ld {} + || true # List contents if directory exists \n# # ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists \n# echo \"\"\n\n# diagnostics\n# exit 0\n\n# Define the path to your Makefile\nMAKEFILE_PATH=\"${PROJECT_DIR}/\"\n\n# Navigate to the directory containing the Makefile\ncd \"$MAKEFILE_PATH\" || exit 1\n\n# Run the make target 'ipa-altbackup'\nmake -B clean-altbackup copy-altbackup ipa-altbackup\n\n# Ensure that the ipa-altbackup process finishes before continuing\nif [ $? -ne 0 ]; then\n echo \"Error: ipa-altbackup failed\"\n exit 1\nelse\n echo \"ipa-altbackup completed successfully\"\nfi\n# Type a script or drag a script file from your workspace to insert its path.\n";
|
||||||
};
|
};
|
||||||
AEDB4E9409D2CEE1EA126980 /* [CP] Check Pods Manifest.lock */ = {
|
AEDB4E9409D2CEE1EA126980 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
@@ -2714,6 +2876,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */,
|
||||||
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */,
|
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */,
|
||||||
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
|
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
|
||||||
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
||||||
@@ -2823,12 +2986,19 @@
|
|||||||
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
|
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
|
||||||
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
|
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
|
||||||
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
||||||
|
A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */,
|
||||||
|
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */,
|
||||||
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
|
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
|
||||||
|
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */,
|
||||||
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
|
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
|
||||||
|
A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */,
|
||||||
|
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */,
|
||||||
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
||||||
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
|
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
|
||||||
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
||||||
D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */,
|
D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */,
|
||||||
|
A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */,
|
||||||
|
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -2838,14 +3008,18 @@
|
|||||||
files = (
|
files = (
|
||||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
||||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||||
|
A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */,
|
||||||
|
A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */,
|
||||||
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
|
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
|
||||||
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
|
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
|
||||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||||
D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
||||||
D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */,
|
D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */,
|
||||||
|
A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */,
|
||||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
||||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||||
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
|
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
|
||||||
|
A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */,
|
||||||
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,
|
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,
|
||||||
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
|
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
|
||||||
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
|
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
|
||||||
@@ -2865,11 +3039,15 @@
|
|||||||
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
||||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
||||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||||
|
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */,
|
||||||
|
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */,
|
||||||
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
||||||
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
||||||
|
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */,
|
||||||
A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */,
|
A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */,
|
||||||
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
||||||
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
||||||
|
A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */,
|
||||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||||
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
||||||
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
|
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
|
||||||
@@ -2909,7 +3087,7 @@
|
|||||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
||||||
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
|
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
|
||||||
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
||||||
0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */,
|
A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */,
|
||||||
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */,
|
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */,
|
||||||
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
||||||
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
||||||
@@ -2919,6 +3097,7 @@
|
|||||||
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
|
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
|
||||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||||
|
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
|
||||||
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
|
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
|
||||||
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
|
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
|
||||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||||
@@ -2929,15 +3108,18 @@
|
|||||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||||
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
|
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
|
||||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
||||||
|
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
|
||||||
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
||||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
||||||
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
|
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
|
||||||
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
|
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
|
||||||
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
|
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
|
||||||
|
A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */,
|
||||||
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
||||||
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */,
|
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */,
|
||||||
D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */,
|
D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */,
|
||||||
|
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */,
|
||||||
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
|
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
|
||||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||||
|
|||||||
@@ -2,14 +2,22 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<!-- <key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<array>
|
||||||
|
<string></string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.applesignin</key>
|
||||||
|
<array>
|
||||||
|
<string></string>
|
||||||
|
</array> -->
|
||||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>development</string>
|
||||||
<key>com.apple.developer.siri</key>
|
<key>com.apple.developer.siri</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
|||||||
@@ -41,8 +41,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
private let intentHandler = IntentHandler()
|
private let intentHandler = IntentHandler()
|
||||||
private let viewAppIntentHandler = ViewAppIntentHandler()
|
private let viewAppIntentHandler = ViewAppIntentHandler()
|
||||||
|
|
||||||
|
public let consoleLog = ConsoleLog()
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
{
|
{
|
||||||
|
// navigation bar buttons spacing is too much (so hack it to use minimal spacing)
|
||||||
|
// this is swift-5 specific behavior and might change
|
||||||
|
// https://stackoverflow.com/a/64988363/11971304
|
||||||
|
//
|
||||||
|
// Warning: this affects all screens through out the app, and basically overrides storyboard
|
||||||
|
let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self])
|
||||||
|
stackViewAppearance.spacing = -8 // adjust as needed
|
||||||
|
|
||||||
|
consoleLog.startCapturing()
|
||||||
|
print("===================================================")
|
||||||
|
print("| App is Starting up |")
|
||||||
|
print("===================================================")
|
||||||
|
print("| Console Logger started capturing output streams |")
|
||||||
|
print("===================================================")
|
||||||
|
print("\n ")
|
||||||
|
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
|
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
|
||||||
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
|
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
|
||||||
@@ -81,9 +99,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||||
|
|
||||||
// #if DEBUG || BETA
|
#if DEBUG && (targetEnvironment(simulator) || BETA)
|
||||||
UserDefaults.standard.isDebugModeEnabled = true
|
UserDefaults.standard.isDebugModeEnabled = true
|
||||||
// #endif
|
#endif
|
||||||
|
|
||||||
self.prepareForBackgroundFetch()
|
self.prepareForBackgroundFetch()
|
||||||
|
|
||||||
@@ -130,6 +148,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
|
// Stop console logging and clean up resources
|
||||||
|
print("\n ")
|
||||||
|
print("===================================================")
|
||||||
|
print("| Console Logger stopped capturing output streams |")
|
||||||
|
print("===================================================")
|
||||||
|
print("| App is being terminated |")
|
||||||
|
print("===================================================")
|
||||||
|
consoleLog.stopCapturing()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppDelegate
|
extension AppDelegate
|
||||||
@@ -269,7 +298,7 @@ extension AppDelegate
|
|||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ private extension AuthenticationViewController
|
|||||||
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
|
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
|
toastView.backgroundColor = .white
|
||||||
toastView.textLabel.textColor = .altPrimary
|
toastView.textLabel.textColor = .altPrimary
|
||||||
toastView.detailTextLabel.textColor = .altPrimary
|
toastView.detailTextLabel.textColor = .altPrimary
|
||||||
self.toastView = toastView
|
self.toastView = toastView
|
||||||
|
|||||||
@@ -67,30 +67,10 @@ class ToastView: RSTToastView
|
|||||||
|
|
||||||
convenience init(error: Error)
|
convenience init(error: Error)
|
||||||
{
|
{
|
||||||
var error = error as NSError
|
let error = error as NSError
|
||||||
var underlyingError = error.underlyingError
|
|
||||||
|
|
||||||
if
|
|
||||||
let unwrappedUnderlyingError = underlyingError,
|
|
||||||
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
|
||||||
{
|
|
||||||
// Treat underlyingError as the primary error, but keep localized title + failure.
|
|
||||||
|
|
||||||
let nsError = error as NSError
|
|
||||||
error = unwrappedUnderlyingError as NSError
|
|
||||||
|
|
||||||
if let localizedTitle = nsError.localizedTitle {
|
|
||||||
error = error.withLocalizedTitle(localizedTitle)
|
|
||||||
}
|
|
||||||
if let localizedFailure = nsError.localizedFailure {
|
|
||||||
error = error.withLocalizedFailure(localizedFailure)
|
|
||||||
}
|
|
||||||
|
|
||||||
underlyingError = nil
|
|
||||||
}
|
|
||||||
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
||||||
let detailText = error.localizedDescription
|
let detailText = ErrorProcessing(.fullError).getDescription(error: error)
|
||||||
|
|
||||||
|
|
||||||
self.init(text: text, detailText: detailText)
|
self.init(text: text, detailText: detailText)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,7 +248,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
target_minimuxer_address()
|
target_minimuxer_address()
|
||||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
do {
|
do {
|
||||||
try start(pairing_file, documentsDirectory)
|
// enable minimuxer console logging only if enabled in settings
|
||||||
|
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
||||||
|
try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled)
|
||||||
} catch {
|
} catch {
|
||||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||||
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||||
@@ -311,6 +313,9 @@ extension LaunchViewController
|
|||||||
guard case .failure(let error) = result else { return }
|
guard case .failure(let error) = result else { return }
|
||||||
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
|
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
|
||||||
|
|
||||||
|
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
|
||||||
|
print("Failed to update sources on launch. \(errorDesc)")
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||||
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
|
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
|
||||||
@@ -318,6 +323,7 @@ extension LaunchViewController
|
|||||||
|
|
||||||
self.updateKnownSources()
|
self.updateKnownSources()
|
||||||
|
|
||||||
|
// Ask widgets to be refreshed
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
|
||||||
// Add view controller as child (rather than presenting modally)
|
// Add view controller as child (rather than presenting modally)
|
||||||
|
|||||||
@@ -626,6 +626,11 @@ extension AppManager
|
|||||||
self.fetchSources() { (result) in
|
self.fetchSources() { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
|
// Check if the result is failure and rethrow
|
||||||
|
if case .failure(let error) = result {
|
||||||
|
throw error // Rethrow the error
|
||||||
|
}
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (_, context) = try result.get()
|
let (_, context) = try result.get()
|
||||||
@@ -1146,27 +1151,28 @@ private extension AppManager
|
|||||||
case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
|
case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
|
||||||
case .refresh(let app):
|
case .refresh(let app):
|
||||||
// Check if backup app is installed in place of real app.
|
// Check if backup app is installed in place of real app.
|
||||||
let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
// let altBackupUti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||||
|
|
||||||
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
// if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
||||||
uti != nil ||
|
// altBackupUti != nil || // why would altbackup requires reinstall? it shouldn't cause we are just renewing profiles
|
||||||
app.needsResign ||
|
// app.needsResign || // why would an app require resign during refresh? it shouldn't!
|
||||||
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
|
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
|
||||||
app.bundleIdentifier == StoreApp.altstoreAppID
|
// => mahee96: jkcoxson confirmed misagent manages profiles independently without requiring lockdownd or installd intervention, so sidestore profile renewal shouldn't require reinstall
|
||||||
{
|
// app.bundleIdentifier == StoreApp.altstoreAppID
|
||||||
|
// {
|
||||||
// Resign app instead of just refreshing profiles because either:
|
// Resign app instead of just refreshing profiles because either:
|
||||||
// * Refreshing using different certificate
|
// * Refreshing using different certificate // when can this happen?, lets assume, refreshing with different certificate, why not just ask user to re-install manually? (probably we need re-install button)
|
||||||
// * Backup app is still installed
|
// * Backup app is still installed // but why? I mean the AltBackup was put in place for a reason? ie during refresh just renew appIDs don't care about the app itself.
|
||||||
// * App explicitly needs resigning
|
// * App explicitly needs resigning // when can this happen?
|
||||||
// * Device is jailbroken and using AltDaemon on iOS 14.0 or later (b/c refreshing with provisioning profiles is broken)
|
// * Device is jailbroken and using AltDaemon on iOS 14.0 or later (b/c refreshing with provisioning profiles is broken)
|
||||||
|
|
||||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
// let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||||
self.finish(operation, result: result, group: group, progress: progress)
|
// self.finish(operation, result: result, group: group, progress: progress)
|
||||||
}
|
// }
|
||||||
progress?.addChild(installProgress, withPendingUnitCount: 80)
|
// progress?.addChild(installProgress, withPendingUnitCount: 80)
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
// Refreshing with same certificate as last time, and backup app isn't still installed,
|
// Refreshing with same certificate as last time, and backup app isn't still installed,
|
||||||
// so we can just refresh provisioning profiles.
|
// so we can just refresh provisioning profiles.
|
||||||
|
|
||||||
@@ -1174,7 +1180,7 @@ private extension AppManager
|
|||||||
self.finish(operation, result: result, group: group, progress: progress)
|
self.finish(operation, result: result, group: group, progress: progress)
|
||||||
}
|
}
|
||||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
||||||
}
|
// }
|
||||||
|
|
||||||
case .activate(let app):
|
case .activate(let app):
|
||||||
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
|
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
|
||||||
@@ -1234,132 +1240,6 @@ private extension AppManager
|
|||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeAppExtensions(
|
|
||||||
from application: ALTApplication,
|
|
||||||
existingApp: InstalledApp?,
|
|
||||||
extensions: Set<ALTApplication>,
|
|
||||||
_ presentingViewController: UIViewController?,
|
|
||||||
completion: @escaping (Result<Void, Error>) -> Void
|
|
||||||
) {
|
|
||||||
|
|
||||||
// App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match
|
|
||||||
if let existingApp {
|
|
||||||
_ = RSTAsyncBlockOperation { _ in
|
|
||||||
let existingAppEx: Set<InstalledExtension> = existingApp.appExtensions
|
|
||||||
let currentAppEx: Set<ALTApplication> = application.appExtensions
|
|
||||||
|
|
||||||
let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier}
|
|
||||||
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
|
|
||||||
|
|
||||||
let excessExtensions = currentAppEx.filter{
|
|
||||||
!(existingAppExNames.contains($0.bundleIdentifier))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty
|
|
||||||
let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n"
|
|
||||||
+ "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n"
|
|
||||||
print(diagnosticsMsg)
|
|
||||||
|
|
||||||
|
|
||||||
// if background mode, then remove only the excess extensions
|
|
||||||
guard let presentingViewController: UIViewController = presentingViewController else {
|
|
||||||
// perform silent extensions cleanup for those that aren't already present in existing app
|
|
||||||
print("\n Performing background mode Extensions removal \n")
|
|
||||||
print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)")
|
|
||||||
|
|
||||||
do {
|
|
||||||
for appExtension in excessExtensions {
|
|
||||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
|
||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
|
||||||
}
|
|
||||||
return completion(.success(()))
|
|
||||||
} catch {
|
|
||||||
return completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
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? There are \(extensions.count) Extensions", 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
|
|
||||||
do
|
|
||||||
{
|
|
||||||
for appExtension in application.appExtensions
|
|
||||||
{
|
|
||||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
|
||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if let presentingViewController {
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
|
|
||||||
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
for appExtension in selection
|
|
||||||
{
|
|
||||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
|
||||||
|
|
||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let suiview = popoverContentController.view!
|
|
||||||
suiview.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
popoverContentController.modalPresentationStyle = .popover
|
|
||||||
|
|
||||||
if let popoverPresentationController = popoverContentController.popoverPresentationController {
|
|
||||||
popoverPresentationController.sourceView = presentingViewController.view
|
|
||||||
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
|
|
||||||
popoverPresentationController.delegate = popoverContentController
|
|
||||||
|
|
||||||
presentingViewController.present(popoverContentController, animated: true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
presentingViewController.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func _install(_ app: AppProtocol,
|
private func _install(_ app: AppProtocol,
|
||||||
operation appOperation: AppOperation,
|
operation appOperation: AppOperation,
|
||||||
group: RefreshGroup,
|
group: RefreshGroup,
|
||||||
@@ -1469,52 +1349,20 @@ private extension AppManager
|
|||||||
verifyOperation.addDependency(downloadOperation)
|
verifyOperation.addDependency(downloadOperation)
|
||||||
|
|
||||||
/* Remove App Extensions */
|
/* Remove App Extensions */
|
||||||
|
let localAppExtensions = (app as? ALTApplication)?.appExtensions
|
||||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
let removeAppExtensionsOperation = RemoveAppExtensionsOperation(context: context,
|
||||||
do
|
localAppExtensions: localAppExtensions)
|
||||||
{
|
removeAppExtensionsOperation.resultHandler = { (result) in
|
||||||
if let error = context.error
|
switch result
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
guard case .install = appOperation else {
|
|
||||||
operation.finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
guard let extensions = context.app?.appExtensions else {
|
|
||||||
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let currentApp = context.app else {
|
|
||||||
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
self?.removeAppExtensions(from: currentApp,
|
|
||||||
existingApp: app as? InstalledApp,
|
|
||||||
extensions: extensions,
|
|
||||||
context.authenticatedContext.presentingViewController
|
|
||||||
) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(): break
|
|
||||||
case .failure(let error): context.error = error
|
|
||||||
}
|
|
||||||
operation.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
|
case .failure(let error):
|
||||||
context.error = error
|
context.error = error
|
||||||
operation.finish()
|
case .success: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAppExtensionsOperation.addDependency(verifyOperation)
|
removeAppExtensionsOperation.addDependency(verifyOperation)
|
||||||
|
|
||||||
|
|
||||||
/* Refresh Anisette Data */
|
/* Refresh Anisette Data */
|
||||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||||
@@ -1529,7 +1377,7 @@ private extension AppManager
|
|||||||
|
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
/* Fetch Provisioning Profiles */
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesInstallOperation(context: context)
|
||||||
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
||||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||||
switch result
|
switch result
|
||||||
@@ -1763,7 +1611,7 @@ private extension AppManager
|
|||||||
private func exportResginedAppsToDocsDir(_ resignedApp: ALTApplication)
|
private func exportResginedAppsToDocsDir(_ resignedApp: ALTApplication)
|
||||||
{
|
{
|
||||||
// Check if the user has enabled exporting resigned apps to the Documents directory and continue
|
// Check if the user has enabled exporting resigned apps to the Documents directory and continue
|
||||||
guard UserDefaults.standard.isResignedAppExportEnabled else {
|
guard UserDefaults.standard.isExportResignedAppEnabled else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1815,26 +1663,27 @@ private extension AppManager
|
|||||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
context.app = ALTApplication(fileURL: app.fileURL)
|
context.app = ALTApplication(fileURL: app.fileURL)
|
||||||
|
|
||||||
//App-Extensions: Ensure DB data and disk state must match
|
// Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
|
||||||
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
|
//App-Extensions: Ensure DB data and disk state must match
|
||||||
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
|
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
|
||||||
let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
|
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
|
||||||
let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
|
let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
|
||||||
let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
|
let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
|
||||||
|
let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
|
||||||
|
|
||||||
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
|
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
|
||||||
|
|
||||||
let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
|
let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
|
||||||
+ "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
|
+ "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
|
||||||
print(errMessage)
|
print(errMessage)
|
||||||
if(!isMatching){
|
if(!isMatching){
|
||||||
completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
|
completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
|
||||||
}
|
}
|
||||||
op.finish()
|
op.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
/* Fetch Provisioning Profiles */
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesRefreshOperation(context: context)
|
||||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
@@ -1844,7 +1693,7 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
|
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
|
||||||
fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
|
// fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
|
||||||
|
|
||||||
/* Refresh */
|
/* Refresh */
|
||||||
let refreshAppOperation = RefreshAppOperation(context: context)
|
let refreshAppOperation = RefreshAppOperation(context: context)
|
||||||
@@ -1853,7 +1702,10 @@ private extension AppManager
|
|||||||
{
|
{
|
||||||
case .success(let installedApp):
|
case .success(let installedApp):
|
||||||
completionHandler(.success(installedApp))
|
completionHandler(.success(installedApp))
|
||||||
|
|
||||||
|
|
||||||
|
// refreshing local app's provisioning profile means talking to misagent daemon
|
||||||
|
// which requires loopback vpn
|
||||||
case .failure(MinimuxerError.ProfileInstall):
|
case .failure(MinimuxerError.ProfileInstall):
|
||||||
completionHandler(.failure(OperationError.noWiFi))
|
completionHandler(.failure(OperationError.noWiFi))
|
||||||
|
|
||||||
@@ -1878,7 +1730,8 @@ private extension AppManager
|
|||||||
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
|
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
|
||||||
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
||||||
|
|
||||||
let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
|
// let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
|
||||||
|
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
|
||||||
group.add(operations)
|
group.add(operations)
|
||||||
self.run(operations, context: group.context)
|
self.run(operations, context: group.context)
|
||||||
|
|
||||||
@@ -2280,6 +2133,7 @@ private extension AppManager
|
|||||||
AnalyticsManager.shared.trackEvent(event)
|
AnalyticsManager.shared.trackEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask widgets to be refreshed
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -1151,7 +1151,9 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func refresh(_ installedApp: InstalledApp)
|
func refresh(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
guard minimuxerStatus else { return }
|
// we do need minimuxer, coz it needs to talk to misagent daemon which manages profiles
|
||||||
|
// so basically loopback vpn is still required
|
||||||
|
guard minimuxerStatus else { return } // we don't need minimuxer when renewing appIDs only do we, heck we can even do it on mobile internet
|
||||||
|
|
||||||
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
||||||
guard previousProgress == nil else {
|
guard previousProgress == nil else {
|
||||||
@@ -1357,6 +1359,50 @@ private extension MyAppsViewController
|
|||||||
self.present(alertController, animated: true, completion: nil)
|
self.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func importBackup(for installedApp: InstalledApp){
|
||||||
|
ImportExport.importBackup(presentingViewController: self, for: installedApp) { result in
|
||||||
|
var toast: ToastView
|
||||||
|
switch(result){
|
||||||
|
case .failure(let error):
|
||||||
|
toast = ToastView(error: error, opensLog: false)
|
||||||
|
break
|
||||||
|
case .success:
|
||||||
|
toast = ToastView(text: "Import Backup successful for \(installedApp.name)",
|
||||||
|
detailText: "Use 'Restore Backup' option to restore data from this imported backup")
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
toast.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getPreviousBackupURL(_ installedApp: InstalledApp) -> URL
|
||||||
|
{
|
||||||
|
let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
|
||||||
|
let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
|
||||||
|
return backupBakURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func restorePreviousBackup(for installedApp: InstalledApp){
|
||||||
|
let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
|
||||||
|
let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
|
||||||
|
|
||||||
|
// backupBakURL is expected to exist at this point, this needs to be ensured by caller logic
|
||||||
|
// or invoke this action only when backupBakURL exists
|
||||||
|
|
||||||
|
// delete the current backup
|
||||||
|
if(FileManager.default.fileExists(atPath: backupURL.path)){
|
||||||
|
try! FileManager.default.removeItem(at: backupURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore the previously saved backup as current backup
|
||||||
|
// (don't delete the N-1 backup yet so copy instead of move)
|
||||||
|
try! FileManager.default.copyItem(at: backupBakURL, to: backupURL)
|
||||||
|
|
||||||
|
//perform restore of data from the backup
|
||||||
|
restore(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
func restore(_ installedApp: InstalledApp)
|
func restore(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
guard minimuxerStatus else { return }
|
guard minimuxerStatus else { return }
|
||||||
@@ -1423,7 +1469,10 @@ private extension MyAppsViewController
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||||
tempApp.needsResign = true
|
tempApp.needsResign = true // why do we want to resign it during refresh ?!!!!
|
||||||
|
// I see now, so here we just mark that icon needs to be changed but leave it for refresh/install to do it
|
||||||
|
// this is bad, coz now the weight of installing goes to refresh step !!! which is not what we want
|
||||||
|
|
||||||
tempApp.hasAlternateIcon = (image != nil)
|
tempApp.hasAlternateIcon = (image != nil)
|
||||||
|
|
||||||
if let image = image
|
if let image = image
|
||||||
@@ -1459,27 +1508,27 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableJIT(for installedApp: InstalledApp)
|
func enableJIT(for installedApp: InstalledApp) {
|
||||||
{
|
|
||||||
|
|
||||||
let sidejitenabled = UserDefaults.standard.sidejitenable
|
let sidejitenabled = UserDefaults.standard.sidejitenable
|
||||||
|
|
||||||
if #unavailable(iOS 17) {
|
if #unavailable(iOS 17), !sidejitenabled {
|
||||||
guard minimuxerStatus else { return }
|
guard minimuxerStatus else { return }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if #available(iOS 17, *), !sidejitenabled {
|
if #available(iOS 17, *), !sidejitenabled {
|
||||||
ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
|
let error = OperationError.tooNewError as NSError
|
||||||
AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
|
let localizedError = error.withLocalizedTitle("No iOS 17 On Device JIT!")
|
||||||
|
|
||||||
|
ToastView(error: localizedError, opensLog: true).show(in: self)
|
||||||
|
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
AppManager.shared.enableJIT(for: installedApp) { result in
|
AppManager.shared.enableJIT(for: installedApp) { result in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch result
|
switch result {
|
||||||
{
|
case .success:
|
||||||
case .success: break
|
break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
ToastView(error: error, opensLog: true).show(in: self)
|
ToastView(error: error, opensLog: true).show(in: self)
|
||||||
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
|
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
|
||||||
@@ -1810,9 +1859,17 @@ extension MyAppsViewController
|
|||||||
self.exportBackup(for: installedApp)
|
self.exportBackup(for: installedApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
let importBackupAction = UIAction(title: NSLocalizedString("Import Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||||
|
self.importBackup(for: installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: "Restores the last or current backup of this app"), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||||
self.restore(installedApp)
|
self.restore(installedApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let restorePreviousBackupAction = UIAction(title: NSLocalizedString("Restore Previous Backup", comment: "Restores the backup saved before the current backup was created."), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||||
|
self.restorePreviousBackup(for: installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
|
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
|
||||||
self.chooseIcon(for: installedApp)
|
self.chooseIcon(for: installedApp)
|
||||||
@@ -1878,7 +1935,8 @@ extension MyAppsViewController
|
|||||||
var outError: NSError? = nil
|
var outError: NSError? = nil
|
||||||
|
|
||||||
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
||||||
#if DEBUG
|
|
||||||
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
backupExists = true
|
backupExists = true
|
||||||
#else
|
#else
|
||||||
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
||||||
@@ -1903,15 +1961,21 @@ extension MyAppsViewController
|
|||||||
if installedApp.isActive
|
if installedApp.isActive
|
||||||
{
|
{
|
||||||
actions.append(deactivateAction)
|
actions.append(deactivateAction)
|
||||||
|
// import backup into shared backups dir is allowed
|
||||||
|
actions.append(importBackupAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
// have an option to restore the n-1 backup
|
||||||
|
if FileManager.default.fileExists(atPath: getPreviousBackupURL(installedApp).path){
|
||||||
|
actions.append(restorePreviousBackupAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||||
{
|
{
|
||||||
actions.append(removeAction)
|
actions.append(removeAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||||
@@ -1929,6 +1993,26 @@ extension MyAppsViewController
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the order of entries to make changes to how the context menu is displayed
|
||||||
|
let orderedActions = [
|
||||||
|
openMenu,
|
||||||
|
refreshAction,
|
||||||
|
activateAction,
|
||||||
|
jitAction,
|
||||||
|
changeIconMenu,
|
||||||
|
backupAction,
|
||||||
|
exportBackupAction,
|
||||||
|
importBackupAction,
|
||||||
|
restoreBackupAction,
|
||||||
|
restorePreviousBackupAction,
|
||||||
|
deactivateAction,
|
||||||
|
removeAction,
|
||||||
|
]
|
||||||
|
|
||||||
|
// remove non-selected actions from the all-actions ordered list
|
||||||
|
// this way the declaration of the action in the above code doesn't determine the context menu order
|
||||||
|
actions = orderedActions.filter{ action in actions.contains(action)}
|
||||||
|
|
||||||
var title: String?
|
var title: String?
|
||||||
|
|
||||||
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
|
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
|
||||||
|
|||||||
@@ -715,9 +715,10 @@ private extension AuthenticationOperation
|
|||||||
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
|
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
|
||||||
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
|
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
|
||||||
|
|
||||||
#if DEBUG
|
// #if DEBUG && targetEnvironment(simulator)
|
||||||
completionHandler(false)
|
// completionHandler(false)
|
||||||
#else
|
// #else
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let context = AuthenticatedOperationContext(context: self.context)
|
let context = AuthenticatedOperationContext(context: self.context)
|
||||||
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
||||||
@@ -733,7 +734,7 @@ private extension AuthenticationOperation
|
|||||||
completionHandler(false)
|
completionHandler(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
// #endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
|||||||
target_minimuxer_address()
|
target_minimuxer_address()
|
||||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
do {
|
do {
|
||||||
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
|
// enable minimuxer console logging only if enabled in settings
|
||||||
|
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
||||||
|
|
||||||
|
try minimuxer.startWithLogger(
|
||||||
|
try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")),
|
||||||
|
documentsDirectory,
|
||||||
|
isMinimuxerConsoleLoggingEnabled
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,38 +53,37 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
|||||||
guard let installedApp = self.context.installedApp else {
|
guard let installedApp = self.context.installedApp else {
|
||||||
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
|
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
|
||||||
}
|
}
|
||||||
if #available(iOS 17, *) {
|
|
||||||
let sideJITenabled = UserDefaults.standard.sidejitenable
|
let userdefaults = UserDefaults.standard
|
||||||
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
|
||||||
|
if #available(iOS 17, *), userdefaults.sidejitenable {
|
||||||
if sideJITenabled {
|
let SideJITIP = userdefaults.textInputSideJITServerurl ?? "http://sidejitserver._http._tcp.local:8080"
|
||||||
installedApp.managedObjectContext?.perform {
|
installedApp.managedObjectContext?.perform {
|
||||||
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
|
enableJITSideJITServer(serverURL: URL(string: SideJITIP)!, installedApp: installedApp) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
switch error {
|
switch error {
|
||||||
case .invalidURL, .errorConnecting:
|
case .invalidURL, .errorConnecting:
|
||||||
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
||||||
case .deviceNotFound:
|
case .deviceNotFound:
|
||||||
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
||||||
case .other(let message):
|
case .other(let message):
|
||||||
if let startRange = message.range(of: "<p>"),
|
if let startRange = message.range(of: "<p>"),
|
||||||
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
||||||
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
||||||
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
||||||
print(message + " + " + String(pContent))
|
print(message + " + " + String(pContent))
|
||||||
} else {
|
} else {
|
||||||
print(message)
|
print(message)
|
||||||
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .success():
|
|
||||||
self.finish(.success(()))
|
|
||||||
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
|
|
||||||
}
|
}
|
||||||
|
case .success():
|
||||||
|
self.finish(.success(()))
|
||||||
|
print("JIT Enabled Successfully :3 (code made by Stossy11!)")
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
installedApp.managedObjectContext?.perform {
|
installedApp.managedObjectContext?.perform {
|
||||||
@@ -107,48 +106,40 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 17, *)
|
@available(iOS 17, *)
|
||||||
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
||||||
guard let udid = fetch_udid()?.toString() else {
|
guard let udid = fetch_udid()?.toString() else {
|
||||||
completion(.failure(.other("Unable to get UDID")))
|
completion(.failure(.other("Unable to get UDID")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var SJSURL = serverurl
|
let serverURLWithUDID = serverURL.appendingPathComponent(udid)
|
||||||
|
let fullURL = serverURLWithUDID.appendingPathComponent(installedApp.resignedBundleIdentifier)
|
||||||
|
|
||||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
let task = URLSession.shared.dataTask(with: fullURL) { (data, response, error) in
|
||||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !SJSURL.hasPrefix("http") {
|
|
||||||
completion(.failure(.invalidURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
|
|
||||||
|
|
||||||
let url = URL(string: fullurl)!
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
completion(.failure(.errorConnecting))
|
completion(.failure(.errorConnecting))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
|
guard let data = data, let dataString = String(data: data, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
|
if dataString == "Enabled JIT for '\(installedApp.name)'!" {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = "JIT Successfully Enabled"
|
content.title = "JIT Successfully Enabled"
|
||||||
content.subtitle = "JIT Enabled For \(installedapp.name)"
|
content.subtitle = "JIT Enabled For \(installedApp.name)"
|
||||||
content.sound = UNNotificationSound.default
|
content.sound = .default
|
||||||
|
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
||||||
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
} else {
|
} else {
|
||||||
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
|
let errorType: SideJITServerErrorType = dataString == "Could not find device!"
|
||||||
|
? .deviceNotFound
|
||||||
|
: .other(dataString)
|
||||||
completion(.failure(errorType))
|
completion(.failure(errorType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ extension OperationError
|
|||||||
case anisetteV3Error//(message: String)
|
case anisetteV3Error//(message: String)
|
||||||
case cacheClearError//(errors: [String])
|
case cacheClearError//(errors: [String])
|
||||||
case noWiFi
|
case noWiFi
|
||||||
|
|
||||||
|
case invalidOperationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
static var cancelled: CancellationError { CancellationError() }
|
static var cancelled: CancellationError { CancellationError() }
|
||||||
@@ -130,6 +132,10 @@ extension OperationError
|
|||||||
OperationError(code: .invalidParameters, failureReason: message)
|
OperationError(code: .invalidParameters, failureReason: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func invalidOperationContext(_ message: String? = nil) -> OperationError {
|
||||||
|
OperationError(code: .invalidOperationContext, failureReason: message)
|
||||||
|
}
|
||||||
|
|
||||||
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||||
}
|
}
|
||||||
@@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError {
|
|||||||
|
|
||||||
case .invalidParameters:
|
case .invalidParameters:
|
||||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||||
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message)
|
||||||
|
case .invalidOperationContext:
|
||||||
|
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||||
|
return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message)
|
||||||
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
|
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
|
||||||
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
|
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
|
||||||
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
|
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import AltStoreCore
|
|||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
|
class ANISETTE_VERBOSITY: Operation {} // dummy tag iface
|
||||||
|
|
||||||
@objc(FetchAnisetteDataOperation)
|
@objc(FetchAnisetteDataOperation)
|
||||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
||||||
{
|
{
|
||||||
@@ -58,7 +60,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
UserDefaults.standard.menuAnisetteURL = urlString
|
UserDefaults.standard.menuAnisetteURL = urlString
|
||||||
let url = URL(string: urlString)
|
let url = URL(string: urlString)
|
||||||
self.url = url
|
self.url = url
|
||||||
print("Anisette URL: \(self.url!.absoluteString)")
|
self.printOut("Anisette URL: \(self.url!.absoluteString)")
|
||||||
|
|
||||||
if let identifier = Keychain.shared.identifier,
|
if let identifier = Keychain.shared.identifier,
|
||||||
let adiPb = Keychain.shared.adiPb {
|
let adiPb = Keychain.shared.adiPb {
|
||||||
@@ -107,7 +109,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
guard let url = URL(string: currentServerUrlString) else {
|
guard let url = URL(string: currentServerUrlString) else {
|
||||||
// Invalid URL, skip to next
|
// Invalid URL, skip to next
|
||||||
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
|
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
|
||||||
print(errmsg)
|
self.printOut(errmsg)
|
||||||
showToast(viewContext: viewContext, message: errmsg)
|
showToast(viewContext: viewContext, message: errmsg)
|
||||||
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
||||||
return
|
return
|
||||||
@@ -118,7 +120,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
if success {
|
if success {
|
||||||
// If the server is reachable, return the URL
|
// If the server is reachable, return the URL
|
||||||
let okmsg = "Found working server: \(url.absoluteString)"
|
let okmsg = "Found working server: \(url.absoluteString)"
|
||||||
print(okmsg)
|
self.printOut(okmsg)
|
||||||
if(currentIndex > 0){
|
if(currentIndex > 0){
|
||||||
// notify user if available server is different the user-specified one
|
// notify user if available server is different the user-specified one
|
||||||
self.showToast(viewContext: viewContext, message: okmsg)
|
self.showToast(viewContext: viewContext, message: okmsg)
|
||||||
@@ -127,7 +129,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
} else {
|
} else {
|
||||||
// If not, try the next URL
|
// If not, try the next URL
|
||||||
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
|
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
|
||||||
print(errmsg)
|
self.printOut(errmsg)
|
||||||
self.showToast(viewContext: viewContext, message: errmsg)
|
self.showToast(viewContext: viewContext, message: errmsg)
|
||||||
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
||||||
}
|
}
|
||||||
@@ -170,10 +172,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
if v3 {
|
if v3 {
|
||||||
if json["result"] == "GetHeadersError" {
|
if json["result"] == "GetHeadersError" {
|
||||||
let message = json["message"]
|
let message = json["message"]
|
||||||
print("Error getting V3 headers: \(message ?? "no message")")
|
self.printOut("Error getting V3 headers: \(message ?? "no message")")
|
||||||
if let message = message,
|
if let message = message,
|
||||||
message.contains("-45061") {
|
message.contains("-45061") {
|
||||||
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
self.printOut("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||||
Keychain.shared.adiPb = nil
|
Keychain.shared.adiPb = nil
|
||||||
return provision()
|
return provision()
|
||||||
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
||||||
@@ -214,16 +216,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
if let response = response,
|
if let response = response,
|
||||||
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
||||||
print("Implementation-Version: \(version)")
|
self.printOut("Implementation-Version: \(version)")
|
||||||
} else { print("No Implementation-Version header") }
|
} else { self.printOut("No Implementation-Version header") }
|
||||||
|
|
||||||
print("Anisette used: \(formattedJSON)")
|
self.printOut("Anisette used: \(formattedJSON)")
|
||||||
print("Original JSON: \(json)")
|
self.printOut("Original JSON: \(json)")
|
||||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||||
print("Anisette is valid!")
|
self.printOut("Anisette is valid!")
|
||||||
self.finish(.success(anisette))
|
self.finish(.success(anisette))
|
||||||
} else {
|
} else {
|
||||||
print("Anisette is invalid!!!!")
|
self.printOut("Anisette is invalid!!!!")
|
||||||
if v3 {
|
if v3 {
|
||||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||||
} else {
|
} else {
|
||||||
@@ -242,22 +244,22 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
// MARK: - V1
|
// MARK: - V1
|
||||||
|
|
||||||
func handleV1() {
|
func handleV1() {
|
||||||
print("Server is V1")
|
self.printOut("Server is V1")
|
||||||
|
|
||||||
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
||||||
print("Server has already been trusted, fetching anisette")
|
self.printOut("Server has already been trusted, fetching anisette")
|
||||||
return self.fetchAnisetteV1()
|
return self.fetchAnisetteV1()
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Alerting user about outdated server")
|
self.printOut("Alerting user about outdated server")
|
||||||
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
|
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
|
||||||
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
||||||
print("Fetching anisette via V1")
|
self.printOut("Fetching anisette via V1")
|
||||||
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
||||||
self.fetchAnisetteV1()
|
self.fetchAnisetteV1()
|
||||||
}))
|
}))
|
||||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
||||||
print("Cancelled anisette operation")
|
self.printOut("Cancelled anisette operation")
|
||||||
self.finish(.failure(OperationError.cancelled))
|
self.finish(.failure(OperationError.cancelled))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -273,14 +275,14 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchAnisetteV1() {
|
func fetchAnisetteV1() {
|
||||||
print("Fetching anisette V1")
|
self.printOut("Fetching anisette V1")
|
||||||
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
||||||
do {
|
do {
|
||||||
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
|
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
|
||||||
|
|
||||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
|
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
print("Failed to load: \(error.localizedDescription)")
|
self.printOut("Failed to load: \(error.localizedDescription)")
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
@@ -290,7 +292,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
func provision() {
|
func provision() {
|
||||||
fetchClientInfo {
|
fetchClientInfo {
|
||||||
print("Getting provisioning URLs")
|
self.printOut("Getting provisioning URLs")
|
||||||
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
@@ -302,12 +304,12 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
let endProvisioningURL = URL(string: endProvisioningString) {
|
let endProvisioningURL = URL(string: endProvisioningString) {
|
||||||
self.startProvisioningURL = startProvisioningURL
|
self.startProvisioningURL = startProvisioningURL
|
||||||
self.endProvisioningURL = endProvisioningURL
|
self.endProvisioningURL = endProvisioningURL
|
||||||
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
self.printOut("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||||
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
self.printOut("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||||
print("Starting a provisioning session")
|
self.printOut("Starting a provisioning session")
|
||||||
self.startProvisioningSession()
|
self.startProvisioningSession()
|
||||||
} else {
|
} else {
|
||||||
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
self.printOut("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
@@ -329,19 +331,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
do {
|
do {
|
||||||
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
||||||
guard let result = json["result"] as? String else {
|
guard let result = json["result"] as? String else {
|
||||||
print("The server didn't give us a result")
|
self.printOut("The server didn't give us a result")
|
||||||
client.disconnect(closeCode: 0)
|
client.disconnect(closeCode: 0)
|
||||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("Received result: \(result)")
|
self.printOut("Received result: \(result)")
|
||||||
switch result {
|
switch result {
|
||||||
case "GiveIdentifier":
|
case "GiveIdentifier":
|
||||||
print("Giving identifier")
|
self.printOut("Giving identifier")
|
||||||
client.json(["identifier": Keychain.shared.identifier!])
|
client.json(["identifier": Keychain.shared.identifier!])
|
||||||
|
|
||||||
case "GiveStartProvisioningData":
|
case "GiveStartProvisioningData":
|
||||||
print("Getting start provisioning data")
|
self.printOut("Getting start provisioning data")
|
||||||
let body = [
|
let body = [
|
||||||
"Header": [String: Any](),
|
"Header": [String: Any](),
|
||||||
"Request": [String: Any](),
|
"Request": [String: Any](),
|
||||||
@@ -353,19 +355,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
if let data = data,
|
if let data = data,
|
||||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||||
let spim = plist["Response"]?["spim"] as? String {
|
let spim = plist["Response"]?["spim"] as? String {
|
||||||
print("Giving start provisioning data")
|
self.printOut("Giving start provisioning data")
|
||||||
client.json(["spim": spim])
|
client.json(["spim": spim])
|
||||||
} else {
|
} else {
|
||||||
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
self.printOut("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||||
client.disconnect(closeCode: 0)
|
client.disconnect(closeCode: 0)
|
||||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
|
|
||||||
case "GiveEndProvisioningData":
|
case "GiveEndProvisioningData":
|
||||||
print("Getting end provisioning data")
|
self.printOut("Getting end provisioning data")
|
||||||
guard let cpim = json["cpim"] as? String else {
|
guard let cpim = json["cpim"] as? String else {
|
||||||
print("The server didn't give us a cpim")
|
self.printOut("The server didn't give us a cpim")
|
||||||
client.disconnect(closeCode: 0)
|
client.disconnect(closeCode: 0)
|
||||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
||||||
return
|
return
|
||||||
@@ -384,20 +386,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||||
let ptm = plist["Response"]?["ptm"] as? String,
|
let ptm = plist["Response"]?["ptm"] as? String,
|
||||||
let tk = plist["Response"]?["tk"] as? String {
|
let tk = plist["Response"]?["tk"] as? String {
|
||||||
print("Giving end provisioning data")
|
self.printOut("Giving end provisioning data")
|
||||||
client.json(["ptm": ptm, "tk": tk])
|
client.json(["ptm": ptm, "tk": tk])
|
||||||
} else {
|
} else {
|
||||||
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
self.printOut("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||||
client.disconnect(closeCode: 0)
|
client.disconnect(closeCode: 0)
|
||||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
|
|
||||||
case "ProvisioningSuccess":
|
case "ProvisioningSuccess":
|
||||||
print("Provisioning succeeded!")
|
self.printOut("Provisioning succeeded!")
|
||||||
client.disconnect(closeCode: 0)
|
client.disconnect(closeCode: 0)
|
||||||
guard let adiPb = json["adi_pb"] as? String else {
|
guard let adiPb = json["adi_pb"] as? String else {
|
||||||
print("The server didn't give us an adi.pb file")
|
self.printOut("The server didn't give us an adi.pb file")
|
||||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -406,27 +408,27 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
||||||
print("Failing because of \(result)")
|
self.printOut("Failing because of \(result)")
|
||||||
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
print("Failed to handle text: \(error.localizedDescription)")
|
self.printOut("Failed to handle text: \(error.localizedDescription)")
|
||||||
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
||||||
}
|
}
|
||||||
|
|
||||||
case .connected:
|
case .connected:
|
||||||
print("Connected")
|
self.printOut("Connected")
|
||||||
|
|
||||||
case .disconnected(let string, let code):
|
case .disconnected(let string, let code):
|
||||||
print("Disconnected: \(code); \(string)")
|
self.printOut("Disconnected: \(code); \(string)")
|
||||||
|
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
print("Got error: \(String(describing: error))")
|
self.printOut("Got error: \(String(describing: error))")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
print("Unknown event: \(event)")
|
self.printOut("Unknown event: \(event)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,10 +462,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
self.mdLu != nil &&
|
self.mdLu != nil &&
|
||||||
self.deviceId != nil &&
|
self.deviceId != nil &&
|
||||||
Keychain.shared.identifier != nil {
|
Keychain.shared.identifier != nil {
|
||||||
print("Skipping client_info fetch since all the properties we need aren't nil")
|
self.printOut("Skipping client_info fetch since all the properties we need aren't nil")
|
||||||
return callback()
|
return callback()
|
||||||
}
|
}
|
||||||
print("Trying to get client_info")
|
self.printOut("Trying to get client_info")
|
||||||
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
||||||
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
||||||
do {
|
do {
|
||||||
@@ -473,20 +475,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||||
if let clientInfo = json["client_info"] {
|
if let clientInfo = json["client_info"] {
|
||||||
print("Server is V3")
|
self.printOut("Server is V3")
|
||||||
|
|
||||||
self.clientInfo = clientInfo
|
self.clientInfo = clientInfo
|
||||||
self.userAgent = json["user_agent"]!
|
self.userAgent = json["user_agent"]!
|
||||||
print("Client-Info: \(self.clientInfo!)")
|
self.printOut("Client-Info: \(self.clientInfo!)")
|
||||||
print("User-Agent: \(self.userAgent!)")
|
self.printOut("User-Agent: \(self.userAgent!)")
|
||||||
|
|
||||||
if Keychain.shared.identifier == nil {
|
if Keychain.shared.identifier == nil {
|
||||||
print("Generating identifier")
|
self.printOut("Generating identifier")
|
||||||
var bytes = [Int8](repeating: 0, count: 16)
|
var bytes = [Int8](repeating: 0, count: 16)
|
||||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||||
|
|
||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
print("ERROR GENERATING IDENTIFIER!!! \(status)")
|
self.printOut("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||||
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,16 +497,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
||||||
self.mdLu = decoded.sha256().hexEncodedString()
|
self.mdLu = decoded.sha256().hexEncodedString()
|
||||||
print("X-Apple-I-MD-LU: \(self.mdLu!)")
|
self.printOut("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||||
let uuid: UUID = decoded.object()
|
let uuid: UUID = decoded.object()
|
||||||
self.deviceId = uuid.uuidString.uppercased()
|
self.deviceId = uuid.uuidString.uppercased()
|
||||||
print("X-Mme-Device-Id: \(self.deviceId!)")
|
self.printOut("X-Mme-Device-Id: \(self.deviceId!)")
|
||||||
|
|
||||||
callback()
|
callback()
|
||||||
} else { self.handleV1() }
|
} else { self.handleV1() }
|
||||||
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
|
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
print("Failed to load: \(error.localizedDescription)")
|
self.printOut("Failed to load: \(error.localizedDescription)")
|
||||||
self.handleV1()
|
self.handleV1()
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
@@ -512,7 +514,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||||
fetchClientInfo {
|
fetchClientInfo {
|
||||||
print("Fetching anisette V3")
|
self.printOut("Fetching anisette V3")
|
||||||
let url = UserDefaults.standard.menuAnisetteURL
|
let url = UserDefaults.standard.menuAnisetteURL
|
||||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
@@ -527,12 +529,21 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
|
|
||||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
print("Failed to load: \(error.localizedDescription)")
|
self.printOut("Failed to load: \(error.localizedDescription)")
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func printOut(_ text: String?){
|
||||||
|
let isInternalLoggingEnabled = OperationsLoggingControl.getFromDatabase(for: ANISETTE_VERBOSITY.self)
|
||||||
|
if(isInternalLoggingEnabled){
|
||||||
|
// logging enabled, so log it
|
||||||
|
text.map{ _ in print(text!) } ?? print()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebSocketClient {
|
extension WebSocketClient {
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ import AltSign
|
|||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchProvisioningProfilesOperation)
|
@objc(FetchProvisioningProfilesOperation)
|
||||||
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
var additionalEntitlements: [ALTEntitlement: Any]?
|
var additionalEntitlements: [ALTEntitlement: Any]?
|
||||||
|
|
||||||
private let appGroupsLock = NSLock()
|
internal let appGroupsLock = NSLock()
|
||||||
|
|
||||||
init(context: AppOperationContext)
|
// this class is abstract or shouldn't be instantiated outside, use the subclasses
|
||||||
|
fileprivate init(context: AppOperationContext)
|
||||||
{
|
{
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@@ -40,11 +41,13 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard let team = self.context.team,
|
||||||
let team = self.context.team,
|
let session = self.context.session else {
|
||||||
let session = self.context.session
|
|
||||||
else {
|
return self.finish(.failure(
|
||||||
return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
|
OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
|
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
|
||||||
|
|
||||||
@@ -120,7 +123,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
|||||||
|
|
||||||
extension FetchProvisioningProfilesOperation
|
extension FetchProvisioningProfilesOperation
|
||||||
{
|
{
|
||||||
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
private func prepareProvisioningProfile(for app: ALTApplication,
|
||||||
|
parentApp: ALTApplication?,
|
||||||
|
team: ALTTeam,
|
||||||
|
session: ALTAppleAPISession, c
|
||||||
|
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||||
{
|
{
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
|
||||||
@@ -134,19 +141,21 @@ extension FetchProvisioningProfilesOperation
|
|||||||
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
|
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
|
||||||
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
|
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
|
||||||
|
|
||||||
// #if DEBUG
|
// TODO: @mahee96: Try to keep the debug build and release build operations similar, refactor later with proper reasoning
|
||||||
//
|
// for now, restricted it to debug on simulator only
|
||||||
// if app.isAltStoreApp
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
// {
|
|
||||||
// // Use legacy bundle ID format for AltStore.
|
if app.isAltStoreApp
|
||||||
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
{
|
||||||
// }
|
// Use legacy bundle ID format for AltStore.
|
||||||
// else
|
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||||
// {
|
}
|
||||||
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
else
|
||||||
// }
|
{
|
||||||
//
|
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
|
||||||
// #else
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
if teamsMatch
|
if teamsMatch
|
||||||
{
|
{
|
||||||
@@ -160,7 +169,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
preferredBundleID = nil
|
preferredBundleID = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endif
|
#endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -211,35 +220,22 @@ extension FetchProvisioningProfilesOperation
|
|||||||
{
|
{
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
case .success(let appID):
|
case .success(let appID):
|
||||||
|
|
||||||
// Update features
|
//process
|
||||||
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
self.fetchProvisioningProfile(
|
||||||
switch result
|
for: appID, team: team, session: session, completionHandler: completionHandler
|
||||||
{
|
)
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
|
||||||
case .success(let appID):
|
|
||||||
|
|
||||||
// Update app groups
|
|
||||||
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
|
||||||
case .success(let appID):
|
|
||||||
|
|
||||||
// Fetch Provisioning Profile
|
|
||||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
|
||||||
completionHandler(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
private func registerAppID(for application: ALTApplication,
|
||||||
|
name: String,
|
||||||
|
bundleIdentifier: String,
|
||||||
|
team: ALTTeam,
|
||||||
|
session: ALTAppleAPISession,
|
||||||
|
completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||||
{
|
{
|
||||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||||
do
|
do
|
||||||
@@ -333,7 +329,81 @@ extension FetchProvisioningProfilesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||||
|
{
|
||||||
|
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||||
|
switch Result(profile, error)
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let profile):
|
||||||
|
|
||||||
|
// Delete existing profile
|
||||||
|
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||||
|
switch Result(success, error)
|
||||||
|
{
|
||||||
|
case .failure:
|
||||||
|
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
||||||
|
// So instead, we just return the fetched profile from above.
|
||||||
|
completionHandler(.success(profile))
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
|
||||||
|
|
||||||
|
// Fetch new provisioning profile
|
||||||
|
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||||
|
completionHandler(Result(profile, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
|
||||||
|
override init(context: AppOperationContext)
|
||||||
|
{
|
||||||
|
super.init(context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
|
||||||
|
override init(context: AppOperationContext)
|
||||||
|
{
|
||||||
|
super.init(context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify Operations are allowed for the app groups and other stuffs
|
||||||
|
func fetchProvisioningProfile(appID: ALTAppID,
|
||||||
|
for app: ALTApplication,
|
||||||
|
team: ALTTeam,
|
||||||
|
session: ALTAppleAPISession,
|
||||||
|
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Update features
|
||||||
|
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let appID):
|
||||||
|
|
||||||
|
// Update app groups
|
||||||
|
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let appID):
|
||||||
|
|
||||||
|
// Fetch Provisioning Profile
|
||||||
|
super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||||
{
|
{
|
||||||
var entitlements = app.entitlements
|
var entitlements = app.entitlements
|
||||||
for (key, value) in additionalEntitlements ?? [:]
|
for (key, value) in additionalEntitlements ?? [:]
|
||||||
@@ -412,7 +482,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
private func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||||
{
|
{
|
||||||
var entitlements = app.entitlements
|
var entitlements = app.entitlements
|
||||||
for (key, value) in additionalEntitlements ?? [:]
|
for (key, value) in additionalEntitlements ?? [:]
|
||||||
@@ -511,7 +581,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
|
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||||
errors.append(error)
|
errors.append(error)
|
||||||
}
|
}
|
||||||
@@ -547,34 +617,4 @@ extension FetchProvisioningProfilesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
|
||||||
{
|
|
||||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
|
||||||
switch Result(profile, error)
|
|
||||||
{
|
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
|
||||||
case .success(let profile):
|
|
||||||
|
|
||||||
// Delete existing profile
|
|
||||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
|
||||||
switch Result(success, error)
|
|
||||||
{
|
|
||||||
case .failure:
|
|
||||||
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
|
||||||
// So instead, we just return the fetched profile from above.
|
|
||||||
completionHandler(.success(profile))
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
|
|
||||||
|
|
||||||
// Fetch new provisioning profile
|
|
||||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
|
||||||
completionHandler(Result(profile, error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,14 @@ class ResultOperation<ResultType>: Operation
|
|||||||
result = .failure(error)
|
result = .failure(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// diagnostics logging
|
// Diagnostics: perform verbose logging of the operations only if enabled (so as to not flood console logs)
|
||||||
let resultStatus = String(describing: result).prefix("success".count).uppercased()
|
let isLoggingEnabledForThisOperation = OperationsLoggingControl.getFromDatabase(for: type(of: self))
|
||||||
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
|
if UserDefaults.standard.isVerboseOperationsLoggingEnabled && isLoggingEnabledForThisOperation {
|
||||||
" Result: \(result)\n")
|
// diagnostics logging
|
||||||
|
let resultStatus = String(describing: result).prefix("success".count).uppercased()
|
||||||
|
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
|
||||||
|
" Result: \(result)\n")
|
||||||
|
}
|
||||||
|
|
||||||
self.resultHandler?(result)
|
self.resultHandler?(result)
|
||||||
|
|
||||||
|
|||||||
@@ -58,17 +58,18 @@ final class RemoveAppBackupOperation: ResultOperation<Void>
|
|||||||
}
|
}
|
||||||
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
|
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
|
||||||
{
|
{
|
||||||
#if DEBUG
|
// TODO: @mahee96: Find out why should in debug builds the app-groups is not expected to match
|
||||||
|
// #if DEBUG
|
||||||
// When debugging, it's expected that app groups don't match, so ignore.
|
//
|
||||||
self.finish(.success(()))
|
// // When debugging, it's expected that app groups don't match, so ignore.
|
||||||
|
// self.finish(.success(()))
|
||||||
#else
|
//
|
||||||
|
// #else
|
||||||
|
|
||||||
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
|
|
||||||
#endif
|
// #endif
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
219
AltStore/Operations/RemoveAppExtensionsOperation.swift
Normal file
219
AltStore/Operations/RemoveAppExtensionsOperation.swift
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
//
|
||||||
|
// RefreshAppOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 2/27/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@objc(RemoveAppExtensionsOperation)
|
||||||
|
final class RemoveAppExtensionsOperation: ResultOperation<Void>
|
||||||
|
{
|
||||||
|
let context: AppOperationContext
|
||||||
|
let localAppExtensions: Set<ALTApplication>?
|
||||||
|
|
||||||
|
init(context: AppOperationContext, localAppExtensions: Set<ALTApplication>?)
|
||||||
|
{
|
||||||
|
self.context = context
|
||||||
|
self.localAppExtensions = localAppExtensions
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let targetAppBundle = context.app else {
|
||||||
|
return self.finish(.failure(
|
||||||
|
OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.removeAppExtensions(from: targetAppBundle,
|
||||||
|
localAppExtensions: localAppExtensions,
|
||||||
|
extensions: targetAppBundle.appExtensions,
|
||||||
|
context.authenticatedContext.presentingViewController)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func removeExtensions(from extensions: Set<ALTApplication>) throws {
|
||||||
|
for appExtension in extensions {
|
||||||
|
print("Deleting extension \(appExtension.bundleIdentifier)")
|
||||||
|
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private func removeAppExtensions(from targetAppBundle: ALTApplication,
|
||||||
|
localAppExtensions: Set<ALTApplication>?,
|
||||||
|
extensions: Set<ALTApplication>,
|
||||||
|
_ presentingViewController: UIViewController?)
|
||||||
|
{
|
||||||
|
|
||||||
|
// target App Bundle doesn't contain extensions so don't bother
|
||||||
|
guard !targetAppBundle.appExtensions.isEmpty else {
|
||||||
|
return self.finish(.success(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process extensionsInfo
|
||||||
|
let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let presentingViewController: UIViewController = presentingViewController,
|
||||||
|
presentingViewController.viewIfLoaded?.window != nil else {
|
||||||
|
// background mode: remove only the excess extensions automatically for re-installs
|
||||||
|
// keep all extensions for fresh install (localAppBundle = nil)
|
||||||
|
return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// present prompt to the user if we have a view context
|
||||||
|
let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController)
|
||||||
|
presentingViewController.present(alertController, animated: true){
|
||||||
|
|
||||||
|
// if for any reason the view wasn't presented, then just signal that as error
|
||||||
|
if presentingViewController.presentedViewController == nil {
|
||||||
|
let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
|
||||||
|
"\nDid you move to different screen or background after starting the operation?"
|
||||||
|
self.finish(.failure(
|
||||||
|
OperationError.invalidOperationContext(errMsg)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createAlertDialog(from targetAppBundle: ALTApplication,
|
||||||
|
extensions: Set<ALTApplication>,
|
||||||
|
_ presentingViewController: UIViewController) -> UIAlertController
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Foreground prompt:
|
||||||
|
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? There are \(extensions.count) Extensions", 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
|
||||||
|
self.finish(.failure(OperationError.cancelled))
|
||||||
|
}))
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||||
|
self.finish(.success(()))
|
||||||
|
})
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||||
|
do {
|
||||||
|
try Self.removeExtensions(from: targetAppBundle.appExtensions)
|
||||||
|
return self.finish(.success(()))
|
||||||
|
} catch {
|
||||||
|
return self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
|
||||||
|
|
||||||
|
|
||||||
|
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
|
||||||
|
do {
|
||||||
|
try Self.removeExtensions(from: Set(selection))
|
||||||
|
return self.finish(.success(()))
|
||||||
|
} catch {
|
||||||
|
return self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let suiview = popoverContentController.view!
|
||||||
|
suiview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
popoverContentController.modalPresentationStyle = .popover
|
||||||
|
|
||||||
|
if let popoverPresentationController = popoverContentController.popoverPresentationController {
|
||||||
|
popoverPresentationController.sourceView = presentingViewController.view
|
||||||
|
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
|
||||||
|
popoverPresentationController.delegate = popoverContentController
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
presentingViewController.present(popoverContentController, animated: true)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
self.finish(.failure(
|
||||||
|
OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return alertController
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExtensionsInfo{
|
||||||
|
let excessInTarget: Set<ALTApplication>
|
||||||
|
let necessaryInExisting: Set<ALTApplication>
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processExtensionsInfo(from targetAppBundle: ALTApplication,
|
||||||
|
localAppExtensions: Set<ALTApplication>?) -> Set<ALTApplication>
|
||||||
|
{
|
||||||
|
//App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match
|
||||||
|
let targetAppEx: Set<ALTApplication> = targetAppBundle.appExtensions
|
||||||
|
let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier}
|
||||||
|
|
||||||
|
guard let extensionsInExistingApp = localAppExtensions else {
|
||||||
|
let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle"
|
||||||
|
+ "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)"
|
||||||
|
print(diagnosticsMsg)
|
||||||
|
return Set() // nothing is excess since we are keeping all, so returning empty
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingAppEx: Set<ALTApplication> = extensionsInExistingApp
|
||||||
|
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
|
||||||
|
|
||||||
|
let excessExtensionsInTargetApp = targetAppEx.filter{
|
||||||
|
!(existingAppExNames.contains($0.bundleIdentifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty
|
||||||
|
let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n"
|
||||||
|
+ "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n"
|
||||||
|
print(diagnosticsMsg)
|
||||||
|
|
||||||
|
return excessExtensionsInTargetApp
|
||||||
|
}
|
||||||
|
|
||||||
|
private func backgroundModeExtensionsCleanup(excessExtensions: Set<ALTApplication>) {
|
||||||
|
// perform silent extensions cleanup for those that aren't already present in existing app
|
||||||
|
print("\n Performing background mode Extensions removal \n")
|
||||||
|
print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensions.map{$0.bundleIdentifier})")
|
||||||
|
|
||||||
|
do {
|
||||||
|
try Self.removeExtensions(from: excessExtensions)
|
||||||
|
return self.finish(.success(()))
|
||||||
|
} catch {
|
||||||
|
return self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,6 +244,7 @@ private extension ResignAppOperation
|
|||||||
{
|
{
|
||||||
for case let fileURL as URL in enumerator
|
for case let fileURL as URL in enumerator
|
||||||
{
|
{
|
||||||
|
// for both sim and device, in debug mode builds, remove the tests bundles (if any)
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else {
|
guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else {
|
||||||
// Remove embedded XCTest (+ dSYM) bundle from copied app bundle.
|
// Remove embedded XCTest (+ dSYM) bundle from copied app bundle.
|
||||||
|
|||||||
226
AltStore/Settings/Error Log/ConsoleLogView.swift
Normal file
226
AltStore/Settings/Error Log/ConsoleLogView.swift
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
//
|
||||||
|
// ConsoleLogView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 29/12/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class ConsoleLogViewModel: ObservableObject {
|
||||||
|
@Published var logLines: [String] = []
|
||||||
|
|
||||||
|
@Published var searchTerm: String = ""
|
||||||
|
@Published var currentSearchIndex: Int = 0
|
||||||
|
@Published var searchResults: [Int] = [] // Stores indices of matching lines
|
||||||
|
|
||||||
|
private var fileWatcher: DispatchSourceFileSystemObject?
|
||||||
|
|
||||||
|
private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
|
||||||
|
private var logURL: URL
|
||||||
|
|
||||||
|
init(logURL: URL) {
|
||||||
|
self.logURL = logURL
|
||||||
|
startFileWatcher() // Start monitoring the log file for changes
|
||||||
|
reloadLogData() // Load initial log data
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startFileWatcher() {
|
||||||
|
let fileDescriptor = open(logURL.path, O_RDONLY)
|
||||||
|
guard fileDescriptor != -1 else {
|
||||||
|
print("Unable to open file for reading.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileWatcher = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: backgroundQueue)
|
||||||
|
fileWatcher?.setEventHandler {
|
||||||
|
self.reloadLogData()
|
||||||
|
}
|
||||||
|
fileWatcher?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadLogData() {
|
||||||
|
if let fileContents = try? String(contentsOf: logURL) {
|
||||||
|
let lines = fileContents.split(whereSeparator: \.isNewline).map { String($0) }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.logLines = lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
fileWatcher?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func performSearch() {
|
||||||
|
searchResults = logLines.enumerated()
|
||||||
|
.filter { $0.element.localizedCaseInsensitiveContains(searchTerm) }
|
||||||
|
.map { $0.offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextSearchResult() {
|
||||||
|
guard !searchResults.isEmpty else { return }
|
||||||
|
currentSearchIndex = (currentSearchIndex + 1) % searchResults.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func previousSearchResult() {
|
||||||
|
guard !searchResults.isEmpty else { return }
|
||||||
|
currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct ConsoleLogView: View {
|
||||||
|
|
||||||
|
@ObservedObject var viewModel: ConsoleLogViewModel
|
||||||
|
@State private var scrollToBottom: Bool = false // State variable to trigger scroll
|
||||||
|
@State private var searchBarState: Bool = false
|
||||||
|
@FocusState private var isSearchFieldFocused: Bool
|
||||||
|
|
||||||
|
@State private var searchText: String = ""
|
||||||
|
@State private var scrollToIndex: Int?
|
||||||
|
|
||||||
|
private let resultHighlightColor = Color.orange
|
||||||
|
private let resultHighlightOpacity = 0.5
|
||||||
|
private let otherResultsColor = Color.yellow
|
||||||
|
private let otherResultsOpacity = 0.3
|
||||||
|
|
||||||
|
init(logURL: URL) {
|
||||||
|
self.viewModel = ConsoleLogViewModel(logURL: logURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack {
|
||||||
|
|
||||||
|
// Custom Header Bar (similar to QuickLook's preview screen)
|
||||||
|
HStack {
|
||||||
|
Text("Console Log")
|
||||||
|
.font(.system(size: 22, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if(!searchBarState){
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
searchBarState.toggle()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.padding(.trailing)
|
||||||
|
}
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
scrollToBottom.toggle()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(15)
|
||||||
|
.padding(.top, 5)
|
||||||
|
.padding(.bottom, 2.5)
|
||||||
|
.background(Color.black.opacity(0.9))
|
||||||
|
.overlay(
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
if(searchBarState){
|
||||||
|
// Search bar
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.padding(.trailing, 4)
|
||||||
|
|
||||||
|
TextField("Search", text: $searchText)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.onChange(of: searchText) { newValue in
|
||||||
|
viewModel.searchTerm = newValue
|
||||||
|
viewModel.performSearch()
|
||||||
|
}
|
||||||
|
.keyboardShortcut("f", modifiers: .command) // Focus search field
|
||||||
|
|
||||||
|
if !searchText.isEmpty {
|
||||||
|
// Search navigation buttons
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
viewModel.previousSearchResult()
|
||||||
|
scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
|
||||||
|
}) {
|
||||||
|
Image(systemName: "chevron.up")
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.return, modifiers: [.command, .shift])
|
||||||
|
.disabled(viewModel.searchResults.isEmpty)
|
||||||
|
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
viewModel.nextSearchResult()
|
||||||
|
scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
|
||||||
|
}) {
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.return, modifiers: .command)
|
||||||
|
.disabled(viewModel.searchResults.isEmpty)
|
||||||
|
|
||||||
|
// Results counter
|
||||||
|
Text("\(viewModel.currentSearchIndex + 1)/\(viewModel.searchResults.count)")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
searchBarState.toggle()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Main Log Display (scrollable area)
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
LazyVStack(alignment: .leading, spacing: 4) {
|
||||||
|
ForEach(viewModel.logLines.indices, id: \.self) { index in
|
||||||
|
Text(viewModel.logLines[index])
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.background(
|
||||||
|
viewModel.searchResults.contains(index) ?
|
||||||
|
otherResultsColor.opacity(otherResultsOpacity) : Color.clear
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
viewModel.searchResults[safe: viewModel.currentSearchIndex] == index ?
|
||||||
|
resultHighlightColor.opacity(resultHighlightOpacity) : Color.clear
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: scrollToIndex) { newIndex in
|
||||||
|
if let index = newIndex {
|
||||||
|
withAnimation {
|
||||||
|
proxy.scrollTo(index, anchor: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: scrollToBottom) { _ in
|
||||||
|
viewModel.logLines.indices.last.map { last in
|
||||||
|
proxy.scrollTo(last, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.black) // Set background color to mimic QL's dark theme
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper extension for safe array access
|
||||||
|
extension Array {
|
||||||
|
subscript(safe index: Index) -> Element? {
|
||||||
|
indices.contains(index) ? self[index] : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import Roxas
|
|||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
import QuickLook
|
import QuickLook
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
|
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
|
||||||
{
|
{
|
||||||
@@ -59,8 +60,46 @@ final class ErrorLogViewController: UITableViewController, QLPreviewControllerDe
|
|||||||
// Assign just clearLogButton to hide export button.
|
// Assign just clearLogButton to hide export button.
|
||||||
self.navigationItem.rightBarButtonItems = [self.clearLogButton]
|
self.navigationItem.rightBarButtonItems = [self.clearLogButton]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // Adjust the width of the right bar button items
|
||||||
|
// adjustRightBarButtonWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// func adjustRightBarButtonWidth() {
|
||||||
|
// // Access the current rightBarButtonItems
|
||||||
|
// if let rightBarButtonItems = self.navigationItem.rightBarButtonItems {
|
||||||
|
// for barButtonItem in rightBarButtonItems {
|
||||||
|
// // Check if the button is a system button, and if so, replace it with a custom button
|
||||||
|
// if barButtonItem.customView == nil {
|
||||||
|
// // Replace with a custom UIButton for each bar button item
|
||||||
|
// let customButton = UIButton(type: .custom)
|
||||||
|
// if let image = barButtonItem.image {
|
||||||
|
// customButton.setImage(image, for: .normal)
|
||||||
|
// }
|
||||||
|
// if let action = barButtonItem.action{
|
||||||
|
// customButton.addTarget(barButtonItem.target, action: action, for: .touchUpInside)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Calculate the original size based on the system button
|
||||||
|
// let originalSize = customButton.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
|
||||||
|
//
|
||||||
|
// let scaleFactor = 0.7
|
||||||
|
//
|
||||||
|
// // Scale the size by 0.7 (70%)
|
||||||
|
// let scaledSize = CGSize(width: originalSize.width * scaleFactor, height: originalSize.height * scaleFactor)
|
||||||
|
//
|
||||||
|
// // Adjust the custom button's width
|
||||||
|
//// customButton.frame.size = CGSize(width: 22, height: 22) // Adjust width as needed
|
||||||
|
// customButton.frame.size = scaledSize // Adjust width as needed
|
||||||
|
//
|
||||||
|
// // Set the custom button as the custom view for the UIBarButtonItem
|
||||||
|
// barButtonItem.customView = customButton
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||||
{
|
{
|
||||||
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
|
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
|
||||||
@@ -216,15 +255,86 @@ private extension ErrorLogViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
|
|
||||||
{
|
enum LogView: String {
|
||||||
// Show minimuxer.log
|
case consoleLog = "console-log"
|
||||||
let previewController = QLPreviewController()
|
case minimuxerLog = "minimuxer-log"
|
||||||
previewController.dataSource = self
|
|
||||||
let navigationController = UINavigationController(rootViewController: previewController)
|
// This class will manage the QLPreviewController and the timer.
|
||||||
present(navigationController, animated: true, completion: nil)
|
private class LogViewManager {
|
||||||
|
var previewController: QLPreviewController
|
||||||
|
var refreshTimer: Timer?
|
||||||
|
var logView: LogView
|
||||||
|
|
||||||
|
init(previewController: QLPreviewController, logView: LogView) {
|
||||||
|
self.previewController = previewController
|
||||||
|
self.logView = logView
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start refreshing the preview controller every second
|
||||||
|
func startRefreshing() {
|
||||||
|
refreshTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(refreshPreview), userInfo: nil, repeats: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshPreview() {
|
||||||
|
previewController.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the timer to prevent leaks
|
||||||
|
func stopRefreshing() {
|
||||||
|
refreshTimer?.invalidate()
|
||||||
|
refreshTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLogPath() {
|
||||||
|
// Force the QLPreviewController to reload by changing the file path
|
||||||
|
previewController.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get the QLPreviewController for this log type
|
||||||
|
func getViewController(_ dataSource: QLPreviewControllerDataSource) -> QLPreviewController {
|
||||||
|
let previewController = QLPreviewController()
|
||||||
|
previewController.restorationIdentifier = self.rawValue
|
||||||
|
previewController.dataSource = dataSource
|
||||||
|
|
||||||
|
// Create LogViewManager and start refreshing
|
||||||
|
let manager = LogViewManager(previewController: previewController, logView: self)
|
||||||
|
// manager.startRefreshing() // DO NOT REFRESH the full view contents causing flickering
|
||||||
|
|
||||||
|
return previewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogPath() -> URL {
|
||||||
|
switch self {
|
||||||
|
case .consoleLog:
|
||||||
|
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||||
|
return appDelegate.consoleLog.logFileURL
|
||||||
|
case .minimuxerLog:
|
||||||
|
return FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction func showConsoleLogs(_ sender: UIBarButtonItem) {
|
||||||
|
// Create the SwiftUI ConsoleLogView with the URL
|
||||||
|
let consoleLogView = ConsoleLogView(logURL: (UIApplication.shared.delegate as! AppDelegate).consoleLog.logFileURL)
|
||||||
|
|
||||||
|
// Create the UIHostingController
|
||||||
|
let consoleLogController = UIHostingController(rootView: consoleLogView)
|
||||||
|
|
||||||
|
// Configure the bottom sheet presentation
|
||||||
|
consoleLogController.modalPresentationStyle = .pageSheet
|
||||||
|
if let sheet = consoleLogController.sheetPresentationController {
|
||||||
|
sheet.detents = [.medium(), .large()] // You can adjust the size of the sheet (medium/large)
|
||||||
|
sheet.prefersGrabberVisible = true // Optional: Shows a grabber at the top of the sheet
|
||||||
|
sheet.selectedDetentIdentifier = .large // Default size when presented
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the bottom sheet
|
||||||
|
present(consoleLogController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem)
|
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem)
|
||||||
{
|
{
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
|
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||||
@@ -285,58 +395,74 @@ private extension ErrorLogViewController
|
|||||||
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
|
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 15, *)
|
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
|
||||||
@IBAction func exportDetailedLog(_ sender: UIBarButtonItem)
|
|
||||||
{
|
{
|
||||||
self.exportLogButton.isIndicatingActivity = true
|
// Show minimuxer.log
|
||||||
|
let previewController = LogView.minimuxerLog.getViewController(self)
|
||||||
Task<Void, Never>.detached(priority: .userInitiated) {
|
let navigationController = UINavigationController(rootViewController: previewController)
|
||||||
do
|
present(navigationController, animated: true, completion: nil)
|
||||||
{
|
|
||||||
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
|
||||||
|
|
||||||
// All logs since the app launched.
|
|
||||||
let position = store.position(timeIntervalSinceLatestBoot: 0)
|
|
||||||
let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
|
|
||||||
|
|
||||||
let entries = try store.getEntries(at: position, matching: predicate)
|
|
||||||
.compactMap { $0 as? OSLogEntryLog }
|
|
||||||
.map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
|
||||||
|
|
||||||
let outputText = entries.joined(separator: "\n")
|
|
||||||
|
|
||||||
let outputDirectory = FileManager.default.uniqueTemporaryURL()
|
|
||||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
|
||||||
|
|
||||||
let outputURL = outputDirectory.appendingPathComponent("altstore.log")
|
|
||||||
try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self._exportedLogURL = outputURL
|
|
||||||
|
|
||||||
let previewController = QLPreviewController()
|
|
||||||
previewController.delegate = self
|
|
||||||
previewController.dataSource = self
|
|
||||||
previewController.view.tintColor = .altPrimary
|
|
||||||
self.present(previewController, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)")
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
|
||||||
alertController.addAction(.ok)
|
|
||||||
self.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.exportLogButton.isIndicatingActivity = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @available(iOS 15, *)
|
||||||
|
// @IBAction func exportDetailedLog(_ sender: UIBarButtonItem)
|
||||||
|
// {
|
||||||
|
// self.exportLogButton.isIndicatingActivity = true
|
||||||
|
//
|
||||||
|
// Task<Void, Never>.detached(priority: .userInitiated) {
|
||||||
|
// do
|
||||||
|
// {
|
||||||
|
// let store = try OSLogStore(scope: .currentProcessIdentifier)
|
||||||
|
//
|
||||||
|
// // All logs since the app launched.
|
||||||
|
// let position = store.position(timeIntervalSinceLatestBoot: 0)
|
||||||
|
//// let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
|
||||||
|
////
|
||||||
|
//// let entries = try store.getEntries(at: position, matching: predicate)
|
||||||
|
//// .compactMap { $0 as? OSLogEntryLog }
|
||||||
|
//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||||
|
////
|
||||||
|
// // Remove the predicate to get all log entries
|
||||||
|
//// let entries = try store.getEntries(at: position)
|
||||||
|
//// .compactMap { $0 as? OSLogEntryLog }
|
||||||
|
//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||||
|
//
|
||||||
|
// let entries = try store.getEntries(at: position)
|
||||||
|
//
|
||||||
|
//// let outputText = entries.joined(separator: "\n")
|
||||||
|
// let outputText = entries.map { $0.description }.joined(separator: "\n")
|
||||||
|
//
|
||||||
|
// let outputDirectory = FileManager.default.uniqueTemporaryURL()
|
||||||
|
// try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||||
|
//
|
||||||
|
// let outputURL = outputDirectory.appendingPathComponent("altstore.log")
|
||||||
|
// try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
|
||||||
|
//
|
||||||
|
// await MainActor.run {
|
||||||
|
// self._exportedLogURL = outputURL
|
||||||
|
//
|
||||||
|
// let previewController = QLPreviewController()
|
||||||
|
// previewController.delegate = self
|
||||||
|
// previewController.dataSource = self
|
||||||
|
// previewController.view.tintColor = .altPrimary
|
||||||
|
// self.present(previewController, animated: true)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)")
|
||||||
|
//
|
||||||
|
// await MainActor.run {
|
||||||
|
// let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
// alertController.addAction(.ok)
|
||||||
|
// self.present(alertController, animated: true)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// await MainActor.run {
|
||||||
|
// self.exportLogButton.isIndicatingActivity = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ErrorLogViewController
|
extension ErrorLogViewController
|
||||||
@@ -412,9 +538,13 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
|
||||||
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
|
{
|
||||||
return fileURL as QLPreviewItem
|
guard let identifier = controller.restorationIdentifier,
|
||||||
|
let logView = LogView(rawValue: identifier) else {
|
||||||
|
fatalError("Invalid restorationIdentifier")
|
||||||
|
}
|
||||||
|
return logView.getLogPath() as QLPreviewItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
284
AltStore/Settings/OperationsLoggingContolView.swift
Normal file
284
AltStore/Settings/OperationsLoggingContolView.swift
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
//
|
||||||
|
// SettingsView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 14/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
|
||||||
|
private final class DummyConformance: EnableJITContext
|
||||||
|
{
|
||||||
|
private init(){} // non instantiatable
|
||||||
|
var installedApp: AltStoreCore.InstalledApp?
|
||||||
|
var error: (any Error)?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct OperationsLoggingControlView: View {
|
||||||
|
let TITLE = "Operations Logging"
|
||||||
|
let BACKGROUND_COLOR = Color(.settingsBackground)
|
||||||
|
|
||||||
|
var viewModel = OperationsLoggingControl()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ZStack {
|
||||||
|
// BACKGROUND_COLOR.ignoresSafeArea() // Force background to cover the entire screen
|
||||||
|
VStack{
|
||||||
|
Group{}.padding(12)
|
||||||
|
|
||||||
|
CustomList {
|
||||||
|
CustomSection(header: Text("Install Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. Authentication", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: AuthenticationOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: AuthenticationOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("2. VerifyAppPledge", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: VerifyAppPledgeOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: VerifyAppPledgeOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("3. DownloadApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: DownloadAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: DownloadAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("4. VerifyApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: VerifyAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: VerifyAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("5. RemoveAppExtensions", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: RemoveAppExtensionsOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: RemoveAppExtensionsOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("6. FetchAnisetteData", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: FetchAnisetteDataOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: FetchAnisetteDataOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("7. FetchProvisioningProfiles(I)", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesInstallOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: FetchProvisioningProfilesInstallOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("8. ResignApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: ResignAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: ResignAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("9. SendApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: SendAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: SendAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("10. InstallApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: InstallAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: InstallAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Refresh Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. FetchProvisioningProfiles(R)", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesRefreshOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: FetchProvisioningProfilesRefreshOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("2. RefreshApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: RefreshAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: RefreshAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("AppIDs related Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. FetchAppIDs", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: FetchAppIDsOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: FetchAppIDsOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Sources related Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. FetchSource", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: FetchSourceOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: FetchSourceOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("2. UpdateKnownSources", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: UpdateKnownSourcesOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: UpdateKnownSourcesOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Backup Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. BackupApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: BackupAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: BackupAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
CustomToggle("2. RemoveAppBackup", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: RemoveAppBackupOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: RemoveAppBackupOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Activate/Deactive Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. RemoveApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: RemoveAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: RemoveAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
CustomToggle("2. DeactivateApp", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: DeactivateAppOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: DeactivateAppOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Background refresh Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. BackgroundRefreshApps", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: BackgroundRefreshAppsOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: BackgroundRefreshAppsOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Enable JIT Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. EnableJIT", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: EnableJITOperation<DummyConformance>.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: EnableJITOperation<DummyConformance>.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Patrons Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. UpdatePatrons", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: UpdatePatronsOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: UpdatePatronsOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Cache Operations"))
|
||||||
|
{
|
||||||
|
CustomToggle("1. ClearAppCache", isOn: Binding(
|
||||||
|
get: { self.viewModel.getFromDatabase(for: ClearAppCacheOperation.self) },
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: ClearAppCacheOperation.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSection(header: Text("Misc Logging"))
|
||||||
|
{
|
||||||
|
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
|
||||||
|
)},
|
||||||
|
set: { value in
|
||||||
|
self.viewModel.updateDatabase(for: ANISETTE_VERBOSITY.self, value: value)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(TITLE)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(edges: .all)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func CustomList<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||||
|
// ScrollView {
|
||||||
|
List {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
// .listStyle(.plain)
|
||||||
|
// .listStyle(InsetGroupedListStyle()) // Or PlainListStyle for iOS 15
|
||||||
|
// .background(Color.clear)
|
||||||
|
// .background(Color(.settingsBackground))
|
||||||
|
// .onAppear(perform: {
|
||||||
|
// // cache the current background color
|
||||||
|
// UITableView.appearance().backgroundColor = UIColor.red
|
||||||
|
// })
|
||||||
|
// .onDisappear(perform: {
|
||||||
|
// // reset the background color to the cached value
|
||||||
|
// UITableView.appearance().backgroundColor = UIColor.systemBackground
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
private func CustomSection<Content: View>(header: Text, @ViewBuilder content: () -> Content) -> some View {
|
||||||
|
Section(header: header) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
// .listRowBackground(Color.clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func CustomToggle(_ title: String, isOn: Binding<Bool>) -> some View {
|
||||||
|
Toggle(title, isOn: isOn)
|
||||||
|
.padding(3)
|
||||||
|
// .foregroundColor(.white) // Ensures text color is always white
|
||||||
|
// .font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
OperationsLoggingControlView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -318,7 +318,8 @@ extension PatreonViewController
|
|||||||
case .none: footerView.button.isIndicatingActivity = true
|
case .none: footerView.button.isIndicatingActivity = true
|
||||||
case .success?: footerView.button.isHidden = true
|
case .success?: footerView.button.isHidden = true
|
||||||
case .failure?:
|
case .failure?:
|
||||||
#if DEBUG
|
// In simulator debug builds only enable debug mode flag
|
||||||
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
let debug = true
|
let debug = true
|
||||||
#else
|
#else
|
||||||
let debug = false
|
let debug = false
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||||
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="separatorColor" white="1" alpha="0.25" 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">
|
<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="1618.0000038146973" width="402" height="125"/>
|
<rect key="frame" x="0.0" y="1726.3333358764648" width="402" height="125"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<subviews>
|
<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">
|
<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">
|
||||||
@@ -91,8 +92,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Version 2.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5u7-mb-jJj">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Version 0.6.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5u7-mb-jJj">
|
||||||
<rect key="frame" x="165" y="108" width="72.333333333333314" height="17"/>
|
<rect key="frame" x="159" y="108" width="84" height="17"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -345,7 +346,6 @@
|
|||||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
<integer key="value" value="1"/>
|
<integer key="value" value="1"/>
|
||||||
</userDefinedRuntimeAttribute>
|
</userDefinedRuntimeAttribute>
|
||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="GYp-O0-pse" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="GYp-O0-pse" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
@@ -381,7 +381,6 @@
|
|||||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
<integer key="value" value="2"/>
|
<integer key="value" value="2"/>
|
||||||
</userDefinedRuntimeAttribute>
|
</userDefinedRuntimeAttribute>
|
||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
@@ -445,7 +444,6 @@
|
|||||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
<integer key="value" value="3"/>
|
<integer key="value" value="3"/>
|
||||||
</userDefinedRuntimeAttribute>
|
</userDefinedRuntimeAttribute>
|
||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</cells>
|
</cells>
|
||||||
@@ -499,14 +497,14 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="119" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="119" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -535,8 +533,8 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="114.5" height="20.5"/>
|
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="114.33333333333331" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -567,23 +565,23 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="86" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="86" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
|
||||||
<rect key="frame" x="187.5" y="15.5" width="157.5" height="20.5"/>
|
<rect key="frame" x="214.66666666666663" y="15.333333333333334" width="157.33333333333337" height="20.333333333333329"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="125.5" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="125.33333333333333" height="20.333333333333332"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
||||||
<rect key="frame" x="139.5" y="0.0" width="18" height="20.5"/>
|
<rect key="frame" x="139.33333333333334" y="0.0" width="18" height="20.333333333333332"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
@@ -611,23 +609,23 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="89" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
||||||
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
|
<rect key="frame" x="225" y="15.333333333333334" width="147" height="20.333333333333329"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="115" height="20.333333333333332"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
<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.5"/>
|
<rect key="frame" x="129" y="0.0" width="18" height="20.333333333333332"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
@@ -655,23 +653,23 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
|
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
||||||
<rect key="frame" x="206" y="15.5" width="139" height="20.5"/>
|
<rect key="frame" x="233" y="15.333333333333334" width="139" height="20.333333333333329"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="107" height="20.333333333333332"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
<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.5"/>
|
<rect key="frame" x="121" y="0.0" width="18" height="20.333333333333332"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
@@ -699,14 +697,14 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="67.333333333333329" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
|
||||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -740,13 +738,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
|
||||||
<rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/>
|
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="125.33333333333331" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
|
<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="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -773,13 +771,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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" ambiguous="YES" 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.5" width="187.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="187.66666666666666" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
|
<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="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -809,13 +807,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
|
||||||
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
|
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
|
<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="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -842,13 +840,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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" ambiguous="YES" 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.5" width="140" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="140" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
|
<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="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -875,13 +873,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
|
||||||
<rect key="frame" x="30" y="15.5" width="135.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="135.66666666666666" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
|
<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="327" y="16.5" width="18" height="18"/>
|
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -900,57 +898,21 @@
|
|||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-nMH" 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-nMH" id="AtM-bL-7pS">
|
|
||||||
<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="2ox-HD-0UT">
|
|
||||||
<rect key="frame" x="30" y="15.5" width="215.5" height="20.5"/>
|
|
||||||
<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="e30-w4-5fk">
|
|
||||||
<rect key="frame" x="296" y="10" width="51" height="31"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleDisableResponseCaching:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uuG-Gf-7GK"/>
|
|
||||||
</connections>
|
|
||||||
</switch>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="2ox-HD-0UT" firstAttribute="centerY" secondItem="AtM-bL-7pS" secondAttribute="centerY" id="07n-jt-3rz"/>
|
|
||||||
<constraint firstItem="2ox-HD-0UT" firstAttribute="leading" secondItem="AtM-bL-7pS" secondAttribute="leadingMargin" id="Koi-9G-bG8"/>
|
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="e30-w4-5fk" secondAttribute="trailing" id="Wa7-n6-lcl"/>
|
|
||||||
<constraint firstItem="e30-w4-5fk" firstAttribute="centerY" secondItem="AtM-bL-7pS" secondAttribute="centerY" id="n7R-aQ-FBX"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
|
||||||
<userDefinedRuntimeAttributes>
|
|
||||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
|
||||||
<integer key="value" value="2"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
|
||||||
</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">
|
<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="1505.0000038146973" width="402" height="51"/>
|
<rect key="frame" x="0.0" y="1454.0000038146973" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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">
|
<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.5" width="215.5" height="20.5"/>
|
<rect key="frame" x="30" y="15.333333333333334" width="169" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e32-w4-5fk">
|
<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="296" y="10" width="51" height="31"/>
|
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="toggleEnableBetaUpdates:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uxG-df-7GK"/>
|
<action selector="toggleEnableBetaUpdates:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uxG-df-7GK"/>
|
||||||
</connections>
|
</connections>
|
||||||
@@ -967,36 +929,210 @@
|
|||||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
<userDefinedRuntimeAttributes>
|
<userDefinedRuntimeAttributes>
|
||||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
<integer key="value" value="2"/>
|
<integer key="value" value="3"/>
|
||||||
</userDefinedRuntimeAttribute>
|
</userDefinedRuntimeAttribute>
|
||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-tXH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
</cells>
|
||||||
<rect key="frame" x="0.0" y="1556.0000038146973" width="402" height="51"/>
|
</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"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XW5-Zc-tXH" id="AtM-bL-4pS">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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="2px-HX-0UT">
|
<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">
|
||||||
<rect key="frame" x="30" y="15.5" width="215.5" height="20.5"/>
|
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="215.33333333333337" height="20.333333333333329"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e32-w4-3fk">
|
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AAh-cu-qw8">
|
||||||
<rect key="frame" x="296" y="10" width="51" height="31"/>
|
<rect key="frame" x="323" y="10" width="51" height="31"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="toggleResignedAppExport:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uxG-af-7GK"/>
|
<action selector="toggleDisableResponseCaching:" destination="aMk-Xp-UL8" eventType="valueChanged" id="lCm-qi-piH"/>
|
||||||
</connections>
|
</connections>
|
||||||
</switch>
|
</switch>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="2px-HX-0UT" firstAttribute="centerY" secondItem="AtM-bL-4pS" secondAttribute="centerY" id="07r-jt-8rz"/>
|
<constraint firstItem="jFh-36-AP2" firstAttribute="centerY" secondItem="ZkW-ZR-twy" secondAttribute="centerY" id="2u3-2Y-3VF"/>
|
||||||
<constraint firstItem="2px-HX-0UT" firstAttribute="leading" secondItem="AtM-bL-4pS" secondAttribute="leadingMargin" id="K1i-9G-bG8"/>
|
<constraint firstItem="jFh-36-AP2" firstAttribute="leading" secondItem="ZkW-ZR-twy" secondAttribute="leadingMargin" id="98e-6Q-2Wd"/>
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="e32-w4-3fk" secondAttribute="trailing" id="Wa7-m1-lcl"/>
|
<constraint firstAttribute="trailingMargin" secondItem="AAh-cu-qw8" secondAttribute="trailing" id="GjM-1M-598"/>
|
||||||
<constraint firstItem="e32-w4-3fk" firstAttribute="centerY" secondItem="AtM-bL-4pS" secondAttribute="centerY" id="n8R-av-FBX"/>
|
<constraint firstItem="AAh-cu-qw8" firstAttribute="centerY" secondItem="ZkW-ZR-twy" secondAttribute="centerY" id="X4E-o0-tJC"/>
|
||||||
|
</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="1"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</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"/>
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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"/>
|
||||||
|
</connections>
|
||||||
|
</switch>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="GYP-qn-wzh" firstAttribute="centerY" secondItem="JhE-O4-pRg" secondAttribute="centerY" id="6hG-aB-zmb"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="GYP-qn-wzh" secondAttribute="trailing" id="Bci-7r-MMJ"/>
|
||||||
|
<constraint firstItem="d5F-bf-6kB" firstAttribute="centerY" secondItem="JhE-O4-pRg" secondAttribute="centerY" id="g1h-ov-Teh"/>
|
||||||
|
<constraint firstItem="d5F-bf-6kB" firstAttribute="leading" secondItem="JhE-O4-pRg" secondAttribute="leadingMargin" id="rFw-nM-3Og"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
|
<integer key="value" value="2"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</tableViewCell>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="JoN-Aj-XtZ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="1647.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">
|
||||||
|
<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">
|
||||||
|
<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"/>
|
||||||
|
</connections>
|
||||||
|
</switch>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="Q5X-Mo-KpE" secondAttribute="trailing" id="1bV-B0-S5q"/>
|
||||||
|
<constraint firstItem="7bz-tI-tLY" firstAttribute="centerY" secondItem="v8Q-VQ-Q1h" secondAttribute="centerY" id="FJj-Bb-xmv"/>
|
||||||
|
<constraint firstItem="7bz-tI-tLY" firstAttribute="leading" secondItem="v8Q-VQ-Q1h" secondAttribute="leadingMargin" id="m5U-ml-KNJ"/>
|
||||||
|
<constraint firstItem="Q5X-Mo-KpE" firstAttribute="centerY" secondItem="v8Q-VQ-Q1h" secondAttribute="centerY" id="och-PX-wo9"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
|
<integer key="value" value="2"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</tableViewCell>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="QOO-bO-4M5" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="1698.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"/>
|
||||||
|
<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>
|
||||||
|
<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"/>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="zl4-ti-HTW" secondAttribute="trailing" id="2MC-2C-NDd"/>
|
||||||
|
<constraint firstItem="LW3-gm-lj5" firstAttribute="centerY" secondItem="bc9-41-6mE" secondAttribute="centerY" id="4Ft-s9-hPY"/>
|
||||||
|
<constraint firstItem="zl4-ti-HTW" firstAttribute="centerY" secondItem="bc9-41-6mE" secondAttribute="centerY" id="8Gt-oe-nd0"/>
|
||||||
|
<constraint firstItem="LW3-gm-lj5" firstAttribute="leading" secondItem="bc9-41-6mE" secondAttribute="leadingMargin" id="Pg6-I4-3d4"/>
|
||||||
|
</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="pvu-IV-Poa" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="1800.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"/>
|
||||||
|
<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">
|
||||||
|
<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"/>
|
||||||
|
</connections>
|
||||||
|
</switch>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="ZRk-8S-kBQ" firstAttribute="leading" secondItem="zck-an-8cK" secondAttribute="leadingMargin" id="Ogt-dT-Z8F"/>
|
||||||
|
<constraint firstItem="uGv-Lb-Ita" firstAttribute="centerY" secondItem="zck-an-8cK" secondAttribute="centerY" id="UJV-OX-oF6"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="uGv-Lb-Ita" secondAttribute="trailing" id="hEe-F9-T6Z"/>
|
||||||
|
<constraint firstItem="ZRk-8S-kBQ" firstAttribute="centerY" secondItem="zck-an-8cK" secondAttribute="centerY" id="kVs-il-AuO"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</tableViewCellContentView>
|
</tableViewCellContentView>
|
||||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -1005,7 +1141,6 @@
|
|||||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||||
<integer key="value" value="3"/>
|
<integer key="value" value="3"/>
|
||||||
</userDefinedRuntimeAttribute>
|
</userDefinedRuntimeAttribute>
|
||||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</cells>
|
</cells>
|
||||||
@@ -1022,15 +1157,17 @@
|
|||||||
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
|
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
|
||||||
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
|
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
|
||||||
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
|
<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="disableAppLimitSwitch" destination="1aa-og-ZXD" id="oVL-Md-yZ8"/>
|
||||||
<outlet property="disableResponseCachingSwitch" destination="e30-w4-5fk" id="Duy-Yb-eo1"/>
|
<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="githubButton" destination="oqj-4S-I9l" id="sxB-LE-gA2"/>
|
||||||
<outlet property="isBetaUpdatesEnabled" destination="e32-w4-5fk" id="Dty-Yb-eo1"/>
|
|
||||||
<outlet property="isResignedAppExportEnabled" destination="e32-w4-3fk" id="Dxy-Yb-eo1"/>
|
|
||||||
<outlet property="mastodonButton" destination="B8Q-e7-beR" id="Kbe-Og-rsg"/>
|
<outlet property="mastodonButton" destination="B8Q-e7-beR" id="Kbe-Og-rsg"/>
|
||||||
|
<outlet property="minimuxerConsoleLoggingSwitch" destination="uGv-Lb-Ita" id="aTL-Md-tZ8"/>
|
||||||
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
|
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
|
||||||
<outlet property="threadsButton" destination="AWk-yE-9LI" id="SOc-ei-4gK"/>
|
<outlet property="threadsButton" destination="AWk-yE-9LI" id="SOc-ei-4gK"/>
|
||||||
<outlet property="twitterButton" destination="uYZ-Vu-RzK" id="anA-jh-w4z"/>
|
<outlet property="twitterButton" destination="uYZ-Vu-RzK" id="anA-jh-w4z"/>
|
||||||
|
<outlet property="verboseOperationsLoggingSwitch" destination="Q5X-Mo-KpE" id="aVL-Md-tZ8"/>
|
||||||
<outlet property="versionLabel" destination="5u7-mb-jJj" id="zvU-TQ-lO6"/>
|
<outlet property="versionLabel" destination="5u7-mb-jJj" id="zvU-TQ-lO6"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
@@ -1444,19 +1581,22 @@ Settings by i cons from the Noun Project</string>
|
|||||||
</tableView>
|
</tableView>
|
||||||
<navigationItem key="navigationItem" title="Error Log" largeTitleDisplayMode="never" id="a1p-3W-bSi">
|
<navigationItem key="navigationItem" title="Error Log" largeTitleDisplayMode="never" id="a1p-3W-bSi">
|
||||||
<rightBarButtonItems>
|
<rightBarButtonItems>
|
||||||
<barButtonItem systemItem="trash" id="BnQ-Eh-1gC">
|
<barButtonItem title="trash" id="BnQ-Eh-1gC">
|
||||||
|
<imageReference key="image" image="trash" catalog="system" symbolScale="large"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
|
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
|
||||||
</connections>
|
</connections>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
<barButtonItem image="ladybug" catalog="system" id="1cD-4y-vTJ" userLabel="Share">
|
<barButtonItem title="minimuxer" id="BNj-HE-KHr" userLabel="Minimuxer Log Button">
|
||||||
|
<imageReference key="image" image="ladybug" catalog="system" symbolScale="large"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="showMinimuxerLogs:" destination="g8a-Rf-zWa" id="V0f-0y-C6C"/>
|
<action selector="showMinimuxerLogs:" destination="g8a-Rf-zWa" id="Kbw-Q5-9WO"/>
|
||||||
</connections>
|
</connections>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
<barButtonItem systemItem="action" id="BNj-HE-KHr">
|
<barButtonItem title="console" id="1cD-4y-vTJ" userLabel="Console Log Button">
|
||||||
|
<imageReference key="image" image="terminal" catalog="system" symbolScale="large"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="exportDetailedLog:" destination="g8a-Rf-zWa" id="Kbw-Q5-9WO"/>
|
<action selector="showConsoleLogs:" destination="g8a-Rf-zWa" id="V0f-0y-C6C"/>
|
||||||
</connections>
|
</connections>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
</rightBarButtonItems>
|
</rightBarButtonItems>
|
||||||
@@ -1539,6 +1679,8 @@ Settings by i cons from the Noun Project</string>
|
|||||||
<image name="Threads" width="130" height="130"/>
|
<image name="Threads" width="130" height="130"/>
|
||||||
<image name="Twitter" width="130" height="130"/>
|
<image name="Twitter" width="130" height="130"/>
|
||||||
<image name="ladybug" catalog="system" width="128" height="122"/>
|
<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"/>
|
||||||
<namedColor name="SettingsBackground">
|
<namedColor name="SettingsBackground">
|
||||||
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ extension SettingsViewController
|
|||||||
case instructions
|
case instructions
|
||||||
case techyThings
|
case techyThings
|
||||||
case credits
|
case credits
|
||||||
case debug
|
case advancedSettings
|
||||||
|
// diagnostics section, will be enabled on release builds only on swipe down with 3 fingers 3 times
|
||||||
|
case diagnostics
|
||||||
// case macDirtyCow
|
// case macDirtyCow
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,17 +37,17 @@ extension SettingsViewController
|
|||||||
{
|
{
|
||||||
case backgroundRefresh
|
case backgroundRefresh
|
||||||
case noIdleTimeout
|
case noIdleTimeout
|
||||||
@available(iOS 14, *)
|
|
||||||
case addToSiri
|
case addToSiri
|
||||||
case disableAppLimit
|
case disableAppLimit
|
||||||
|
|
||||||
static var allCases: [AppRefreshRow] {
|
static var allCases: [AppRefreshRow] {
|
||||||
var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout]
|
var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout, .addToSiri]
|
||||||
guard #available(iOS 14, *) else { return c }
|
|
||||||
c.append(.addToSiri)
|
|
||||||
|
|
||||||
// conditional entries go at the last to preserve ordering
|
// conditional entries go at the last to preserve ordering
|
||||||
if !ProcessInfo().sparseRestorePatched { c.append(.disableAppLimit) }
|
if UserDefaults.standard.isCowExploitSupported || !ProcessInfo().sparseRestorePatched
|
||||||
|
{
|
||||||
|
c.append(.disableAppLimit)
|
||||||
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,17 +66,25 @@ extension SettingsViewController
|
|||||||
case clearCache
|
case clearCache
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate enum DebugRow: Int, CaseIterable
|
fileprivate enum AdvancedSettingsRow: Int, CaseIterable
|
||||||
{
|
{
|
||||||
case sendFeedback
|
case sendFeedback
|
||||||
case refreshAttempts
|
case refreshAttempts
|
||||||
case refreshSideJITServer
|
case refreshSideJITServer
|
||||||
case resetPairingFile
|
case resetPairingFile
|
||||||
case anisetteServers
|
case anisetteServers
|
||||||
case responseCaching
|
|
||||||
case betaUpdates
|
case betaUpdates
|
||||||
case resignedAppExport
|
// case hiddenSettings
|
||||||
// case advancedSettings
|
}
|
||||||
|
|
||||||
|
fileprivate enum DiagnosticsRow: Int, CaseIterable
|
||||||
|
{
|
||||||
|
case responseCaching
|
||||||
|
case exportResignedApp
|
||||||
|
case verboseOperationsLogging
|
||||||
|
case exportSqliteDB
|
||||||
|
case operationsLoggingControl
|
||||||
|
case minimuxerConsoleLogging
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +104,12 @@ final class SettingsViewController: UITableViewController
|
|||||||
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
||||||
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
|
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
|
||||||
@IBOutlet private var disableAppLimitSwitch: UISwitch!
|
@IBOutlet private var disableAppLimitSwitch: UISwitch!
|
||||||
@IBOutlet private var isBetaUpdatesEnabled: UISwitch!
|
@IBOutlet private var betaUpdatesSwitch: UISwitch!
|
||||||
@IBOutlet private var isResignedAppExportEnabled: UISwitch!
|
@IBOutlet private var exportResignedAppsSwitch: UISwitch!
|
||||||
|
@IBOutlet private var verboseOperationsLoggingSwitch: UISwitch!
|
||||||
|
@IBOutlet private var minimuxerConsoleLoggingSwitch: UISwitch!
|
||||||
|
|
||||||
@IBOutlet private var refreshSideJITServer: UILabel!
|
// @IBOutlet private var refreshSideJITServer: UILabel!
|
||||||
@IBOutlet private var disableResponseCachingSwitch: UISwitch!
|
@IBOutlet private var disableResponseCachingSwitch: UISwitch!
|
||||||
|
|
||||||
@IBOutlet private var mastodonButton: UIButton!
|
@IBOutlet private var mastodonButton: UIButton!
|
||||||
@@ -111,6 +123,8 @@ final class SettingsViewController: UITableViewController
|
|||||||
return .lightContent
|
return .lightContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var exportDBInProgress = false
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder)
|
required init?(coder aDecoder: NSCoder)
|
||||||
{
|
{
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
@@ -127,53 +141,15 @@ final class SettingsViewController: UITableViewController
|
|||||||
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
|
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
|
||||||
|
|
||||||
self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView")
|
self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView")
|
||||||
|
|
||||||
let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
|
let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
|
||||||
debugModeGestureRecognizer.delegate = self
|
debugModeGestureRecognizer.delegate = self
|
||||||
debugModeGestureRecognizer.direction = .up
|
debugModeGestureRecognizer.direction = .up
|
||||||
debugModeGestureRecognizer.numberOfTouchesRequired = 3
|
debugModeGestureRecognizer.numberOfTouchesRequired = 3
|
||||||
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
|
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
|
||||||
|
|
||||||
var versionString: String = ""
|
// set the version label to show in settings screen
|
||||||
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
|
self.versionLabel.text = getVersionLabel()
|
||||||
{
|
|
||||||
#if BETA
|
|
||||||
// Only show build version for BETA builds.
|
|
||||||
let localizedVersion = if let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String {
|
|
||||||
"\(installedApp.version) (\(bundleVersion))"
|
|
||||||
} else {
|
|
||||||
installedApp.localizedVersion
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
let localizedVersion = installedApp.version
|
|
||||||
#endif
|
|
||||||
|
|
||||||
self.versionLabel.text = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
|
|
||||||
}
|
|
||||||
else if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
|
||||||
{
|
|
||||||
versionString += "SideStore \(version)"
|
|
||||||
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
|
|
||||||
versionString += " - Xcode \(xcode) - "
|
|
||||||
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
|
|
||||||
versionString += "\(build)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
|
|
||||||
let pair_test = pairing == "<insert pairing file here>"
|
|
||||||
if !pair_test {
|
|
||||||
versionString += " - \(!pair_test)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.versionLabel.text = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.versionLabel.text = nil
|
|
||||||
versionString += "SideStore\t"
|
|
||||||
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
|
|
||||||
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
|
|
||||||
}
|
|
||||||
|
|
||||||
self.versionLabel.numberOfLines = 0
|
self.versionLabel.numberOfLines = 0
|
||||||
self.versionLabel.lineBreakMode = .byWordWrapping
|
self.versionLabel.lineBreakMode = .byWordWrapping
|
||||||
@@ -233,6 +209,74 @@ final class SettingsViewController: UITableViewController
|
|||||||
|
|
||||||
private extension SettingsViewController
|
private extension SettingsViewController
|
||||||
{
|
{
|
||||||
|
|
||||||
|
private func getVersionLabel() -> String {
|
||||||
|
let MARKETING_VERSION_KEY = "CFBundleShortVersionString"
|
||||||
|
let BUILD_REVISION = "CFBundleRevision" // commit ID for now (but could be any, set by build env vars
|
||||||
|
let CURRENT_PROJECT_VERSION = kCFBundleVersionKey as String
|
||||||
|
|
||||||
|
func getXcodeVersion() -> String {
|
||||||
|
let XCODE_VERSION = "DTXcode"
|
||||||
|
let XCODE_REVISION = "DTXcodeBuild"
|
||||||
|
|
||||||
|
let xcode = Bundle.main.object(forInfoDictionaryKey: XCODE_VERSION) as? String
|
||||||
|
let build = Bundle.main.object(forInfoDictionaryKey: XCODE_REVISION) as? String
|
||||||
|
|
||||||
|
var xcodeVersion = xcode.map { version in
|
||||||
|
// " - Xcode \(version) - " + (build.map { revision in "\(revision)" } ?? "") // Ex: "0.6.0 - Xcode 16.2 - 21ac1ef"
|
||||||
|
"Xcode \(version) - " + (build.map { revision in "\(revision)" } ?? "") // Ex: "0.6.0 - Xcode 16.2 - 21ac1ef"
|
||||||
|
} ?? ""
|
||||||
|
|
||||||
|
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
|
||||||
|
pairing != "<insert pairing file here>"{
|
||||||
|
xcodeVersion += " - true"
|
||||||
|
}
|
||||||
|
return xcodeVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionLabel: String = ""
|
||||||
|
|
||||||
|
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
|
||||||
|
{
|
||||||
|
#if BETA
|
||||||
|
// Only show build version (and build revision) for BETA builds.
|
||||||
|
let bundleVersion: String? = Bundle.main.object(forInfoDictionaryKey: CURRENT_PROJECT_VERSION) as? String
|
||||||
|
let buildRevision: String? = Bundle.main.object(forInfoDictionaryKey: BUILD_REVISION) as? String
|
||||||
|
|
||||||
|
var localizedVersion = bundleVersion.map { version in
|
||||||
|
"\(installedApp.version) (\(version))" + (buildRevision.map { revision in " - \(revision)" } ?? "") // Ex: "0.6.0 (0600) - 1acdef3"
|
||||||
|
} ?? installedApp.localizedVersion
|
||||||
|
|
||||||
|
#else
|
||||||
|
var localizedVersion = installedApp.version
|
||||||
|
#endif
|
||||||
|
|
||||||
|
versionLabel = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
|
||||||
|
}
|
||||||
|
else if let version = Bundle.main.object(forInfoDictionaryKey: MARKETING_VERSION_KEY) as? String
|
||||||
|
{
|
||||||
|
var version = "SideStore \(version)"
|
||||||
|
|
||||||
|
version += getXcodeVersion()
|
||||||
|
|
||||||
|
versionLabel = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var version = "SideStore\t"
|
||||||
|
version += "\n\(Bundle.Info.appbundleIdentifier)"
|
||||||
|
versionLabel = NSLocalizedString(version, comment: "SideStore Version")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add xcode build version if in debug mode
|
||||||
|
#if DEBUG
|
||||||
|
versionLabel += "\n\(getXcodeVersion())"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return versionLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func update()
|
func update()
|
||||||
{
|
{
|
||||||
if let team = DatabaseManager.shared.activeTeam()
|
if let team = DatabaseManager.shared.activeTeam()
|
||||||
@@ -248,12 +292,19 @@ private extension SettingsViewController
|
|||||||
self.activeTeam = nil
|
self.activeTeam = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppRefreshRow
|
||||||
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
||||||
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
|
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
|
||||||
|
|
||||||
|
// AdvancedSettingsRow
|
||||||
|
self.betaUpdatesSwitch.isOn = UserDefaults.standard.isBetaUpdatesEnabled
|
||||||
|
|
||||||
|
// DiagnosticsRow
|
||||||
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
|
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
|
||||||
self.isBetaUpdatesEnabled.isOn = UserDefaults.standard.isBetaUpdatesEnabled
|
self.exportResignedAppsSwitch.isOn = UserDefaults.standard.isExportResignedAppEnabled
|
||||||
self.isResignedAppExportEnabled.isOn = UserDefaults.standard.isResignedAppExportEnabled
|
self.verboseOperationsLoggingSwitch.isOn = UserDefaults.standard.isVerboseOperationsLoggingEnabled
|
||||||
|
self.minimuxerConsoleLoggingSwitch.isOn = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
||||||
|
|
||||||
if self.isViewLoaded
|
if self.isViewLoaded
|
||||||
{
|
{
|
||||||
@@ -335,6 +386,12 @@ private extension SettingsViewController
|
|||||||
case .credits:
|
case .credits:
|
||||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
|
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
|
||||||
|
|
||||||
|
case .advancedSettings:
|
||||||
|
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ADVANCED SETTINGS", comment: "")
|
||||||
|
|
||||||
|
case .diagnostics:
|
||||||
|
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DIAGNOSTICS", comment: "")
|
||||||
|
|
||||||
// case .macDirtyCow:
|
// case .macDirtyCow:
|
||||||
// if isHeader
|
// if isHeader
|
||||||
// {
|
// {
|
||||||
@@ -345,8 +402,6 @@ private extension SettingsViewController
|
|||||||
// settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "")
|
// settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
case .debug:
|
|
||||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,16 +480,32 @@ private extension SettingsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
|
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
|
||||||
UserDefaults.standard.isAppLimitDisabled = sender.isOn
|
if UserDefaults.standard.isCowExploitSupported || !ProcessInfo().sparseRestorePatched {
|
||||||
if UserDefaults.standard.activeAppsLimit != nil
|
// accept state change only when valid
|
||||||
{
|
UserDefaults.standard.isAppLimitDisabled = sender.isOn
|
||||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
|
||||||
|
// TODO: Here we force reload the activeAppsLimit after detecting change in isAppLimitDisabled
|
||||||
|
// Why do we need to do this, once identified if this is intentional and working as expected, remove this todo
|
||||||
|
if UserDefaults.standard.activeAppsLimit != nil
|
||||||
|
{
|
||||||
|
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleResignedAppExport(_ sender: UISwitch) {
|
@IBAction func toggleResignedAppExport(_ sender: UISwitch) {
|
||||||
// update it in database
|
// update it in database
|
||||||
UserDefaults.standard.isResignedAppExportEnabled = sender.isOn
|
UserDefaults.standard.isExportResignedAppEnabled = sender.isOn
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func toggleVerboseOperationsLogging(_ sender: UISwitch) {
|
||||||
|
// update it in database
|
||||||
|
UserDefaults.standard.isVerboseOperationsLoggingEnabled = sender.isOn
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func toggleMinimuxerConsoleLogging(_ sender: UISwitch) {
|
||||||
|
// update it in database
|
||||||
|
UserDefaults.standard.isMinimuxerConsoleLoggingEnabled = sender.isOn
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleEnableBetaUpdates(_ sender: UISwitch) {
|
@IBAction func toggleEnableBetaUpdates(_ sender: UISwitch) {
|
||||||
@@ -457,7 +528,7 @@ private extension SettingsViewController
|
|||||||
UserDefaults.standard.responseCachingDisabled = sender.isOn
|
UserDefaults.standard.responseCachingDisabled = sender.isOn
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func addRefreshAppsShortcut()
|
func addRefreshAppsShortcut()
|
||||||
{
|
{
|
||||||
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
|
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
|
||||||
|
|
||||||
@@ -679,8 +750,7 @@ extension SettingsViewController
|
|||||||
case _ where isSectionHidden(section): return nil
|
case _ where isSectionHidden(section): return nil
|
||||||
case .signIn where self.activeTeam != nil: return nil
|
case .signIn where self.activeTeam != nil: return nil
|
||||||
case .account where self.activeTeam == nil: return nil
|
case .account where self.activeTeam == nil: return nil
|
||||||
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
|
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics /* ,.macDirtyCow */:
|
||||||
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .debug:
|
|
||||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||||
self.prepare(headerView, for: section, isHeader: true)
|
self.prepare(headerView, for: section, isHeader: true)
|
||||||
return headerView
|
return headerView
|
||||||
@@ -702,7 +772,7 @@ extension SettingsViewController
|
|||||||
self.prepare(footerView, for: section, isHeader: false)
|
self.prepare(footerView, for: section, isHeader: false)
|
||||||
return footerView
|
return footerView
|
||||||
|
|
||||||
case .account, .credits, .debug, .instructions: return nil
|
case .account, .credits, .advancedSettings, .instructions, .diagnostics: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,8 +784,8 @@ extension SettingsViewController
|
|||||||
case _ where isSectionHidden(section): return 1.0
|
case _ where isSectionHidden(section): return 1.0
|
||||||
case .signIn where self.activeTeam != nil: return 1.0
|
case .signIn where self.activeTeam != nil: return 1.0
|
||||||
case .account where self.activeTeam == nil: return 1.0
|
case .account where self.activeTeam == nil: return 1.0
|
||||||
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
|
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .advanced:
|
||||||
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .debug:
|
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics:
|
||||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
|
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
|
||||||
return height
|
return height
|
||||||
|
|
||||||
@@ -732,11 +802,11 @@ extension SettingsViewController
|
|||||||
case .signIn where self.activeTeam != nil: return 1.0
|
case .signIn where self.activeTeam != nil: return 1.0
|
||||||
case .account where self.activeTeam == nil: return 1.0
|
case .account where self.activeTeam == nil: return 1.0
|
||||||
// case .signIn, .patreon, .display, .appRefresh, .techyThings, .macDirtyCow:
|
// case .signIn, .patreon, .display, .appRefresh, .techyThings, .macDirtyCow:
|
||||||
case .signIn, .patreon, .display, .appRefresh, .techyThings:
|
case .signIn, .patreon, .display, .appRefresh, .techyThings, .diagnostics:
|
||||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
||||||
return height
|
return height
|
||||||
|
|
||||||
case .account, .credits, .debug, .instructions: return 0.0
|
case .account, .credits, .advancedSettings, .instructions: return 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,7 +827,7 @@ extension SettingsViewController
|
|||||||
case .noIdleTimeout: break
|
case .noIdleTimeout: break
|
||||||
case .disableAppLimit: break
|
case .disableAppLimit: break
|
||||||
case .addToSiri:
|
case .addToSiri:
|
||||||
guard #available(iOS 14, *) else { return }
|
// guard #available(iOS 14, *) else { return } // our min deployment is iOS 15 now :) so commented out
|
||||||
self.addRefreshAppsShortcut()
|
self.addRefreshAppsShortcut()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,8 +854,8 @@ extension SettingsViewController
|
|||||||
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .debug:
|
case .advancedSettings:
|
||||||
let row = DebugRow.allCases[indexPath.row]
|
let row = AdvancedSettingsRow.allCases[indexPath.row]
|
||||||
switch row
|
switch row
|
||||||
{
|
{
|
||||||
case .sendFeedback:
|
case .sendFeedback:
|
||||||
@@ -823,11 +893,11 @@ extension SettingsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.present(mailViewController, animated: true, completion: nil)
|
self.present(mailViewController, animated: true, completion: nil)
|
||||||
} else {
|
} else {
|
||||||
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
|
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Cancel action
|
// Cancel action
|
||||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||||
@@ -995,7 +1065,7 @@ extension SettingsViewController
|
|||||||
|
|
||||||
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil)
|
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil)
|
||||||
|
|
||||||
// case .advancedSettings:
|
// case .hiddenSettings:
|
||||||
// // Create the URL that deep links to your app's custom settings.
|
// // Create the URL that deep links to your app's custom settings.
|
||||||
// if let url = URL(string: UIApplication.openSettingsURLString) {
|
// if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
// // Ask the system to open that URL.
|
// // Ask the system to open that URL.
|
||||||
@@ -1003,9 +1073,49 @@ extension SettingsViewController
|
|||||||
// } else {
|
// } else {
|
||||||
// ELOG("UIApplication.openSettingsURLString invalid")
|
// ELOG("UIApplication.openSettingsURLString invalid")
|
||||||
// }
|
// }
|
||||||
case .refreshAttempts, .responseCaching, .betaUpdates, .resignedAppExport : break
|
case .refreshAttempts, .betaUpdates : break
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .diagnostics:
|
||||||
|
let row = DiagnosticsRow.allCases[indexPath.row]
|
||||||
|
switch row {
|
||||||
|
|
||||||
|
case .exportSqliteDB:
|
||||||
|
// do not accept simulatenous export requests
|
||||||
|
if !exportDBInProgress {
|
||||||
|
exportDBInProgress = true
|
||||||
|
Task{
|
||||||
|
var toastView: ToastView?
|
||||||
|
do{
|
||||||
|
let exportedURL = try await CoreDataHelper.exportCoreDataStore()
|
||||||
|
print("exportSqliteDB: ExportedURL: \(exportedURL)")
|
||||||
|
toastView = ToastView(text: "Export Successful", detailText: nil)
|
||||||
|
}catch{
|
||||||
|
print("exportSqliteDB: \(error)")
|
||||||
|
toastView = ToastView(error: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// show toast to user about the result
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
toastView?.show(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update that work has finished
|
||||||
|
exportDBInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .operationsLoggingControl:
|
||||||
|
// Instantiate SwiftUI View inside UIHostingController
|
||||||
|
let operationsLoggingControlView = OperationsLoggingControlView()
|
||||||
|
let operationsLoggingController = UIHostingController(rootView: operationsLoggingControlView)
|
||||||
|
let segue = UIStoryboardSegue(identifier: "operationsLoggingControl", source: self, destination: operationsLoggingController)
|
||||||
|
self.present(segue.destination, animated: true, completion: nil)
|
||||||
|
|
||||||
|
case .responseCaching, .exportResignedApp, .verboseOperationsLogging, .minimuxerConsoleLogging : break
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// case .account, .patreon, .display, .instructions, .macDirtyCow: break
|
// case .account, .patreon, .display, .instructions, .macDirtyCow: break
|
||||||
case .account, .patreon, .display, .instructions: break
|
case .account, .patreon, .display, .instructions: break
|
||||||
|
|||||||
@@ -71,20 +71,20 @@ fileprivate struct BuildVersion: Comparable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ProcessInfo {
|
extension ProcessInfo {
|
||||||
var shortVersion: String {
|
public var shortVersion: String {
|
||||||
operatingSystemVersionString
|
operatingSystemVersionString
|
||||||
.replacingOccurrences(of: "Version ", with: "")
|
.replacingOccurrences(of: "Version ", with: "")
|
||||||
.replacingOccurrences(of: "Build ", with: "")
|
.replacingOccurrences(of: "Build ", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
var operatingSystemBuild: String {
|
public var operatingSystemBuild: String {
|
||||||
if let start = shortVersion.range(of: "(")?.upperBound,
|
if let start = shortVersion.range(of: "(")?.upperBound,
|
||||||
let end = shortVersion.range(of: ")")?.lowerBound {
|
let end = shortVersion.range(of: ")")?.lowerBound {
|
||||||
shortVersion[start..<end].replacingOccurrences(of: "Build ", with: "")
|
shortVersion[start..<end].replacingOccurrences(of: "Build ", with: "")
|
||||||
} else { "???" }
|
} else { "???" }
|
||||||
}
|
}
|
||||||
|
|
||||||
var sparseRestorePatched: Bool {
|
public var sparseRestorePatched: Bool {
|
||||||
if operatingSystemVersion < OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0) { false }
|
if operatingSystemVersion < OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0) { false }
|
||||||
else if operatingSystemVersion > OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
|
else if operatingSystemVersion > OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
|
||||||
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
|
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
|
||||||
@@ -33,7 +33,9 @@ public extension UserDefaults
|
|||||||
@NSManaged var isIdleTimeoutDisableEnabled: Bool
|
@NSManaged var isIdleTimeoutDisableEnabled: Bool
|
||||||
@NSManaged var isAppLimitDisabled: Bool
|
@NSManaged var isAppLimitDisabled: Bool
|
||||||
@NSManaged var isBetaUpdatesEnabled: Bool
|
@NSManaged var isBetaUpdatesEnabled: Bool
|
||||||
@NSManaged var isResignedAppExportEnabled: Bool
|
@NSManaged var isExportResignedAppEnabled: Bool
|
||||||
|
@NSManaged var isVerboseOperationsLoggingEnabled: Bool
|
||||||
|
@NSManaged var isMinimuxerConsoleLoggingEnabled: Bool
|
||||||
@NSManaged var isPairingReset: Bool
|
@NSManaged var isPairingReset: Bool
|
||||||
@NSManaged var isDebugModeEnabled: Bool
|
@NSManaged var isDebugModeEnabled: Bool
|
||||||
@NSManaged var presentedLaunchReminderNotification: Bool
|
@NSManaged var presentedLaunchReminderNotification: Bool
|
||||||
@@ -106,11 +108,12 @@ public extension UserDefaults
|
|||||||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
|
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
|
||||||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
|
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
|
||||||
|
|
||||||
#if DEBUG
|
// TODO: @mahee96: why should the permissions checking be any different, for now, it shouldn't so commented debug mode code
|
||||||
let permissionCheckingDisabled = true
|
// #if DEBUG
|
||||||
#else
|
// let permissionCheckingDisabled = true
|
||||||
|
// #else
|
||||||
let permissionCheckingDisabled = false
|
let permissionCheckingDisabled = false
|
||||||
#endif
|
// #endif
|
||||||
|
|
||||||
// Pre-iOS 15 doesn't support custom sorting, so default to sorting by name.
|
// Pre-iOS 15 doesn't support custom sorting, so default to sorting by name.
|
||||||
// Otherwise, default to `default` sorting (a.k.a. "source order").
|
// Otherwise, default to `default` sorting (a.k.a. "source order").
|
||||||
@@ -119,7 +122,10 @@ public extension UserDefaults
|
|||||||
let defaults = [
|
let defaults = [
|
||||||
#keyPath(UserDefaults.isAppLimitDisabled): false,
|
#keyPath(UserDefaults.isAppLimitDisabled): false,
|
||||||
#keyPath(UserDefaults.isBetaUpdatesEnabled): false,
|
#keyPath(UserDefaults.isBetaUpdatesEnabled): false,
|
||||||
#keyPath(UserDefaults.isResignedAppExportEnabled): 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.isBackgroundRefreshEnabled): true,
|
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||||
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
|
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
|
||||||
#keyPath(UserDefaults.isPairingReset): true,
|
#keyPath(UserDefaults.isPairingReset): true,
|
||||||
@@ -137,7 +143,8 @@ public extension UserDefaults
|
|||||||
UserDefaults.standard.register(defaults: defaults)
|
UserDefaults.standard.register(defaults: defaults)
|
||||||
UserDefaults.shared.register(defaults: defaults)
|
UserDefaults.shared.register(defaults: defaults)
|
||||||
|
|
||||||
if !isMacDirtyCowSupported
|
// MDC is unsupported and spareRestore is patched
|
||||||
|
if !isMacDirtyCowSupported && ProcessInfo().sparseRestorePatched
|
||||||
{
|
{
|
||||||
// Disable isAppLimitDisabled if running iOS version that doesn't support MacDirtyCow.
|
// Disable isAppLimitDisabled if running iOS version that doesn't support MacDirtyCow.
|
||||||
UserDefaults.standard.isAppLimitDisabled = false
|
UserDefaults.standard.isAppLimitDisabled = false
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ public extension DatabaseManager
|
|||||||
|
|
||||||
guard !self.isStarted else { return finish(nil) }
|
guard !self.isStarted else { return finish(nil) }
|
||||||
|
|
||||||
#if DEBUG
|
// In simulator, when previews are generated, it initializes the db, in doing so this removal may be required
|
||||||
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
// Wrap in #if DEBUG to *ensure* we never accidentally delete production databases.
|
// Wrap in #if DEBUG to *ensure* we never accidentally delete production databases.
|
||||||
if ProcessInfo.processInfo.isPreview
|
if ProcessInfo.processInfo.isPreview
|
||||||
{
|
{
|
||||||
@@ -354,7 +355,8 @@ private extension DatabaseManager
|
|||||||
|
|
||||||
let fileURL = installedApp.fileURL
|
let fileURL = installedApp.fileURL
|
||||||
|
|
||||||
#if DEBUG
|
// @mahee96: it shouldn't matter if it is debug/release, the file is expected to be in its place (except for simulator probably coz it doesn't suppor app installs anyway)
|
||||||
|
#if DEBUG && targetEnvironment(simulator)
|
||||||
let replaceCachedApp = true
|
let replaceCachedApp = true
|
||||||
#else
|
#else
|
||||||
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version || installedApp.buildVersion != localApp.buildVersion
|
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version || installedApp.buildVersion != localApp.buildVersion
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,4 +53,29 @@ extension View
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func pageUpButton(_ widgetID: Int?, _ widgetKind: String) -> some View {
|
||||||
|
if #available(iOSApplicationExtension 17, *) {
|
||||||
|
Button(intent: PaginationIntent(widgetID, .up, widgetKind)){
|
||||||
|
self
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func pageDownButton(_ widgetID: Int?, _ widgetKind: String) -> some View {
|
||||||
|
if #available(iOSApplicationExtension 17, *) {
|
||||||
|
Button(intent: PaginationIntent(widgetID, .down, widgetKind)){
|
||||||
|
self
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
71
AltWidget/Intents/PaginationIntent.swift
Normal file
71
AltWidget/Intents/PaginationIntent.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// PaginationIntent.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 08/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppIntents
|
||||||
|
import Intents
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
public enum Direction: String, Sendable{
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NavigationEvent {
|
||||||
|
let direction: Direction?
|
||||||
|
var consumed: Bool = false
|
||||||
|
var dataHolder: PaginationDataHolder?
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17, *)
|
||||||
|
class PaginationIntent: AppIntent, @unchecked Sendable {
|
||||||
|
|
||||||
|
static var title: LocalizedStringResource = "Page Navigation Intent"
|
||||||
|
static var isDiscoverable: Bool = false
|
||||||
|
|
||||||
|
@Parameter(title: "widgetID")
|
||||||
|
var widgetID: Int
|
||||||
|
|
||||||
|
@Parameter(title: "Direction")
|
||||||
|
var direction: String
|
||||||
|
|
||||||
|
@Parameter(title: "widgetKind")
|
||||||
|
var widgetKind: String
|
||||||
|
|
||||||
|
required init(){}
|
||||||
|
|
||||||
|
// NOTE: widgetID here means the configurable value using edit widget button
|
||||||
|
// but widgetKind is the kind set in when instantiating the widget configuration
|
||||||
|
init(_ widgetID: Int?, _ direction: Direction, _ widgetKind: String){
|
||||||
|
// if id was not passed in, then we assume the widget isn't customized yet
|
||||||
|
// hence we use the common ID, if this is not present in registry of PageInfoManager
|
||||||
|
// then it will return nil, triggering to show first page in the provider
|
||||||
|
self.widgetID = widgetID ?? WidgetUpdateIntent.COMMON_WIDGET_ID
|
||||||
|
self.direction = direction.rawValue
|
||||||
|
self.widgetKind = widgetKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult {
|
||||||
|
// Post the navigation event into the shared db (Dictionary) and ask to reload
|
||||||
|
DispatchQueue(label: String(widgetID)).sync {
|
||||||
|
self.postThisNavigationEvent()
|
||||||
|
WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
|
||||||
|
}
|
||||||
|
return .result()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postThisNavigationEvent(){
|
||||||
|
// re-use an existing event if available and update only required parts
|
||||||
|
let navEvent = PageInfoManager.shared.getPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID)
|
||||||
|
let navigationEvent = NavigationEvent(
|
||||||
|
direction: Direction(rawValue: direction),
|
||||||
|
consumed: false, // event is never consumed at origin :D
|
||||||
|
dataHolder: navEvent?.dataHolder ?? nil
|
||||||
|
)
|
||||||
|
PageInfoManager.shared.setPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID, value: navigationEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
AltWidget/Intents/WidgetUpdateIntent.swift
Normal file
20
AltWidget/Intents/WidgetUpdateIntent.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// WidgetUpdateIntent.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 10/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
@available(iOS 17, *)
|
||||||
|
final class WidgetUpdateIntent: WidgetConfigurationIntent, @unchecked Sendable {
|
||||||
|
public static let COMMON_WIDGET_ID = 1
|
||||||
|
|
||||||
|
static var title: LocalizedStringResource { "Widget ID update Intent" }
|
||||||
|
static var isDiscoverable: Bool { false }
|
||||||
|
|
||||||
|
@Parameter(title: "ID", description: "Provide a numeric ID to identify the widget", default: 1)
|
||||||
|
var ID: Int?
|
||||||
|
}
|
||||||
44
AltWidget/Manager/PageInfoManager.swift
Normal file
44
AltWidget/Manager/PageInfoManager.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// PageInfoManager.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 11/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: See if we can persist these values instead of keeping in memory to prevent memory leaks
|
||||||
|
// Possible ways: Userdefaults.standard - set/get ?
|
||||||
|
class PageInfoManager {
|
||||||
|
static var shared = PageInfoManager()
|
||||||
|
private var pageInfoMap: [String: NavigationEvent] = [:]
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
private func getKey(forWidgetKind kind: String, forWidgetID id: Int) -> String{
|
||||||
|
return "\(kind)@\(id)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPageInfo(forWidgetKind kind: String, forWidgetID id: Int, value: NavigationEvent?) {
|
||||||
|
let key = getKey(forWidgetKind: kind, forWidgetID: id)
|
||||||
|
// UserDefaults.standard.set(value, forKey: key)
|
||||||
|
pageInfoMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPageInfo(forWidgetKind kind: String, forWidgetID id: Int) -> NavigationEvent? {
|
||||||
|
let key = getKey(forWidgetKind: kind, forWidgetID: id)
|
||||||
|
// return UserDefaults.standard.value(forKey: key)
|
||||||
|
return pageInfoMap[key]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func popPageInfo(forWidgetKind kind: String, forWidgetID id: Int) -> NavigationEvent? {
|
||||||
|
let key = getKey(forWidgetKind: kind, forWidgetID: id)
|
||||||
|
return pageInfoMap.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearAll() {
|
||||||
|
pageInfoMap.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// ActiveAppsTimelineProvider+Simulator.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 10/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/// Simulator data generator
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
@available(iOS 17, *)
|
||||||
|
extension ActiveAppsTimelineProvider {
|
||||||
|
|
||||||
|
func getSimulatedData(apps: [AppSnapshot]) -> [AppSnapshot]{
|
||||||
|
var apps = apps
|
||||||
|
var newSets: [AppSnapshot] = []
|
||||||
|
// this dummy data is for simulator (uncomment when testing ActiveAppsWidget pagination)
|
||||||
|
if (apps.count > 0){
|
||||||
|
let app = apps[0]
|
||||||
|
for i in 1...10 {
|
||||||
|
let name = "\(app.name) - \(i)"
|
||||||
|
let x = AppSnapshot(name: name,
|
||||||
|
bundleIdentifier: app.bundleIdentifier,
|
||||||
|
expirationDate: app.expirationDate,
|
||||||
|
refreshedDate: app.refreshedDate
|
||||||
|
)
|
||||||
|
newSets.append(x)
|
||||||
|
}
|
||||||
|
apps = newSets
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
115
AltWidget/Providers/ActiveAppsTimelineProvider.swift
Normal file
115
AltWidget/Providers/ActiveAppsTimelineProvider.swift
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// ActiveAppsTimelineProvider.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 10/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
protocol WidgetInfo{
|
||||||
|
var ID: Int? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17, *)
|
||||||
|
class ActiveAppsTimelineProvider<T: WidgetInfo>: AppsTimelineProviderBase<WidgetInfo> {
|
||||||
|
public struct WidgetData: WidgetInfo {
|
||||||
|
let ID: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
private let defaultDataHolder = PaginationDataHolder(
|
||||||
|
itemsPerPage: ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
let widgetKind: String
|
||||||
|
|
||||||
|
init(widgetKind: String){
|
||||||
|
self.widgetKind = widgetKind
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit{
|
||||||
|
// if this provider goes out of scope, clear all entries
|
||||||
|
PageInfoManager.shared.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func getUpdatedData(_ apps: [AppSnapshot], _ context: WidgetInfo?) -> [AppSnapshot] {
|
||||||
|
var apps = apps
|
||||||
|
|
||||||
|
// if simulator, get the 10 simulated entries based on first entry
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
apps = getSimulatedData(apps: apps)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// always start with first page since defaultDataHolder is never updated
|
||||||
|
var currentPageApps = defaultDataHolder.currentPage(inItems: apps)
|
||||||
|
|
||||||
|
if let widgetInfo = context,
|
||||||
|
let widgetID = widgetInfo.ID {
|
||||||
|
|
||||||
|
var navEvent = getPageInfo(widgetID: widgetID)
|
||||||
|
if let event = navEvent,
|
||||||
|
let direction = event.direction
|
||||||
|
{
|
||||||
|
let dataHolder = event.dataHolder!
|
||||||
|
currentPageApps = dataHolder.currentPage(inItems: apps)
|
||||||
|
|
||||||
|
// process navigation request only if event wasn't consumed yet
|
||||||
|
if !event.consumed {
|
||||||
|
switch (direction){
|
||||||
|
case Direction.up:
|
||||||
|
currentPageApps = dataHolder.prevPage(inItems: apps, whenUnavailable: .current)!
|
||||||
|
case Direction.down:
|
||||||
|
currentPageApps = dataHolder.nextPage(inItems: apps, whenUnavailable: .current)!
|
||||||
|
}
|
||||||
|
// mark the event as consumed
|
||||||
|
// this prevents duplicate getUpdatedData() requests for same navigation event
|
||||||
|
navEvent!.consumed = true
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// construct fresh/replace existing
|
||||||
|
navEvent = NavigationEvent(direction: nil, consumed: true, dataHolder: PaginationDataHolder(other: defaultDataHolder))
|
||||||
|
}
|
||||||
|
// put back the data
|
||||||
|
updatePageInfo(widgetID: widgetID, navEvent: navEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentPageApps
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func getPageInfo(widgetID: Int) -> NavigationEvent?{
|
||||||
|
return PageInfoManager.shared.getPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePageInfo(widgetID: Int, navEvent: NavigationEvent?) {
|
||||||
|
PageInfoManager.shared.setPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID, value: navEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TimelineProvider for WidgetAppIntentConfiguration widget type
|
||||||
|
@available(iOS 17, *)
|
||||||
|
extension ActiveAppsTimelineProvider: AppIntentTimelineProvider {
|
||||||
|
|
||||||
|
typealias Intent = WidgetUpdateIntent
|
||||||
|
|
||||||
|
func snapshot(for intent: Intent, in context: Context) async -> AppsEntry<WidgetInfo> {
|
||||||
|
// system retains the previously configured ID value and posts the same here
|
||||||
|
let widgetData = WidgetData(ID: intent.ID)
|
||||||
|
|
||||||
|
let bundleIDs = await super.fetchActiveAppBundleIDs()
|
||||||
|
let snapshot = await self.snapshot(for: bundleIDs, in: widgetData)
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for intent: Intent, in context: Context) async -> Timeline<AppsEntry<WidgetInfo>> {
|
||||||
|
// system retains the previously configured ID value and posts the same here
|
||||||
|
let widgetData = WidgetData(ID: intent.ID)
|
||||||
|
|
||||||
|
let bundleIDs = await self.fetchActiveAppBundleIDs()
|
||||||
|
let timeline = await self.timeline(for: bundleIDs, in: widgetData)
|
||||||
|
|
||||||
|
return timeline
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,53 +11,67 @@ import CoreData
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
struct AppsEntry: TimelineEntry
|
struct AppsEntry<T>: TimelineEntry
|
||||||
{
|
{
|
||||||
var date: Date
|
var date: Date
|
||||||
var relevance: TimelineEntryRelevance?
|
var relevance: TimelineEntryRelevance?
|
||||||
|
|
||||||
var apps: [AppSnapshot]
|
var apps: [AppSnapshot]
|
||||||
var isPlaceholder: Bool = false
|
var isPlaceholder: Bool = false
|
||||||
|
|
||||||
|
var context: T?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppsTimelineProvider
|
class AppsTimelineProviderBase<T>
|
||||||
{
|
{
|
||||||
typealias Entry = AppsEntry
|
typealias Entry = AppsEntry
|
||||||
|
|
||||||
func placeholder(in context: TimelineProviderContext) -> AppsEntry
|
func placeholder(in context: TimelineProviderContext) -> AppsEntry<T>
|
||||||
{
|
{
|
||||||
return AppsEntry(date: Date(), apps: [], isPlaceholder: true)
|
return AppsEntry(date: Date(), apps: [], isPlaceholder: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshot(for appBundleIDs: [String]) async -> AppsEntry
|
func snapshot(for appBundleIDs: [String], in context: T? = nil) async -> AppsEntry<T>
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
try await self.prepare()
|
try await self.prepare()
|
||||||
|
|
||||||
let apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
|
var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
|
||||||
|
|
||||||
let entry = AppsEntry(date: Date(), apps: apps)
|
apps = getUpdatedData(apps, context)
|
||||||
|
|
||||||
|
let entry = AppsEntry(date: Date(), apps: apps, context: context)
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
print("Failed to prepare widget snapshot:", error)
|
print("Failed to prepare widget snapshot:", error)
|
||||||
|
|
||||||
let entry = AppsEntry(date: Date(), apps: [])
|
let entry = AppsEntry(date: Date(), apps: [], context: context)
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeline(for appBundleIDs: [String]) async -> Timeline<AppsEntry>
|
func timeline(for appBundleIDs: [String], in context: T? = nil) async -> Timeline<AppsEntry<T>>
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
try await self.prepare()
|
try await self.prepare()
|
||||||
|
|
||||||
let apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
|
var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
|
||||||
|
|
||||||
|
apps = getUpdatedData(apps, context)
|
||||||
|
|
||||||
|
var entries = self.makeEntries(for: apps, in: context)
|
||||||
|
|
||||||
|
// #if targetEnvironment(simulator)
|
||||||
|
// if let first = entries.first{
|
||||||
|
// entries = [first]
|
||||||
|
// }
|
||||||
|
// #endif
|
||||||
|
|
||||||
let entries = self.makeEntries(for: apps)
|
|
||||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||||
return timeline
|
return timeline
|
||||||
}
|
}
|
||||||
@@ -65,21 +79,27 @@ struct AppsTimelineProvider
|
|||||||
{
|
{
|
||||||
print("Failed to prepare widget timeline:", error)
|
print("Failed to prepare widget timeline:", error)
|
||||||
|
|
||||||
let entry = AppsEntry(date: Date(), apps: [])
|
let entry = AppsEntry(date: Date(), apps: [], context: context)
|
||||||
let timeline = Timeline(entries: [entry], policy: .atEnd)
|
let timeline = Timeline(entries: [entry], policy: .atEnd)
|
||||||
return timeline
|
return timeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUpdatedData(_ apps: [AppSnapshot], _ context: T?) -> [AppSnapshot]{
|
||||||
|
// override in subclasses as required
|
||||||
|
return apps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AppsTimelineProvider
|
extension AppsTimelineProviderBase
|
||||||
{
|
{
|
||||||
func prepare() async throws
|
|
||||||
|
private func prepare() async throws
|
||||||
{
|
{
|
||||||
try await DatabaseManager.shared.start()
|
try await DatabaseManager.shared.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot]
|
private func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot]
|
||||||
{
|
{
|
||||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
let apps = try await context.performAsync {
|
let apps = try await context.performAsync {
|
||||||
@@ -99,7 +119,7 @@ private extension AppsTimelineProvider
|
|||||||
return apps
|
return apps
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeEntries(for snapshots: [AppSnapshot]) -> [AppsEntry]
|
func makeEntries(for snapshots: [AppSnapshot], in context: T? = nil) -> [AppsEntry<T>]
|
||||||
{
|
{
|
||||||
let sortedAppsByExpirationDate = snapshots.sorted { $0.expirationDate < $1.expirationDate }
|
let sortedAppsByExpirationDate = snapshots.sorted { $0.expirationDate < $1.expirationDate }
|
||||||
guard let firstExpiringApp = sortedAppsByExpirationDate.first, let lastExpiringApp = sortedAppsByExpirationDate.last else { return [] }
|
guard let firstExpiringApp = sortedAppsByExpirationDate.first, let lastExpiringApp = sortedAppsByExpirationDate.last else { return [] }
|
||||||
@@ -108,16 +128,16 @@ private extension AppsTimelineProvider
|
|||||||
let numberOfDays = lastExpiringApp.expirationDate.numberOfCalendarDays(since: currentDate)
|
let numberOfDays = lastExpiringApp.expirationDate.numberOfCalendarDays(since: currentDate)
|
||||||
|
|
||||||
// Generate a timeline consisting of one entry per day.
|
// Generate a timeline consisting of one entry per day.
|
||||||
var entries: [AppsEntry] = []
|
var entries: [AppsEntry<T>] = []
|
||||||
|
|
||||||
switch numberOfDays
|
switch numberOfDays
|
||||||
{
|
{
|
||||||
case ..<0:
|
case ..<0:
|
||||||
let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), apps: snapshots)
|
let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), apps: snapshots, context: context)
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
|
|
||||||
case 0:
|
case 0:
|
||||||
let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), apps: snapshots)
|
let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), apps: snapshots, context: context)
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -141,7 +161,7 @@ private extension AppsTimelineProvider
|
|||||||
score = 0
|
score = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = AppsEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), apps: snapshots)
|
let entry = AppsEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), apps: snapshots, context: context)
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,31 +170,8 @@ private extension AppsTimelineProvider
|
|||||||
|
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension AppsTimelineProvider: TimelineProvider
|
|
||||||
{
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (AppsEntry) -> Void)
|
|
||||||
{
|
|
||||||
Task<Void, Never> {
|
|
||||||
let bundleIDs = await self.fetchActiveAppBundleIDs()
|
|
||||||
|
|
||||||
let snapshot = await self.snapshot(for: bundleIDs)
|
|
||||||
completion(snapshot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<AppsEntry>) -> Void)
|
func fetchActiveAppBundleIDs() async -> [String]
|
||||||
{
|
|
||||||
Task<Void, Never> {
|
|
||||||
let bundleIDs = await self.fetchActiveAppBundleIDs()
|
|
||||||
|
|
||||||
let timeline = await self.timeline(for: bundleIDs)
|
|
||||||
completion(timeline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchActiveAppBundleIDs() async -> [String]
|
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -201,26 +198,26 @@ extension AppsTimelineProvider: TimelineProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppsTimelineProvider: IntentTimelineProvider
|
typealias Intent = ViewAppIntent
|
||||||
|
|
||||||
|
class AppsTimelineProvider: AppsTimelineProviderBase<Intent>, IntentTimelineProvider
|
||||||
{
|
{
|
||||||
typealias Intent = ViewAppIntent
|
func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry<Intent>) -> Void)
|
||||||
|
|
||||||
func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry) -> Void)
|
|
||||||
{
|
{
|
||||||
Task<Void, Never> {
|
Task<Void, Never> {
|
||||||
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
|
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
|
||||||
|
|
||||||
let snapshot = await self.snapshot(for: bundleIDs)
|
let snapshot = await self.snapshot(for: bundleIDs, in: intent)
|
||||||
completion(snapshot)
|
completion(snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimeline(for intent: Intent, in context: Context, completion: @escaping (Timeline<AppsEntry>) -> Void)
|
func getTimeline(for intent: Intent, in context: Context, completion: @escaping (Timeline<AppsEntry<Intent>>) -> Void)
|
||||||
{
|
{
|
||||||
Task<Void, Never> {
|
Task<Void, Never> {
|
||||||
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
|
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
|
||||||
|
|
||||||
let timeline = await self.timeline(for: bundleIDs)
|
let timeline = await self.timeline(for: bundleIDs, in: intent)
|
||||||
completion(timeline)
|
completion(timeline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// HomeScreenWidget.swift
|
// ActiveAppsWidget.swift
|
||||||
// AltWidgetExtension
|
// AltWidgetExtension
|
||||||
//
|
//
|
||||||
// Created by Riley Testut on 8/16/23.
|
// Created by Riley Testut on 8/16/23.
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
import CoreData
|
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
private extension Color
|
private extension Color
|
||||||
{
|
{
|
||||||
@@ -21,20 +21,42 @@ private extension Color
|
|||||||
static let altGradientExtraDark = Color.init(.displayP3, red: 2.0/255.0, green: 82.0/255.0, blue: 103.0/255.0)
|
static let altGradientExtraDark = Color.init(.displayP3, red: 2.0/255.0, green: 82.0/255.0, blue: 103.0/255.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WidgetTag: WidgetInfo{
|
||||||
|
let ID: Int?
|
||||||
|
}
|
||||||
|
|
||||||
//@available(iOS 17, *)
|
//@available(iOS 17, *)
|
||||||
struct ActiveAppsWidget: Widget
|
struct ActiveAppsWidget: Widget
|
||||||
{
|
{
|
||||||
private let kind: String = "ActiveApps"
|
struct Constants{
|
||||||
|
static let MAX_ROWS_PER_PAGE: UInt = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var id: Int = 1
|
||||||
|
private let widgetKind: String
|
||||||
|
|
||||||
|
init(){
|
||||||
|
widgetKind = "ActiveApps - \(Self.id)"
|
||||||
|
Self.id += 1
|
||||||
|
}
|
||||||
|
|
||||||
public var body: some WidgetConfiguration {
|
public var body: some WidgetConfiguration {
|
||||||
|
|
||||||
if #available(iOS 17, *)
|
if #available(iOS 17, *)
|
||||||
{
|
{
|
||||||
return StaticConfiguration(kind: kind, provider: AppsTimelineProvider()) { entry in
|
|
||||||
ActiveAppsWidgetView(entry: entry)
|
let widgetConfig = AppIntentConfiguration(
|
||||||
|
kind: widgetKind,
|
||||||
|
intent: WidgetUpdateIntent.self,
|
||||||
|
provider: ActiveAppsTimelineProvider<WidgetTag>(widgetKind: widgetKind)
|
||||||
|
) { entry in
|
||||||
|
ActiveAppsWidgetView(entry: entry, widgetKind: widgetKind)
|
||||||
}
|
}
|
||||||
.supportedFamilies([.systemMedium])
|
.supportedFamilies([.systemMedium])
|
||||||
.configurationDisplayName("Active Apps")
|
.configurationDisplayName("Active Apps")
|
||||||
.description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.")
|
.description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.")
|
||||||
|
|
||||||
|
return widgetConfig
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -48,11 +70,12 @@ struct ActiveAppsWidget: Widget
|
|||||||
@available(iOS 17, *)
|
@available(iOS 17, *)
|
||||||
private struct ActiveAppsWidgetView: View
|
private struct ActiveAppsWidgetView: View
|
||||||
{
|
{
|
||||||
var entry: AppsEntry
|
var entry: AppsEntry<WidgetInfo>
|
||||||
|
var widgetKind: String
|
||||||
|
|
||||||
@Environment(\.colorScheme)
|
@Environment(\.colorScheme)
|
||||||
private var colorScheme
|
private var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if entry.apps.isEmpty
|
if entry.apps.isEmpty
|
||||||
@@ -79,24 +102,39 @@ private struct ActiveAppsWidgetView: View
|
|||||||
|
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
GeometryReader { (geometry) in
|
GeometryReader { (geometry) in
|
||||||
|
HStack(alignment: .center) {
|
||||||
let numberOfApps = max(entry.apps.count, 1) // Ensure we don't divide by 0
|
|
||||||
let preferredRowHeight = (geometry.size.height / Double(numberOfApps)) - 8
|
let itemsPerPage = ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE
|
||||||
let rowHeight = min(preferredRowHeight, geometry.size.height / 2)
|
|
||||||
|
let preferredRowHeight = (geometry.size.height / Double(itemsPerPage)) - 8
|
||||||
ZStack(alignment: .center) {
|
let rowHeight = min(preferredRowHeight, geometry.size.height / 2)
|
||||||
VStack(spacing: 12) {
|
|
||||||
ForEach(entry.apps, id: \.bundleIdentifier) { app in
|
LazyVStack(spacing: 12) {
|
||||||
|
ForEach(Array(entry.apps.enumerated()), id: \.offset) { index, app in
|
||||||
|
|
||||||
|
let icon: UIImage = app.icon ?? UIImage(named: "SideStore")!
|
||||||
|
|
||||||
let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date)
|
// 1024x1024 images are not supported by previews but supported by device
|
||||||
|
// so we scale the image to 97% so as to reduce its actual size but not too much
|
||||||
|
// to somewhere below value, acceptable by previews ie < 1042x948
|
||||||
|
let scalingFactor = 0.97
|
||||||
|
|
||||||
|
let resizedSize = CGSize(
|
||||||
|
width: icon.size.width * scalingFactor,
|
||||||
|
height: icon.size.height * scalingFactor
|
||||||
|
)
|
||||||
|
|
||||||
|
let resizedIcon = icon.resizing(to: resizedSize)!
|
||||||
let cornerRadius = rowHeight / 5.0
|
let cornerRadius = rowHeight / 5.0
|
||||||
|
let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date)
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Image(uiImage: app.icon ?? UIImage(named: "AltStore")!)
|
Image(uiImage: resizedIcon)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.cornerRadius(cornerRadius)
|
.cornerRadius(cornerRadius)
|
||||||
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
Text(app.name)
|
Text(app.name)
|
||||||
.font(.system(size: 15, weight: .semibold, design: .rounded))
|
.font(.system(size: 15, weight: .semibold, design: .rounded))
|
||||||
@@ -127,13 +165,40 @@ private struct ActiveAppsWidgetView: View
|
|||||||
.padding(.all, -5)
|
.padding(.all, -5)
|
||||||
}
|
}
|
||||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||||
.invalidatableContent()
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.activatesRefreshAllAppsIntent()
|
.activatesRefreshAllAppsIntent()
|
||||||
|
// this modifier invalidates the view (disables user interaction and shows a blinking effect)
|
||||||
|
.invalidatableContent()
|
||||||
|
|
||||||
}
|
}
|
||||||
.frame(height: rowHeight)
|
.frame(height: rowHeight)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 16)
|
||||||
|
|
||||||
|
let buttonWidth: CGFloat = 16
|
||||||
|
let widgetID = entry.context?.ID
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "arrow.up")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: buttonWidth, height: buttonWidth)
|
||||||
|
.opacity(0.3)
|
||||||
|
// .mask(Capsule())
|
||||||
|
.pageUpButton(widgetID, widgetKind)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "arrow.down")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: buttonWidth, height: buttonWidth)
|
||||||
|
.opacity(0.3)
|
||||||
|
// .mask(Capsule())
|
||||||
|
.pageDownButton(widgetID, widgetKind)
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
@@ -154,14 +219,14 @@ private struct ActiveAppsWidgetView: View
|
|||||||
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
||||||
let (altstore, delta, clip, longAltStore, longDelta, longClip) = AppSnapshot.makePreviewSnapshots()
|
let (altstore, delta, clip, longAltStore, longDelta, longClip) = AppSnapshot.makePreviewSnapshots()
|
||||||
|
|
||||||
AppsEntry(date: Date(), apps: [altstore, delta, clip])
|
AppsEntry<Any>(date: Date(), apps: [altstore, delta, clip])
|
||||||
AppsEntry(date: Date(), apps: [longAltStore, longDelta, longClip])
|
AppsEntry<Any>(date: Date(), apps: [longAltStore, longDelta, longClip])
|
||||||
|
|
||||||
AppsEntry(date: expiredDate, apps: [altstore, delta, clip])
|
AppsEntry<Any>(date: expiredDate, apps: [altstore, delta, clip])
|
||||||
|
|
||||||
AppsEntry(date: Date(), apps: [altstore, delta])
|
AppsEntry<Any>(date: Date(), apps: [altstore, delta])
|
||||||
AppsEntry(date: Date(), apps: [altstore])
|
AppsEntry<Any>(date: Date(), apps: [altstore])
|
||||||
|
|
||||||
AppsEntry(date: Date(), apps: [])
|
AppsEntry<Any>(date: Date(), apps: [])
|
||||||
AppsEntry(date: Date(), apps: [], isPlaceholder: true)
|
AppsEntry<Any>(date: Date(), apps: [], isPlaceholder: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ struct AppDetailWidget: Widget
|
|||||||
|
|
||||||
private struct AppDetailWidgetView: View
|
private struct AppDetailWidgetView: View
|
||||||
{
|
{
|
||||||
var entry: AppsEntry
|
var entry: AppsEntry<Intent>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -130,7 +130,12 @@ private struct AppDetailWidgetView: View
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.widgetBackground(backgroundView(icon: entry.apps.first?.icon, tintColor: entry.apps.first?.tintColor))
|
.widgetBackground(
|
||||||
|
backgroundView(
|
||||||
|
icon: entry.apps.first?.icon,
|
||||||
|
tintColor: entry.apps.first?.tintColor
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,17 +151,35 @@ private extension AppDetailWidgetView
|
|||||||
let blurRadius = 5 as CGFloat
|
let blurRadius = 5 as CGFloat
|
||||||
let tintOpacity = 0.45
|
let tintOpacity = 0.45
|
||||||
|
|
||||||
|
// 1024x1024 images are not supported by previews but supported by device
|
||||||
|
// so we scale the image to 97% so as to reduce its actual size but not too much
|
||||||
|
// to somewhere below value, acceptable by previews ie < 1042x948
|
||||||
|
let scalingFactor = 0.97
|
||||||
|
|
||||||
|
let resizedSize = CGSize(
|
||||||
|
width: icon.size.width * scalingFactor,
|
||||||
|
height: icon.size.height * scalingFactor
|
||||||
|
)
|
||||||
|
|
||||||
|
let resizedIcon = icon.resizing(to: resizedSize)!
|
||||||
|
|
||||||
return ZStack(alignment: .topTrailing) {
|
return ZStack(alignment: .topTrailing) {
|
||||||
// Blurred Image
|
// Blurred Image
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
Image(uiImage: icon)
|
Image(uiImage: resizedIcon)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: imageHeight, height: imageHeight, alignment: .center)
|
.frame(width: imageHeight, height: imageHeight, alignment: .center)
|
||||||
.saturation(saturation)
|
.saturation(saturation)
|
||||||
.blur(radius: blurRadius, opaque: true)
|
.blur(radius: blurRadius, opaque: true)
|
||||||
.scaleEffect(geometry.size.width / imageHeight, anchor: .center)
|
.scaleEffect(geometry.size.width / imageHeight, anchor: .center)
|
||||||
|
// .onAppear {
|
||||||
|
// print("Geometry size: \(geometry.size)")
|
||||||
|
// print("Image height: \(imageHeight), Geometry width: \(geometry.size.width)")
|
||||||
|
// print("Icon size: \(icon.size)")
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
Color(tintColor)
|
Color(tintColor)
|
||||||
.opacity(tintOpacity)
|
.opacity(tintOpacity)
|
||||||
@@ -176,14 +199,12 @@ private extension AppDetailWidgetView
|
|||||||
AppDetailWidget()
|
AppDetailWidget()
|
||||||
} timeline: {
|
} timeline: {
|
||||||
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
||||||
let (altstore, delta, _, _, longDelta, _) = AppSnapshot.makePreviewSnapshots()
|
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
|
||||||
|
AppsEntry<Any>(date: Date(), apps: [altstore])
|
||||||
|
AppsEntry<Any>(date: Date(), apps: [longAltStore])
|
||||||
|
|
||||||
AppsEntry(date: Date(), apps: [altstore])
|
AppsEntry<Any>(date: expiredDate, apps: [altstore])
|
||||||
AppsEntry(date: Date(), apps: [delta])
|
|
||||||
AppsEntry(date: Date(), apps: [longDelta])
|
|
||||||
|
|
||||||
AppsEntry(date: expiredDate, apps: [delta])
|
AppsEntry<Any>(date: Date(), apps: [])
|
||||||
|
AppsEntry<Any>(date: Date(), apps: [], isPlaceholder: true)
|
||||||
AppsEntry(date: Date(), apps: [])
|
|
||||||
AppsEntry(date: Date(), apps: [], isPlaceholder: true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ extension ComplicationView
|
|||||||
@available(iOS 16, *)
|
@available(iOS 16, *)
|
||||||
private struct ComplicationView: View
|
private struct ComplicationView: View
|
||||||
{
|
{
|
||||||
let entry: AppsEntry
|
let entry: AppsEntry<Intent>
|
||||||
let style: Style
|
let style: Style
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -82,6 +82,7 @@ private struct ComplicationView: View
|
|||||||
|
|
||||||
let progress = Double(daysRemaining) / Double(totalDays)
|
let progress = Double(daysRemaining) / Double(totalDays)
|
||||||
|
|
||||||
|
// TODO: Gauge initialized with an out-of-bounds progress amount. The amount will be clamped to the nearest bound.
|
||||||
Gauge(value: progress) {
|
Gauge(value: progress) {
|
||||||
if daysRemaining < 0
|
if daysRemaining < 0
|
||||||
{
|
{
|
||||||
@@ -143,10 +144,10 @@ private let widgetFamily = if #available(iOS 16, *) { WidgetFamily.accessoryCirc
|
|||||||
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
||||||
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
|
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
|
||||||
|
|
||||||
AppsEntry(date: Date(), apps: [altstore])
|
AppsEntry<Any>(date: Date(), apps: [altstore])
|
||||||
AppsEntry(date: Date(), apps: [longAltStore])
|
AppsEntry<Any>(date: Date(), apps: [longAltStore])
|
||||||
|
|
||||||
AppsEntry(date: expiredDate, apps: [altstore])
|
AppsEntry<Any>(date: expiredDate, apps: [altstore])
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 17, *)
|
@available(iOS 17, *)
|
||||||
@@ -156,8 +157,8 @@ private let widgetFamily = if #available(iOS 16, *) { WidgetFamily.accessoryCirc
|
|||||||
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
|
||||||
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
|
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
|
||||||
|
|
||||||
AppsEntry(date: Date(), apps: [altstore])
|
AppsEntry<Any>(date: Date(), apps: [altstore])
|
||||||
AppsEntry(date: Date(), apps: [longAltStore])
|
AppsEntry<Any>(date: Date(), apps: [longAltStore])
|
||||||
|
|
||||||
AppsEntry(date: expiredDate, apps: [altstore])
|
AppsEntry<Any>(date: expiredDate, apps: [altstore])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ ORG_PREFIX = $(ORG_IDENTIFIER)
|
|||||||
PRODUCT_NAME = SideStore
|
PRODUCT_NAME = SideStore
|
||||||
//PRODUCT_NAME[configuration=Debug] = Prov Debug
|
//PRODUCT_NAME[configuration=Debug] = Prov Debug
|
||||||
|
|
||||||
|
|
||||||
//PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
|
//PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
|
||||||
//PRODUCT_BUNDLE_IDENTIFIER[config=Release] = $(ORG_PREFIX).SideStore
|
//PRODUCT_BUNDLE_IDENTIFIER[config=Release] = $(ORG_PREFIX).SideStore
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
|
// preserve unmodified bundle ID (without any extra suffixes)
|
||||||
|
GROUP_ID = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = $(GROUP_ID)
|
||||||
|
|
||||||
EXTENSION_PREFIX = $(PRODUCT_BUNDLE_IDENTIFIER)
|
EXTENSION_PREFIX = $(PRODUCT_BUNDLE_IDENTIFIER)
|
||||||
APP_GROUP_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
|
APP_GROUP_IDENTIFIER = $(GROUP_ID)
|
||||||
ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME)
|
ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME)
|
||||||
|
|
||||||
// preserve unmodified bundle ID (without any extra suffixes)
|
|
||||||
GROUP_ID = $(PRODUCT_BUNDLE_IDENTIFIER)
|
|
||||||
|
|
||||||
// Suppress noise from os activity in xcode console log for release builds
|
// Suppress noise from os activity in xcode console log for release builds
|
||||||
DEBUG_ACTIVITY_MODE = disable
|
DEBUG_ACTIVITY_MODE = disable
|
||||||
|
|
||||||
|
|||||||
57
Makefile
57
Makefile
@@ -1,3 +1,5 @@
|
|||||||
|
default: build # default target for the "make" command
|
||||||
|
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
.PHONY: help ios update tvos
|
.PHONY: help ios update tvos
|
||||||
|
|
||||||
@@ -156,23 +158,32 @@ test:
|
|||||||
|
|
||||||
## -- Building --
|
## -- Building --
|
||||||
|
|
||||||
# Fetch the latest commit ID globally
|
IS_ALPHA_TRUE := $(filter true TRUE 1, $(IS_ALPHA))
|
||||||
ALPHA_COMMIT_ID := $(if $(IS_ALPHA),$(shell git rev-parse --short HEAD),)
|
IS_BETA_TRUE := $(filter true TRUE 1, $(IS_BETA))
|
||||||
|
|
||||||
# Print release type based on the presence of ALPHA_COMMIT_ID
|
# 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:
|
print_release_type:
|
||||||
@echo ""
|
@echo ""
|
||||||
@if [ -n "$(IS_ALPHA)" ]; then \
|
@if [ "$(filter true TRUE 1,$(IS_ALPHA))" ]; then \
|
||||||
echo "'IS_ALPHA' is defined. Fetched the latest commit ID from HEAD..."; \
|
echo "'IS_ALPHA' is set to true. Fetched the latest commit ID from HEAD..."; \
|
||||||
echo " Commit ID: $(ALPHA_COMMIT_ID)"; \
|
echo " Commit ID: $(COMMIT_ID)"; \
|
||||||
echo ""; \
|
echo ""; \
|
||||||
echo ">>>>>>>> This is now a ALPHA release for COMMIT_ID = '$(ALPHA_COMMIT_ID)' <<<<<<<<<"; \
|
echo ">>>>>>>> This is now an ALPHA release for COMMIT_ID = '$(COMMIT_ID)' <<<<<<<<<"; \
|
||||||
echo " Building with BUILD_REVISION = '$(ALPHA_COMMIT_ID)'"; \
|
echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
|
||||||
|
elif [ "$(filter true TRUE 1,$(IS_BETA))" ]; 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 \
|
else \
|
||||||
echo "'IS_ALPHA' is not defined. Skipping commit ID fetch."; \
|
echo "'IS_ALPHA' and 'IS_BETA' are not set to true. Skipping commit ID fetch."; \
|
||||||
echo ""; \
|
echo ""; \
|
||||||
echo ">>>>>>>> This is now a STABLE release because IS_ALPHA was NOT SET <<<<<<<<<"; \
|
echo ">>>>>>>> This is now a STABLE release because neither IS_ALPHA nor IS_BETA was true <<<<<<<<<"; \
|
||||||
echo " Building with BUILD_REVISION = '$(ALPHA_COMMIT_ID)'"; \
|
echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
|
||||||
echo ""; \
|
echo ""; \
|
||||||
fi
|
fi
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -185,9 +196,14 @@ print_release_type:
|
|||||||
#
|
#
|
||||||
# However the scheme used is Debug Scheme, so it was deliberately
|
# However the scheme used is Debug Scheme, so it was deliberately
|
||||||
# using scheme = Debug and config = Release (so I have kept it as-is)
|
# 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 := "Debug" # switched to debug build-config to diagnose issue since debugger won't resolve breakpoints in release
|
||||||
# BUILD_CONFIG := "Release"
|
# BUILD_CONFIG := "Release"
|
||||||
|
|
||||||
|
# switched back to release build as default config, unless specified by the incoming environment vars
|
||||||
|
BUILD_CONFIG ?= Release
|
||||||
build: print_release_type
|
build: print_release_type
|
||||||
|
@echo ">>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<"
|
||||||
|
@echo ""
|
||||||
@xcodebuild -workspace AltStore.xcworkspace \
|
@xcodebuild -workspace AltStore.xcworkspace \
|
||||||
-scheme SideStore \
|
-scheme SideStore \
|
||||||
-sdk iphoneos \
|
-sdk iphoneos \
|
||||||
@@ -198,7 +214,7 @@ build: print_release_type
|
|||||||
CODE_SIGNING_ALLOWED=NO \
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||||
ORG_IDENTIFIER=com.SideStore \
|
ORG_IDENTIFIER=com.SideStore \
|
||||||
BUILD_REVISION=$(ALPHA_COMMIT_ID) \
|
BUILD_REVISION=$(COMMIT_ID) \
|
||||||
BUNDLE_ID_SUFFIX=
|
BUNDLE_ID_SUFFIX=
|
||||||
# DWARF_DSYM_FOLDER_PATH="."
|
# DWARF_DSYM_FOLDER_PATH="."
|
||||||
|
|
||||||
@@ -302,13 +318,13 @@ copy-altbackup: checkPaths
|
|||||||
else \
|
else \
|
||||||
rm -rf "$$TGT"; \
|
rm -rf "$$TGT"; \
|
||||||
mkdir -p "$$TGT"; \
|
mkdir -p "$$TGT"; \
|
||||||
cp -R "$(ALT_APP_SRC_PARENT)/$(TGT_NAME)" "$$TGT"; \
|
cp -R -f "$(ALT_APP_SRC_PARENT)/$$TGT_NAME/." "$$TGT"; \
|
||||||
echo " Copied $$TGT_NAME into TARGET = $$TGT"; \
|
echo " Copied $$TGT_NAME into TARGET = $$TGT"; \
|
||||||
echo ""; \
|
echo ""; \
|
||||||
fi; \
|
fi; \
|
||||||
done \
|
done \
|
||||||
'
|
'
|
||||||
@find "$(ALT_APP_DST_ARCHIVE)" -maxdepth 3 -exec ls -ld {} + || true
|
@find "$(ALT_APP_DST_ARCHIVE)" -maxdepth 4 -exec ls -ld {} + || true
|
||||||
@echo ''
|
@echo ''
|
||||||
|
|
||||||
# fakesign-altbackup: copy-altbackup
|
# fakesign-altbackup: copy-altbackup
|
||||||
@@ -325,12 +341,19 @@ ipa-altbackup: checkPaths copy-altbackup
|
|||||||
@rm -rf "$(ALT_APP_PAYLOAD_DST)"
|
@rm -rf "$(ALT_APP_PAYLOAD_DST)"
|
||||||
@mkdir -p "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
@mkdir -p "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
||||||
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
|
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
|
||||||
@cp -R -f "$(ALT_APP_SRC)/" "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
@cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
||||||
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload && popd
|
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload && popd
|
||||||
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources
|
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources
|
||||||
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
|
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
|
||||||
|
|
||||||
clean:
|
clean-altbackup:
|
||||||
|
@echo ""
|
||||||
|
@echo "====> Cleaning up AltBackup related artifacts <===="
|
||||||
|
@rm -rf build/altbackup.xcarchive/
|
||||||
|
@rm -f build/AltBackup.ipa
|
||||||
|
@rm -f AltStore/Resources/AltBackup.ipa
|
||||||
|
|
||||||
|
clean: clean-altbackup
|
||||||
@rm -rf *.xcarchive/
|
@rm -rf *.xcarchive/
|
||||||
@rm -rf *.dSYM/
|
@rm -rf *.dSYM/
|
||||||
@rm -rf SideStore.ipa
|
@rm -rf SideStore.ipa
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ SideStore's goal is to provide an untethered sideloading experience. It's a comm
|
|||||||
- iOS 15+
|
- iOS 15+
|
||||||
- Rustup (`brew install rustup`)
|
- Rustup (`brew install rustup`)
|
||||||
|
|
||||||
Why iOS 14? Targeting such a recent version of iOS allows us to accelerate development, especially since not many developers have older devices to test on. This is corrobated by the fact that SwiftUI support is much better, allowing us to transistion to a more modern UI codebase.
|
Why iOS 15? Targeting such a recent version of iOS allows us to accelerate development, especially since not many developers have older devices to test on. This is corrobated by the fact that SwiftUI support is much better, allowing us to transistion to a more modern UI codebase.
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
### SideStore
|
### SideStore
|
||||||
|
|||||||
12
SideStore/Utils/common/AbstractClassError.swift
Normal file
12
SideStore/Utils/common/AbstractClassError.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// OutputStream.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 28/12/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
public enum AbstractClassError: Error {
|
||||||
|
case abstractInitializerInvoked
|
||||||
|
case abstractMethodInvoked
|
||||||
|
}
|
||||||
26
SideStore/Utils/common/DateTimeUtil.swift
Normal file
26
SideStore/Utils/common/DateTimeUtil.swift
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// DateTimeUtil.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 02/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class DateTimeUtil {
|
||||||
|
public static func getDateInTimeStamp(date: Date) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
// (upto millis accurate for uniqueness)
|
||||||
|
formatter.dateFormat = "yyyyMMdd_HHmmss_SSS" // Format: 20241228_142345_300
|
||||||
|
// Ensures 24-hour clock format coz the locale value overrides it if it is of AM/PM format?! (why apple!)
|
||||||
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getTimeStampSuffixedFileName(fileName: String, timestamp: String, extn: String) -> String {
|
||||||
|
// create a log file with the current timestamp
|
||||||
|
let fnameWithTimestamp = "\(fileName)-\(timestamp)\(extn)"
|
||||||
|
return fnameWithTimestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
29
SideStore/Utils/common/FileOutputStream.swift
Normal file
29
SideStore/Utils/common/FileOutputStream.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// FileOutputStream.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 28/12/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class FileOutputStream: OutputStream {
|
||||||
|
private let fileHandle: FileHandle
|
||||||
|
|
||||||
|
init(_ fileHandle: FileHandle) {
|
||||||
|
self.fileHandle = fileHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
public func write(_ data: Data) {
|
||||||
|
fileHandle.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func flush() {
|
||||||
|
fileHandle.synchronizeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func close() {
|
||||||
|
fileHandle.closeFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
15
SideStore/Utils/common/OutputStream.swift
Normal file
15
SideStore/Utils/common/OutputStream.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// OutputStream.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 28/12/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol OutputStream {
|
||||||
|
func write(_ data: Data)
|
||||||
|
func flush()
|
||||||
|
func close()
|
||||||
|
}
|
||||||
30
SideStore/Utils/datastructures/SingletonGenericMap.swift
Normal file
30
SideStore/Utils/datastructures/SingletonGenericMap.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// SingletonGenericMap.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 10/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
class SingletonGenericMap{
|
||||||
|
static var shared = SingletonGenericMap()
|
||||||
|
private var pageInfoMap: [AnyHashable: Any] = [:]
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func setPageInfo<T: Hashable, U>(for key: T, value: U?) {
|
||||||
|
pageInfoMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPageInfo<T: Hashable, U>(for key: T) -> U? {
|
||||||
|
return pageInfoMap[key] as? U
|
||||||
|
}
|
||||||
|
|
||||||
|
func popPageInfo<T: Hashable, U>(for key: T) -> U? {
|
||||||
|
return pageInfoMap.removeValue(forKey: key) as? U
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearAll() {
|
||||||
|
pageInfoMap.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
169
SideStore/Utils/dignostics/database/CoreDataHelper.swift
Normal file
169
SideStore/Utils/dignostics/database/CoreDataHelper.swift
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
//
|
||||||
|
// CoreDataHelper.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 02/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import System
|
||||||
|
|
||||||
|
class CoreDataHelper{
|
||||||
|
|
||||||
|
private static let STORE_XCMODELD_NAME = "AltStore"
|
||||||
|
private static let COREDATA_BUNDLE_ID = "com.SideStore.SideStore.AltStoreCore"
|
||||||
|
|
||||||
|
// Create a serial dispatch queue to lock access to the Core Data store
|
||||||
|
private static let datastoreQueue = DispatchQueue(label: "com.SideStore.AltStore.datastoreQueue")
|
||||||
|
|
||||||
|
public static func exportCoreDataStore() async throws -> URL {
|
||||||
|
|
||||||
|
// Locate the bundle containing the Core Data model
|
||||||
|
guard let bundle = Bundle(identifier: COREDATA_BUNDLE_ID) else {
|
||||||
|
let errorDescription = "AltStoreCore bundle not found"
|
||||||
|
throw getCoreDataError(code: 1, localizedDescription: errorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the model from the bundle
|
||||||
|
guard let modelURL = bundle.url(forResource: STORE_XCMODELD_NAME, withExtension: "momd"),
|
||||||
|
let model = NSManagedObjectModel(contentsOf: modelURL) else {
|
||||||
|
|
||||||
|
let errorDescription = "Failed to load model \(STORE_XCMODELD_NAME) from AltStoreCore bundle"
|
||||||
|
throw getCoreDataError(code: 2, localizedDescription: errorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// let container = NSPersistentContainer(name: STORE_XCMODELD_NAME)
|
||||||
|
let container = NSPersistentContainer(name: STORE_XCMODELD_NAME, managedObjectModel: model)
|
||||||
|
|
||||||
|
|
||||||
|
// bridge callback into async-await pattern
|
||||||
|
return try await withCheckedThrowingContinuation{ (continuation: CheckedContinuation<URL, Error>) in
|
||||||
|
|
||||||
|
// async callback processing
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func lockSQLiteFile(at url: URL) -> FileDescriptor? {
|
||||||
|
// Open the SQLite file for locking
|
||||||
|
let fileDescriptor = open(url.path, O_RDWR)
|
||||||
|
guard fileDescriptor >= 0 else {
|
||||||
|
print("Failed to open SQLite file for locking.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the file using flock (exclusive lock)
|
||||||
|
let lockResult = flock(fileDescriptor, LOCK_EX)
|
||||||
|
guard lockResult == 0 else {
|
||||||
|
print("Failed to lock SQLite file.")
|
||||||
|
close(fileDescriptor)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileDescriptor(rawValue: fileDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func unlockSQLiteFile(fileDescriptor: FileDescriptor) {
|
||||||
|
let fileDescriptor = fileDescriptor.rawValue
|
||||||
|
// Unlock the file after backup
|
||||||
|
flock(fileDescriptor, LOCK_UN)
|
||||||
|
close(fileDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getCoreDataError(code: Int, localizedDescription: String) -> Error {
|
||||||
|
return NSError(domain: "CoreDataExport", code: code, userInfo: [NSLocalizedDescriptionKey: localizedDescription])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func backupCoreDataStore(container: NSPersistentContainer, loadError: Error?) throws -> URL {
|
||||||
|
|
||||||
|
// Check for load errors
|
||||||
|
if let error = loadError {
|
||||||
|
let errorDescription = "Failed to load persistent store: \(error.localizedDescription)"
|
||||||
|
throw getCoreDataError(code: 3, localizedDescription: errorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let storeURL = container.persistentStoreCoordinator.persistentStores.first?.url else {
|
||||||
|
let errorDescription = "Persistent store URL not found"
|
||||||
|
throw getCoreDataError(code: 4, localizedDescription: errorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we can't lock on the sqlite file for serialization coz coredata might be holding
|
||||||
|
// active database connection handle to the sqlite
|
||||||
|
|
||||||
|
// // Lock the SQLite file
|
||||||
|
// guard let fileDescriptor = lockSQLiteFile(at: storeURL) else {
|
||||||
|
// throw getCoreDataError(code: 5, localizedDescription: "Failed to lock SQLite file")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// defer {
|
||||||
|
// // Ensure that the file is unlocked when the backup completes or fails
|
||||||
|
// unlockSQLiteFile(fileDescriptor: fileDescriptor)
|
||||||
|
// }
|
||||||
|
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
|
let exportedDir = documentsURL.appendingPathComponent("ExportedCoreDataStores", isDirectory: true)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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)")
|
||||||
|
} else {
|
||||||
|
print("Failed to list directory contents.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentDirectory = destinationURL.deletingLastPathComponent()
|
||||||
|
|
||||||
|
|
||||||
|
do {
|
||||||
|
// create intermediate dirs as required
|
||||||
|
try FileManager.default.createDirectory(at: parentDirectory,
|
||||||
|
withIntermediateDirectories: true,
|
||||||
|
attributes: nil)
|
||||||
|
|
||||||
|
// Copy main SQLite file
|
||||||
|
try fileManager.copyItem(at: storeURL, to: destinationURL)
|
||||||
|
|
||||||
|
// Copy -shm and -wal files if they exist
|
||||||
|
let additionalFiles = ["-shm", "-wal"].compactMap {
|
||||||
|
destinationURL.deletingPathExtension().appendingPathExtension(destinationURL.pathExtension + $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in additionalFiles where fileManager.fileExists(atPath: file.path) {
|
||||||
|
let destination = documentsURL.appendingPathComponent(file.lastPathComponent)
|
||||||
|
try fileManager.copyItem(at: file, to: destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Core Data store exported to: \(destinationURL.path)")
|
||||||
|
return destinationURL
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
let errorDescription = "Failed to copy Core Data files: \(error.localizedDescription)"
|
||||||
|
throw getCoreDataError(code: 6, localizedDescription: errorDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
SideStore/Utils/dignostics/errors/ErrorProcessing.swift
Normal file
81
SideStore/Utils/dignostics/errors/ErrorProcessing.swift
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// ErrorProcessing.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 20/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
class ErrorProcessing {
|
||||||
|
|
||||||
|
enum InfoMode: String {
|
||||||
|
case fullError
|
||||||
|
case localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
let info: InfoMode
|
||||||
|
let unique: Bool
|
||||||
|
let recur: Bool
|
||||||
|
|
||||||
|
|
||||||
|
var errors: Set<String> = []
|
||||||
|
|
||||||
|
// by default we will process only the localDesc on first level errors
|
||||||
|
init(_ mode: InfoMode = .localizedDescription, unique: Bool = false, recur: Bool = false){
|
||||||
|
self.info = mode
|
||||||
|
self.unique = unique
|
||||||
|
self.recur = recur
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processError(_ error: NSError, getMoreErrors: (_ error: NSError)->String) -> String{
|
||||||
|
// if unique was requested and if this error is duplicate, ignore processing it
|
||||||
|
let serializedError = "\(error)"
|
||||||
|
if unique && errors.contains(serializedError) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
errors.insert(serializedError) // mark this as processed
|
||||||
|
|
||||||
|
var title = ""
|
||||||
|
var desc = ""
|
||||||
|
switch (info){
|
||||||
|
case .localizedDescription:
|
||||||
|
title = (error.localizedTitle.map{$0+"\n"} ?? "")
|
||||||
|
desc = error.localizedDescription
|
||||||
|
case .fullError:
|
||||||
|
desc = serializedError
|
||||||
|
}
|
||||||
|
var moreErrors = getMoreErrors(error)
|
||||||
|
moreErrors = moreErrors == "" ? "" : "\n" + moreErrors
|
||||||
|
return title + desc + moreErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDescription(error: NSError) -> String{
|
||||||
|
errors = [] // reinit for each request
|
||||||
|
return getDescriptionText(error: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var recurseErrors = { error in
|
||||||
|
self.getDescriptionText(error: error) // recursively process underlying error(s) if any
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDescriptionText(error: NSError) -> String{
|
||||||
|
var description = ""
|
||||||
|
|
||||||
|
// process current error only if recur was not requested
|
||||||
|
let processMoreErrors = recur ? recurseErrors : {_ in ""}
|
||||||
|
|
||||||
|
let underlyingErrors = error.underlyingErrors
|
||||||
|
if !underlyingErrors.isEmpty {
|
||||||
|
description += underlyingErrors.map{ error in
|
||||||
|
let error = error as NSError
|
||||||
|
return processError(error, getMoreErrors: processMoreErrors)
|
||||||
|
}.joined(separator: "\n")
|
||||||
|
} else if let underlyingError = error.underlyingError as? NSError {
|
||||||
|
let error = underlyingError as NSError
|
||||||
|
description += processError(error, getMoreErrors: processMoreErrors)
|
||||||
|
} else {
|
||||||
|
description += processError(error, getMoreErrors: processMoreErrors)
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// OperationsLoggingControl.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 14/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class OperationsLoggingControl {
|
||||||
|
|
||||||
|
func updateDatabase(for operation: Operation.Type, value: Bool) {
|
||||||
|
Self.updateDatabase(for: operation, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func updateDatabase(for operation: Operation.Type, value: Bool) {
|
||||||
|
// This method should handle the database update logic based on the operation and value
|
||||||
|
let key = Self.getKey(operation)
|
||||||
|
print("Updating database for key: \(key), value: \(value)")
|
||||||
|
UserDefaults.standard.set(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func stripGenericTypeName(from string: String) -> String {
|
||||||
|
// ex: 1. "EnableJITOperation<DummyConformance>"
|
||||||
|
// ex: 1. "EnableJITOperation<DummyConformance<SomeMoreType>>"
|
||||||
|
// will become EnableJITOperation without the generics type info
|
||||||
|
if let range = string.range(of: "<") {
|
||||||
|
return String(string[..<range.lowerBound])
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getKey(_ operation: Operation.Type) -> String {
|
||||||
|
let processedOperation = Self.stripGenericTypeName(from: "\(operation)")
|
||||||
|
return "\(processedOperation)LoggingEnabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFromDatabase(for operation: Operation.Type) -> Bool{
|
||||||
|
return Self.getFromDatabase(for: operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getUpdatedFromDatabase(for operation: Operation.Type, defaultVal: Bool) -> Bool{
|
||||||
|
let key = Self.getKey(operation)
|
||||||
|
let valueInDb = UserDefaults.standard.value(forKey: key) as? Bool
|
||||||
|
if valueInDb == nil {
|
||||||
|
// put the value if not already present
|
||||||
|
updateDatabase(for: operation, value: defaultVal)
|
||||||
|
}
|
||||||
|
return valueInDb ?? defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getFromDatabase(for operation: Operation.Type) -> Bool {
|
||||||
|
let key = Self.getKey(operation)
|
||||||
|
return UserDefaults.standard.bool(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
SideStore/Utils/importexport/ImportExport.swift
Normal file
135
SideStore/Utils/importexport/ImportExport.swift
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
//
|
||||||
|
// ImportExport.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 07/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
class ImportExport {
|
||||||
|
|
||||||
|
private static var documentPickerHandler: DocumentPickerHandler?
|
||||||
|
|
||||||
|
public static func getPreviousBackupURL(_ backupURL: URL) -> URL {
|
||||||
|
let backupParentDirectory = backupURL.deletingLastPathComponent()
|
||||||
|
let backupName = backupURL.lastPathComponent
|
||||||
|
let backupBakURL = backupParentDirectory.appendingPathComponent("\(backupName).bak")
|
||||||
|
return backupBakURL
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renames the existing backup contents at `backupURL` to `<foldername>.bak`.
|
||||||
|
private static func renameBackupContents(at backupURL: URL) throws {
|
||||||
|
|
||||||
|
// rename backup to backup.bak dir only if backup dir exists
|
||||||
|
guard FileManager.default.fileExists(atPath: backupURL.path) else { return }
|
||||||
|
|
||||||
|
let backupBakURL = getPreviousBackupURL(backupURL)
|
||||||
|
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
if fileManager.fileExists(atPath: backupBakURL.path) {
|
||||||
|
try fileManager.removeItem(at: backupBakURL) // Remove any existing .bak directory
|
||||||
|
}
|
||||||
|
|
||||||
|
try fileManager.moveItem(at: backupURL, to: backupBakURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles importing new backup data into the designated backup directory.
|
||||||
|
private static func importBackupContents(from documentPickerURL: URL, to backupURL: URL) throws {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
|
// Ensure the backup directory exists.
|
||||||
|
if !fileManager.fileExists(atPath: backupURL.path) {
|
||||||
|
try fileManager.createDirectory(at: backupURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Backup URL: \(backupURL)")
|
||||||
|
print("Document Picker URL: \(documentPickerURL)")
|
||||||
|
|
||||||
|
// Enumerate the contents of the selected directory and copy them to the backup directory.
|
||||||
|
let selectedContents = try fileManager.contentsOfDirectory(
|
||||||
|
at: documentPickerURL,
|
||||||
|
includingPropertiesForKeys: nil,
|
||||||
|
options: .skipsHiddenFiles
|
||||||
|
)
|
||||||
|
for itemURL in selectedContents {
|
||||||
|
let destinationURL = backupURL.appendingPathComponent(itemURL.lastPathComponent)
|
||||||
|
|
||||||
|
// Remove the existing file if it exists at the destination.
|
||||||
|
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||||
|
try fileManager.removeItem(at: destinationURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the item.
|
||||||
|
try fileManager.copyItem(at: itemURL, to: destinationURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func importBackup(presentingViewController: UIViewController,
|
||||||
|
for installedApp: InstalledApp,
|
||||||
|
completionHandler: @escaping (Result<Void, Error>) -> Void){
|
||||||
|
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else {
|
||||||
|
return completionHandler(.failure(OperationError.invalidParameters("Error: Backup directory URL not found.")))
|
||||||
|
}
|
||||||
|
|
||||||
|
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder], asCopy: false)
|
||||||
|
documentPicker.allowsMultipleSelection = false
|
||||||
|
|
||||||
|
// Create a handler and set it as the delegate
|
||||||
|
Self.documentPickerHandler = DocumentPickerHandler { selectedURL in
|
||||||
|
guard let selectedURL = selectedURL else {
|
||||||
|
return completionHandler(.failure( OperationError.cancelled))
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve symlinks if any, so that prefix match works
|
||||||
|
let appUserDataDir = FileManager.default.documentsDirectory.resolvingSymlinksInPath()
|
||||||
|
guard selectedURL.resolvingSymlinksInPath().path.hasPrefix(appUserDataDir.path) else {
|
||||||
|
return completionHandler(.failure(
|
||||||
|
OperationError.forbidden(failureReason: "Selected backup data directory is not within the app's user data directory"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Rename existing backup contents to `<foldername>.bak`.
|
||||||
|
try Self.renameBackupContents(at: backupURL)
|
||||||
|
|
||||||
|
// Import the contents of the selected folder into the backup directory.
|
||||||
|
try Self.importBackupContents(from: selectedURL, to: backupURL)
|
||||||
|
|
||||||
|
print("Backup imported successfully to:", backupURL.path)
|
||||||
|
return completionHandler(.success(()))
|
||||||
|
} catch {
|
||||||
|
print("Backup Error:", error)
|
||||||
|
return completionHandler(.failure( OperationError.invalidParameters(error.localizedDescription)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
documentPicker.delegate = Self.documentPickerHandler
|
||||||
|
// Present the picker
|
||||||
|
presentingViewController.present(documentPicker, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AssociatedKeys {
|
||||||
|
static var documentPickerHandler = "documentPickerHandler"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentPickerHandler: NSObject, UIDocumentPickerDelegate {
|
||||||
|
private let completion: (URL?) -> Void
|
||||||
|
|
||||||
|
init(completion: @escaping (URL?) -> Void) {
|
||||||
|
self.completion = completion
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
|
completion(urls.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
SideStore/Utils/iostreams/ConsoleLog.swift
Normal file
71
SideStore/Utils/iostreams/ConsoleLog.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// ConsoleLog.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 25/11/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ConsoleLog {
|
||||||
|
private static let CONSOLE_LOGS_DIRECTORY = "ConsoleLogs"
|
||||||
|
private static let CONSOLE_LOG_NAME_PREFIX = "console"
|
||||||
|
private static let CONSOLE_LOG_EXTN = ".log"
|
||||||
|
|
||||||
|
private lazy var consoleLogger: ConsoleLogger = {
|
||||||
|
let logFileHandle = createLogFileHandle()
|
||||||
|
let fileOutputStream = FileOutputStream(logFileHandle)
|
||||||
|
|
||||||
|
return UnBufferedConsoleLogger(stream: fileOutputStream)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var consoleLogsDir: URL = {
|
||||||
|
// create a directory for console logs
|
||||||
|
let docsDir = FileManager.default.documentsDirectory
|
||||||
|
let consoleLogsDir = docsDir.appendingPathComponent(ConsoleLog.CONSOLE_LOGS_DIRECTORY)
|
||||||
|
if !FileManager.default.fileExists(atPath: consoleLogsDir.path) {
|
||||||
|
try! FileManager.default.createDirectory(at: consoleLogsDir, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
return consoleLogsDir
|
||||||
|
}()
|
||||||
|
|
||||||
|
public lazy var logName: String = {
|
||||||
|
logFileURL.lastPathComponent
|
||||||
|
}()
|
||||||
|
|
||||||
|
public lazy var logFileURL: URL = {
|
||||||
|
// get current timestamp
|
||||||
|
let currentTime = Date()
|
||||||
|
let dateTimeStamp = DateTimeUtil.getDateInTimeStamp(date: currentTime)
|
||||||
|
|
||||||
|
// create a log file with the current timestamp
|
||||||
|
let logName = DateTimeUtil.getTimeStampSuffixedFileName(
|
||||||
|
fileName: ConsoleLog.CONSOLE_LOG_NAME_PREFIX,
|
||||||
|
timestamp: dateTimeStamp,
|
||||||
|
extn: ConsoleLog.CONSOLE_LOG_EXTN
|
||||||
|
)
|
||||||
|
let logFileURL = consoleLogsDir.appendingPathComponent(logName)
|
||||||
|
return logFileURL
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
private func createLogFileHandle() -> FileHandle {
|
||||||
|
if !FileManager.default.fileExists(atPath: logFileURL.path) {
|
||||||
|
FileManager.default.createFile(atPath: logFileURL.path, contents: nil, attributes: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the file handle
|
||||||
|
return try! FileHandle(forWritingTo: logFileURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCapturing() {
|
||||||
|
consoleLogger.startCapturing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCapturing() {
|
||||||
|
consoleLogger.stopCapturing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
166
SideStore/Utils/iostreams/ConsoleLogger.swift
Normal file
166
SideStore/Utils/iostreams/ConsoleLogger.swift
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
//
|
||||||
|
// ConsoleCapture.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 25/11/24.
|
||||||
|
// Copyright © 2024 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol ConsoleLogger{
|
||||||
|
func startCapturing()
|
||||||
|
func stopCapturing()
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AbstractConsoleLogger<T: OutputStream>: ConsoleLogger{
|
||||||
|
var outPipe: Pipe?
|
||||||
|
var errPipe: Pipe?
|
||||||
|
|
||||||
|
var outputHandle: FileHandle?
|
||||||
|
var errorHandle: FileHandle?
|
||||||
|
|
||||||
|
var originalStdout: Int32?
|
||||||
|
var originalStderr: Int32?
|
||||||
|
|
||||||
|
let ostream: T
|
||||||
|
|
||||||
|
let writeQueue = DispatchQueue(label: "async-write-queue")
|
||||||
|
|
||||||
|
public init(stream: T) throws {
|
||||||
|
// Since swift doesn't support compile time abstract classes Instantiation checking,
|
||||||
|
// we are using runtime check to prevent direct instantiation :(
|
||||||
|
if Self.self === AbstractConsoleLogger.self {
|
||||||
|
throw AbstractClassError.abstractInitializerInvoked
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ostream = stream
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
stopCapturing()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startCapturing() { // made it public coz, let client ask for capturing
|
||||||
|
|
||||||
|
// if already initialized within current instance, bail out
|
||||||
|
guard outPipe == nil, errPipe == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new pipes for stdout and stderr
|
||||||
|
self.outPipe = Pipe()
|
||||||
|
self.errPipe = Pipe()
|
||||||
|
|
||||||
|
outputHandle = self.outPipe?.fileHandleForReading
|
||||||
|
errorHandle = self.errPipe?.fileHandleForReading
|
||||||
|
|
||||||
|
// Store original file descriptors
|
||||||
|
originalStdout = dup(STDOUT_FILENO)
|
||||||
|
originalStderr = dup(STDERR_FILENO)
|
||||||
|
|
||||||
|
// Redirect stdout and stderr to our pipes
|
||||||
|
dup2(self.outPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDOUT_FILENO)
|
||||||
|
dup2(self.errPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDERR_FILENO)
|
||||||
|
|
||||||
|
// Setup readability handlers for raw data
|
||||||
|
setupReadabilityHandler(for: outputHandle, isError: false)
|
||||||
|
setupReadabilityHandler(for: errorHandle, isError: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupReadabilityHandler(for handle: FileHandle?, isError: Bool) {
|
||||||
|
handle?.readabilityHandler = { [weak self] handle in
|
||||||
|
let data = handle.availableData
|
||||||
|
if !data.isEmpty {
|
||||||
|
self?.writeQueue.async {
|
||||||
|
try? self?.writeData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to original std stream
|
||||||
|
if let originalFD = isError ? self?.originalStderr : self?.originalStdout {
|
||||||
|
data.withUnsafeBytes { (bufferPointer) -> Void in
|
||||||
|
if let baseAddress = bufferPointer.baseAddress, bufferPointer.count > 0 {
|
||||||
|
write(originalFD, baseAddress, bufferPointer.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeData(_ data: Data) throws {
|
||||||
|
throw AbstractClassError.abstractMethodInvoked
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCapturing() {
|
||||||
|
ostream.close()
|
||||||
|
|
||||||
|
// Restore original stdout and stderr
|
||||||
|
if let stdout = originalStdout {
|
||||||
|
dup2(stdout, STDOUT_FILENO)
|
||||||
|
close(stdout)
|
||||||
|
}
|
||||||
|
if let stderr = originalStderr {
|
||||||
|
dup2(stderr, STDERR_FILENO)
|
||||||
|
close(stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
outPipe?.fileHandleForReading.readabilityHandler = nil
|
||||||
|
errPipe?.fileHandleForReading.readabilityHandler = nil
|
||||||
|
outPipe = nil
|
||||||
|
errPipe = nil
|
||||||
|
outputHandle = nil
|
||||||
|
errorHandle = nil
|
||||||
|
originalStdout = nil
|
||||||
|
originalStderr = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class UnBufferedConsoleLogger<T: OutputStream>: AbstractConsoleLogger<T> {
|
||||||
|
|
||||||
|
required override init(stream: T) {
|
||||||
|
// cannot throw abstractInitializerInvoked, so need to override else client needs to handle it unnecessarily
|
||||||
|
try! super.init(stream: stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writeData(_ data: Data) throws {
|
||||||
|
// directly write data to the stream without buffering
|
||||||
|
ostream.write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BufferedConsoleLogger<T: OutputStream>: AbstractConsoleLogger<T> {
|
||||||
|
|
||||||
|
// Buffer size (bytes) and storage
|
||||||
|
private let maxBufferSize: Int
|
||||||
|
private var bufferedData = Data()
|
||||||
|
|
||||||
|
required init(stream: T, bufferSize: Int = 1024) {
|
||||||
|
self.maxBufferSize = bufferSize
|
||||||
|
try! super.init(stream: stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writeData(_ data: Data) throws {
|
||||||
|
// Append data to buffer
|
||||||
|
self.bufferedData.append(data)
|
||||||
|
|
||||||
|
// Check if the buffer is full and flush
|
||||||
|
if self.bufferedData.count >= self.maxBufferSize {
|
||||||
|
self.flushBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flushBuffer() {
|
||||||
|
// Write all buffered data to the stream
|
||||||
|
ostream.write(bufferedData)
|
||||||
|
bufferedData.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func stopCapturing() {
|
||||||
|
// Flush buffer and close the file handles first
|
||||||
|
flushBuffer()
|
||||||
|
super.stopCapturing()
|
||||||
|
}
|
||||||
|
}
|
||||||
93
SideStore/Utils/pagination/PaginationDataHolder.swift
Normal file
93
SideStore/Utils/pagination/PaginationDataHolder.swift
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// PaginationDataHolder.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 09/01/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class PaginationDataHolder {
|
||||||
|
|
||||||
|
public let itemsPerPage: UInt
|
||||||
|
public private(set) var currentPageindex: UInt
|
||||||
|
|
||||||
|
init(itemsPerPage: UInt, startPageIndex: UInt = 0) {
|
||||||
|
self.itemsPerPage = itemsPerPage
|
||||||
|
self.currentPageindex = startPageIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
init(other: PaginationDataHolder) {
|
||||||
|
self.itemsPerPage = other.itemsPerPage
|
||||||
|
self.currentPageindex = other.currentPageindex
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PageLimitResult{
|
||||||
|
case null
|
||||||
|
case empty
|
||||||
|
case current
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePageIndexForDirection(_ direction: Direction, itemsCount: Int) -> Bool {
|
||||||
|
|
||||||
|
var targetPageIndex = Int(currentPageindex)
|
||||||
|
let availablePages = UInt(ceil(Double(itemsCount) / Double(itemsPerPage)))
|
||||||
|
|
||||||
|
switch(direction){
|
||||||
|
case .up:
|
||||||
|
targetPageIndex -= 1
|
||||||
|
case .down:
|
||||||
|
targetPageIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let isUpdateValid = (targetPageIndex >= 0 && targetPageIndex < availablePages)
|
||||||
|
|
||||||
|
if isUpdateValid{
|
||||||
|
self.currentPageindex = UInt(targetPageIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isUpdateValid
|
||||||
|
}
|
||||||
|
|
||||||
|
public func nextPage<T>(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
|
||||||
|
return targetPage(for: .down, inItems: inItems, whenUnavailable: whenUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func prevPage<T>(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
|
||||||
|
return targetPage(for: .up, inItems: inItems, whenUnavailable: whenUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func targetPage<T>(for direction: Direction, inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
|
||||||
|
if updatePageIndexForDirection(direction, itemsCount: inItems.count){
|
||||||
|
return currentPage(inItems: inItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch whenUnavailable {
|
||||||
|
case .null:
|
||||||
|
return nil // null was requested
|
||||||
|
case .empty:
|
||||||
|
return [] // empty list was requested
|
||||||
|
case .current:
|
||||||
|
return currentPage(inItems: inItems) // Stay on the current page and return the same items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func currentPage<T>(inItems items: [T]) -> [T] {
|
||||||
|
let count = UInt(items.count)
|
||||||
|
|
||||||
|
if(count == 0) { return items }
|
||||||
|
|
||||||
|
// since we operate on any input items list at any time,
|
||||||
|
// we set currentPageIndex as last availablePage if out of bounds
|
||||||
|
let availablePages = UInt(ceil(Double(count) / Double(itemsPerPage)))
|
||||||
|
|
||||||
|
self.currentPageindex = min(availablePages-1, currentPageindex)
|
||||||
|
|
||||||
|
let startIndex = currentPageindex * itemsPerPage
|
||||||
|
let estimatedEndIndex = startIndex + (itemsPerPage-1)
|
||||||
|
let endIndex: UInt = min(count-1, estimatedEndIndex)
|
||||||
|
let currentPageEntries = items[Int(startIndex) ... Int(endIndex)]
|
||||||
|
return Array(currentPageEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule SideStore/apps-v2.json updated: 9166c28c45...e0914d3463
Submodule SideStore/minimuxer updated: a23abf6d1c...baa22acbff
127
update_apps.py
127
update_apps.py
@@ -4,16 +4,28 @@ import os
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
SIDESTORE_BUNDLE_ID = "com.SideStore.SideStore"
|
||||||
|
|
||||||
# Set environment variables with default values
|
# Set environment variables with default values
|
||||||
VERSION_IPA = os.getenv("VERSION_IPA", "0.0.0")
|
VERSION_IPA = os.getenv("VERSION_IPA")
|
||||||
VERSION_DATE = os.getenv("VERSION_DATE", "2000-12-18T00:00:00Z")
|
VERSION_DATE = os.getenv("VERSION_DATE")
|
||||||
BETA = os.getenv("BETA", "true").lower() == "true" # Convert to boolean
|
RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL")
|
||||||
COMMIT_ID = os.getenv("COMMIT_ID", "1234567")
|
COMMIT_ID = os.getenv("COMMIT_ID")
|
||||||
SIZE = int(os.getenv("SIZE", "0")) # Convert to integer
|
SIZE = os.getenv("SIZE")
|
||||||
SHA256 = os.getenv("SHA256", "")
|
SHA256 = os.getenv("SHA256")
|
||||||
LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION", "Invalid Update")
|
LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION")
|
||||||
DOWNLOAD_URL = os.getenv("DOWNLOAD_URL", "https://github.com/SideStore/SideStore/releases/download/0.0.0/SideStore.ipa")
|
DOWNLOAD_URL = os.getenv("DOWNLOAD_URL")
|
||||||
BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER", "com.SideStore.SideStore")
|
BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER", SIDESTORE_BUNDLE_ID)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
# DOWNLOAD_URL = os.getenv("DOWNLOAD_URL", "https://github.com/SideStore/SideStore/releases/download/0.0.0/SideStore.ipa")
|
||||||
|
|
||||||
# Check if input file is provided
|
# Check if input file is provided
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
@@ -24,9 +36,10 @@ input_file = sys.argv[1]
|
|||||||
print(f"Input File: {input_file}")
|
print(f"Input File: {input_file}")
|
||||||
|
|
||||||
# Debugging the environment variables
|
# Debugging the environment variables
|
||||||
|
print(" ====> Required parameter list <====")
|
||||||
print("Version:", VERSION_IPA)
|
print("Version:", VERSION_IPA)
|
||||||
print("Version Date:", VERSION_DATE)
|
print("Version Date:", VERSION_DATE)
|
||||||
print("Beta:", BETA)
|
print("ReleaseChannel:", RELEASE_CHANNEL)
|
||||||
print("Commit ID:", COMMIT_ID)
|
print("Commit ID:", COMMIT_ID)
|
||||||
print("Size:", SIZE)
|
print("Size:", SIZE)
|
||||||
print("Sha256:", SHA256)
|
print("Sha256:", SHA256)
|
||||||
@@ -41,50 +54,70 @@ except Exception as e:
|
|||||||
print(f"Error reading the input file: {e}")
|
print(f"Error reading the input file: {e}")
|
||||||
sys.exit(1)
|
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):
|
||||||
|
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
|
# Process the JSON data
|
||||||
updated = False
|
updated = False
|
||||||
for app in data.get("apps", []):
|
for app in data.get("apps", []):
|
||||||
if app.get("bundleIdentifier") == BUNDLE_IDENTIFIER:
|
if app.get("bundleIdentifier") == BUNDLE_IDENTIFIER:
|
||||||
# Update app-level metadata
|
if RELEASE_CHANNEL == "stable" :
|
||||||
app.update({
|
# Update app-level metadata for store front page
|
||||||
"version": VERSION_IPA,
|
app.update({
|
||||||
"versionDate": VERSION_DATE,
|
"version": VERSION_IPA,
|
||||||
"beta": BETA,
|
"versionDate": VERSION_DATE,
|
||||||
"commitID": COMMIT_ID,
|
"size": SIZE,
|
||||||
"size": SIZE,
|
"sha256": SHA256,
|
||||||
"sha256": SHA256,
|
"localizedDescription": LOCALIZED_DESCRIPTION,
|
||||||
"localizedDescription": LOCALIZED_DESCRIPTION,
|
"downloadURL": DOWNLOAD_URL,
|
||||||
"downloadURL": DOWNLOAD_URL,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
# Process the versions array
|
# Process the versions array
|
||||||
versions = app.get("versions", [])
|
channels = app.get("releaseChannels", {})
|
||||||
if not versions or not (versions[0].get("version") == VERSION_IPA and versions[0].get("beta") == BETA):
|
if not channels:
|
||||||
# Prepend a new version if no matching version exists
|
app["releaseChannels"] = channels
|
||||||
new_version = {
|
|
||||||
"version": VERSION_IPA,
|
# create an entry and keep ready
|
||||||
"date": VERSION_DATE,
|
new_version = {
|
||||||
"localizedDescription": LOCALIZED_DESCRIPTION,
|
"version": VERSION_IPA,
|
||||||
"downloadURL": DOWNLOAD_URL,
|
"date": VERSION_DATE,
|
||||||
"beta": BETA,
|
"localizedDescription": LOCALIZED_DESCRIPTION,
|
||||||
"commitID": COMMIT_ID,
|
"downloadURL": DOWNLOAD_URL,
|
||||||
"size": SIZE,
|
"size": SIZE,
|
||||||
"sha256": SHA256,
|
"sha256": SHA256,
|
||||||
}
|
}
|
||||||
versions.insert(0, new_version)
|
# 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:
|
else:
|
||||||
# Update the existing version object
|
# Update the existing TOP version object entry
|
||||||
versions[0].update({
|
channels[RELEASE_CHANNEL][0] = new_version
|
||||||
"version": VERSION_IPA,
|
|
||||||
"date": VERSION_DATE,
|
|
||||||
"localizedDescription": LOCALIZED_DESCRIPTION,
|
|
||||||
"downloadURL": DOWNLOAD_URL,
|
|
||||||
"beta": BETA,
|
|
||||||
"commitID": COMMIT_ID,
|
|
||||||
"size": SIZE,
|
|
||||||
"sha256": SHA256,
|
|
||||||
})
|
|
||||||
app["versions"] = versions
|
|
||||||
updated = True
|
updated = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#include "../Build.xcconfig"
|
#include "../Build.xcconfig"
|
||||||
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
|
||||||
|
|
||||||
// retain the non-suffixed bundleID set in $GROUP_ID for APP_GROUP_IDENTIFIER
|
|
||||||
APP_GROUP_IDENTIFIER = $(GROUP_ID)
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
#include "../Build.xcconfig"
|
#include "../Build.xcconfig"
|
||||||
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltWidget
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltWidget
|
||||||
|
|
||||||
// retain the non-suffixed bundleID set in $GROUP_ID for APP_GROUP_IDENTIFIER
|
|
||||||
APP_GROUP_IDENTIFIER = $(GROUP_ID)
|
|
||||||
Reference in New Issue
Block a user