From 711dd69b749ce9f45830a4c8f2a7e80f6d26a3c1 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 19 Jul 2019 16:42:40 -0700 Subject: [PATCH] [AltStore] Adds redesigned MyAppsViewController to refresh/update installed apps --- AltStore.xcodeproj/project.pbxproj | 52 +- AltStore/Apps/AppTableViewCell.swift | 53 -- AltStore/Apps/AppsViewController.swift | 96 --- AltStore/Base.lproj/Main.storyboard | 435 +++++--------- .../Browse/BrowseCollectionViewCell.swift | 2 +- AltStore/Browse/BrowseViewController.swift | 39 +- AltStore/Components/AppIconImageView.swift | 2 + ...{ProgressButton.swift => PillButton.swift} | 51 +- AltStore/Components/ToastView.swift | 19 + AltStore/Extensions/UIColor+AltStore.swift | 5 + AltStore/My Apps/MyAppsComponents.swift | 46 ++ AltStore/My Apps/MyAppsViewController.swift | 561 +++++++++++++----- .../My Apps/UpdateCollectionViewCell.swift | 121 ++++ AltStore/My Apps/UpdateCollectionViewCell.xib | 136 +++++ AltStore/Operations/InstallAppOperation.swift | 11 + .../RefreshGreen.colorset/Contents.json | 20 + .../RefreshOrange.colorset/Contents.json | 20 + .../Colors/RefreshRed.colorset/Contents.json | 20 + .../RefreshYellow.colorset/Contents.json | 20 + AltStore/Updates/UpdatesViewController.swift | 202 ------- Dependencies/Roxas | 2 +- 21 files changed, 1069 insertions(+), 844 deletions(-) delete mode 100644 AltStore/Apps/AppTableViewCell.swift delete mode 100644 AltStore/Apps/AppsViewController.swift rename AltStore/Components/{ProgressButton.swift => PillButton.swift} (55%) create mode 100644 AltStore/Components/ToastView.swift create mode 100644 AltStore/My Apps/MyAppsComponents.swift create mode 100644 AltStore/My Apps/UpdateCollectionViewCell.swift create mode 100644 AltStore/My Apps/UpdateCollectionViewCell.xib create mode 100644 AltStore/Resources/Assets.xcassets/Colors/RefreshGreen.colorset/Contents.json create mode 100644 AltStore/Resources/Assets.xcassets/Colors/RefreshOrange.colorset/Contents.json create mode 100644 AltStore/Resources/Assets.xcassets/Colors/RefreshRed.colorset/Contents.json create mode 100644 AltStore/Resources/Assets.xcassets/Colors/RefreshYellow.colorset/Contents.json delete mode 100644 AltStore/Updates/UpdatesViewController.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index e5b8b93f..2e9eceb5 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -11,7 +11,10 @@ BF0201BB22C2EFA3000B93E4 /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; }; BF0201BE22C2EFBC000B93E4 /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; }; + BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; }; BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */; }; + BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B0F022E25DF9005C4CF5 /* ToastView.swift */; }; BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3129229F474900370A3C /* ConnectionManager.swift */; }; BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; }; BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAC8852295C90300587369 /* Result+Conveniences.swift */; }; @@ -104,19 +107,17 @@ BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; }; BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; }; BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; }; - BF9ABA4D22DD16DE008935CF /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* ProgressButton.swift */; }; + BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; }; BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */; }; BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; }; BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; }; BFB1169D22932DB100BB457C /* Apps-Dev.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Dev.json */; }; - BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169F22933DEB00BB457C /* UpdatesViewController.swift */; }; + BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; }; BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; }; BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; }; BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; }; BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; }; BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; }; - BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476F2284B9A500981D42 /* AppsViewController.swift */; }; - BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* MyAppsViewController.swift */; }; BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; }; BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; }; BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247782284B9A700981D42 /* LaunchScreen.storyboard */; }; @@ -124,7 +125,6 @@ BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFD247862284BB3B00981D42 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478B2284C4C300981D42 /* AppIconImageView.swift */; }; BFD2478F2284C8F900981D42 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478E2284C8F900981D42 /* Button.swift */; }; - BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247922284D4B700981D42 /* AppTableViewCell.swift */; }; BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */; }; BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; }; BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; }; @@ -159,6 +159,7 @@ BFD52C2022A1A9EC000B7ED1 /* node.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1D22A1A9EC000B7ED1 /* node.c */; }; BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; }; BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; }; + BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */; }; BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */; }; BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; }; BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */; }; @@ -239,7 +240,10 @@ /* Begin PBXFileReference section */ 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.release.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.release.xcconfig"; sourceTree = ""; }; + BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = ""; }; + BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = ""; }; BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; + BF18B0F022E25DF9005C4CF5 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; BF1E3128229F474900370A3C /* ServerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = ""; }; BF1E3129229F474900370A3C /* ConnectionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionManager.swift; sourceTree = ""; }; BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AltStore.swift"; sourceTree = ""; }; @@ -338,13 +342,13 @@ BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCollectionViewCell.swift; sourceTree = ""; }; BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotCollectionViewCell.swift; sourceTree = ""; }; BF9ABA4A22DD137F008935CF /* NavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; - BF9ABA4C22DD16DE008935CF /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = ""; }; + BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = ""; }; BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = ""; }; BFB1169C22932DB100BB457C /* Apps-Dev.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Apps-Dev.json"; sourceTree = ""; }; - BFB1169F22933DEB00BB457C /* UpdatesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesViewController.swift; sourceTree = ""; }; + BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = ""; }; BFBAC8852295C90300587369 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = ""; }; BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = ""; }; BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; @@ -352,8 +356,6 @@ BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = ""; }; BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - BFD2476F2284B9A500981D42 /* AppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViewController.swift; sourceTree = ""; }; - BFD247712284B9A500981D42 /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = ""; }; BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BFD247762284B9A700981D42 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BFD247792284B9A700981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -361,7 +363,6 @@ BFD247862284BB3B00981D42 /* Roxas.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = ""; }; BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; - BFD247922284D4B700981D42 /* AppTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTableViewCell.swift; sourceTree = ""; }; BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailViewController.swift; sourceTree = ""; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = ""; }; BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = ""; }; @@ -397,6 +398,7 @@ BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; }; BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; }; BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; }; + BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = ""; }; BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = ""; }; BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignAppOperation.swift; sourceTree = ""; }; @@ -667,18 +669,13 @@ path = Browse; sourceTree = ""; }; - BFB1169E22933DDC00BB457C /* Updates */ = { - isa = PBXGroup; - children = ( - BFB1169F22933DEB00BB457C /* UpdatesViewController.swift */, - ); - path = Updates; - sourceTree = ""; - }; BFBBE2E2229320A2002097FA /* My Apps */ = { isa = PBXGroup; children = ( - BFD247712284B9A500981D42 /* MyAppsViewController.swift */, + BF08858222DE795100DE9F1E /* MyAppsViewController.swift */, + BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */, + BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */, + BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */, ); path = "My Apps"; sourceTree = ""; @@ -726,7 +723,6 @@ BF9ABA4322DCFF33008935CF /* Browse */, BFD2478A2284C49000981D42 /* Apps */, BFBBE2E2229320A2002097FA /* My Apps */, - BFB1169E22933DDC00BB457C /* Updates */, BFDB69FB22A9A7A6007EA6D6 /* Account */, BFC51D7922972F1F00388324 /* Server */, BFD247982284D7FC00981D42 /* Model */, @@ -758,8 +754,6 @@ BFD2478A2284C49000981D42 /* Apps */ = { isa = PBXGroup; children = ( - BFD2476F2284B9A500981D42 /* AppsViewController.swift */, - BFD247922284D4B700981D42 /* AppTableViewCell.swift */, BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */, BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */, ); @@ -774,7 +768,8 @@ BF43002D22A714AF0051E2BC /* Keychain.swift */, BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */, BF9ABA4A22DD137F008935CF /* NavigationBar.swift */, - BF9ABA4C22DD16DE008935CF /* ProgressButton.swift */, + BF9ABA4C22DD16DE008935CF /* PillButton.swift */, + BF18B0F022E25DF9005C4CF5 /* ToastView.swift */, ); path = Components; sourceTree = ""; @@ -1079,6 +1074,7 @@ buildActionMask = 2147483647; files = ( BFB1169D22932DB100BB457C /* Apps-Dev.json in Resources */, + BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */, BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */, BFD247772284B9A700981D42 /* Assets.xcassets in Resources */, @@ -1231,7 +1227,6 @@ BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */, BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */, - BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */, BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, @@ -1244,16 +1239,17 @@ BFE6326622A857C200F30809 /* Team.swift in Sources */, BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, - BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, - BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */, BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */, BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */, BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */, - BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */, + BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */, + BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */, + BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, + BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */, BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */, BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, @@ -1261,7 +1257,7 @@ BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, - BF9ABA4D22DD16DE008935CF /* ProgressButton.swift in Sources */, + BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, diff --git a/AltStore/Apps/AppTableViewCell.swift b/AltStore/Apps/AppTableViewCell.swift deleted file mode 100644 index e8ad4411..00000000 --- a/AltStore/Apps/AppTableViewCell.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// AppTableViewCell.swift -// AltStore -// -// Created by Riley Testut on 5/9/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -import UIKit - -@objc class AppTableViewCell: UITableViewCell -{ - @IBOutlet var nameLabel: UILabel! - @IBOutlet var developerLabel: UILabel! - @IBOutlet var appIconImageView: UIImageView! - @IBOutlet var button: UIButton! - - override func awakeFromNib() - { - super.awakeFromNib() - - self.selectionStyle = .none - } - - override func setHighlighted(_ highlighted: Bool, animated: Bool) - { - super.setHighlighted(highlighted, animated: animated) - - self.update() - } - - override func setSelected(_ selected: Bool, animated: Bool) - { - super.setSelected(selected, animated: animated) - - self.update() - } -} - -private extension AppTableViewCell -{ - func update() - { - if self.isHighlighted || self.isSelected - { - self.contentView.backgroundColor = UIColor(white: 0.9, alpha: 1.0) - } - else - { - self.contentView.backgroundColor = .white - } - } -} diff --git a/AltStore/Apps/AppsViewController.swift b/AltStore/Apps/AppsViewController.swift deleted file mode 100644 index 083ee7f0..00000000 --- a/AltStore/Apps/AppsViewController.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// AppsViewController.swift -// AltStore -// -// Created by Riley Testut on 5/9/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -import UIKit -import Roxas - -class AppsViewController: UITableViewController -{ - private lazy var dataSource = self.makeDataSource() - - override func viewDidLoad() - { - super.viewDidLoad() - - self.tableView.dataSource = self.dataSource - - // Hide trailing row separators. - self.tableView.tableFooterView = UIView() - } - - override func viewWillAppear(_ animated: Bool) - { - super.viewWillAppear(animated) - - self.fetchApps() - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) - { - guard segue.identifier == "showAppDetail" else { return } - - guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return } - - let app = self.dataSource.item(at: indexPath) - - let appDetailViewController = segue.destination as! AppDetailViewController - appDetailViewController.app = app - } -} - -private extension AppsViewController -{ - func makeDataSource() -> RSTFetchedResultsTableViewDataSource - { - let fetchRequest = App.fetchRequest() as NSFetchRequest - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)] - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.name, ascending: true)] - fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.identifier), App.altstoreAppID) - fetchRequest.returnsObjectsAsFaults = false - - let dataSource = RSTFetchedResultsTableViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) - dataSource.cellConfigurationHandler = { (cell, app, indexPath) in - let cell = cell as! AppTableViewCell - cell.nameLabel.text = app.name - cell.developerLabel.text = app.developerName - cell.appIconImageView.image = UIImage(named: app.iconName) - - if app.installedApp != nil - { - cell.button.isEnabled = false - cell.button.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal) - } - else - { - cell.button.isEnabled = true - cell.button.setTitle(NSLocalizedString("Download", comment: ""), for: .normal) - } - } - - return dataSource - } - - func fetchApps() - { - AppManager.shared.fetchApps { (result) in - do - { - let apps = try result.get() - try apps.first?.managedObjectContext?.save() - } - catch - { - DispatchQueue.main.async { - let toastView = RSTToastView(text: NSLocalizedString("Failed to fetch apps", comment: ""), detailText: error.localizedDescription) - toastView.tintColor = .altPurple - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) - } - } - } - } -} diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index be2890f7..7536f716 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -21,80 +21,14 @@ - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -144,7 +78,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -532,103 +367,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -645,7 +384,7 @@ - + @@ -762,26 +501,7 @@ - - - - - - - - - - - - - - - - - - - - + @@ -803,17 +523,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - diff --git a/AltStore/Browse/BrowseCollectionViewCell.swift b/AltStore/Browse/BrowseCollectionViewCell.swift index c358324a..c1993a9a 100644 --- a/AltStore/Browse/BrowseCollectionViewCell.swift +++ b/AltStore/Browse/BrowseCollectionViewCell.swift @@ -24,7 +24,7 @@ import Roxas @IBOutlet var nameLabel: UILabel! @IBOutlet var developerLabel: UILabel! @IBOutlet var appIconImageView: UIImageView! - @IBOutlet var actionButton: ProgressButton! + @IBOutlet var actionButton: PillButton! @IBOutlet var subtitleLabel: UILabel! @IBOutlet private var screenshotsContentView: UIView! diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index f38c1dc7..5af484d5 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -49,9 +49,7 @@ private extension BrowseViewController fetchRequest.returnsObjectsAsFaults = false let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) - dataSource.cellConfigurationHandler = { [weak self] (cell, app, indexPath) in - guard let `self` = self else { return } - + dataSource.cellConfigurationHandler = { (cell, app, indexPath) in let cell = cell as! BrowseCollectionViewCell cell.nameLabel.text = app.name cell.developerLabel.text = app.developerName @@ -59,41 +57,28 @@ private extension BrowseViewController cell.imageNames = Array(app.screenshotNames.prefix(3)) cell.appIconImageView.image = UIImage(named: app.iconName) - cell.actionButton.tag = indexPath.item cell.actionButton.activityIndicatorView.style = .white // Explicitly set to false to ensure we're starting from a non-activity indicating state. // Otherwise, cell reuse can mess up some cached values. cell.actionButton.isIndicatingActivity = false - let tintColor = app.tintColor ?? self.collectionView.tintColor! + let tintColor = app.tintColor ?? .altGreen cell.tintColor = tintColor - cell.actionButton.progressTintColor = tintColor if app.installedApp == nil { cell.actionButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal) - cell.actionButton.setTitleColor(.altGreen, for: .normal) - cell.actionButton.backgroundColor = UIColor.altGreen.withAlphaComponent(0.1) - if let progress = AppManager.shared.installationProgress(for: app) - { - cell.actionButton.progress = progress - cell.actionButton.isIndicatingActivity = true - cell.actionButton.activityIndicatorView.isUserInteractionEnabled = false - cell.actionButton.isUserInteractionEnabled = true - } - else - { - cell.actionButton.progress = nil - cell.actionButton.isIndicatingActivity = false - } + let progress = AppManager.shared.installationProgress(for: app) + cell.actionButton.progress = progress + cell.actionButton.isInverted = false } else { cell.actionButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) - cell.actionButton.setTitleColor(.white, for: .normal) - cell.actionButton.backgroundColor = .altGreen + cell.actionButton.progress = nil + cell.actionButton.isInverted = true } } @@ -104,7 +89,7 @@ private extension BrowseViewController { AppManager.shared.fetchApps() { (result) in do - { + { let apps = try result.get() try apps.first?.managedObjectContext?.save() } @@ -122,9 +107,11 @@ private extension BrowseViewController private extension BrowseViewController { - @IBAction func performAppAction(_ sender: ProgressButton) + @IBAction func performAppAction(_ sender: PillButton) { - let indexPath = IndexPath(item: sender.tag, section: 0) + let point = self.collectionView.convert(sender.center, from: sender.superview) + guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return } + let app = self.dataSource.item(at: indexPath) if let installedApp = app.installedApp @@ -155,7 +142,7 @@ private extension BrowseViewController toastView.tintColor = .altGreen toastView.show(in: self.navigationController!.view, duration: 2) - case .success(let installedApp): print("Installed app:", installedApp.app.identifier) + case .success: print("Installed app:", app.identifier) } self.collectionView.reloadItems(at: [indexPath]) diff --git a/AltStore/Components/AppIconImageView.swift b/AltStore/Components/AppIconImageView.swift index 092b3d18..8559d453 100644 --- a/AltStore/Components/AppIconImageView.swift +++ b/AltStore/Components/AppIconImageView.swift @@ -17,6 +17,8 @@ class AppIconImageView: UIImageView self.contentMode = .scaleAspectFill self.clipsToBounds = true + self.backgroundColor = .white + self.layer.borderWidth = 0.5 self.layer.borderColor = UIColor.lightGray.cgColor diff --git a/AltStore/Components/ProgressButton.swift b/AltStore/Components/PillButton.swift similarity index 55% rename from AltStore/Components/ProgressButton.swift rename to AltStore/Components/PillButton.swift index b406f651..5cdb743a 100644 --- a/AltStore/Components/ProgressButton.swift +++ b/AltStore/Components/PillButton.swift @@ -1,5 +1,5 @@ // -// ProgressButton.swift +// PillButton.swift // AltStore // // Created by Riley Testut on 7/15/19. @@ -8,12 +8,16 @@ import UIKit -class ProgressButton: UIButton +class PillButton: UIButton { var progress: Progress? { - didSet { + didSet { self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0) self.progressView.observedProgress = self.progress + + let isUserInteractionEnabled = self.isUserInteractionEnabled + self.isIndicatingActivity = (self.progress != nil) + self.isUserInteractionEnabled = isUserInteractionEnabled } } @@ -26,12 +30,18 @@ class ProgressButton: UIButton } } + var isInverted: Bool = false { + didSet { + self.update() + } + } + private let progressView = UIProgressView(progressViewStyle: .default) override var intrinsicContentSize: CGSize { var size = super.intrinsicContentSize - size.width += 32 - size.height += 4 + size.width += 26 + size.height += 3 return size } @@ -41,10 +51,15 @@ class ProgressButton: UIButton self.layer.masksToBounds = true + self.activityIndicatorView.style = .white + self.activityIndicatorView.isUserInteractionEnabled = false + self.progressView.progress = 0 self.progressView.trackImage = UIImage() self.progressView.isUserInteractionEnabled = false self.addSubview(self.progressView) + + self.update() } override func layoutSubviews() @@ -60,4 +75,30 @@ class ProgressButton: UIButton self.layer.cornerRadius = self.bounds.midY } + + override func tintColorDidChange() + { + super.tintColorDidChange() + + self.update() + } +} + +private extension PillButton +{ + func update() + { + if self.isInverted + { + self.setTitleColor(.white, for: .normal) + self.backgroundColor = self.tintColor + self.progressView.progressTintColor = self.tintColor.withAlphaComponent(0.15) + } + else + { + self.setTitleColor(self.tintColor, for: .normal) + self.backgroundColor = self.tintColor.withAlphaComponent(0.15) + self.progressView.progressTintColor = self.tintColor + } + } } diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift new file mode 100644 index 00000000..6961dc62 --- /dev/null +++ b/AltStore/Components/ToastView.swift @@ -0,0 +1,19 @@ +// +// ToastView.swift +// AltStore +// +// Created by Riley Testut on 7/19/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Roxas + +class ToastView: RSTToastView +{ + override func layoutSubviews() + { + super.layoutSubviews() + + self.layer.cornerRadius = self.bounds.midY + } +} diff --git a/AltStore/Extensions/UIColor+AltStore.swift b/AltStore/Extensions/UIColor+AltStore.swift index 51d1a33c..030a2949 100644 --- a/AltStore/Extensions/UIColor+AltStore.swift +++ b/AltStore/Extensions/UIColor+AltStore.swift @@ -12,4 +12,9 @@ extension UIColor { static let altPurple = UIColor(named: "Purple")! static let altGreen = UIColor(named: "Green")! + + static let refreshRed = UIColor(named: "RefreshRed")! + static let refreshOrange = UIColor(named: "RefreshOrange")! + static let refreshYellow = UIColor(named: "RefreshYellow")! + static let refreshGreen = UIColor(named: "RefreshGreen")! } diff --git a/AltStore/My Apps/MyAppsComponents.swift b/AltStore/My Apps/MyAppsComponents.swift new file mode 100644 index 00000000..a285fd9e --- /dev/null +++ b/AltStore/My Apps/MyAppsComponents.swift @@ -0,0 +1,46 @@ +// +// MyAppsComponents.swift +// AltStore +// +// Created by Riley Testut on 7/17/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +class InstalledAppCollectionViewCell: UICollectionViewCell +{ + @IBOutlet var appIconImageView: UIImageView! + @IBOutlet var nameLabel: UILabel! + @IBOutlet var developerLabel: UILabel! + @IBOutlet var refreshButton: PillButton! +} + +class InstalledAppsCollectionHeaderView: UICollectionReusableView +{ + @IBOutlet var textLabel: UILabel! + @IBOutlet var button: UIButton! +} + +class UpdatesCollectionHeaderView: UICollectionReusableView +{ + let button = PillButton(type: .system) + + override init(frame: CGRect) + { + super.init(frame: frame) + + self.button.translatesAutoresizingMaskIntoConstraints = false + self.button.setTitle(">", for: .normal) + self.addSubview(self.button) + + NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20), + self.button.topAnchor.constraint(equalTo: self.topAnchor), + self.button.widthAnchor.constraint(equalToConstant: 50), + self.button.heightAnchor.constraint(equalToConstant: 26)]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index a84a0293..95a8fe19 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -2,129 +2,222 @@ // MyAppsViewController.swift // AltStore // -// Created by Riley Testut on 5/9/19. +// Created by Riley Testut on 7/16/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit + +import AltKit import Roxas -import AltSign +private let maximumCollapsedUpdatesCount = 2 -class MyAppsViewController: UITableViewController +extension MyAppsViewController +{ + private enum Section: Int, CaseIterable + { + case updates + case installedApps + } +} + +private extension Date +{ + func numberOfCalendarDays(since date: Date) -> Int + { + let today = Calendar.current.startOfDay(for: self) + let previousDay = Calendar.current.startOfDay(for: date) + + let components = Calendar.current.dateComponents([.day], from: previousDay, to: today) + return components.day! + } +} + +class MyAppsViewController: UICollectionViewController { - private var refreshErrors = [String: Error]() - private lazy var dataSource = self.makeDataSource() + private lazy var updatesDataSource = self.makeUpdatesDataSource() + private lazy var installedAppsDataSource = self.makeInstalledAppsDataSource() + + private var prototypeUpdateCell: UpdateCollectionViewCell! + + // State + private var isUpdateSectionCollapsed = true + private var expandedAppUpdates = Set() + private var isRefreshingAllApps = false + private var refreshGroup: OperationGroup? + + // Cache + private var cachedUpdateSizes = [String: CGSize]() private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short - dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none return dateFormatter }() - private var refreshGroup: OperationGroup? - - @IBOutlet private var progressView: UIProgressView! + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchApps(_:)), name: AppManager.didFetchAppsNotification, object: nil) + } override func viewDidLoad() { super.viewDidLoad() - self.tableView.dataSource = self.dataSource + self.collectionView.dataSource = self.dataSource + self.collectionView.prefetchDataSource = self.dataSource + + self.prototypeUpdateCell = UpdateCollectionViewCell.instantiate(with: UpdateCollectionViewCell.nib!) + self.prototypeUpdateCell.translatesAutoresizingMaskIntoConstraints = false + self.prototypeUpdateCell.contentView.translatesAutoresizingMaskIntoConstraints = false - if let navigationBar = self.navigationController?.navigationBar - { - self.progressView.translatesAutoresizingMaskIntoConstraints = false - navigationBar.addSubview(self.progressView) - - NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor), - self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)]) - } - - self.update() - } - - override func viewWillAppear(_ animated: Bool) - { - super.viewWillAppear(animated) - - self.update() - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) - { - guard segue.identifier == "showAppDetail" else { return } - - guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return } - - let installedApp = self.dataSource.item(at: indexPath) - guard let app = installedApp.app else { return } - - let appDetailViewController = segue.destination as! AppDetailViewController - appDetailViewController.app = app + self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell") + self.collectionView.register(UpdatesCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader") } } private extension MyAppsViewController { - func makeDataSource() -> RSTFetchedResultsTableViewDataSource + func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource + { + let dataSource = RSTCompositeCollectionViewPrefetchingDataSource(dataSources: [self.updatesDataSource, self.installedAppsDataSource]) + dataSource.proxy = self + return dataSource + } + + func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)] - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)] + fetchRequest.predicate = NSPredicate(format: "%K != %K", #keyPath(InstalledApp.version), #keyPath(InstalledApp.app.version)) + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.app?.versionDate, ascending: true), + NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)] fetchRequest.returnsObjectsAsFaults = false - let dataSource = RSTFetchedResultsTableViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) - dataSource.proxy = self - dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in + let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) + dataSource.liveFetchLimit = maximumCollapsedUpdatesCount + dataSource.cellIdentifierHandler = { _ in "UpdateCell" } + dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in guard let app = installedApp.app else { return } - cell.textLabel?.text = app.name + " (\(installedApp.version))" + let cell = cell as! UpdateCollectionViewCell + cell.tintColor = app.tintColor ?? .altGreen + cell.nameLabel.text = app.name + cell.versionDescriptionTextView.text = app.versionDescription + cell.appIconImageView.image = UIImage(named: app.iconName) - let detailText = - """ - Expires: \(self?.dateFormatter.string(from: installedApp.expirationDate) ?? "-") - """ + cell.updateButton.isIndicatingActivity = false + cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) - cell.detailTextLabel?.numberOfLines = 1 - cell.detailTextLabel?.text = detailText - cell.detailTextLabel?.textColor = .red - - if let _ = self?.refreshErrors[installedApp.bundleIdentifier] + if self.expandedAppUpdates.contains(app.identifier) { - cell.accessoryType = .detailButton - cell.tintColor = .red + cell.mode = .expanded } else { - cell.accessoryType = .none - cell.tintColor = nil + cell.mode = .collapsed } + + cell.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered) + + let progress = AppManager.shared.installationProgress(for: app) + cell.updateButton.progress = progress + + cell.dateLabel.text = self.dateFormatter.string(from: app.versionDate) + + let numberOfDays = Date().numberOfCalendarDays(since: app.versionDate) + switch numberOfDays + { + case 0: cell.dateLabel.text = NSLocalizedString("Today", comment: "") + case 1: cell.dateLabel.text = NSLocalizedString("Yesterday", comment: "") + case 2...7: cell.dateLabel.text = String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays)) + default: cell.dateLabel.text = self.dateFormatter.string(from: app.versionDate) + } + + cell.setNeedsLayout() } return dataSource } - func update() + func makeInstalledAppsDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { - self.navigationItem.rightBarButtonItem?.isEnabled = !(self.dataSource.fetchedResultsController.fetchedObjects?.isEmpty ?? true) + let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest + fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)] + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true), + NSSortDescriptor(keyPath: \InstalledApp.refreshedDate, ascending: false), + NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)] + fetchRequest.returnsObjectsAsFaults = false - self.tableView.reloadData() + let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) + dataSource.cellIdentifierHandler = { _ in "AppCell" } + dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in + guard let app = installedApp.app else { return } + + let tintColor = app.tintColor ?? .altGreen + + let cell = cell as! InstalledAppCollectionViewCell + cell.tintColor = tintColor + cell.appIconImageView.image = UIImage(named: app.iconName) + cell.refreshButton.isIndicatingActivity = false + cell.refreshButton.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered) + + let currentDate = Date() + + let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate) + + if numberOfDays == 1 + { + cell.refreshButton.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal) + } + else + { + cell.refreshButton.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal) + } + + cell.nameLabel.text = app.name + cell.developerLabel.text = app.developerName + + // Make sure refresh button is correct size. + cell.layoutIfNeeded() + + switch numberOfDays + { + case 2...3: cell.refreshButton.tintColor = .refreshOrange + case 4...5: cell.refreshButton.tintColor = .refreshYellow + case 6...: cell.refreshButton.tintColor = .refreshGreen + default: cell.refreshButton.tintColor = .refreshRed + } + + if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: app), progress.fractionCompleted < 1.0 + { + cell.refreshButton.progress = progress + } + else + { + cell.refreshButton.progress = nil + } + } + + return dataSource } } private extension MyAppsViewController { - @IBAction func refreshAllApps(_ sender: UIBarButtonItem) + func update() { - sender.isIndicatingActivity = true - - let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext) - - self.refresh(installedApps) { (result) in - sender.isIndicatingActivity = false + if self.updatesDataSource.itemCount > 0 + { + self.navigationController?.tabBarItem.badgeValue = String(describing: self.updatesDataSource.itemCount) + } + else + { + self.navigationController?.tabBarItem.badgeValue = nil } } @@ -132,74 +225,60 @@ private extension MyAppsViewController { func refresh() { - if self.refreshGroup == nil - { - let toastView = RSTToastView(text: "Refreshing...", detailText: nil) - toastView.tintColor = .altPurple - toastView.activityIndicatorView.startAnimating() - toastView.show(in: self.navigationController?.view ?? self.view) - } - let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup) group.completionHandler = { (result) in DispatchQueue.main.async { switch result { case .failure(let error): - let toastView = RSTToastView(text: error.localizedDescription, detailText: nil) - toastView.tintColor = .red + let toastView = ToastView(text: error.localizedDescription, detailText: nil) + toastView.tintColor = .refreshRed + toastView.setNeedsLayout() toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) - self.refreshErrors = [:] - case .success(let results): - let failures = results.compactMapValues { $0.error } + 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 + } + } - if failures.isEmpty + guard !failures.isEmpty else { break } + + let localizedText: String + if let failure = failures.first, failures.count == 1 { - let toastView = RSTToastView(text: NSLocalizedString("Successfully refreshed apps!", comment: ""), detailText: nil) - toastView.tintColor = .altPurple - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + localizedText = failure.value.localizedDescription } else { - let localizedText: String - if failures.count == 1 - { - localizedText = String(format: NSLocalizedString("Failed to refresh %@ app.", comment: ""), NSNumber(value: failures.count)) - } - else - { - localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) - } - - let toastView = RSTToastView(text: localizedText, detailText: nil) - toastView.tintColor = .red - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) } - self.refreshErrors = failures + let toastView = ToastView(text: localizedText, detailText: nil) + toastView.tintColor = .refreshRed + toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) } - self.progressView.observedProgress = nil - self.progressView.progress = 0.0 - - self.update() - self.refreshGroup = nil completionHandler(result) } } - self.progressView.observedProgress = group.progress - self.refreshGroup = group + + self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue)) } if installedApps.contains(where: { $0.app.identifier == App.altstoreAppID }) { let alertController = UIAlertController(title: NSLocalizedString("Refresh AltStore?", comment: ""), message: NSLocalizedString("AltStore will quit when it is finished refreshing.", comment: ""), preferredStyle: .alert) - alertController.addAction(.cancel) + alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in + completionHandler(.failure(OperationError.cancelled)) + }) alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .default) { (action) in refresh() }) @@ -212,50 +291,248 @@ private extension MyAppsViewController } } -extension MyAppsViewController +private extension MyAppsViewController { - override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? + @IBAction func toggleAppUpdates(_ sender: UIButton) { - let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in - let installedApp = self.dataSource.item(at: indexPath) + let visibleCells = self.collectionView.visibleCells + + self.collectionView.performBatchUpdates({ - DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - let installedApp = context.object(with: installedApp.objectID) as! InstalledApp - context.delete(installedApp) - - do + self.isUpdateSectionCollapsed.toggle() + + UIView.animate(withDuration: 0.3, animations: { + if self.isUpdateSectionCollapsed { - try context.save() + self.updatesDataSource.liveFetchLimit = maximumCollapsedUpdatesCount + self.expandedAppUpdates.removeAll() + + for case let cell as UpdateCollectionViewCell in visibleCells + { + cell.mode = .collapsed + } + + self.cachedUpdateSizes.removeAll() + + sender.titleLabel?.transform = .identity } - catch + else { - print("Failed to delete installed app.", error) + self.updatesDataSource.liveFetchLimit = 0 + + sender.titleLabel?.transform = CGAffineTransform.identity.rotated(by: .pi) } - } - } - - let refreshAction = UITableViewRowAction(style: .normal, title: "Refresh") { (action, indexPath) in - let installedApp = self.dataSource.item(at: indexPath) - self.refresh([installedApp]) { (result) in - print("Refreshed", installedApp.app.identifier) - } - } - - return [deleteAction, refreshAction] + }) + + self.collectionView.collectionViewLayout.invalidateLayout() + + }, completion: nil) } - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) - { - } - - override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) + @IBAction func toggleUpdateCellMode(_ sender: UIButton) { + let point = self.collectionView.convert(sender.center, from: sender.superview) + guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return } + let installedApp = self.dataSource.item(at: indexPath) - guard let error = self.refreshErrors[installedApp.bundleIdentifier] else { return } + let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell - let alertController = UIAlertController(title: "Failed to Refresh \(installedApp.app.name)", message: error.localizedDescription, preferredStyle: .alert) - alertController.addAction(.ok) - self.present(alertController, animated: true, completion: nil) + if self.expandedAppUpdates.contains(installedApp.app.identifier) + { + self.expandedAppUpdates.remove(installedApp.app.identifier) + cell?.mode = .collapsed + } + else + { + self.expandedAppUpdates.insert(installedApp.app.identifier) + cell?.mode = .expanded + } + + self.cachedUpdateSizes[installedApp.app.identifier] = nil + + self.collectionView.performBatchUpdates({ + self.collectionView.collectionViewLayout.invalidateLayout() + }, completion: nil) + } + + @IBAction func refreshApp(_ sender: UIButton) + { + let point = self.collectionView.convert(sender.center, from: sender.superview) + guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return } + + let installedApp = self.dataSource.item(at: indexPath) + + let previousProgress = AppManager.shared.refreshProgress(for: installedApp.app) + guard previousProgress == nil else { + previousProgress?.cancel() + return + } + + self.refresh([installedApp]) { (result) in + DispatchQueue.main.async { + self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue)) + } + } + } + + @IBAction func refreshAllApps(_ sender: UIBarButtonItem) + { + self.isRefreshingAllApps = true + self.collectionView.collectionViewLayout.invalidateLayout() + + let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext) + + self.refresh(installedApps) { (result) in + DispatchQueue.main.async { + self.isRefreshingAllApps = false + self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue)) + } + } + } + + @IBAction func updateApp(_ sender: UIButton) + { + let point = self.collectionView.convert(sender.center, from: sender.superview) + guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return } + + let app = self.dataSource.item(at: indexPath).app! + + let previousProgress = AppManager.shared.installationProgress(for: app) + guard previousProgress == nil else { + previousProgress?.cancel() + return + } + + _ = AppManager.shared.install(app, presentingViewController: self) { (result) in + DispatchQueue.main.async { + switch result + { + case .failure(OperationError.cancelled): + self.collectionView.reloadItems(at: [indexPath]) + + case .failure(let error): + let toastView = RSTToastView(text: "Failed to update \(app.name)", detailText: error.localizedDescription) + toastView.tintColor = .altGreen + toastView.show(in: self.navigationController!.view, duration: 2) + + self.collectionView.reloadItems(at: [indexPath]) + + case .success: + print("Updated app:", app.identifier) + // No need to reload, since the the update cell is gone now. + } + } + } + + self.collectionView.reloadItems(at: [indexPath]) + } + + @objc func didFetchApps(_ notification: Notification) + { + DispatchQueue.main.async { + if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil + { + do { try self.updatesDataSource.fetchedResultsController.performFetch() } + catch { print("Error fetching:", error) } + } + + self.update() + } + } +} + +extension MyAppsViewController +{ + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView + { + if indexPath.section == 0 + { + let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader", for: indexPath) as! UpdatesCollectionHeaderView + + UIView.performWithoutAnimation { + headerView.button.backgroundColor = UIColor.altGreen.withAlphaComponent(0.15) + headerView.button.setTitle("▾", for: .normal) + headerView.button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 28) + headerView.button.setTitleColor(.altGreen, for: .normal) + headerView.button.addTarget(self, action: #selector(MyAppsViewController.toggleAppUpdates), for: .primaryActionTriggered) + + headerView.button.layoutIfNeeded() + } + + return headerView + } + else + { + let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InstalledAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView + + UIView.performWithoutAnimation { + headerView.textLabel.text = NSLocalizedString("Installed", comment: "") + + headerView.button.isIndicatingActivity = false + headerView.button.activityIndicatorView.color = .altGreen + headerView.button.setTitle(NSLocalizedString("Refresh All", comment: ""), for: .normal) + headerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshAllApps(_:)), for: .primaryActionTriggered) + headerView.button.isIndicatingActivity = self.isRefreshingAllApps + + headerView.button.layoutIfNeeded() + } + + return headerView + } + } +} + +extension MyAppsViewController: UICollectionViewDelegateFlowLayout +{ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize + { + let section = Section.allCases[indexPath.section] + switch section + { + case .updates: + let item = self.dataSource.item(at: indexPath) + + if let previousHeight = self.cachedUpdateSizes[item.app!.identifier] + { + return previousHeight + } + + let padding = 30 as CGFloat + let width = collectionView.bounds.width - padding + + let widthConstraint = self.prototypeUpdateCell.contentView.widthAnchor.constraint(equalToConstant: width) + NSLayoutConstraint.activate([widthConstraint]) + defer { NSLayoutConstraint.deactivate([widthConstraint]) } + + self.dataSource.cellConfigurationHandler(self.prototypeUpdateCell, item, indexPath) + + let size = self.prototypeUpdateCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + self.cachedUpdateSizes[item.app!.identifier] = size + return size + + case .installedApps: + return CGSize(width: collectionView.bounds.width, height: 60) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize + { + let section = Section.allCases[section] + switch section + { + case .updates: return CGSize(width: collectionView.bounds.width, height: 26) + case .installedApps: return CGSize(width: collectionView.bounds.width, height: 29) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets + { + let section = Section.allCases[section] + switch section + { + case .updates: return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15) + case .installedApps: return UIEdgeInsets(top: 13, left: 0, bottom: 20, right: 0) + } } } diff --git a/AltStore/My Apps/UpdateCollectionViewCell.swift b/AltStore/My Apps/UpdateCollectionViewCell.swift new file mode 100644 index 00000000..f610cc80 --- /dev/null +++ b/AltStore/My Apps/UpdateCollectionViewCell.swift @@ -0,0 +1,121 @@ +// +// UpdateCollectionViewCell.swift +// AltStore +// +// Created by Riley Testut on 7/16/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +extension UpdateCollectionViewCell +{ + enum Mode + { + case collapsed + case expanded + } +} + +@objc class UpdateCollectionViewCell: UICollectionViewCell +{ + var mode: Mode = .expanded { + didSet { + self.update() + } + } + + @IBOutlet var appIconImageView: UIImageView! + @IBOutlet var nameLabel: UILabel! + @IBOutlet var dateLabel: UILabel! + @IBOutlet var updateButton: PillButton! + @IBOutlet var versionDescriptionTitleLabel: UILabel! + @IBOutlet var versionDescriptionTextView: UITextView! + + @IBOutlet var moreButton: UIButton! + + override func awakeFromNib() + { + super.awakeFromNib() + + self.contentView.layer.cornerRadius = 20 + self.contentView.layer.masksToBounds = true + + self.versionDescriptionTextView.textContainerInset = .zero + self.versionDescriptionTextView.textContainer.lineFragmentPadding = 0 + self.versionDescriptionTextView.textContainer.lineBreakMode = .byTruncatingTail + self.versionDescriptionTextView.textContainer.heightTracksTextView = true + + self.update() + } + + override func tintColorDidChange() + { + super.tintColorDidChange() + + self.update() + } + + override func layoutSubviews() + { + super.layoutSubviews() + + let textContainer = self.versionDescriptionTextView.textContainer + + switch self.mode + { + case .collapsed: + // Extra wide to make sure it wraps to next line. + let frame = CGRect(x: textContainer.size.width - self.moreButton.bounds.width - 8, + y: textContainer.size.height - 4, + width: textContainer.size.width, + height: textContainer.size.height) + + textContainer.maximumNumberOfLines = 2 + textContainer.exclusionPaths = [UIBezierPath(rect: frame)] + + if let font = self.versionDescriptionTextView.font, self.versionDescriptionTextView.bounds.height > font.lineHeight * 1.5 + { + self.moreButton.isHidden = false + } + else + { + // One (or less) lines, so hide more button. + self.moreButton.isHidden = true + } + + case .expanded: + textContainer.maximumNumberOfLines = 10 + textContainer.exclusionPaths = [] + + self.moreButton.isHidden = true + } + + self.versionDescriptionTextView.invalidateIntrinsicContentSize() + } + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) + { + // Animates transition to new attributes. + let animator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters()) { + self.layoutIfNeeded() + } + animator.startAnimation() + } +} + +private extension UpdateCollectionViewCell +{ + func update() + { + self.versionDescriptionTitleLabel.textColor = self.tintColor + self.contentView.backgroundColor = self.tintColor.withAlphaComponent(0.1) + + self.updateButton.setTitleColor(self.tintColor, for: .normal) + self.updateButton.backgroundColor = self.tintColor.withAlphaComponent(0.15) + self.updateButton.progressTintColor = self.tintColor + + self.setNeedsLayout() + self.layoutIfNeeded() + } +} diff --git a/AltStore/My Apps/UpdateCollectionViewCell.xib b/AltStore/My Apps/UpdateCollectionViewCell.xib new file mode 100644 index 00000000..035acc78 --- /dev/null +++ b/AltStore/My Apps/UpdateCollectionViewCell.xib @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 5f416a05..bc5a946b 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -55,6 +55,16 @@ class InstallAppOperation: ResultOperation case .success: self.receive(from: connection, server: server) { (result) in + switch result + { + case .success: + installedApp.managedObjectContext?.performAndWait { + installedApp.refreshedDate = Date() + } + + case .failure: break + } + self.finish(result) } } @@ -75,6 +85,7 @@ class InstallAppOperation: ResultOperation } else if response.progress == 1.0 { + self.progress.completedUnitCount = self.progress.totalUnitCount self.finish(.success(())) } else diff --git a/AltStore/Resources/Assets.xcassets/Colors/RefreshGreen.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/RefreshGreen.colorset/Contents.json new file mode 100644 index 00000000..b22f0388 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Colors/RefreshGreen.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "52", + "alpha" : "1.000", + "blue" : "89", + "green" : "199" + } + } + } + ] +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Colors/RefreshOrange.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/RefreshOrange.colorset/Contents.json new file mode 100644 index 00000000..424ffac7 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Colors/RefreshOrange.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "255", + "alpha" : "1.000", + "blue" : "0", + "green" : "149" + } + } + } + ] +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Colors/RefreshRed.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/RefreshRed.colorset/Contents.json new file mode 100644 index 00000000..9fea595f --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Colors/RefreshRed.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "255", + "alpha" : "1.000", + "blue" : "48", + "green" : "59" + } + } + } + ] +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Colors/RefreshYellow.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/RefreshYellow.colorset/Contents.json new file mode 100644 index 00000000..5e4840ce --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Colors/RefreshYellow.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "255", + "alpha" : "1.000", + "blue" : "0", + "green" : "204" + } + } + } + ] +} \ No newline at end of file diff --git a/AltStore/Updates/UpdatesViewController.swift b/AltStore/Updates/UpdatesViewController.swift deleted file mode 100644 index e4007ce4..00000000 --- a/AltStore/Updates/UpdatesViewController.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// UpdatesViewController.swift -// AltStore -// -// Created by Riley Testut on 5/20/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -import UIKit - -import Roxas - -class UpdatesViewController: UITableViewController -{ - private lazy var dataSource = self.makeDataSource() - - private lazy var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short - dateFormatter.timeStyle = .none - return dateFormatter - }() - - @IBOutlet private var progressView: UIProgressView! - - required init?(coder aDecoder: NSCoder) - { - super.init(coder: aDecoder) - - NotificationCenter.default.addObserver(self, selector: #selector(UpdatesViewController.didFetchApps(_:)), name: AppManager.didFetchAppsNotification, object: nil) - } - - override func viewDidLoad() - { - super.viewDidLoad() - - self.tableView.dataSource = self.dataSource - - if let navigationBar = self.navigationController?.navigationBar - { - self.progressView.translatesAutoresizingMaskIntoConstraints = false - navigationBar.addSubview(self.progressView) - - NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor), - self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)]) - } - } - - override func viewDidAppear(_ animated: Bool) - { - super.viewDidAppear(animated) - - self.update() - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) - { - guard segue.identifier == "showAppDetail" else { return } - - guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return } - - let installedApp = self.dataSource.item(at: indexPath) - guard let app = installedApp.app else { return } - - let appDetailViewController = segue.destination as! AppDetailViewController - appDetailViewController.app = app - } -} - -private extension UpdatesViewController -{ - func makeDataSource() -> RSTFetchedResultsTableViewDataSource - { - let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest - fetchRequest.predicate = NSPredicate(format: "%K != %K", #keyPath(InstalledApp.version), #keyPath(InstalledApp.app.version)) - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.app?.versionDate, ascending: false)] - fetchRequest.returnsObjectsAsFaults = false - - let dataSource = RSTFetchedResultsTableViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) - dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in - guard let app = installedApp.app else { return } - - cell.textLabel?.text = app.name + " (\(app.version))" - - let detailText = self.dateFormatter.string(from: app.versionDate) + "\n\n" + (app.versionDescription ?? "No Update Description") - - cell.detailTextLabel?.numberOfLines = 0 - cell.detailTextLabel?.text = detailText - } - - let placeholderView = RSTPlaceholderView() - placeholderView.textLabel.text = NSLocalizedString("No Updates", comment: "") - placeholderView.detailTextLabel.text = NSLocalizedString("There are no app updates at this time.", comment: "") - dataSource.placeholderView = placeholderView - - return dataSource - } - - func update() - { - if let count = self.dataSource.fetchedResultsController.fetchedObjects?.count, count > 0 - { - self.navigationController?.tabBarItem.badgeValue = String(describing: count) - } - else - { - self.navigationController?.tabBarItem.badgeValue = nil - } - } -} - -private extension UpdatesViewController -{ - func update(_ installedApp: InstalledApp) - { - func updateApp() - { - let toastView = RSTToastView(text: "Updating...", detailText: nil) - toastView.tintColor = .altPurple - toastView.activityIndicatorView.startAnimating() - toastView.show(in: self.navigationController?.view ?? self.view) - - let progress = AppManager.shared.install(installedApp.app, presentingViewController: self) { (result) in - do - { - _ = try result.get() - - DispatchQueue.main.async { - let installedApp = DatabaseManager.shared.persistentContainer.viewContext.object(with: installedApp.objectID) as! InstalledApp - - let toastView = RSTToastView(text: "Updated \(installedApp.app.name) to version \(installedApp.version)!", detailText: nil) - toastView.tintColor = .altPurple - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) - - self.update() - } - } - catch - { - DispatchQueue.main.async { - let toastView = RSTToastView(text: "Failed to update \(installedApp.app.name)", detailText: error.localizedDescription) - toastView.tintColor = .altPurple - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) - } - } - - DispatchQueue.main.async { - self.progressView.observedProgress = nil - self.progressView.progress = 0.0 - } - } - - self.progressView.observedProgress = progress - } - - if installedApp.app.identifier == App.altstoreAppID - { - let alertController = UIAlertController(title: NSLocalizedString("Update AltStore?", comment: ""), message: NSLocalizedString("AltStore will quit upon completion.", comment: ""), preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Update and Quit", comment: ""), style: .default, handler: { (action) in - updateApp() - })) - alertController.addAction(.cancel) - - self.present(alertController, animated: true, completion: nil) - } - else - { - updateApp() - } - } - - @objc func didFetchApps(_ notification: Notification) - { - DispatchQueue.main.async { - if self.dataSource.fetchedResultsController.fetchedObjects == nil - { - do { try self.dataSource.fetchedResultsController.performFetch() } - catch { print("Error fetching:", error) } - } - - self.update() - } - } -} - -extension UpdatesViewController -{ - override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? - { - let updateAction = UITableViewRowAction(style: .normal, title: "Update") { [weak self] (action, indexPath) in - guard let installedApp = self?.dataSource.item(at: indexPath) else { return } - self?.update(installedApp) - } - updateAction.backgroundColor = .altPurple - - return [updateAction] - } - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) - { - } -} diff --git a/Dependencies/Roxas b/Dependencies/Roxas index 7ceaa403..2642cb76 160000 --- a/Dependencies/Roxas +++ b/Dependencies/Roxas @@ -1 +1 @@ -Subproject commit 7ceaa403a2ba833ff9686db3d02c8ffd8ffeef3b +Subproject commit 2642cb76c5de73563deb232157afd81790313033