Compare commits

...

63 Commits

Author SHA1 Message Date
Fabian Thies
2939919ddb Fix disabling horizontal scroll on onboarding screens and made showing only certain steps more reusable 2023-05-20 22:16:55 +02:00
Fabian Thies
38a1c7eef6 Fix rebase issues 2023-05-20 20:05:36 +02:00
Fabian Thies
f6252c3a8b Fix trusted sources being enabled in onboarding process regardless of user choice 2023-05-20 19:25:16 +02:00
Fabian Thies
653d80b88e Add onboarding screens for an easy setup of SideStore 2023-05-20 19:25:14 +02:00
Fabian Thies
89609ad35c [ADD] UI for writing an app review and submit an app rating 2023-05-20 19:24:18 +02:00
Fabian Thies
2211013e57 [CHANGE] UI fixes and SwiftUI previews for easier development 2023-05-20 19:24:18 +02:00
Fabian Thies
f206ee1406 [ADD] Refresh all apps functionality in MyAppsView 2023-05-20 19:24:18 +02:00
Fabian Thies
00dc9b36af [FIX] STDOUT output not visible in Xcode console 2023-05-20 19:24:18 +02:00
Fabian Thies
24146cef90 [ADD] LocalConsole showing STDOUT and STDERR 2023-05-20 19:24:18 +02:00
Fabian Thies
c46a50ec58 [FIX] App compatibility info 2023-05-20 19:24:18 +02:00
Fabian Thies
de7e909c01 [ADD] Debug entries for refresh attempts, sending feedback, advanced settings, and resetting the pairing file 2023-05-20 19:24:18 +02:00
Fabian Thies
fbc754d8b7 [ADD] Error log view 2023-05-20 19:24:18 +02:00
Fabian Thies
767d878051 [FIX] Various UI issues 2023-05-20 19:24:18 +02:00
Fabian Thies
132b140af2 [ADD] App report button and trusted source badge in app detail view 2023-05-20 19:24:18 +02:00
Fabian Thies
df7d8871ff [FIX] AppIDsView and authentication workflow 2023-05-20 19:24:18 +02:00
Fabian Thies
ca2398e4c7 [FIX] Full screen app screenshot previews 2023-05-20 19:24:18 +02:00
Fabian Thies
b8f02d2152 [FIX] Accent color 2023-05-20 19:24:18 +02:00
Fabian Thies
e85876cd24 [CHANGE] Overhaul of the AppDetailView with version history, reviews & ratings, and app information 2023-05-20 19:24:16 +02:00
Fabian Thies
3f06a53058 [UPDATE] AppPillButton dimensions and expiration text 2023-05-20 19:22:47 +02:00
Fabian Thies
4ee053a4f9 [FIX] Show App IDs button only if user is logged in with their Apple ID 2023-05-20 19:22:47 +02:00
Fabian Thies
e5369524ce [ADD] Load and show trusted sources with option to add them to the app 2023-05-20 19:22:47 +02:00
Fabian Thies
77465cebd0 [ADD] Credits section in SettingsView 2023-05-20 19:22:47 +02:00
Fabian Thies
f90bf3bfcf [CHANGE] Extracted all strings into the Localizable.strings 2023-05-20 19:22:47 +02:00
Fabian Thies
0000610b9d [FIX] Text alignment in SettingsView 2023-05-20 19:22:47 +02:00
Fabian Thies
c7e095583d [ADD] Hint for new users who don't have any sources 2023-05-20 19:22:47 +02:00
Fabian Thies
a725f3e9cc [ADD] AppScreenshot view with ImageProcessor to automatically rotate landscape screenshots 2023-05-20 19:22:47 +02:00
Fabian Thies
b5dea18073 [FIX] Issues introduced by changes to the AltSource specification. 2023-05-20 19:22:47 +02:00
Fabian Thies
b9b309e603 [UPDATE] Translations (#7)
This PR merges all the new translations made on the SideStore weblate instance (https://translate.sidestore.io/projects/sidestore/app).

New translations:
- French
- Korean

Updated translations:
- Spanish

Co-authored-by: bogotesr <bogotesr@gmail.com>
Co-authored-by: GABO1423 <35014183+GABO1423@users.noreply.github.com>
Co-authored-by: Joss Laymon <71040782+bogotesr@users.noreply.github.com>
Co-authored-by: mindfreakdev <shost212@gmail.com>
Co-authored-by: Python <rjp2030@proton.me>
Co-authored-by: Testi Cules <ervd516@gmail.com>
2023-05-20 19:22:47 +02:00
Fabian Thies
15f1be0aa8 [FIX] Changes made by Xcode 14 after building the app 2023-05-20 19:22:47 +02:00
Upal
ffd80ce0b4 Added Hindi Language (#5)
* Added Hindi Language
2023-05-20 19:22:47 +02:00
mindfreakdev
350891ee2a Added Dutch Language 2023-05-20 19:22:47 +02:00
mindfreakdev
5dec1cd561 Added Ukrainian Language 2023-05-20 19:22:47 +02:00
mindfreakdev
c4d235d742 Added Ukrainian Language 2023-05-20 19:22:47 +02:00
Gabriel Morazán
cdc6675dd5 Screen Crunch sucks
Signed-off-by: Gabriel Morazán <35014183+GABO1423@users.noreply.github.com>
2023-05-20 19:22:47 +02:00
GABO1423
85635bb26e Spanish Translation Tweaks 2023-05-20 19:22:47 +02:00
bogotesr
3be0a4a89c Add es-419 and finish adding support for the translations
Added Latin American Spanish (probably not the best translation)

Made everything reference the swiftgen stuff rather than having strings
2023-05-20 19:22:47 +02:00
Fabian Thies
47e47fb3cf [CHANGE] Extracted some example strings and replaced them by generated localized strings 2023-05-20 19:22:47 +02:00
Fabian Thies
48903034b6 [ADD] SwiftGen configuration and generated files 2023-05-20 19:22:47 +02:00
Fabian Thies
6952218ee7 [ADD] Empty Localizable.strings 2023-05-20 19:22:47 +02:00
Fabian Thies
80146c1e03 [WIP] AppScreenshot view with ImageProcessor to automatically rotate landscape images. Possible through my fork of the AsyncImage framework. 2023-05-20 19:22:47 +02:00
Fabian Thies
642ae996c9 [WIP] Fetch trusted sources in SourcesView 2023-05-20 19:22:47 +02:00
Fabian Thies
8040636aa5 [WIP] AppIDs view in My Apps section 2023-05-20 19:22:47 +02:00
Fabian Thies
731fcfaca7 [ADD] Badge in AppDetailView for apps from the official source and (WIP) trusted sources 2023-05-20 19:22:47 +02:00
Fabian Thies
708fb3fccd [ADD] Hint view in MyAppsView telling the user about where to find updates in the future if no updates are available 2023-05-20 19:22:47 +02:00
Fabian Thies
9f429fb068 [FIX] App permission icon color 2023-05-20 19:22:47 +02:00
Fabian Thies
29fc693f4d [ADD] Show source name and external url domain in NewsItemView 2023-05-20 19:22:47 +02:00
Fabian Thies
6f373ad305 [ADD] Full-screen app screenshot preview 2023-05-20 19:22:47 +02:00
Fabian Thies
c069d779d9 [CHANGE] Replace system image name strings with SFSymbols 2023-05-20 19:22:47 +02:00
Fabian Thies
cd88970a22 [ADD] Dependency: SFSafeSymbols 2023-05-20 19:22:47 +02:00
Fabian Thies
6b6708e43c [ADD] WIP: Promoted category cards and app list filter button in BrowseView 2023-05-20 19:22:47 +02:00
Fabian Thies
9206eeb9e3 [FIX] AccentColor in dark mode 2023-05-20 19:22:47 +02:00
Fabian Thies
080bbb3c51 [ADD] Carousel for SideStore-specific announcements in NewsView 2023-05-20 19:22:47 +02:00
Fabian Thies
ea2c862900 [ADD] WIP: Add My Apps view with support for sideloading new apps, refreshing installed apps and much more 2023-05-20 19:22:45 +02:00
Fabian Thies
4fe72ea113 [CHANGE] Fixed the AppRowView background blur effect 2023-05-20 19:22:13 +02:00
Fabian Thies
c486a62b50 [ADD] Backported dismiss() environment variable to let views dismiss themselves 2023-05-20 19:22:13 +02:00
Fabian Thies
3ce4451da4 [ADD] Search bar for BrowseView on iOS 15 2023-05-20 19:22:13 +02:00
Fabian Thies
294ba12391 [CHANGE] Fetch news when NewsView appears 2023-05-20 19:22:13 +02:00
Fabian Thies
4a3343fe61 Improved app detail view 2023-05-20 19:22:13 +02:00
Fabian Thies
d1e6ddd435 [ADD] Authentication view for connecting SideStore to an Apple ID 2023-05-20 19:22:13 +02:00
Fabian Thies
3e0379dc70 [WIP] Fixed the app permissions grid in AppDetailView 2023-05-20 19:22:12 +02:00
Fabian Thies
d99674f8bd [ADD] Expandable app and version description texts 2023-05-20 19:21:24 +02:00
Fabian Thies
ca7acc17da [ADD] iOS 13 compatible AsyncImage implementation with cache 2023-05-20 19:21:21 +02:00
Fabian Thies
16a8bce102 [ADD] News, Browse and Settings views ported to SwiftUI
This commit contains WIP SwiftUI versions of most of the views in SideStore.
2023-05-20 19:20:32 +02:00
79 changed files with 7784 additions and 119 deletions

View File

@@ -17,6 +17,74 @@
191E607E290B2EA7001A3B7C /* jplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 191E5FCF290A651D001A3B7C /* jplist.c */; }; 191E607E290B2EA7001A3B7C /* jplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 191E5FCF290A651D001A3B7C /* jplist.c */; };
1920B04F2924AC8300744F60 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1920B04E2924AC8300744F60 /* Settings.bundle */; }; 1920B04F2924AC8300744F60 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1920B04E2924AC8300744F60 /* Settings.bundle */; };
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; }; 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 */; }; 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; };
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
9922FFEC29B501C50020F868 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9922FFEB29B501C50020F868 /* Starscream */; }; 9922FFEC29B501C50020F868 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9922FFEB29B501C50020F868 /* Starscream */; };
@@ -511,6 +579,69 @@
191E5FD1290A651D001A3B7C /* jsmn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = jsmn.h; path = Dependencies/libplist/src/jsmn.h; 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>"; }; 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>"; }; 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; }; 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; }; 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; }; 99F87D1729D8E4C900B40039 /* minimuxer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = minimuxer.swift; path = Dependencies/minimuxer/minimuxer.swift; sourceTree = SOURCE_ROOT; };
@@ -849,6 +980,7 @@
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -899,6 +1031,7 @@
B3C395F1284F2DE700DA9E2F /* KeychainAccess in Frameworks */, B3C395F1284F2DE700DA9E2F /* KeychainAccess in Frameworks */,
99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */, 99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */,
4879A95F2861046500FC1BBD /* AltSign in Frameworks */, 4879A95F2861046500FC1BBD /* AltSign in Frameworks */,
1F07F5692955D3EC00F7BE95 /* SFSafeSymbols in Frameworks */,
B39575F5284F29E20080B4FF /* Roxas.framework in Frameworks */, B39575F5284F29E20080B4FF /* Roxas.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -918,16 +1051,21 @@
B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */, B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */,
191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */, 191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */,
19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */, 19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */,
1F1295812989B51F0048FCB9 /* ExpandableText in Frameworks */,
19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */, 19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */,
B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */, B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */,
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */, D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */, B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */,
9922FFEC29B501C50020F868 /* Starscream in Frameworks */, 9922FFEC29B501C50020F868 /* Starscream in Frameworks */,
1FF8C61B2A1782F10041352C /* Reachability in Frameworks */,
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */, D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */, 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */,
B3C395F4284F35DD00DA9E2F /* Nuke in Frameworks */, B3C395F4284F35DD00DA9E2F /* Nuke in Frameworks */,
1F74FF1E295263510047C051 /* AsyncImage in Frameworks */,
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */, BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
B3C395F7284F362400DA9E2F /* AppCenterAnalytics in Frameworks */, B3C395F7284F362400DA9E2F /* AppCenterAnalytics in Frameworks */,
1F07F5672955D16A00F7BE95 /* SFSafeSymbols in Frameworks */,
1FFA56C52999978C0011B6F5 /* LocalConsole in Frameworks */,
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */, BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -971,6 +1109,193 @@
path = "libimobiledevice-glue/src"; path = "libimobiledevice-glue/src";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
1F07F553295458D800F7BE95 /* Generated */ = {
isa = PBXGroup;
children = (
1F07F554295458D800F7BE95 /* Assets.swift */,
1F07F555295458D800F7BE95 /* Localizations.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 */ = { 99F87D1429D8E3F100B40039 /* Generated */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -978,6 +1303,7 @@
99F87D1729D8E4C900B40039 /* minimuxer.swift */, 99F87D1729D8E4C900B40039 /* minimuxer.swift */,
); );
name = Generated; name = Generated;
path = minimuxer;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B3146EC7284F580500BBC3FD /* Products */ = { B3146EC7284F580500BBC3FD /* Products */ = {
@@ -1545,6 +1871,14 @@
BFD2476C2284B9A500981D42 /* AltStore */ = { BFD2476C2284B9A500981D42 /* AltStore */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
1FAFC5B72927E06C00B8D837 /* App */,
1FB96FC1292A6D6C007E68D1 /* Helper */,
1F07F553295458D800F7BE95 /* Generated */,
1FB96FEA292C1704007E68D1 /* Manager */,
1FAFC5C22927E17100B8D837 /* Protocols */,
1FAFC5B02927E01400B8D837 /* Views */,
1FB84BA72928E073006A5CF4 /* View Components */,
1F6E08DD29280AF1005059C0 /* View Extensions */,
B39F16112918D7B5002E9404 /* Consts */, B39F16112918D7B5002E9404 /* Consts */,
BF219A7E22CAC431007676A6 /* AltStore.entitlements */, BF219A7E22CAC431007676A6 /* AltStore.entitlements */,
BFD2476D2284B9A500981D42 /* AppDelegate.swift */, BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
@@ -1622,6 +1956,7 @@
BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */, BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */,
BFD247762284B9A700981D42 /* Assets.xcassets */, BFD247762284B9A700981D42 /* Assets.xcassets */,
BF770E6822BD57DD002A40FE /* Silence.m4a */, BF770E6822BD57DD002A40FE /* Silence.m4a */,
1F07F552295455A300F7BE95 /* Localizable.strings */,
); );
path = Resources; path = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1645,6 +1980,9 @@
BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */, BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */,
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */, D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */,
B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */, B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */,
1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */,
1F180F91298E7A1B00D1C98B /* StoreApp+Trusted.swift */,
1F180F93298E7A2500D1C98B /* Source+Trusted.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1944,6 +2282,7 @@
B3C395F0284F2DE700DA9E2F /* KeychainAccess */, B3C395F0284F2DE700DA9E2F /* KeychainAccess */,
4879A95E2861046500FC1BBD /* AltSign */, 4879A95E2861046500FC1BBD /* AltSign */,
99C4EF4C2979132100CB538D /* SemanticVersion */, 99C4EF4C2979132100CB538D /* SemanticVersion */,
1F07F5682955D3EC00F7BE95 /* SFSafeSymbols */,
); );
productName = AltStoreCore; productName = AltStoreCore;
productReference = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; productReference = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */;
@@ -1994,6 +2333,11 @@
B3C395F8284F362400DA9E2F /* AppCenterCrashes */, B3C395F8284F362400DA9E2F /* AppCenterCrashes */,
4879A9612861049C00FC1BBD /* OpenSSL */, 4879A9612861049C00FC1BBD /* OpenSSL */,
9922FFEB29B501C50020F868 /* Starscream */, 9922FFEB29B501C50020F868 /* Starscream */,
1F74FF1D295263510047C051 /* AsyncImage */,
1F07F5662955D16A00F7BE95 /* SFSafeSymbols */,
1F1295802989B51F0048FCB9 /* ExpandableText */,
1FFA56C42999978C0011B6F5 /* LocalConsole */,
1FF8C61A2A1782F10041352C /* Reachability */,
); );
productName = AltStore; productName = AltStore;
productReference = BFD2476A2284B9A500981D42 /* SideStore.app */; productReference = BFD2476A2284B9A500981D42 /* SideStore.app */;
@@ -2005,7 +2349,7 @@
BFD247622284B9A500981D42 /* Project object */ = { BFD247622284B9A500981D42 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1400; LastSwiftUpdateCheck = 1410;
LastUpgradeCheck = 1020; LastUpgradeCheck = 1020;
ORGANIZATIONNAME = SideStore; ORGANIZATIONNAME = SideStore;
TargetAttributes = { TargetAttributes = {
@@ -2053,6 +2397,7 @@
knownRegions = ( knownRegions = (
en, en,
Base, Base,
"es-419",
); );
mainGroup = BFD247612284B9A500981D42; mainGroup = BFD247612284B9A500981D42;
packageReferences = ( packageReferences = (
@@ -2066,6 +2411,11 @@
4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */, 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */,
99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */, 99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
9922FFEA29B501C50020F868 /* XCRemoteSwiftPackageReference "Starscream" */, 9922FFEA29B501C50020F868 /* XCRemoteSwiftPackageReference "Starscream" */,
1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */,
1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */,
1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */,
1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */,
); );
productRefGroup = BFD2476B2284B9A500981D42 /* Products */; productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -2204,6 +2554,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
1F07F550295455A300F7BE95 /* Localizable.strings in Resources */,
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */, BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */, BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */, BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
@@ -2456,46 +2807,82 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
1F6E08E029280B12005059C0 /* SafariView.swift in Sources */,
1F943C6C2927F90400ABE095 /* SettingsView.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
1F66F5C02938F07C00A910CA /* Filterable.swift in Sources */,
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
1F07F556295458D800F7BE95 /* Assets.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.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 */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */,
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */,
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */, D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */, BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
1FB96FC7292A853D007E68D1 /* SourcesView.swift in Sources */,
1F66F5BE2938F06100A910CA /* StoreApp+Filterable.swift in Sources */,
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */, D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */,
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */, D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
BFC57A652416C72400EB891E /* DeactivateAppOperation.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 */, BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */,
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */, BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */, BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */, BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */, BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */, BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
1F0DD84129368056007608A4 /* EnvironmentValues.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */, BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */, BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */,
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */, D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
1FFA56C2299994390011B6F5 /* OutputCapturer.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.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 */, BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */,
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */, BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
1F07F56B2955F11500F7BE95 /* AppScreenshotsPreview.swift in Sources */,
1F981B1329AA101F0014950E /* OnboardingStepView.swift in Sources */,
BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */, BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */, BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
1F981B1729AA34A70014950E /* AppStoreProductView.swift in Sources */,
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */, BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
B3EE16B62925E27D00B3B1F5 /* AnisetteManager.swift in Sources */, B3EE16B62925E27D00B3B1F5 /* AnisetteManager.swift in Sources */,
1FF8C6182A1780C60041352C /* ActivityView.swift in Sources */,
1F943C6E2927F90400ABE095 /* NewsItemView.swift in Sources */,
BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */, BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */,
B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */, B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */,
1F6E08E629280F4B005059C0 /* RatingStars.swift in Sources */,
1F07F557295458D800F7BE95 /* Localizations.swift in Sources */,
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */, BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */,
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */, BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */,
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */, BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
@@ -2506,6 +2893,9 @@
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */, D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */,
B39F16132918D7C5002E9404 /* Consts.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 */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */, BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */,
@@ -2517,20 +2907,36 @@
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */, BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */, B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */,
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */, BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
1F943C6B2927F8F700ABE095 /* NavigationTab.swift in Sources */,
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */, BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */, BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */, D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
1F44634529744E570070E514 /* HintView.swift in Sources */,
1F6E08E429280D1E005059C0 /* PillButtonStyle.swift in Sources */,
1F6284D7295218980060AAD8 /* DocumentPicker.swift in Sources */,
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
1F0DD83F29367F6C007608A4 /* ConnectAppleIDView.swift in Sources */,
1F07F56F2955FB2000F7BE95 /* AppIDsView.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */, D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */,
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */, BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift 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 */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.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 */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
@@ -2541,10 +2947,16 @@
BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */, BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */,
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */, BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.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 */, D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */,
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */, BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
1FA5A6CC298E8FE4007BA946 /* MailComposeView.swift in Sources */,
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */, BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
1FB96FC5292A7251007E68D1 /* BrowseAppPreviewView.swift in Sources */,
BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */, BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */,
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */, BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
); );
@@ -2604,6 +3016,15 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
1F07F552295455A300F7BE95 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
1F07F551295455A300F7BE95 /* en */,
DFEE02F82957998D00518C34 /* es-419 */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
BF580488246A28F9008AE704 /* LaunchScreen.storyboard */ = { BF580488246A28F9008AE704 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
@@ -3075,6 +3496,7 @@
baseConfigurationReference = B3C39607284F4C8400DA9E2F /* Build.xcconfig */; baseConfigurationReference = B3C39607284F4C8400DA9E2F /* Build.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -3143,6 +3565,7 @@
baseConfigurationReference = B3C39607284F4C8400DA9E2F /* Build.xcconfig */; baseConfigurationReference = B3C39607284F4C8400DA9E2F /* Build.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -3207,6 +3630,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements; CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
@@ -3241,6 +3665,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements; CODE_SIGN_ENTITLEMENTS = AltStore/AltStore.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
@@ -3355,6 +3780,46 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference 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" */ = { 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SideStore/AltSign"; repositoryURL = "https://github.com/SideStore/AltSign";
@@ -3448,6 +3913,36 @@
package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */; package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */;
productName = 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 */ = { 4879A95E2861046500FC1BBD /* AltSign */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */; package = 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */;

View File

@@ -18,6 +18,24 @@
"version" : "4.4.2" "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", "identity" : "keychainaccess",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -36,6 +54,15 @@
"version" : "4.2.0" "version" : "4.2.0"
} }
}, },
{
"identity" : "localconsole",
"kind" : "remoteSourceControl",
"location" : "https://github.com/duraidabdul/LocalConsole.git",
"state" : {
"revision" : "2c5d5e018acd4963fe6dfe858f6d6fecef7cbf2f",
"version" : "1.12.1"
}
},
{ {
"identity" : "nuke", "identity" : "nuke",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -63,6 +90,15 @@
"version" : "1.10.1" "version" : "1.10.1"
} }
}, },
{
"identity" : "reachability.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"branch" : "master",
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
}
},
{ {
"identity" : "semanticversion", "identity" : "semanticversion",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -72,6 +108,15 @@
"version" : "0.3.5" "version" : "0.3.5"
} }
}, },
{
"identity" : "sfsafesymbols",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
"state" : {
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
"version" : "4.0.0"
}
},
{ {
"identity" : "sparkle", "identity" : "sparkle",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -0,0 +1,18 @@
//
// 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()
}
}
}

View File

@@ -58,6 +58,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool 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. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()

View File

@@ -0,0 +1,19 @@
//
// 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
}
}

View File

@@ -0,0 +1,17 @@
//
// 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())
}
}

View File

@@ -0,0 +1,19 @@
//
// 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
}
}

View File

@@ -0,0 +1,201 @@
// 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

View File

@@ -0,0 +1,351 @@
// 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

View File

@@ -0,0 +1,72 @@
//
// 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)
}
}

View File

@@ -0,0 +1,254 @@
//
// 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)
}
}

View File

@@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import SwiftUI
import Roxas import Roxas
import EmotionalDamage import EmotionalDamage
import minimuxer import minimuxer
@@ -42,23 +43,41 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
{ {
defer { defer {
// Create destinationViewController now so view controllers can register for receiving Notifications. // Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController // self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
let rootView = RootView()
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
self.destinationViewController = UIHostingController(rootView: rootView)
} }
super.viewDidLoad() super.viewDidLoad()
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
if !UserDefaults.standard.onboardingComplete {
self.showOnboarding()
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else { guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.") self.showOnboarding(enabledSteps: [.pairing])
return return
} }
start_minimuxer_threads(pf) start_minimuxer_threads(pf)
#endif #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? { func fetchPairingFile() -> String? {
let filename = "ALTPairingFile.mobiledevicepairing" let filename = "ALTPairingFile.mobiledevicepairing"

View File

@@ -0,0 +1,80 @@
//
// 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)
}
}

View File

@@ -0,0 +1,56 @@
//
// 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)
}
}
}

View File

@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import SwiftUI
import Roxas import Roxas
import Network import Network
@@ -39,17 +40,17 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
let context: AuthenticatedOperationContext let context: AuthenticatedOperationContext
private weak var presentingViewController: UIViewController? private weak var presentingViewController: UIViewController?
private lazy var navigationController: UINavigationController = { // private lazy var navigationController: UINavigationController = {
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController // let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
if #available(iOS 13.0, *) // if #available(iOS 13.0, *)
{ // {
navigationController.isModalInPresentation = true // navigationController.isModalInPresentation = true
} // }
return navigationController // return navigationController
}() // }()
//
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil) // private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
private var appleIDEmailAddress: String? private var appleIDEmailAddress: String?
private var appleIDPassword: String? private var appleIDPassword: String?
@@ -266,7 +267,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
super.finish(result) super.finish(result)
DispatchQueue.main.async { DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil) // self.navigationController.dismiss(animated: true, completion: nil)
self.dismiss()
} }
} }
} }
@@ -276,7 +278,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
super.finish(result) super.finish(result)
DispatchQueue.main.async { DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil) // self.navigationController.dismiss(animated: true, completion: nil)
self.dismiss()
} }
} }
} }
@@ -287,25 +290,33 @@ private extension AuthenticationOperation
{ {
func present(_ viewController: UIViewController) -> Bool func present(_ viewController: UIViewController) -> Bool
{ {
guard let presentingViewController = self.presentingViewController else { return false } UIApplication.shared.keyWindow?.rootViewController?.present(viewController, animated: true)
// guard let presentingViewController = self.presentingViewController else { return false }
self.navigationController.view.tintColor = .white //
// self.navigationController.view.tintColor = .white
if self.navigationController.viewControllers.isEmpty //
{ // if self.navigationController.viewControllers.isEmpty
guard presentingViewController.presentedViewController == nil else { return false } // {
// guard presentingViewController.presentedViewController == nil else { return false }
self.navigationController.setViewControllers([viewController], animated: false) //
presentingViewController.present(self.navigationController, animated: true, completion: nil) // self.navigationController.setViewControllers([viewController], animated: false)
} // presentingViewController.present(self.navigationController, animated: true, completion: nil)
else // }
{ // else
viewController.navigationItem.leftBarButtonItem = nil // {
self.navigationController.pushViewController(viewController, animated: true) // viewController.navigationItem.leftBarButtonItem = nil
} // self.navigationController.pushViewController(viewController, animated: true)
// }
return true return true
} }
func dismiss() {
if let presentingViewController {
presentingViewController.dismiss(animated: true)
}
// UIApplication.shared.keyWindow?.rootViewController?.presentedViewController?.dismiss(animated: true)
}
} }
private extension AuthenticationOperation private extension AuthenticationOperation
@@ -315,29 +326,29 @@ private extension AuthenticationOperation
func authenticate() func authenticate()
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController let viewController = UIHostingController(rootView: NavigationView {
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in ConnectAppleIDView { appleID, password, completionHandler in
self.authenticate(appleID: appleID, password: password) { (result) in self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result) 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))
}
} }
} })
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(authenticationViewController) if !self.present(viewController)
{ {
completionHandler(.failure(OperationError.notAuthenticated)) completionHandler(.failure(OperationError.notAuthenticated))
} }
@@ -379,8 +390,8 @@ private extension AuthenticationOperation
case .success(let anisetteData): case .success(let anisetteData):
let verificationHandler: ((@escaping (String?) -> Void) -> Void)? let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
if let presentingViewController = self.presentingViewController // if let presentingViewController = self.presentingViewController
{ // {
verificationHandler = { (completionHandler) in verificationHandler = { (completionHandler) in
DispatchQueue.main.async { 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) let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
@@ -406,22 +417,22 @@ private extension AuthenticationOperation
completionHandler(nil) completionHandler(nil)
}) })
if self.navigationController.presentingViewController != nil // if self.navigationController.presentingViewController != nil
{ // {
self.navigationController.present(alertController, animated: true, completion: nil) // self.navigationController.present(alertController, animated: true, completion: nil)
} // }
else // else
{ // {
presentingViewController.present(alertController, animated: true, completion: nil) // presentingViewController.present(alertController, animated: true, completion: nil)
} // }
} }
} }
} // }
else // else
{ // {
// No view controller to present security code alert, so don't provide verificationHandler. // // No view controller to present security code alert, so don't provide verificationHandler.
verificationHandler = nil // verificationHandler = nil
} // }
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in verificationHandler: verificationHandler) { (account, session, error) in
@@ -452,15 +463,15 @@ private extension AuthenticationOperation
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController // let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
//
selectTeamViewController.teams = teams // selectTeamViewController.teams = teams
selectTeamViewController.completionHandler = completionHandler // selectTeamViewController.completionHandler = completionHandler
//
if !self.present(selectTeamViewController) // if !self.present(selectTeamViewController)
{ // {
return completionHandler(.failure(AuthenticationError.noTeam)) // return completionHandler(.failure(AuthenticationError.noTeam))
} // }
} }
} }
} }
@@ -642,20 +653,21 @@ private extension AuthenticationOperation
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void) func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{ {
guard self.shouldShowInstructions else { return completionHandler(false) } return completionHandler(false)
// guard self.shouldShowInstructions else { return completionHandler(false) }
DispatchQueue.main.async { //
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController // DispatchQueue.main.async {
instructionsViewController.showsBottomButton = true // let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
instructionsViewController.completionHandler = { // instructionsViewController.showsBottomButton = true
completionHandler(true) // instructionsViewController.completionHandler = {
} // completionHandler(true)
// }
if !self.present(instructionsViewController) //
{ // if !self.present(instructionsViewController)
completionHandler(false) // {
} // completionHandler(false)
} // }
// }
} }
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void) func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)

View File

@@ -0,0 +1,23 @@
//
// 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) }
}
}

View File

@@ -0,0 +1,22 @@
//
// 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
}
}

View File

@@ -0,0 +1,11 @@
//
// ViewModel.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
protocol ViewModel: ObservableObject {}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFA",
"green" : "0x05",
"red" : "0xA4"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,166 @@
/*
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.";

View File

@@ -0,0 +1,124 @@
/*
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";

View File

@@ -0,0 +1,112 @@
/* 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";

View File

@@ -0,0 +1,128 @@
/*
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" = "मेरी एप्प्स";

View File

@@ -0,0 +1,117 @@
"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" = "설치 중...";

View File

@@ -0,0 +1,128 @@
/*
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";

View File

@@ -0,0 +1,128 @@
/*
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" = "Мої програми";

View File

@@ -0,0 +1,61 @@
//
// 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)
}
}
}
}

View File

@@ -0,0 +1,144 @@
//
// 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)
}
}
}
}

View File

@@ -0,0 +1,53 @@
//
// 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()
// }
//}

View File

@@ -0,0 +1,55 @@
//
// 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()
}
}

View File

@@ -0,0 +1,42 @@
//
// 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)
}
}
}
}

View File

@@ -0,0 +1,45 @@
//
// 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")
}
}
}

View File

@@ -0,0 +1,47 @@
//
// 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()
}
}

View File

@@ -0,0 +1,31 @@
//
// 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)
}
}

View File

@@ -0,0 +1,52 @@
//
// 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))
)
}
}
}

View File

@@ -0,0 +1,16 @@
//
// 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() }
}
}

View File

@@ -0,0 +1,35 @@
//
// 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))
}
}

View File

@@ -0,0 +1,47 @@
//
// 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())
}
}
}

View File

@@ -0,0 +1,28 @@
//
// 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))
}
}

View File

@@ -0,0 +1,59 @@
//
// 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))
}
}

View File

@@ -0,0 +1,21 @@
//
// 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>) {}
}

View File

@@ -0,0 +1,87 @@
//
// 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)
}
}

View File

@@ -0,0 +1,52 @@
//
// 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
}
}

View File

@@ -0,0 +1,48 @@
//
// 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
}
}
}

View File

@@ -0,0 +1,71 @@
//
// 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)
}
}
}

View File

@@ -0,0 +1,20 @@
//
// 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) { }
}

View File

@@ -0,0 +1,52 @@
//
// 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)
}
}

View File

@@ -0,0 +1,28 @@
//
// 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))
}
}

View File

@@ -0,0 +1,454 @@
//
// 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)
}
}
}

View File

@@ -0,0 +1,56 @@
//
// 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()
// }
//}

View File

@@ -0,0 +1,71 @@
//
// 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)
}
}
}
}

View File

@@ -0,0 +1,71 @@
//
// 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)
}
}

View File

@@ -0,0 +1,55 @@
//
// 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)
}
}
}

View File

@@ -0,0 +1,102 @@
//
// 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)
}
}
}

View File

@@ -0,0 +1,56 @@
//
// 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 })
}
}

View File

@@ -0,0 +1,42 @@
//
// 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()
// }
//}

View File

@@ -0,0 +1,165 @@
//
// 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))
}
}

View File

@@ -0,0 +1,105 @@
//
// 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 })
}
}

View File

@@ -0,0 +1,300 @@
//
// 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
}
}

View File

@@ -0,0 +1,54 @@
//
// 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
}
}
}

View File

@@ -0,0 +1,119 @@
//
// 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()
}
}
}

View File

@@ -0,0 +1,437 @@
//
// 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()
}
}
}

View File

@@ -0,0 +1,16 @@
//
// 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?
}

View File

@@ -0,0 +1,129 @@
//
// 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
}
}

View File

@@ -0,0 +1,97 @@
//
// 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()
}
}

View File

@@ -0,0 +1,22 @@
//
// 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() {}
}

View File

@@ -0,0 +1,89 @@
//
// 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)
}
}

View File

@@ -0,0 +1,71 @@
//
// 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())
})
}
}

View File

@@ -0,0 +1,454 @@
//
// 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])
}
}
}
}

View File

@@ -0,0 +1,113 @@
//
// RootView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
struct RootView: View {
@State var selectedTab: Tab = .defaultTab
var body: some View {
TabView(selection: self.$selectedTab) {
ForEach(Tab.allCases) { tab in
NavigationView {
content(for: tab)
}
.tag(tab)
.tabItem {
tab.label
}
}
}
.overlay(self.notificationsOverlay)
}
@ViewBuilder
func content(for tab: Tab) -> some View {
switch tab {
case .news:
NewsView()
case .browse:
BrowseView()
case .myApps:
MyAppsView()
case .settings:
SettingsView()
}
}
@ObservedObject
var notificationManager = NotificationManager.shared
var notificationsOverlay: some View {
VStack {
Spacer()
ForEach(Array(notificationManager.notifications.values)) { notification in
VStack(alignment: .leading) {
Text(notification.title)
.bold()
if let message = notification.message {
Text(message)
.font(.callout)
}
}
.padding()
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(radius: 15)
}
Spacer()
.frame(height: 50)
}
.padding()
.animation(.easeInOut)
}
}
extension RootView {
enum Tab: Int, NavigationTab {
case news, browse, myApps, settings
static var defaultTab: RootView.Tab = .news
var displaySymbol: SFSymbol {
switch self {
case .news: return .newspaper
case .browse: return .booksVertical
case .myApps: return .squareStack
case .settings: return .gearshape
}
}
var displayName: String {
switch self {
case .news: return L10n.RootView.news
case .browse: return L10n.RootView.browse
case .myApps: return L10n.RootView.myApps
case .settings: return L10n.RootView.settings
}
}
var label: some View {
Label(self.displayName, systemSymbol: self.displaySymbol)
}
}
}
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}

View File

@@ -0,0 +1,109 @@
//
// ConnectAppleIDView.swift
// SideStore
//
// Created by Fabian Thies on 29.11.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import AltSign
struct ConnectAppleIDView: View {
typealias AuthenticationHandler = (String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void
typealias CompletionHandler = ((ALTAccount, ALTAppleAPISession, String)?) -> Void
@Environment(\.dismiss)
private var dismiss
var authenticationHandler: AuthenticationHandler?
var completionHandler: CompletionHandler?
@State var email: String = ""
@State var password: String = ""
@State var isLoading: Bool = false
var isFormValid: Bool {
!email.isEmpty && !password.isEmpty
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 32) {
Text(L10n.ConnectAppleIDView.startWithSignIn)
VStack(spacing: 16) {
RoundedTextField(title: L10n.ConnectAppleIDView.appleID, placeholder: "user@sidestore.io", text: $email)
RoundedTextField(title: L10n.ConnectAppleIDView.password, placeholder: "••••••", text: $password, isSecure: true)
}
SwiftUI.Button(action: signIn) {
Text(L10n.ConnectAppleIDView.signIn)
.bold()
}
.buttonStyle(FilledButtonStyle(isLoading: isLoading))
.disabled(!isFormValid)
Spacer()
VStack(alignment: .leading) {
Text(L10n.ConnectAppleIDView.whyDoWeNeedThis)
.bold()
Text(L10n.ConnectAppleIDView.footer)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundColor(Color(.secondarySystemBackground))
)
}
.padding(.horizontal)
}
.frame(maxWidth: .infinity)
.navigationTitle(L10n.ConnectAppleIDView.connectYourAppleID)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button(action: self.cancel) {
Text(L10n.ConnectAppleIDView.cancel)
}
}
}
}
func signIn() {
self.isLoading = true
self.authenticationHandler?(email, password) { (result) in
defer {
self.isLoading = false
}
switch result
{
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
case .failure(let error as NSError):
let error = error.withLocalizedFailure(NSLocalizedString(L10n.ConnectAppleIDView.failedToSignIn, comment: ""))
print(error)
case .success((let account, let session)):
self.completionHandler?((account, session, password))
}
}
}
func cancel() {
self.completionHandler?(nil)
// self.dismiss()
}
}
struct ConnectAppleIDView_Previews: PreviewProvider {
static var previews: some View {
ConnectAppleIDView()
}
}

View File

@@ -0,0 +1,169 @@
//
// ErrorLogView.swift
// SideStore
//
// Created by Fabian Thies on 03.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
import ExpandableText
struct ErrorLogView: View {
@Environment(\.dismiss) var dismiss
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)
])
var loggedErrors: FetchedResults<LoggedError>
var groupedLoggedErrors: [Date: [LoggedError]] {
Dictionary(grouping: loggedErrors, by: { Calendar.current.startOfDay(for: $0.date) })
}
@State var currentFaqUrl: URL?
@State var isShowingMinimuxerLog: Bool = false
@State var isShowingDeleteConfirmation: Bool = false
var body: some View {
List {
ForEach(groupedLoggedErrors.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
Section {
let errors = groupedLoggedErrors[date] ?? []
ForEach(errors, id: \.date) { error in
VStack(spacing: 8) {
HStack(alignment: .top) {
Group {
if let storeApp = error.storeApp {
AppIconView(iconUrl: storeApp.iconURL, size: 50)
} else {
ZStack {
RoundedRectangle(cornerRadius: 50*0.234, style: .continuous)
.foregroundColor(Color(UIColor.secondarySystemFill))
Image(systemSymbol: .exclamationmarkCircle)
.imageScale(.large)
.foregroundColor(.red)
}
.frame(width: 50, height: 50)
}
}
VStack(alignment: .leading) {
Text(error.localizedFailure ?? "Operation Failed")
.bold()
Group {
switch error.domain {
case AltServerErrorDomain: Text("SideServer Error \(error.code)")
case OperationError.domain: Text("SideStore Error \(error.code)")
default: Text(error.error.localizedErrorCode)
}
}
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text(DateFormatterHelper.timeString(for: error.date))
.font(.caption)
.foregroundColor(.secondary)
}
let nsError = error.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
Menu {
SwiftUI.Button {
UIPasteboard.general.string = errorDescription
} label: {
Label("Copy Error Message", systemSymbol: .docOnDoc)
}
SwiftUI.Button {
UIPasteboard.general.string = error.error.localizedErrorCode
} label: {
Label("Copy Error Code", systemSymbol: .docOnDoc)
}
SwiftUI.Button {
self.searchFAQ(for: error)
} label: {
Label("Search FAQ", systemSymbol: .magnifyingglass)
}
} label: {
Text(errorDescription)
.multilineTextAlignment(.leading)
.foregroundColor(.primary)
}
}
}
} header: {
Text(DateFormatterHelper.string(for: date))
}
}
}
.navigationBarTitle("Error Log")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ModalNavigationLink {
FilePreviewView(urls: [
FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
])
.ignoresSafeArea()
} label: {
Image(systemSymbol: .ladybug)
}
SwiftUI.Button {
self.isShowingDeleteConfirmation = true
} label: {
Image(systemSymbol: .trash)
}
.actionSheet(isPresented: self.$isShowingDeleteConfirmation) {
ActionSheet(
title: Text("Are you sure you want to clear the error log?"),
buttons: [
.destructive(Text("Clear Error Log"), action: self.clearLoggedErrors),
.cancel()
]
)
}
}
}
.sheet(item: self.$currentFaqUrl) { url in
SafariView(url: url)
}
}
func searchFAQ(for error: LoggedError) {
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
let query = [error.domain, "\(error.code)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)]
self.currentFaqUrl = components.url ?? baseURL
}
func clearLoggedErrors() {
DatabaseManager.shared.purgeLoggedErrors { result in
if case let .failure(error) = result {
NotificationManager.shared.reportError(error: error)
}
}
}
}
struct ErrorLogView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ErrorLogView()
}
}
}

View File

@@ -0,0 +1,112 @@
//
// LicensesView.swift
// SideStore
//
// Created by Fabian Thies on 21.01.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
struct LicensesView: View {
let licenses = """
Jay Freeman (ldid)
Copyright (C) 2007-2012 Jay Freeman (saurik)
libimobiledevice
© 2007-2015 by the contributors of libimobiledevice - All rights reserved.
Gilles Vollant (minizip)
Copyright (C) 1998-2005 Gilles Vollant
Kishikawa Katsumi (KeychainAccess)
Copyright (c) 2014 kishikawa katsumi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Alexander Grebenyuk (Nuke)
Copyright (c) 2015-2019 Alexander Grebenyuk
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Craig Hockenberry (MarkdownAttributedString)
Copyright (c) 2020 The Iconfactory, Inc. https://iconfactory.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The OpenSSL Project (OpenSSL)
Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org.
5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project.
6. Redistributions of any form whatsoever must retain the following acknowledgment:
"This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)"
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com).
Eric Young (SSLeay)
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
All rights reserved.
This package is an SSL implementation written by Eric Young (eay@cryptsoft.com).
The implementation was written so as to conform with Netscapes SSL. This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com).
Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
"This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)"
The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-).
4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement:
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)" THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.]
Toni Ronkko (dirent)
Copyright (c) 1998-2019 Toni Ronkko
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Microsoft Corporation (C++ REST SDK)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Kutuzov Viktor (mman-win32)
Copyright (c) Kutuzov Viktor
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
ICONS
Settings by i cons from the Noun Project
"""
var body: some View {
ScrollView {
Text(licenses)
.padding()
}
.navigationTitle("Software Licenses")
}
}
struct LicensesView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
LicensesView()
}
}
}

View File

@@ -0,0 +1,90 @@
//
// RefreshAttemptsView.swift
// SideStore
//
// Created by Fabian Thies on 04.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
struct RefreshAttemptsView: View {
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \RefreshAttempt.date, ascending: false)
])
var refreshAttempts: FetchedResults<RefreshAttempt>
var groupedRefreshAttempts: [Date: [RefreshAttempt]] {
Dictionary(grouping: refreshAttempts, by: { Calendar.current.startOfDay(for: $0.date) })
}
var body: some View {
List {
ForEach(groupedRefreshAttempts.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
Section {
let attempts = groupedRefreshAttempts[date] ?? []
ForEach(attempts, id: \.date) { attempt in
VStack(alignment: .leading, spacing: 8) {
HStack {
if attempt.isSuccess {
Text("Success")
.bold()
.foregroundColor(.green)
} else {
Text("Failure")
.bold()
.foregroundColor(.red)
}
Spacer()
Text(DateFormatterHelper.timeString(for: attempt.date))
.font(.caption)
.foregroundColor(.secondary)
}
if let description = attempt.errorDescription {
Text(description)
}
}
}
} header: {
Text(DateFormatterHelper.string(for: date))
}
}
}
.background(self.listBackground)
.navigationTitle("Refresh Attempts")
}
@ViewBuilder
var listBackground: some View {
if self.refreshAttempts.isEmpty {
VStack(spacing: 8) {
Spacer()
Text("No Refresh Attempts")
.font(.title)
Text("The more you use SideStore, the more often iOS will allow it to refresh apps in the background.")
Spacer()
}
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.padding()
} else {
Color.clear
}
}
}
struct RefreshAttemptsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
RefreshAttemptsView()
}
}
}

View File

@@ -0,0 +1,318 @@
//
// SettingsView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AsyncImage
import SFSafeSymbols
import LocalConsole
import AltStoreCore
import Intents
struct SettingsView: View {
var connectedAppleID: Team? {
DatabaseManager.shared.activeTeam()
}
@SwiftUI.FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam)))
var connectedTeams: FetchedResults<Team>
@AppStorage("isBackgroundRefreshEnabled")
var isBackgroundRefreshEnabled: Bool = true
@State var isShowingConnectAppleIDView = false
@State var isShowingResetPairingFileConfirmation = false
@State var externalURLToShow: URL?
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown Version"
var body: some View {
List {
Section {
if let connectedAppleID = connectedTeams.first {
HStack {
Text(L10n.SettingsView.ConnectedAppleID.name)
.foregroundColor(.secondary)
Spacer()
Text(connectedAppleID.name)
}
HStack {
Text(L10n.SettingsView.ConnectedAppleID.eMail)
.foregroundColor(.secondary)
Spacer()
Text(connectedAppleID.account.appleID)
}
HStack {
Text(L10n.SettingsView.ConnectedAppleID.type)
.foregroundColor(.secondary)
Spacer()
Text(connectedAppleID.type.localizedDescription)
}
} else {
SwiftUI.Button {
self.connectAppleID()
} label: {
Text(L10n.SettingsView.connectAppleID)
}
}
} header: {
if !connectedTeams.isEmpty {
HStack {
Text(L10n.SettingsView.ConnectedAppleID.text)
Spacer()
SwiftUI.Button {
self.disconnectAppleID()
} label: {
Text(L10n.SettingsView.ConnectedAppleID.signOut)
.font(.callout)
.bold()
}
}
}
} footer: {
VStack(alignment: .leading, spacing: 4) {
Text(L10n.SettingsView.ConnectedAppleID.Footer.p1)
Text(L10n.SettingsView.ConnectedAppleID.Footer.p2)
}
}
Section {
Toggle(isOn: self.$isBackgroundRefreshEnabled, label: {
Text(L10n.SettingsView.backgroundRefresh)
})
ModalNavigationLink(L10n.SettingsView.addToSiri) {
if let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) {
SiriShortcutSetupView(shortcut: shortcut)
}
}
} header: {
Text(L10n.SettingsView.refreshingApps)
} footer: {
Text(L10n.SettingsView.refreshingAppsFooter)
}
Section {
SwiftUI.Button {
self.externalURLToShow = URL(string: "https://sidestore.io")!
} label: {
HStack {
Text("Developers")
.foregroundColor(.secondary)
Spacer()
Text("SideStore Team")
Image(systemSymbol: .chevronRight)
.foregroundColor(.secondary)
}
}
.foregroundColor(.primary)
SwiftUI.Button {
self.externalURLToShow = URL(string: "https://fabian-thies.de")!
} label: {
HStack {
Text(L10n.SettingsView.swiftUIRedesign)
.foregroundColor(.secondary)
Spacer()
Text("fabianthdev")
Image(systemSymbol: .chevronRight)
.foregroundColor(.secondary)
}
}
.foregroundColor(.primary)
NavigationLink {
LicensesView()
} label: {
Text("Licenses")
}
} header: {
Text(L10n.SettingsView.credits)
}
Section {
NavigationLink("Show Error Log") {
ErrorLogView()
}
NavigationLink("Show Refresh Attempts") {
RefreshAttemptsView()
}
SwiftUI.Button("Toggle Console") {
LCManager.shared.isVisible.toggle()
}
if MailComposeView.canSendMail {
ModalNavigationLink("Send Feedback") {
MailComposeView(recipients: ["support@sidestore.io"],
subject: "SideStore Beta \(appVersion) Feedback") {
NotificationManager.shared.showNotification(title: "Thank you for your feedback!")
} onError: { error in
NotificationManager.shared.reportError(error: error)
}
.ignoresSafeArea()
}
}
SwiftUI.Button(L10n.SettingsView.switchToUIKit, action: self.switchToUIKit)
SwiftUI.Button("Advanced Settings", action: self.showAdvancedSettings)
SwiftUI.Button(L10n.SettingsView.resetImageCache, action: self.resetImageCache)
.foregroundColor(.red)
SwiftUI.Button("Reset Pairing File") {
self.isShowingResetPairingFileConfirmation = true
}
.foregroundColor(.red)
.actionSheet(isPresented: self.$isShowingResetPairingFileConfirmation) {
ActionSheet(title: Text("Are you sure to reset the pairing file?"), message: Text("You can reset the pairing file when you cannot sideload apps or enable JIT. SideStore will close when the file has been deleted."), buttons: [
.destructive(Text("Delete and Reset"), action: self.resetPairingFile),
.cancel()
])
}
} header: {
Text(L10n.SettingsView.debug)
}
Section {
} footer: {
Text("SideStore \(appVersion)")
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(L10n.SettingsView.title)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button {
} label: {
Image(systemSymbol: .personCropCircle)
.imageScale(.large)
}
}
}
.sheet(item: $externalURLToShow) { url in
SafariView(url: url)
}
}
// var appleIDSection: some View {
//
// }
func connectAppleID() {
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return
}
AppManager.shared.authenticate(presentingViewController: rootViewController) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled):
// Ignore
break
case .failure(let error):
NotificationManager.shared.reportError(error: error)
case .success: break
}
}
}
}
func disconnectAppleID() {
DatabaseManager.shared.signOut { (error) in
DispatchQueue.main.async {
if let error = error
{
NotificationManager.shared.reportError(error: error)
}
}
}
}
func switchToUIKit() {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
UIApplication.shared.keyWindow?.rootViewController = rootVC
}
func resetImageCache() {
do {
let url = try FileManager.default.url(
for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
try FileManager.default.removeItem(at: url.appendingPathComponent("com.zeu.cache", isDirectory: true))
} catch let error {
fatalError("\(error)")
}
}
func resetPairingFile() {
let filename = "ALTPairingFile.mobiledevicepairing"
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent(filename)
// Delete the pairing file if it exists
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
try FileManager.default.removeItem(at: fileURL)
print("Pairing file deleted successfully.")
} catch {
print("Failed to delete pairing file:", error)
}
}
// Close and exit SideStore
UIApplication.shared.perform(#selector(URLSessionTask.suspend))
DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(500))) {
exit(0)
}
}
func showAdvancedSettings() {
// Create the URL that deep links to our app's custom settings.
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
// Ask the system to open that URL.
UIApplication.shared.open(url)
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SettingsView()
}
}
}

View File

@@ -20,6 +20,7 @@ public extension UserDefaults
}() }()
@NSManaged var firstLaunch: Date? @NSManaged var firstLaunch: Date?
@NSManaged var onboardingComplete: Bool
@NSManaged var requiresAppGroupMigration: Bool @NSManaged var requiresAppGroupMigration: Bool
@NSManaged var textServer: Bool @NSManaged var textServer: Bool
@NSManaged var textInputAnisetteURL: String? @NSManaged var textInputAnisetteURL: String?
@@ -71,6 +72,7 @@ public extension UserDefaults
let localServerSupportsRefreshing = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14) let localServerSupportsRefreshing = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14)
let defaults = [ let defaults = [
#keyPath(UserDefaults.onboardingComplete): false,
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true, #keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions, #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,

View File

@@ -7,6 +7,7 @@
// //
import CoreData import CoreData
import SFSafeSymbols
import UIKit import UIKit
public extension ALTAppPermissionType public extension ALTAppPermissionType
@@ -46,28 +47,51 @@ public extension ALTAppPermissionType
} }
var icon: UIImage? { var icon: UIImage? {
switch self let symbol: SFSymbol? = {
{ switch self {
case .photos: return UIImage(systemName: "photo.on.rectangle.angled") case .photos: return .photoOnRectangleAngled
case .camera: return UIImage(systemName: "camera.fill") case .camera: return .cameraFill
case .location: return UIImage(systemName: "location.fill") case .location: return .locationFill
case .contacts: return UIImage(systemName: "person.2.fill") case .contacts: return .person2Fill
case .reminders: return UIImage(systemName: "checklist") case .reminders:
case .appleMusic: return UIImage(systemName: "music.note") if #available(iOS 15.0, *) {
case .microphone: return UIImage(systemName: "mic.fill") return .checklist
case .speechRecognition: return UIImage(systemName: "waveform.and.mic") }
case .backgroundAudio: return UIImage(systemName: "speaker.fill") return .listBullet
case .backgroundFetch: return UIImage(systemName: "square.and.arrow.down") case .appleMusic: return .musicNote
case .bluetooth: return UIImage(systemName: "wave.3.right") case .microphone: return .micFill
case .network: return UIImage(systemName: "network") case .speechRecognition:
case .calendars: return UIImage(systemName: "calendar") if #available(iOS 15.0, *) {
case .touchID: return UIImage(systemName: "touchid") return .waveformAndMic
case .faceID: return UIImage(systemName: "faceid") }
case .siri: return UIImage(systemName: "mic.and.signal.meter.fill") return .recordingtape
case .motion: return UIImage(systemName: "figure.walk.motion") case .backgroundAudio: return .speakerFill
default: case .backgroundFetch: return .squareAndArrowDown
case .bluetooth: return .wave3Right
case .network: return .network
case .calendars: return .calendar
case .touchID: return .touchid
case .faceID: return .faceid
case .siri:
if #available(iOS 16.0, *) {
return .micAndSignalMeterFill
}
return .waveform
case .motion:
if #available(iOS 16.0, *) {
return .figureWalkMotion
}
return .figureWalk
default:
return nil
}
}()
guard let symbol = symbol else {
return nil return nil
} }
return UIImage(systemSymbol: symbol)
} }
} }

View File

@@ -342,25 +342,31 @@ public extension StoreApp
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp
{ {
let app = StoreApp(context: context) let app = StoreApp(context: context)
app.source = Source.makeAltStoreSource(in: context)
app.name = "SideStore" app.name = "SideStore"
app.bundleIdentifier = StoreApp.altstoreAppID app.bundleIdentifier = StoreApp.altstoreAppID
app.developerName = "Side Team" app.developerName = "Side Team"
app.localizedDescription = "SideStore is an alternative App Store." app.localizedDescription = "SideStore is an alternative App Store."
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")! app.iconURL = URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/icon.png")!
app.screenshotURLs = [] app.screenshotURLs = [
app.sourceIdentifier = Source.altStoreIdentifier URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/browse-dark.png")!,
URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/apps-dark.png")!,
URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/news-dark.png")!,
URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/browse-light.png")!,
URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/apps-light.png")!,
URL(string: "https://apps.sidestore.io/apps/sidestore/v0.1.1/news-light.png")!,
]
app.tintColor = UIColor(named: "AccentColor")
let appVersion = AppVersion.makeAppVersion(version: "0.3.0", let appVersion = AppVersion.makeAppVersion(version: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown",
date: Date(), date: Date(),
downloadURL: URL(string: "http://rileytestut.com")!, downloadURL: URL(string: "https://github.com/SideStore/SideStore/releases/download/0.1.1/SideStore.ipa")!,
size: 0, size: 0,
appBundleID: app.bundleIdentifier, appBundleID: app.bundleIdentifier,
sourceID: Source.altStoreIdentifier, sourceID: Source.altStoreIdentifier,
in: context) in: context)
app.setVersions([appVersion]) app.setVersions([appVersion])
print("makeAltStoreApp StoreApp: \(String(describing: app))")
#if BETA #if BETA
app.isBeta = true app.isBeta = true
#endif #endif

23
swiftgen.yml Normal file
View File

@@ -0,0 +1,23 @@
# colors:
# inputs: colors.xml
# outputs:
# templateName: swift5
# output: Colors.swift
#
# fonts:
# inputs: Fonts
# outputs:
# templateName: swift5
# output: Fonts.swift
strings:
- inputs: AltStore/Resources/en.lproj/Localizable.strings
outputs:
templateName: structured-swift5
output: AltStore/Generated/Localizations.swift
xcassets:
inputs: AltStore/Resources/Assets.xcassets
outputs:
templateName: swift5
output: AltStore/Generated/Assets.swift