mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-15 17:53:31 +01:00
[ADD] WIP: Add My Apps view with support for sideloading new apps, refreshing installed apps and much more
This commit is contained in:
committed by
Joe Mattiello
parent
a0eb30f98e
commit
02e48a207f
@@ -19,7 +19,6 @@
|
|||||||
191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FB5290A5E1F001A3B7C /* libminimuxer.a */; };
|
191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FB5290A5E1F001A3B7C /* libminimuxer.a */; };
|
||||||
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 */; };
|
||||||
1F0DD810293222DF007608A4 /* AsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0DD80F293222DF007608A4 /* AsyncImage */; };
|
|
||||||
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */; };
|
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */; };
|
||||||
1F0DD81F2932D84C007608A4 /* ExpandableText in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0DD81E2932D84C007608A4 /* ExpandableText */; };
|
1F0DD81F2932D84C007608A4 /* ExpandableText in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0DD81E2932D84C007608A4 /* ExpandableText */; };
|
||||||
1F0DD8212933B749007608A4 /* AppPermissionsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */; };
|
1F0DD8212933B749007608A4 /* AppPermissionsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */; };
|
||||||
@@ -27,6 +26,10 @@
|
|||||||
1F0DD84129368056007608A4 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD84029368056007608A4 /* EnvironmentValues.swift */; };
|
1F0DD84129368056007608A4 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD84029368056007608A4 /* EnvironmentValues.swift */; };
|
||||||
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */; };
|
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */; };
|
||||||
1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8442936B3FE007608A4 /* FilledButtonStyle.swift */; };
|
1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8442936B3FE007608A4 /* FilledButtonStyle.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 */; };
|
||||||
|
1F6284DB295254C80060AAD8 /* AppScreenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6284DA295254C80060AAD8 /* AppScreenshot.swift */; };
|
||||||
1F66F5BA2938CA5700A910CA /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5B92938CA5700A910CA /* VisualEffectView.swift */; };
|
1F66F5BA2938CA5700A910CA /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5B92938CA5700A910CA /* VisualEffectView.swift */; };
|
||||||
1F66F5BC2938F03700A910CA /* Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5BB2938F03700A910CA /* Modifiers.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 */; };
|
1F66F5BE2938F06100A910CA /* StoreApp+Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */; };
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
1F6E08E429280D1E005059C0 /* PillButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */; };
|
1F6E08E429280D1E005059C0 /* PillButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */; };
|
||||||
1F6E08E629280F4B005059C0 /* RatingStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E529280F4B005059C0 /* RatingStars.swift */; };
|
1F6E08E629280F4B005059C0 /* RatingStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E529280F4B005059C0 /* RatingStars.swift */; };
|
||||||
1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6E08E729282174005059C0 /* ConfirmAddSourceView.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 */; };
|
1F943C692927F8F200ABE095 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5B52927E06300B8D837 /* RootView.swift */; };
|
||||||
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F943C672927F39400ABE095 /* ViewModel.swift */; };
|
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F943C672927F39400ABE095 /* ViewModel.swift */; };
|
||||||
1F943C6B2927F8F700ABE095 /* NavigationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5C32927E18100B8D837 /* NavigationTab.swift */; };
|
1F943C6B2927F8F700ABE095 /* NavigationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5C32927E18100B8D837 /* NavigationTab.swift */; };
|
||||||
@@ -46,6 +50,7 @@
|
|||||||
1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */; };
|
1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */; };
|
||||||
1F943C702927F90400ABE095 /* BrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */; };
|
1F943C702927F90400ABE095 /* BrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */; };
|
||||||
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */; };
|
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */; };
|
||||||
|
1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA1C8C9294906890083119D /* MyAppsViewModel.swift */; };
|
||||||
1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */; };
|
1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */; };
|
||||||
1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */; };
|
1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */; };
|
||||||
1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */; };
|
1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */; };
|
||||||
@@ -556,6 +561,10 @@
|
|||||||
1F0DD84029368056007608A4 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.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>"; };
|
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>"; };
|
1F0DD8442936B3FE007608A4 /* FilledButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilledButtonStyle.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>"; };
|
||||||
|
1F6284DA295254C80060AAD8 /* AppScreenshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScreenshot.swift; sourceTree = "<group>"; };
|
||||||
1F66F5B92938CA5700A910CA /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.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>"; };
|
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>"; };
|
1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreApp+Filterable.swift"; sourceTree = "<group>"; };
|
||||||
@@ -569,6 +578,7 @@
|
|||||||
1F943C632927EF4200ABE095 /* NewsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItemView.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>"; };
|
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>"; };
|
1F943C672927F39400ABE095 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
1FA1C8C9294906890083119D /* MyAppsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewModel.swift; sourceTree = "<group>"; };
|
||||||
1FAFC5A42927E00000B8D837 /* SideStoreUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideStoreUIApp.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>"; };
|
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>"; };
|
1FAFC5B82927E0EE00B8D837 /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1000,7 +1010,7 @@
|
|||||||
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 */,
|
||||||
1F0DD810293222DF007608A4 /* AsyncImage in Frameworks */,
|
1F74FF1E295263510047C051 /* AsyncImage in Frameworks */,
|
||||||
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
||||||
B3C395F7284F362400DA9E2F /* AppCenterAnalytics in Frameworks */,
|
B3C395F7284F362400DA9E2F /* AppCenterAnalytics in Frameworks */,
|
||||||
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
||||||
@@ -1064,6 +1074,7 @@
|
|||||||
1F6E08DF29280B12005059C0 /* SafariView.swift */,
|
1F6E08DF29280B12005059C0 /* SafariView.swift */,
|
||||||
1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */,
|
1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */,
|
||||||
1F66F5B92938CA5700A910CA /* VisualEffectView.swift */,
|
1F66F5B92938CA5700A910CA /* VisualEffectView.swift */,
|
||||||
|
1F6284D6295218980060AAD8 /* DocumentPicker.swift */,
|
||||||
);
|
);
|
||||||
path = "UIView Representables";
|
path = "UIView Representables";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1133,6 +1144,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */,
|
1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */,
|
||||||
|
1FA1C8C9294906890083119D /* MyAppsViewModel.swift */,
|
||||||
|
1F6284D4295209DA0060AAD8 /* AppAction.swift */,
|
||||||
);
|
);
|
||||||
path = "My Apps";
|
path = "My Apps";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1165,6 +1178,7 @@
|
|||||||
1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */,
|
1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */,
|
||||||
1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */,
|
1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */,
|
||||||
1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */,
|
1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */,
|
||||||
|
1F6284DA295254C80060AAD8 /* AppScreenshot.swift */,
|
||||||
);
|
);
|
||||||
path = "View Components";
|
path = "View Components";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1173,6 +1187,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */,
|
1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */,
|
||||||
|
1F6284D829523D340060AAD8 /* SideloadingManager.swift */,
|
||||||
);
|
);
|
||||||
path = Helper;
|
path = Helper;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2225,8 +2240,8 @@
|
|||||||
B3C395F6284F362400DA9E2F /* AppCenterAnalytics */,
|
B3C395F6284F362400DA9E2F /* AppCenterAnalytics */,
|
||||||
B3C395F8284F362400DA9E2F /* AppCenterCrashes */,
|
B3C395F8284F362400DA9E2F /* AppCenterCrashes */,
|
||||||
4879A9612861049C00FC1BBD /* OpenSSL */,
|
4879A9612861049C00FC1BBD /* OpenSSL */,
|
||||||
1F0DD80F293222DF007608A4 /* AsyncImage */,
|
|
||||||
1F0DD81E2932D84C007608A4 /* ExpandableText */,
|
1F0DD81E2932D84C007608A4 /* ExpandableText */,
|
||||||
|
1F74FF1D295263510047C051 /* AsyncImage */,
|
||||||
);
|
);
|
||||||
productName = AltStore;
|
productName = AltStore;
|
||||||
productReference = BFD2476A2284B9A500981D42 /* SideStore.app */;
|
productReference = BFD2476A2284B9A500981D42 /* SideStore.app */;
|
||||||
@@ -2299,8 +2314,8 @@
|
|||||||
4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */,
|
4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */,
|
||||||
99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
|
99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
|
||||||
1FB96FB629297C11007E68D1 /* XCRemoteSwiftPackageReference "GridStack" */,
|
1FB96FB629297C11007E68D1 /* XCRemoteSwiftPackageReference "GridStack" */,
|
||||||
1F0DD80E293222DF007608A4 /* XCRemoteSwiftPackageReference "AsyncImage" */,
|
|
||||||
1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */,
|
1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */,
|
||||||
|
1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */,
|
||||||
);
|
);
|
||||||
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
|
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -2685,6 +2700,7 @@
|
|||||||
1F943C6D2927F90400ABE095 /* NewsViewModel.swift in Sources */,
|
1F943C6D2927F90400ABE095 /* NewsViewModel.swift in Sources */,
|
||||||
1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */,
|
1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */,
|
||||||
1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */,
|
1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */,
|
||||||
|
1F6284D929523D340060AAD8 /* SideloadingManager.swift in Sources */,
|
||||||
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
|
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
|
||||||
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */,
|
1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */,
|
||||||
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */,
|
1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */,
|
||||||
@@ -2698,7 +2714,9 @@
|
|||||||
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 */,
|
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */,
|
||||||
|
1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */,
|
||||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
||||||
|
1F6284DB295254C80060AAD8 /* AppScreenshot.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 */,
|
||||||
@@ -2717,6 +2735,7 @@
|
|||||||
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
||||||
1FB96FC9292ABDD0007E68D1 /* AddSourceView.swift in Sources */,
|
1FB96FC9292ABDD0007E68D1 /* AddSourceView.swift in Sources */,
|
||||||
1F6E08DC292807D3005059C0 /* AppIconView.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 */,
|
1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */,
|
||||||
@@ -2758,6 +2777,7 @@
|
|||||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||||
1F6E08E429280D1E005059C0 /* PillButtonStyle.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 */,
|
1F0DD83F29367F6C007608A4 /* ConnectAppleIDView.swift in Sources */,
|
||||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||||
@@ -3612,14 +3632,6 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
1F0DD80E293222DF007608A4 /* XCRemoteSwiftPackageReference "AsyncImage" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/zzzzeu/AsyncImage";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 0.0.1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */ = {
|
1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/NuPlay/ExpandableText";
|
repositoryURL = "https://github.com/NuPlay/ExpandableText";
|
||||||
@@ -3628,6 +3640,14 @@
|
|||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/fabianthdev/AsyncImage";
|
||||||
|
requirement = {
|
||||||
|
branch = main;
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */ = {
|
4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/SideStore/AltSign";
|
repositoryURL = "https://github.com/SideStore/AltSign";
|
||||||
@@ -3713,16 +3733,16 @@
|
|||||||
package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */;
|
package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */;
|
||||||
productName = OpenSSL;
|
productName = OpenSSL;
|
||||||
};
|
};
|
||||||
1F0DD80F293222DF007608A4 /* AsyncImage */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 1F0DD80E293222DF007608A4 /* XCRemoteSwiftPackageReference "AsyncImage" */;
|
|
||||||
productName = AsyncImage;
|
|
||||||
};
|
|
||||||
1F0DD81E2932D84C007608A4 /* ExpandableText */ = {
|
1F0DD81E2932D84C007608A4 /* ExpandableText */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */;
|
package = 1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */;
|
||||||
productName = ExpandableText;
|
productName = ExpandableText;
|
||||||
};
|
};
|
||||||
|
1F74FF1D295263510047C051 /* AsyncImage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */;
|
||||||
|
productName = AsyncImage;
|
||||||
|
};
|
||||||
4879A95E2861046500FC1BBD /* AltSign */ = {
|
4879A95E2861046500FC1BBD /* AltSign */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */;
|
package = 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */;
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
{
|
{
|
||||||
"identity" : "asyncimage",
|
"identity" : "asyncimage",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/zzzzeu/AsyncImage",
|
"location" : "https://github.com/fabianthdev/AsyncImage",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "854d01f6bb9550f4aeee8959ab5b67d7d7775f02",
|
"branch" : "main",
|
||||||
"version" : "0.0.1"
|
"revision" : "04fe7c66f8362b863c926b87a6d5d9820ffc5bad"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
254
AltStore/Helper/SideloadingManager.swift
Normal file
254
AltStore/Helper/SideloadingManager.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,11 @@ struct AppPillButton: View {
|
|||||||
|
|
||||||
func handleButton() {
|
func handleButton() {
|
||||||
if let installedApp {
|
if let installedApp {
|
||||||
self.openApp(installedApp)
|
if showRemainingDays {
|
||||||
|
self.refreshApp(installedApp)
|
||||||
|
} else {
|
||||||
|
self.openApp(installedApp)
|
||||||
|
}
|
||||||
} else if let storeApp {
|
} else if let storeApp {
|
||||||
self.installApp(storeApp)
|
self.installApp(storeApp)
|
||||||
}
|
}
|
||||||
@@ -70,6 +74,10 @@ struct AppPillButton: View {
|
|||||||
UIApplication.shared.open(installedApp.openAppURL)
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshApp(_ installedApp: InstalledApp) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func installApp(_ storeApp: StoreApp) {
|
func installApp(_ storeApp: StoreApp) {
|
||||||
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
guard previousProgress == nil else {
|
guard previousProgress == nil else {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ struct AppRowView: View {
|
|||||||
(app as? StoreApp) ?? (app as? InstalledApp)?.storeApp
|
(app as? StoreApp) ?? (app as? InstalledApp)?.storeApp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showRemainingDays: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 12) {
|
HStack(alignment: .center, spacing: 12) {
|
||||||
AppIconView(iconUrl: storeApp?.iconURL)
|
AppIconView(iconUrl: storeApp?.iconURL)
|
||||||
@@ -36,11 +38,10 @@ struct AppRowView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
AppPillButton(app: app)
|
AppPillButton(app: app, showRemainingDays: showRemainingDays)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.blurBackground(.systemUltraThinMaterialLight)
|
.tintedBackground(Color(storeApp?.tintColor ?? UIColor(Color.accentColor)))
|
||||||
.background(Color(storeApp?.tintColor ?? UIColor.black).opacity(0.4))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 30, style: .circular))
|
.clipShape(RoundedRectangle(cornerRadius: 30, style: .circular))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,10 @@ extension View {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func tintedBackground(_ color: Color) -> some View {
|
||||||
|
self
|
||||||
|
.blurBackground(.systemUltraThinMaterial)
|
||||||
|
.background(color.opacity(0.4))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
53
AltStore/Views/My Apps/AppAction.swift
Normal file
53
AltStore/Views/My Apps/AppAction.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// AppAction.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 20.12.22.
|
||||||
|
// Copyright © 2022 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
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 "Install"
|
||||||
|
case .open: return "Open"
|
||||||
|
case .refresh: return "Refresh"
|
||||||
|
case .activate: return "Activate"
|
||||||
|
case .deactivate: return "Deactivate"
|
||||||
|
case .remove: return "Remove"
|
||||||
|
case .enableJIT: return "Enable JIT"
|
||||||
|
case .backup: return "Back Up"
|
||||||
|
case .exportBackup: return "Export Backup"
|
||||||
|
case .restoreBackup: return "Restore Backup"
|
||||||
|
case .chooseCustomIcon: return "Change Icon"
|
||||||
|
case .resetCustomIcon: return "Reset Icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageName: String {
|
||||||
|
switch self {
|
||||||
|
case .install: return "Install"
|
||||||
|
case .open: return "arrow.up.forward.app"
|
||||||
|
case .refresh: return "arrow.clockwise"
|
||||||
|
case .activate: return "checkmark.circle"
|
||||||
|
case .deactivate: return "xmark.circle"
|
||||||
|
case .remove: return "trash"
|
||||||
|
case .enableJIT: return "bolt"
|
||||||
|
case .backup: return "doc.on.doc"
|
||||||
|
case .exportBackup: return "arrow.up.doc"
|
||||||
|
case .restoreBackup: return "arrow.down.doc"
|
||||||
|
case .chooseCustomIcon: return "photo"
|
||||||
|
case .resetCustomIcon: return "arrow.uturn.left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,378 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import MobileCoreServices
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
struct MyAppsView: View {
|
struct MyAppsView: View {
|
||||||
|
|
||||||
|
// TODO: Refactor
|
||||||
|
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||||
|
NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, 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.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>
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var viewModel = MyAppsViewModel()
|
||||||
|
|
||||||
|
// TODO: Refactor
|
||||||
|
@State var isShowingFilePicker: 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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack(spacing: 16) {
|
||||||
|
if let progress = SideloadingManager.shared.progress {
|
||||||
|
VStack {
|
||||||
|
Text("Sideloading in progress...")
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
ProgressView(progress)
|
||||||
|
.progressViewStyle(LinearProgressViewStyle())
|
||||||
|
}
|
||||||
|
.background(Color(UIColor.secondarySystemBackground))
|
||||||
|
}
|
||||||
|
|
||||||
|
updatesSection
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Active")
|
||||||
|
.font(.title2)
|
||||||
|
.bold()
|
||||||
|
Spacer()
|
||||||
|
SwiftUI.Button {
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
Text("Refresh All")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("\(remainingAppIDs) App IDs Remaining")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
SwiftUI.Button {
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
Text("View App IDs")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
|
||||||
.navigationTitle("My Apps")
|
.navigationTitle("My Apps")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
SwiftUI.Button {
|
||||||
|
self.isShowingFilePicker = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: self.$isShowingFilePicker) {
|
||||||
|
DocumentPicker(selectedUrl: $selectedSideloadingIpaURL, supportedTypes: sideloadFileTypes)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
.onChange(of: self.selectedSideloadingIpaURL) { newValue in
|
||||||
|
guard let url = newValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sideloadApp(at: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatesSection: some View {
|
||||||
|
Text("No Updates Available")
|
||||||
|
.font(.headline)
|
||||||
|
.bold()
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.opacity(0.8)
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.tintedBackground(.accentColor)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 30, style: .circular))
|
||||||
|
}
|
||||||
|
|
||||||
|
@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, systemImage: action.imageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshAllApps() {
|
||||||
|
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
|
||||||
|
|
||||||
|
self.refresh(installedApps) { result in }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 = "Failed to refresh \(failures.count) 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
AltStore/Views/My Apps/MyAppsViewModel.swift
Normal file
16
AltStore/Views/My Apps/MyAppsViewModel.swift
Normal 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?
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user