diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 92c94e88..48429412 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -19,6 +19,32 @@ 191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FB5290A5E1F001A3B7C /* libminimuxer.a */; }; 1920B04F2924AC8300744F60 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1920B04E2924AC8300744F60 /* Settings.bundle */; }; 19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; }; + 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 */; }; + 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 */; }; + 1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */; }; + 1FB96FB829297C11007E68D1 /* GridStack in Frameworks */ = {isa = PBXBuildFile; productRef = 1FB96FB729297C11007E68D1 /* GridStack */; }; + 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 */; }; 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; 99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; }; @@ -513,6 +539,32 @@ 191E5FD7290A6EFB001A3B7C /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = minimuxer.h; path = ../Dependencies/minimuxer/minimuxer.h; sourceTree = ""; }; 1920B04E2924AC8300744F60 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = ""; }; + 1F6E08D9292806E0005059C0 /* AppRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRowView.swift; sourceTree = ""; }; + 1F6E08DB292807D3005059C0 /* AppIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconView.swift; sourceTree = ""; }; + 1F6E08DF29280B12005059C0 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; + 1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonStyle.swift; sourceTree = ""; }; + 1F6E08E529280F4B005059C0 /* RatingStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingStars.swift; sourceTree = ""; }; + 1F6E08E729282174005059C0 /* ConfirmAddSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmAddSourceView.swift; sourceTree = ""; }; + 1F943C632927EF4200ABE095 /* NewsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItemView.swift; sourceTree = ""; }; + 1F943C652927F36600ABE095 /* NewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewModel.swift; sourceTree = ""; }; + 1F943C672927F39400ABE095 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; + 1FAFC5A42927E00000B8D837 /* SideStoreUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideStoreUIApp.swift; sourceTree = ""; }; + 1FAFC5B52927E06300B8D837 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = ""; }; + 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseView.swift; sourceTree = ""; }; + 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsView.swift; sourceTree = ""; }; + 1FAFC5C02927E13C00B8D837 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 1FAFC5C32927E18100B8D837 /* NavigationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTab.swift; sourceTree = ""; }; + 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailView.swift; sourceTree = ""; }; + 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableScrollView.swift; sourceTree = ""; }; + 1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPillButton.swift; sourceTree = ""; }; + 1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterHelper.swift; sourceTree = ""; }; + 1FB96FC4292A7251007E68D1 /* BrowseAppPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseAppPreviewView.swift; sourceTree = ""; }; + 1FB96FC6292A853D007E68D1 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; + 1FB96FC8292ABDD0007E68D1 /* AddSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSourceView.swift; sourceTree = ""; }; + 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriShortcutSetupView.swift; sourceTree = ""; }; + 1FB96FEB292C171D007E68D1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; + 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonProgressViewStyle.swift; sourceTree = ""; }; B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = ""; }; B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = ""; }; B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = ""; }; @@ -920,6 +972,7 @@ 191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */, 19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */, 19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */, + 1FB96FB829297C11007E68D1 /* GridStack in Frameworks */, B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */, D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */, B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */, @@ -972,6 +1025,145 @@ path = "libimobiledevice-glue/src"; sourceTree = ""; }; + 1F6E08DD29280AF1005059C0 /* View Extensions */ = { + isa = PBXGroup; + children = ( + 1FB96FF1292D051F007E68D1 /* Styles */, + 1F6E08DE29280AFF005059C0 /* UIView Representables */, + ); + path = "View Extensions"; + sourceTree = ""; + }; + 1F6E08DE29280AFF005059C0 /* UIView Representables */ = { + isa = PBXGroup; + children = ( + 1F6E08DF29280B12005059C0 /* SafariView.swift */, + 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */, + ); + path = "UIView Representables"; + sourceTree = ""; + }; + 1FAFC5B02927E01400B8D837 /* Views */ = { + isa = PBXGroup; + children = ( + 1FAFC5B52927E06300B8D837 /* RootView.swift */, + 1FAFC5B12927E02E00B8D837 /* Authentication */, + 1FAFC5B22927E03300B8D837 /* News */, + 1FAFC5B32927E03D00B8D837 /* Browse */, + 1FAFC5BC2927E0FD00B8D837 /* My Apps */, + 1FAFC5BF2927E12B00B8D837 /* Settings */, + 1FAFC5B42927E04B00B8D837 /* App Detail */, + ); + path = Views; + sourceTree = ""; + }; + 1FAFC5B12927E02E00B8D837 /* Authentication */ = { + isa = PBXGroup; + children = ( + ); + path = Authentication; + sourceTree = ""; + }; + 1FAFC5B22927E03300B8D837 /* News */ = { + isa = PBXGroup; + children = ( + 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */, + 1F943C652927F36600ABE095 /* NewsViewModel.swift */, + 1F943C632927EF4200ABE095 /* NewsItemView.swift */, + ); + path = News; + sourceTree = ""; + }; + 1FAFC5B32927E03D00B8D837 /* Browse */ = { + isa = PBXGroup; + children = ( + 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */, + 1FB96FC6292A853D007E68D1 /* SourcesView.swift */, + 1FB96FC8292ABDD0007E68D1 /* AddSourceView.swift */, + 1F6E08E729282174005059C0 /* ConfirmAddSourceView.swift */, + 1FB96FC4292A7251007E68D1 /* BrowseAppPreviewView.swift */, + ); + path = Browse; + sourceTree = ""; + }; + 1FAFC5B42927E04B00B8D837 /* App Detail */ = { + isa = PBXGroup; + children = ( + 1FB84BA52928DE08006A5CF4 /* AppDetailView.swift */, + ); + path = "App Detail"; + sourceTree = ""; + }; + 1FAFC5B72927E06C00B8D837 /* App */ = { + isa = PBXGroup; + children = ( + 1FAFC5A42927E00000B8D837 /* SideStoreUIApp.swift */, + ); + path = App; + sourceTree = ""; + }; + 1FAFC5BC2927E0FD00B8D837 /* My Apps */ = { + isa = PBXGroup; + children = ( + 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */, + ); + path = "My Apps"; + sourceTree = ""; + }; + 1FAFC5BF2927E12B00B8D837 /* Settings */ = { + isa = PBXGroup; + children = ( + 1FAFC5C02927E13C00B8D837 /* SettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 1FAFC5C22927E17100B8D837 /* Protocols */ = { + isa = PBXGroup; + children = ( + 1FAFC5C32927E18100B8D837 /* NavigationTab.swift */, + 1F943C672927F39400ABE095 /* ViewModel.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 1FB84BA72928E073006A5CF4 /* View Components */ = { + isa = PBXGroup; + children = ( + 1F6E08D9292806E0005059C0 /* AppRowView.swift */, + 1F6E08DB292807D3005059C0 /* AppIconView.swift */, + 1F6E08E529280F4B005059C0 /* RatingStars.swift */, + 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */, + 1FB96FBF292A63F2007E68D1 /* AppPillButton.swift */, + ); + path = "View Components"; + sourceTree = ""; + }; + 1FB96FC1292A6D6C007E68D1 /* Helper */ = { + isa = PBXGroup; + children = ( + 1FB96FC2292A6D7E007E68D1 /* DateFormatterHelper.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 1FB96FEA292C1704007E68D1 /* Manager */ = { + isa = PBXGroup; + children = ( + 1FB96FEB292C171D007E68D1 /* NotificationManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + 1FB96FF1292D051F007E68D1 /* Styles */ = { + isa = PBXGroup; + children = ( + 1F6E08E329280D1E005059C0 /* PillButtonStyle.swift */, + 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */, + ); + path = Styles; + sourceTree = ""; + }; B3146EC7284F580500BBC3FD /* Products */ = { isa = PBXGroup; children = ( @@ -1537,6 +1729,13 @@ BFD2476C2284B9A500981D42 /* AltStore */ = { isa = PBXGroup; children = ( + 1FAFC5B72927E06C00B8D837 /* App */, + 1FB96FC1292A6D6C007E68D1 /* Helper */, + 1FB96FEA292C1704007E68D1 /* Manager */, + 1FAFC5C22927E17100B8D837 /* Protocols */, + 1FAFC5B02927E01400B8D837 /* Views */, + 1FB84BA72928E073006A5CF4 /* View Components */, + 1F6E08DD29280AF1005059C0 /* View Extensions */, B39F16112918D7B5002E9404 /* Consts */, BF219A7E22CAC431007676A6 /* AltStore.entitlements */, BFD2476D2284B9A500981D42 /* AppDelegate.swift */, @@ -1994,6 +2193,7 @@ B3C395F6284F362400DA9E2F /* AppCenterAnalytics */, B3C395F8284F362400DA9E2F /* AppCenterCrashes */, 4879A9612861049C00FC1BBD /* OpenSSL */, + 1FB96FB729297C11007E68D1 /* GridStack */, ); productName = AltStore; productReference = BFD2476A2284B9A500981D42 /* SideStore.app */; @@ -2005,7 +2205,7 @@ BFD247622284B9A500981D42 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1400; + LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1020; ORGANIZATIONNAME = SideStore; TargetAttributes = { @@ -2064,7 +2264,11 @@ B3C395FD284F3C0900DA9E2F /* XCRemoteSwiftPackageReference "STPrivilegedTask" */, 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */, 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */, +<<<<<<< HEAD 99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */, +======= + 1FB96FB629297C11007E68D1 /* XCRemoteSwiftPackageReference "GridStack" */, +>>>>>>> f1b7033 ([ADD] News, Browse and Settings views ported to SwiftUI) ); productRefGroup = BFD2476B2284B9A500981D42 /* Products */; projectDirPath = ""; @@ -2433,21 +2637,32 @@ buildActionMask = 2147483647; files = ( BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, + 1F6E08E029280B12005059C0 /* SafariView.swift in Sources */, + 1F943C6C2927F90400ABE095 /* SettingsView.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, + 1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, + 1F6E08DA292806E0005059C0 /* AppRowView.swift in Sources */, + 1FB96FC3292A6D7E007E68D1 /* DateFormatterHelper.swift in Sources */, + 1F943C6D2927F90400ABE095 /* NewsViewModel.swift in Sources */, + 1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */, + 1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, + 1F943C6A2927F8F700ABE095 /* ViewModel.swift in Sources */, D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */, BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */, + 1FB96FC7292A853D007E68D1 /* SourcesView.swift in Sources */, D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */, + 1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */, BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */, BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */, BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */, @@ -2461,18 +2676,25 @@ BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */, BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */, D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */, + 1FB96FBE292A20E5007E68D1 /* ObservableScrollView.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, + 1FB96FC9292ABDD0007E68D1 /* AddSourceView.swift in Sources */, + 1F6E08DC292807D3005059C0 /* AppIconView.swift in Sources */, BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, + 1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */, BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */, BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */, BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */, BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */, B3EE16B62925E27D00B3B1F5 /* AnisetteManager.swift in Sources */, + 1F943C6E2927F90400ABE095 /* NewsItemView.swift in Sources */, BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */, B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */, + 1F6E08E629280F4B005059C0 /* RatingStars.swift in Sources */, BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */, + 1FB96FC0292A63F2007E68D1 /* AppPillButton.swift in Sources */, BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */, BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */, BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, @@ -2493,9 +2715,11 @@ BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */, B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */, BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */, + 1F943C6B2927F8F700ABE095 /* NavigationTab.swift in Sources */, BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */, BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */, D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */, + 1F6E08E429280D1E005059C0 /* PillButtonStyle.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */, @@ -2507,6 +2731,7 @@ BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, + 1FB96FCF292BBBCA007E68D1 /* SiriShortcutSetupView.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, @@ -2517,10 +2742,14 @@ BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */, BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */, BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */, + 1F943C702927F90400ABE095 /* BrowseView.swift in Sources */, + 1F943C692927F8F200ABE095 /* RootView.swift in Sources */, + 1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */, D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */, BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */, BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, + 1FB96FC5292A7251007E68D1 /* BrowseAppPreviewView.swift in Sources */, BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */, BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */, ); @@ -3339,6 +3568,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 1FB96FB629297C11007E68D1 /* XCRemoteSwiftPackageReference "GridStack" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pietropizzi/GridStack"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.3; + }; + }; 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SideStore/AltSign"; @@ -3424,6 +3661,11 @@ package = 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */; productName = OpenSSL; }; + 1FB96FB729297C11007E68D1 /* GridStack */ = { + isa = XCSwiftPackageProductDependency; + package = 1FB96FB629297C11007E68D1 /* XCRemoteSwiftPackageReference "GridStack" */; + productName = GridStack; + }; 4879A95E2861046500FC1BBD /* AltSign */ = { isa = XCSwiftPackageProductDependency; package = 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */; diff --git a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 410b636e..cee8ad0d 100644 --- a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "version" : "4.4.2" } }, + { + "identity" : "gridstack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pietropizzi/GridStack", + "state" : { + "revision" : "777c3b9d789d377033d8e4c8f99f9d0a04485dc4", + "version" : "0.4.3" + } + }, { "identity" : "keychainaccess", "kind" : "remoteSourceControl", diff --git a/AltStore/App/SideStoreUIApp.swift b/AltStore/App/SideStoreUIApp.swift new file mode 100644 index 00000000..9ba48190 --- /dev/null +++ b/AltStore/App/SideStoreUIApp.swift @@ -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() + } + } +} diff --git a/AltStore/Helper/DateFormatterHelper.swift b/AltStore/Helper/DateFormatterHelper.swift new file mode 100644 index 00000000..62ef062e --- /dev/null +++ b/AltStore/Helper/DateFormatterHelper.swift @@ -0,0 +1,48 @@ +// +// 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 + }() + + + static func string(forExpirationDate date: Date) -> String { + let startDate = Date() + let interval = date.timeIntervalSince(startDate) + guard interval > 0 else { + return "" + } + + if interval < (1 * 60 * 60) { + self.appExpirationDateFormatter.unitsStyle = .positional + self.appExpirationDateFormatter.allowedUnits = [.minute, .second] + } + else if interval < (2 * 24 * 60 * 60) + { + self.appExpirationDateFormatter.unitsStyle = .positional + self.appExpirationDateFormatter.allowedUnits = [.hour, .minute, .second] + } + else + { + self.appExpirationDateFormatter.unitsStyle = .full + self.appExpirationDateFormatter.allowedUnits = [.day] + +// let numberOfDays = endDate.numberOfCalendarDays(since: startDate) +// text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)) + } + + return self.appExpirationDateFormatter.string(from: startDate, to: date) ?? "" + } +} diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift index 25692f06..918b4c30 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI import Roxas import EmotionalDamage import minimuxer @@ -40,7 +41,10 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg { defer { // 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() } diff --git a/AltStore/Manager/NotificationManager.swift b/AltStore/Manager/NotificationManager.swift new file mode 100644 index 00000000..1fad0452 --- /dev/null +++ b/AltStore/Manager/NotificationManager.swift @@ -0,0 +1,75 @@ +// +// 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 + } + + + let notificationId = UUID() + + self.notifications[notificationId] = Notification(id: notificationId, title: text, message: detailText) + + let dismissWorkItem = DispatchWorkItem { + self.notifications.removeValue(forKey: notificationId) + } + DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .seconds(5)), execute: dismissWorkItem) + } +} diff --git a/AltStore/Protocols/NavigationTab.swift b/AltStore/Protocols/NavigationTab.swift new file mode 100644 index 00000000..0b889af4 --- /dev/null +++ b/AltStore/Protocols/NavigationTab.swift @@ -0,0 +1,21 @@ +// +// NavigationTab.swift +// SideStoreUI +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import Foundation + +protocol NavigationTab: RawRepresentable, Identifiable, CaseIterable, Hashable where RawValue == Int { + static var defaultTab: Self { get } + var displaySymbol: String { get } + var displayName: String { get } +} + +extension NavigationTab { + var id: Int { + self.rawValue + } +} diff --git a/AltStore/Protocols/ViewModel.swift b/AltStore/Protocols/ViewModel.swift new file mode 100644 index 00000000..35a69eeb --- /dev/null +++ b/AltStore/Protocols/ViewModel.swift @@ -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 {} diff --git a/AltStore/Resources/Assets.xcassets/Colors/AccentColor.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..a463888d --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Colors/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.263", + "red" : "0.502" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.404", + "green" : "0.000", + "red" : "0.235" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/View Components/AppIconView.swift b/AltStore/View Components/AppIconView.swift new file mode 100644 index 00000000..fc161166 --- /dev/null +++ b/AltStore/View Components/AppIconView.swift @@ -0,0 +1,35 @@ +// +// SwiftUIView.swift +// SideStore +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI + +struct AppIconView: View { + let iconUrl: URL? + var size: CGFloat = 64 + var cornerRadius: CGFloat { + size * 0.234 + } + + var body: some View { + if let iconUrl, #available(iOS 15.0, *) { + AsyncImage(url: iconUrl) { image in + image + .resizable() + } placeholder: { + Color(UIColor.secondarySystemBackground) + } + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + } else { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .background(Color.secondary) + .frame(width: size, height: size) + } + } +} + diff --git a/AltStore/View Components/AppPillButton.swift b/AltStore/View Components/AppPillButton.swift new file mode 100644 index 00000000..004576d2 --- /dev/null +++ b/AltStore/View Components/AppPillButton.swift @@ -0,0 +1,99 @@ +// +// 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 "Open" + } + + return "Free" + } + + var body: some View { + SwiftUI.Button(action: handleButton, label: { + Text(buttonText.uppercased()) + .bold() + }) + .buttonStyle(PillButtonStyle(tintColor: storeApp?.tintColor ?? .black, progress: progress)) + } + + func handleButton() { + if let installedApp { + self.openApp(installedApp) + } else if let storeApp { + self.installApp(storeApp) + } + } + + func openApp(_ installedApp: InstalledApp) { + UIApplication.shared.open(installedApp.openAppURL) + } + + 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 var previews: some View { +// AppPillButton() +// } +//} diff --git a/AltStore/View Components/AppRowView.swift b/AltStore/View Components/AppRowView.swift new file mode 100644 index 00000000..2c2a9d73 --- /dev/null +++ b/AltStore/View Components/AppRowView.swift @@ -0,0 +1,51 @@ +// +// 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 body: some View { + HStack(alignment: .center, spacing: 12) { + AppIconView(iconUrl: storeApp?.iconURL) + + VStack(alignment: .leading, spacing: 2) { + Text(app.name) + .bold() + + Text(storeApp?.developerName ?? "Sideloaded") + .font(.callout) + .foregroundColor(.secondary) + + RatingStars(rating: 4) + .frame(height: 12) + .foregroundColor(.secondary) + } + .lineLimit(1) + + Spacer() + + AppPillButton(app: app) + } + .padding() + .background(Color(storeApp?.tintColor ?? UIColor.black).opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 30, style: .circular)) + } +} + +//struct AppRowView_Previews: PreviewProvider { +// static var previews: some View { +// AppRowView() +// } +//} diff --git a/AltStore/View Components/ObservableScrollView.swift b/AltStore/View Components/ObservableScrollView.swift new file mode 100644 index 00000000..38fe9270 --- /dev/null +++ b/AltStore/View Components/ObservableScrollView.swift @@ -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: View { + @Namespace var scrollViewNamespace + + @Binding var scrollOffset: CGFloat + + let content: (ScrollViewProxy) -> Content + + init(scrollOffset: Binding, @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() + } +} diff --git a/AltStore/View Components/RatingStars.swift b/AltStore/View Components/RatingStars.swift new file mode 100644 index 00000000..d8a6ce83 --- /dev/null +++ b/AltStore/View Components/RatingStars.swift @@ -0,0 +1,30 @@ +// +// RatingStars.swift +// SideStore +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI + +struct RatingStars: View { + + let rating: Int + + var body: some View { + HStack(spacing: 0) { + ForEach(0..<5) { i in + Image(systemName: i < rating ? "star.fill" : "star") + .resizable() + .aspectRatio(contentMode: .fit) + } + } + } +} + +struct RatingStars_Previews: PreviewProvider { + static var previews: some View { + RatingStars(rating: 4) + } +} diff --git a/AltStore/View Extensions/Styles/PillButtonProgressViewStyle.swift b/AltStore/View Extensions/Styles/PillButtonProgressViewStyle.swift new file mode 100644 index 00000000..7ea56fb2 --- /dev/null +++ b/AltStore/View Extensions/Styles/PillButtonProgressViewStyle.swift @@ -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)) + } +} diff --git a/AltStore/View Extensions/Styles/PillButtonStyle.swift b/AltStore/View Extensions/Styles/PillButtonStyle.swift new file mode 100644 index 00000000..c1bd0a39 --- /dev/null +++ b/AltStore/View Extensions/Styles/PillButtonStyle.swift @@ -0,0 +1,46 @@ +// +// 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, 8) + .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) + } + } + } +} diff --git a/AltStore/View Extensions/UIView Representables/SafariView.swift b/AltStore/View Extensions/UIView Representables/SafariView.swift new file mode 100644 index 00000000..8976acf9 --- /dev/null +++ b/AltStore/View Extensions/UIView Representables/SafariView.swift @@ -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) { } +} diff --git a/AltStore/View Extensions/UIView Representables/SiriShortcutSetupView.swift b/AltStore/View Extensions/UIView Representables/SiriShortcutSetupView.swift new file mode 100644 index 00000000..ccfc3e25 --- /dev/null +++ b/AltStore/View Extensions/UIView Representables/SiriShortcutSetupView.swift @@ -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) + } +} diff --git a/AltStore/Views/App Detail/AppDetailView.swift b/AltStore/Views/App Detail/AppDetailView.swift new file mode 100644 index 00000000..5f71e0dd --- /dev/null +++ b/AltStore/Views/App Detail/AppDetailView.swift @@ -0,0 +1,210 @@ +// +// AppDetailView.swift +// SideStore +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +import GridStack +import AltStoreCore + +struct AppDetailView: View { + + let storeApp: StoreApp + + var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + return dateFormatter + }() + + var byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + return formatter + }() + + @State var scrollOffset: CGFloat = .zero + let maxContentCornerRadius: CGFloat = 24 + let headerViewHeight: CGFloat = 140 + let permissionColumns = 4 + + var isHeaderViewVisible: Bool { + scrollOffset < headerViewHeight + 12 + } + var contentCornerRadius: CGFloat { + max(CGFloat.zero, min(maxContentCornerRadius, maxContentCornerRadius * (1 - self.scrollOffset / self.headerViewHeight))) + } + + var body: some View { + ObservableScrollView(scrollOffset: self.$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: 20) + } + + AppRowView(app: storeApp) + .padding(.horizontal) + } + } + + var contentView: some View { + VStack(alignment: .leading) { + if let subtitle = storeApp.subtitle { + Text(subtitle) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } + + if !storeApp.screenshotURLs.isEmpty { + screenshotsView + } + + Text(storeApp.localizedDescription) + .lineLimit(6) + .padding() + + currentVersionView + + permissionsView + + // TODO: Add review view + // Only let users rate the app if it is currently installed! + } + .background( + RoundedRectangle(cornerRadius: contentCornerRadius) + .foregroundColor(Color(UIColor.systemBackground)) + .shadow(radius: isHeaderViewVisible ? 12 : 0) + ) + } + + var screenshotsView: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(storeApp.screenshotURLs) { url in + if #available(iOS 15.0, *) { + AsyncImage(url: url) { image in + image.resizable() + } placeholder: { + Rectangle() + .foregroundColor(.secondary) + } + .aspectRatio(9/16, contentMode: .fit) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + } + .frame(height: 400) + .shadow(radius: 12) + } + + var currentVersionView: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .bottom) { + VStack(alignment: .leading) { + Text("What's New") + .bold() + .font(.title3) + + Text("Version \(storeApp.version)") + .font(.callout) + .foregroundColor(.secondary) + } + + Spacer() + + VStack(alignment: .trailing) { + Text(dateFormatter.string(from: storeApp.versionDate)) + Text(byteCountFormatter.string(fromByteCount: Int64(storeApp.size))) + } + .font(.callout) + .foregroundColor(.secondary) + } + + if let versionDescription = storeApp.versionDescription { + Text(versionDescription) + .lineLimit(5) + } else { + Text("No version information") + .foregroundColor(.secondary) + } + } + .padding() + } + + var permissionsView: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Permissions") + .bold() + .font(.title3) + + if storeApp.permissions.isEmpty { + Text("The app requires no permissions.") + .font(.callout) + .foregroundColor(.secondary) + } else { + GridStack(minCellWidth: 40, spacing: 8, numItems: storeApp.permissions.count, alignment: .leading) { index, cellWidth in + let permission = storeApp.permissions[index] + + VStack { + Image(systemName: "photo.on.rectangle") + .padding() + .background(Circle().foregroundColor(Color(UIColor.secondarySystemBackground))) + Text(permission.type.localizedShortName ?? "") + } + .frame(width: cellWidth, height: cellWidth * 1.2) + .background(Color.red) + } + } + + Spacer() + } + .padding() + } +} + +//struct AppDetailView_Previews: PreviewProvider { +// static var previews: some View { +// AppDetailView() +// } +//} diff --git a/AltStore/Views/Browse/AddSourceView.swift b/AltStore/Views/Browse/AddSourceView.swift new file mode 100644 index 00000000..553a3201 --- /dev/null +++ b/AltStore/Views/Browse/AddSourceView.swift @@ -0,0 +1,54 @@ +// +// AddSourceView.swift +// SideStore +// +// Created by Fabian Thies on 20.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI + +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("Source URL") + } footer: { + VStack(alignment: .leading, spacing: 4) { + Text("Please enter the source url here. Then, tap continue to validate and add the source in the next step.") + + HStack(alignment: .top) { + Image(systemName: "exclamationmark.triangle.fill") + + Text("Be careful with unvalidated third-party sources! Make sure to only add sources that you trust.") + } + } + } + + SwiftUI.Button { + self.continueHandler(self.sourceUrlText) + } label: { + Text("Continue") + } + .disabled(URL(string: self.sourceUrlText)?.host == nil) + } + .listStyle(InsetGroupedListStyle()) + .navigationTitle("Add Source") + } +} + +struct AddSourceView_Previews: PreviewProvider { + static var previews: some View { + AddSourceView(continueHandler: { _ in }) + } +} diff --git a/AltStore/Views/Browse/BrowseAppPreviewView.swift b/AltStore/Views/Browse/BrowseAppPreviewView.swift new file mode 100644 index 00000000..1acbda62 --- /dev/null +++ b/AltStore/Views/Browse/BrowseAppPreviewView.swift @@ -0,0 +1,48 @@ +// +// BrowseAppPreviewView.swift +// SideStore +// +// Created by Fabian Thies on 20.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +import AltStoreCore + +struct BrowseAppPreviewView: View { + let storeApp: StoreApp + + var body: some View { + VStack { + AppRowView(app: storeApp) + + if let subtitle = storeApp.subtitle { + Text(subtitle) + } + + if !storeApp.screenshotURLs.isEmpty { + HStack { + ForEach(storeApp.screenshotURLs.prefix(2)) { url in + if #available(iOS 15.0, *) { + AsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fit) + } placeholder: { + Color(UIColor.secondarySystemBackground) + } + .cornerRadius(8) + } + } + } + .frame(height: 300) + } + } + } +} + +//struct BrowseAppPreviewView_Previews: PreviewProvider { +// static var previews: some View { +// BrowseAppPreviewView() +// } +//} diff --git a/AltStore/Views/Browse/BrowseView.swift b/AltStore/Views/Browse/BrowseView.swift new file mode 100644 index 00000000..b8b261bc --- /dev/null +++ b/AltStore/Views/Browse/BrowseView.swift @@ -0,0 +1,99 @@ +// +// BrowseView.swift +// SideStoreUI +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +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 + + var filteredApps: [StoreApp] { + apps.filter { $0.matches(self.searchText) } + } + + @State + var selectedStoreApp: StoreApp? + + @State var searchText = "" + + @State var isShowingSourcesView = false + + var body: some View { + ScrollView { + LazyVStack(spacing: 24) { + ForEach(filteredApps, id: \.bundleIdentifier) { app in + NavigationLink { + AppDetailView(storeApp: app) + } label: { + BrowseAppPreviewView(storeApp: app) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding() + .searchable(text: self.$searchText, placeholder: "Search") + } + .background(Color(UIColor.systemGroupedBackground).ignoresSafeArea()) + .navigationTitle("Browse") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + SwiftUI.Button { + self.isShowingSourcesView = true + } label: { + Text("Sources") + } + .sheet(isPresented: self.$isShowingSourcesView) { + NavigationView { + SourcesView() + } + } + } + } + } +} + +struct BrowseView_Previews: PreviewProvider { + static var previews: some View { + BrowseView() + } +} + + +extension StoreApp { + func matches(_ searchText: String) -> Bool { + searchText.isEmpty || + self.name.lowercased().contains(searchText.lowercased()) || + self.developerName.lowercased().contains(searchText.lowercased()) + } +} + +extension View { + + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + + @ViewBuilder func searchable(text: Binding, placeholder: String) -> some View { + if #available(iOS 15.0, *) { + self.searchable(text: text, prompt: Text(placeholder)) + } else { + self + } + } +} diff --git a/AltStore/Views/Browse/ConfirmAddSourceView.swift b/AltStore/Views/Browse/ConfirmAddSourceView.swift new file mode 100644 index 00000000..e295ae58 --- /dev/null +++ b/AltStore/Views/Browse/ConfirmAddSourceView.swift @@ -0,0 +1,114 @@ +// +// ConfirmAddSourceView.swift +// SideStore +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +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) Apps") + + Text(source.apps.map { $0.name }.joined(separator: ", ")) + .font(.callout) + .lineLimit(1) + .foregroundColor(.secondary) + } + + VStack() { + Text("\(source.newsItems.count) News Items") + } + } header: { + Text("Source Contents") + } + + Section { + VStack(alignment: .leading) { + Text("Source Identifier") + Text(source.identifier) + .font(.callout) + .foregroundColor(.secondary) + } + + VStack(alignment: .leading) { + Text("Source URL") + Text(source.sourceURL.absoluteString) + .font(.callout) + .foregroundColor(.secondary) + } + } header: { + Text("Source Info") + } + } + .listStyle(InsetGroupedListStyle()) + + Spacer() + + SwiftUI.Button { + confirmationHandler(fetchedSource) + } label: { + Label(title: { Text("Add Source") }, icon: { Image(systemName: "plus") }) + .multilineTextAlignment(.center) + .foregroundColor(.white) + .padding() + .frame( + maxWidth: .infinity, + alignment: .bottomLeading + ) + .background( + RoundedRectangle(cornerRadius: 10) + .foregroundColor(Color.accentColor) + ) + } + .padding() + } + .background(Color(UIColor.systemGroupedBackground).ignoresSafeArea()) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + SwiftUI.Button { + + } label: { + Image(systemName: "xmark.circle.fill") + .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 }) + } +} diff --git a/AltStore/Views/Browse/SourcesView.swift b/AltStore/Views/Browse/SourcesView.swift new file mode 100644 index 00000000..9c9cfd31 --- /dev/null +++ b/AltStore/Views/Browse/SourcesView.swift @@ -0,0 +1,188 @@ +// +// SourcesView.swift +// SideStore +// +// Created by Fabian Thies on 20.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +import AltStoreCore +import CoreData + +struct SourcesView: View { + + @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 + + + @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("Sources control what apps are available to download through SideStore.") + .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() + .background(Color.accentColor.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 30, style: .circular)) + .if(source.identifier != Source.altStoreIdentifier) { view in + view.contextMenu(ContextMenu(menuItems: { + SwiftUI.Button { + self.removeSource(source) + } label: { + Label("Remove", systemImage: "trash") + } + })) + } + } + } + + // Trusted Sources + LazyVStack(alignment: .leading) { + Text("Trusted Sources") + .font(.title3) + .bold() + + Text("SideStore has reviewed these sources to make sure they meet our safety standards.") + .font(.callout) + .foregroundColor(.secondary) + + + } + } + .padding() + } + .navigationTitle("Sources") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + SwiftUI.Button { + self.isShowingAddSourceAlert = true + } label: { + Image(systemName: "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 { + self.isShowingAddSourceAlert = false + } label: { + Text("Done").bold() + } + } + } + } + + + func fetchSource(with urlText: String) { + self.isShowingAddSourceAlert = false + + guard let url = URL(string: urlText) else { + return + } + + AppManager.shared.fetchSource(sourceURL: url) { result in + + switch result { + case let .success(source): + self.sourceToConfirm = FetchedSource(source: source) + case let .failure(error): + print(error) + } + } + } + + func addSource(_ source: FetchedSource) { + source.context?.perform { + do { + try source.context?.save() + } catch { + print(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) + } + } + } +} + +struct SourcesListView_Previews: PreviewProvider { + static var previews: some View { + 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) { + guard let context = source.managedObjectContext else{ + return nil + } + self.source = source + self.context = context + } +} diff --git a/AltStore/Views/My Apps/MyAppsView.swift b/AltStore/Views/My Apps/MyAppsView.swift new file mode 100644 index 00000000..f944d9be --- /dev/null +++ b/AltStore/Views/My Apps/MyAppsView.swift @@ -0,0 +1,27 @@ +// +// MyAppsView.swift +// SideStoreUI +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI + +struct MyAppsView: View { + var body: some View { + ScrollView { + LazyVStack { + + } + } + .background(Color(UIColor.systemGroupedBackground).ignoresSafeArea()) + .navigationTitle("My Apps") + } +} + +struct MyAppsView_Previews: PreviewProvider { + static var previews: some View { + MyAppsView() + } +} diff --git a/AltStore/Views/News/NewsItemView.swift b/AltStore/Views/News/NewsItemView.swift new file mode 100644 index 00000000..bad2d248 --- /dev/null +++ b/AltStore/Views/News/NewsItemView.swift @@ -0,0 +1,101 @@ +// +// NewsItemView.swift +// SideStoreUI +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +import AltStoreCore + +struct NewsItemView: View { + typealias TapHandler = (T) -> Void + + let newsItem: NewsItem + + private var newsSelectionHandler: TapHandler? = nil + private var appSelectionHandler: TapHandler? = 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) { + Text(newsItem.title) + .font(.title2) + .bold() + .foregroundColor(.white) + + Text(newsItem.caption) + .foregroundColor(.white.opacity(0.7)) + } + .padding(24) + + if let imageUrl = newsItem.imageURL, #available(iOS 15.0, *) { + AsyncImage( + url: imageUrl, + content: { image in + if let image = image.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + } + } + ) + } + } + .frame( + maxWidth: .infinity, + alignment: .topLeading + ) + .background(Color(newsItem.tintColor)) + .clipShape(RoundedRectangle(cornerRadius: 30, style: .circular)) + } + + + func onNewsSelection(_ handler: @escaping TapHandler) -> Self { + var newSelf = self + newSelf.newsSelectionHandler = handler + return newSelf + } + + func onAppSelection(_ handler: @escaping TapHandler) -> 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() +// } +//} diff --git a/AltStore/Views/News/NewsView.swift b/AltStore/Views/News/NewsView.swift new file mode 100644 index 00000000..39ec6f35 --- /dev/null +++ b/AltStore/Views/News/NewsView.swift @@ -0,0 +1,55 @@ +// +// 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 + + @State + var activeExternalUrl: URL? + + @State + var selectedStoreApp: StoreApp? + + var body: some View { + ScrollView { + 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("News") + .sheet(item: self.$activeExternalUrl) { url in + SafariView(url: url) + } + } +} + +struct NewsView_Previews: PreviewProvider { + static var previews: some View { + NewsView() + } +} diff --git a/AltStore/Views/News/NewsViewModel.swift b/AltStore/Views/News/NewsViewModel.swift new file mode 100644 index 00000000..1bffd4e0 --- /dev/null +++ b/AltStore/Views/News/NewsViewModel.swift @@ -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 + + init() {} +} diff --git a/AltStore/Views/RootView.swift b/AltStore/Views/RootView.swift new file mode 100644 index 00000000..3dc9192d --- /dev/null +++ b/AltStore/Views/RootView.swift @@ -0,0 +1,109 @@ +// +// RootView.swift +// SideStoreUI +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI + +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() + .background(Color(UIColor.altPrimary)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + + 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: String { + switch self { + case .news: return "newspaper" + case .browse: return "app.dashed" + case .myApps: return "app.badge" + case .settings: return "gearshape" + } + } + + var displayName: String { + switch self { + case .news: return "News" + case .browse: return "Browse" + case .myApps: return "My Apps" + case .settings: return "Settings" + } + } + + var label: some View { + Label(self.displayName, systemImage: self.displaySymbol) + } + } +} + +struct RootView_Previews: PreviewProvider { + static var previews: some View { + RootView() + } +} diff --git a/AltStore/Views/Settings/SettingsView.swift b/AltStore/Views/Settings/SettingsView.swift new file mode 100644 index 00000000..b3e64c42 --- /dev/null +++ b/AltStore/Views/Settings/SettingsView.swift @@ -0,0 +1,146 @@ +// +// SettingsView.swift +// SideStoreUI +// +// Created by Fabian Thies on 18.11.22. +// Copyright © 2022 Fabian Thies. All rights reserved. +// + +import SwiftUI +import AltStoreCore +import Intents + +struct SettingsView: View { + + @AppStorage("isBackgroundRefreshEnabled") + var isBackgroundRefreshEnabled: Bool = true + + @State var isShowingAddShortcutView = false + + var body: some View { + List { + Section { + + if let team = DatabaseManager.shared.activeTeam() { + HStack { + Text("Name") + .foregroundColor(.secondary) + Spacer() + Text(team.name) + } + + HStack { + Text("E-Mail") + .foregroundColor(.secondary) + Spacer() + Text(team.account.appleID) + } + + HStack { + Text("Type") + .foregroundColor(.secondary) + Spacer() + Text(team.type.localizedDescription) + } + } + } header: { + HStack { + Text("Connected Apple ID") + Spacer() + SwiftUI.Button { + + } label: { + Text("Sign Out") + .font(.callout) + .bold() + } + } + } + + Section { + Toggle(isOn: self.$isBackgroundRefreshEnabled, label: { + Text("Background Refresh") + }) + + if #available(iOS 14.0, *) { + SwiftUI.Button { + self.isShowingAddShortcutView = true + } label: { + Text("Add to Siri...") + } + .sheet(isPresented: self.$isShowingAddShortcutView) { + if let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) { + SiriShortcutSetupView(shortcut: shortcut) + } + } + } + } header: { + Text("Refreshing Apps") + } footer: { + Text("Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.") + } + + + Section { + SwiftUI.Button(action: switchToUIKit) { + Text("Switch to UIKit") + } + + } header: { + Text("Debug") + } + + + Section { + NavigationLink { + SafariView(url: URL(string: "https://fabian-thies.de")!) + } label: { + HStack { + Text("SwiftUI Redesign") + .foregroundColor(.secondary) + Spacer() + Text("fabianthdev") + } + } + + } header: { + Text("Credits") + } + + Section { + + } footer: { + Text("SideStore 1.0.0") + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } + } + .listStyle(InsetGroupedListStyle()) + .navigationTitle("Settings") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + SwiftUI.Button { + + } label: { + Image(systemName: "person.crop.circle") + .imageScale(.large) + } + + } + } + } + + + func switchToUIKit() { + let storyboard = UIStoryboard(name: "Main", bundle: .main) + let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController + + UIApplication.shared.keyWindow?.rootViewController = rootVC + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView() + } +} diff --git a/Dependencies/em_proxy b/Dependencies/em_proxy index c8a280e5..57ab5a00 160000 --- a/Dependencies/em_proxy +++ b/Dependencies/em_proxy @@ -1 +1 @@ -Subproject commit c8a280e54c93be130b7087c41eba83b1c15f96be +Subproject commit 57ab5a000214800288a3cdd151dab3cf040bf376