mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Shows detailed source “About” page when adding 3rd-party sources
Allows users to preview sources before adding them to their AltStore.
This commit is contained in:
@@ -355,6 +355,7 @@
|
|||||||
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||||
D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; };
|
D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; };
|
||||||
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
|
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57968CA29CB99EF00539069 /* VibrantButton.swift */; };
|
||||||
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
|
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
|
||||||
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; };
|
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; };
|
||||||
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; };
|
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; };
|
||||||
@@ -363,12 +364,18 @@
|
|||||||
D586D39B28EF58B0000E101F /* AltTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586D39A28EF58B0000E101F /* AltTests.swift */; };
|
D586D39B28EF58B0000E101F /* AltTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586D39A28EF58B0000E101F /* AltTests.swift */; };
|
||||||
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; };
|
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; };
|
||||||
D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; };
|
D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; };
|
||||||
|
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */; };
|
||||||
|
D59162AD29BA616A005CBF47 /* SourceHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */; };
|
||||||
|
D5935AED29C39DE300C157EF /* SourceDetailsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */; };
|
||||||
D5935AEF29C3B23600C157EF /* Sources.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5935AEE29C3B23600C157EF /* Sources.storyboard */; };
|
D5935AEF29C3B23600C157EF /* Sources.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5935AEE29C3B23600C157EF /* Sources.storyboard */; };
|
||||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
|
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
|
||||||
|
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0537229B91DB400997551 /* SourceDetailContentViewController.swift */; };
|
||||||
D5A2193429B14F94002229FC /* DeprecatedAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */; };
|
D5A2193429B14F94002229FC /* DeprecatedAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */; };
|
||||||
D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; };
|
D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; };
|
||||||
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; };
|
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; };
|
||||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
|
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
|
||||||
|
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */; };
|
||||||
|
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */; };
|
||||||
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; };
|
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; };
|
||||||
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
||||||
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
||||||
@@ -900,6 +907,7 @@
|
|||||||
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
|
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
|
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
|
||||||
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
||||||
|
D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = "<group>"; };
|
||||||
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
|
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
|
||||||
D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = "<group>"; };
|
D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = "<group>"; };
|
||||||
D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = "<group>"; };
|
D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = "<group>"; };
|
||||||
@@ -909,13 +917,19 @@
|
|||||||
D586D39828EF58B0000E101F /* AltTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D586D39828EF58B0000E101F /* AltTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D586D39A28EF58B0000E101F /* AltTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltTests.swift; sourceTree = "<group>"; };
|
D586D39A28EF58B0000E101F /* AltTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltTests.swift; sourceTree = "<group>"; };
|
||||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; };
|
D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; };
|
||||||
|
D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SourceHeaderView.xib; sourceTree = "<group>"; };
|
||||||
|
D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailsComponents.swift; sourceTree = "<group>"; };
|
||||||
D5935AEE29C3B23600C157EF /* Sources.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Sources.storyboard; sourceTree = "<group>"; };
|
D5935AEE29C3B23600C157EF /* Sources.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Sources.storyboard; sourceTree = "<group>"; };
|
||||||
D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; };
|
D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; };
|
||||||
|
D5A0537229B91DB400997551 /* SourceDetailContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailContentViewController.swift; sourceTree = "<group>"; };
|
||||||
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = "<group>"; };
|
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = "<group>"; };
|
||||||
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = "<group>"; };
|
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = "<group>"; };
|
||||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; };
|
D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; };
|
||||||
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = "<group>"; };
|
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = "<group>"; };
|
||||||
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = "<group>"; };
|
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = "<group>"; };
|
||||||
|
D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailViewController.swift; sourceTree = "<group>"; };
|
||||||
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = "<group>"; };
|
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = "<group>"; };
|
||||||
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
|
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
|
||||||
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
|
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
|
||||||
@@ -1605,6 +1619,11 @@
|
|||||||
children = (
|
children = (
|
||||||
D5935AEE29C3B23600C157EF /* Sources.storyboard */,
|
D5935AEE29C3B23600C157EF /* Sources.storyboard */,
|
||||||
BFC84A4C2421A19100853474 /* SourcesViewController.swift */,
|
BFC84A4C2421A19100853474 /* SourcesViewController.swift */,
|
||||||
|
D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */,
|
||||||
|
D5A0537229B91DB400997551 /* SourceDetailContentViewController.swift */,
|
||||||
|
D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */,
|
||||||
|
D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */,
|
||||||
|
D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1723,12 +1742,14 @@
|
|||||||
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */,
|
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */,
|
||||||
BF9ABA4A22DD137F008935CF /* NavigationBar.swift */,
|
BF9ABA4A22DD137F008935CF /* NavigationBar.swift */,
|
||||||
BF9ABA4C22DD16DE008935CF /* PillButton.swift */,
|
BF9ABA4C22DD16DE008935CF /* PillButton.swift */,
|
||||||
|
D57968CA29CB99EF00539069 /* VibrantButton.swift */,
|
||||||
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */,
|
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */,
|
||||||
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
||||||
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
||||||
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
||||||
BF6C8FAD2429597900125131 /* AppBannerCollectionViewCell.swift */,
|
BF6C8FAD2429597900125131 /* AppBannerCollectionViewCell.swift */,
|
||||||
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */,
|
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */,
|
||||||
|
D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2343,6 +2364,7 @@
|
|||||||
files = (
|
files = (
|
||||||
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
|
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
|
||||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||||
|
D59162AD29BA616A005CBF47 /* SourceHeaderView.xib in Resources */,
|
||||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
||||||
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */,
|
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */,
|
||||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||||
@@ -2665,14 +2687,19 @@
|
|||||||
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */,
|
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */,
|
||||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
||||||
|
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */,
|
||||||
|
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */,
|
||||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||||
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
|
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
|
||||||
|
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */,
|
||||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
||||||
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
|
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
|
||||||
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,
|
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,
|
||||||
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
||||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
||||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||||
|
D5935AED29C39DE300C157EF /* SourceDetailsComponents.swift in Sources */,
|
||||||
|
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
||||||
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
||||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||||
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
||||||
@@ -2712,6 +2739,7 @@
|
|||||||
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
||||||
99F87D0529D8B4E200B40039 /* minimuxer-helpers.swift in Sources */,
|
99F87D0529D8B4E200B40039 /* minimuxer-helpers.swift in Sources */,
|
||||||
0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */,
|
0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */,
|
||||||
|
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */,
|
||||||
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
||||||
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
||||||
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
||||||
|
|||||||
568
AltStore/Components/HeaderContentViewController.swift
Normal file
568
AltStore/Components/HeaderContentViewController.swift
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
//
|
||||||
|
// HeaderContentViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/10/23.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
protocol ScrollableContentViewController: UIViewController
|
||||||
|
{
|
||||||
|
var scrollView: UIScrollView { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderContentViewController<Header: UIView, Content: ScrollableContentViewController> : UIViewController,
|
||||||
|
UIAdaptivePresentationControllerDelegate,
|
||||||
|
UIScrollViewDelegate
|
||||||
|
{
|
||||||
|
var tintColor: UIColor? {
|
||||||
|
didSet {
|
||||||
|
guard self.isViewLoaded else { return }
|
||||||
|
|
||||||
|
self.view.tintColor = self.tintColor
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var headerView: Header!
|
||||||
|
private(set) var contentViewController: Content!
|
||||||
|
|
||||||
|
private(set) var backButton: VibrantButton!
|
||||||
|
private(set) var backgroundImageView: UIImageView!
|
||||||
|
|
||||||
|
private(set) var navigationBarNameLabel: UILabel!
|
||||||
|
private(set) var navigationBarIconView: UIImageView!
|
||||||
|
private(set) var navigationBarTitleView: UIStackView!
|
||||||
|
private(set) var navigationBarButton: PillButton!
|
||||||
|
|
||||||
|
private var scrollView: UIScrollView!
|
||||||
|
private var headerScrollView: UIScrollView!
|
||||||
|
private var headerContainerView: UIView!
|
||||||
|
private var backgroundBlurView: UIVisualEffectView!
|
||||||
|
private var contentViewControllerShadowView: UIView!
|
||||||
|
|
||||||
|
private var blurAnimator: UIViewPropertyAnimator?
|
||||||
|
private var navigationBarAnimator: UIViewPropertyAnimator?
|
||||||
|
private var contentSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
private var _shouldResetLayout = false
|
||||||
|
private var _backgroundBlurEffect: UIBlurEffect?
|
||||||
|
private var _backgroundBlurTintColor: UIColor?
|
||||||
|
|
||||||
|
private var isViewingHeader: Bool {
|
||||||
|
let isViewingHeader = (self.headerScrollView.contentOffset.x != self.headerScrollView.contentInset.left)
|
||||||
|
return isViewingHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return _preferredStatusBarStyle
|
||||||
|
}
|
||||||
|
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||||
|
|
||||||
|
init()
|
||||||
|
{
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.blurAnimator?.stopAnimation(true)
|
||||||
|
self.navigationBarAnimator?.stopAnimation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeContentViewController() -> Content
|
||||||
|
{
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHeaderView() -> Header
|
||||||
|
{
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.view.backgroundColor = .white
|
||||||
|
self.view.clipsToBounds = true
|
||||||
|
|
||||||
|
self.navigationItem.largeTitleDisplayMode = .never
|
||||||
|
self.navigationController?.presentationController?.delegate = self
|
||||||
|
|
||||||
|
|
||||||
|
// Background
|
||||||
|
self.backgroundImageView = UIImageView(frame: .zero)
|
||||||
|
self.backgroundImageView.contentMode = .scaleAspectFill
|
||||||
|
self.view.addSubview(self.backgroundImageView)
|
||||||
|
|
||||||
|
let blurEffect = UIBlurEffect(style: .regular)
|
||||||
|
self.backgroundBlurView = UIVisualEffectView(effect: blurEffect)
|
||||||
|
self.view.addSubview(self.backgroundBlurView, pinningEdgesWith: .zero)
|
||||||
|
|
||||||
|
|
||||||
|
// Header View
|
||||||
|
self.headerContainerView = UIView(frame: .zero)
|
||||||
|
self.view.addSubview(self.headerContainerView, pinningEdgesWith: .zero)
|
||||||
|
|
||||||
|
self.headerScrollView = UIScrollView(frame: .zero)
|
||||||
|
self.headerScrollView.delegate = self
|
||||||
|
self.headerScrollView.isPagingEnabled = true
|
||||||
|
self.headerScrollView.clipsToBounds = false
|
||||||
|
self.headerScrollView.indicatorStyle = .white
|
||||||
|
self.headerScrollView.showsVerticalScrollIndicator = false
|
||||||
|
self.headerContainerView.addSubview(self.headerScrollView)
|
||||||
|
self.headerContainerView.addGestureRecognizer(self.headerScrollView.panGestureRecognizer) // Allow panning outside headerScrollView bounds.
|
||||||
|
|
||||||
|
self.headerView = self.makeHeaderView()
|
||||||
|
self.headerScrollView.addSubview(self.headerView)
|
||||||
|
|
||||||
|
let imageConfiguration = UIImage.SymbolConfiguration(weight: .semibold)
|
||||||
|
let image = UIImage(systemName: "chevron.backward", withConfiguration: imageConfiguration)
|
||||||
|
|
||||||
|
self.backButton = VibrantButton(type: .system)
|
||||||
|
self.backButton.image = image
|
||||||
|
self.backButton.tintColor = self.tintColor
|
||||||
|
self.backButton.sizeToFit()
|
||||||
|
self.backButton.addTarget(self.navigationController, action: #selector(UINavigationController.popViewController(animated:)), for: .primaryActionTriggered)
|
||||||
|
self.view.addSubview(self.backButton)
|
||||||
|
|
||||||
|
|
||||||
|
// Content View Controller
|
||||||
|
self.contentViewController = self.makeContentViewController()
|
||||||
|
self.contentViewController.view.frame = self.view.bounds
|
||||||
|
self.contentViewController.view.layer.cornerRadius = 38
|
||||||
|
self.contentViewController.view.layer.masksToBounds = true
|
||||||
|
|
||||||
|
self.addChild(self.contentViewController)
|
||||||
|
self.view.addSubview(self.contentViewController.view)
|
||||||
|
self.contentViewController.didMove(toParent: self)
|
||||||
|
|
||||||
|
self.contentViewControllerShadowView = UIView()
|
||||||
|
self.contentViewControllerShadowView.backgroundColor = .white
|
||||||
|
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
||||||
|
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
|
||||||
|
self.contentViewControllerShadowView.layer.shadowRadius = 10
|
||||||
|
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
|
||||||
|
self.view.insertSubview(self.contentViewControllerShadowView, belowSubview: self.contentViewController.view)
|
||||||
|
|
||||||
|
// Add scrollView to front so the scroll indicators are visible, but disable user interaction.
|
||||||
|
self.scrollView = UIScrollView(frame: CGRect(origin: .zero, size: self.view.bounds.size))
|
||||||
|
self.scrollView.delegate = self
|
||||||
|
self.scrollView.isUserInteractionEnabled = false
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
self.view.addSubview(self.scrollView, pinningEdgesWith: .zero)
|
||||||
|
self.view.addGestureRecognizer(self.scrollView.panGestureRecognizer)
|
||||||
|
|
||||||
|
self.contentViewController.scrollView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||||
|
self.contentViewController.scrollView.showsVerticalScrollIndicator = false
|
||||||
|
self.contentViewController.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
|
||||||
|
|
||||||
|
// Navigation Bar Title View
|
||||||
|
self.navigationBarNameLabel = UILabel(frame: .zero)
|
||||||
|
self.navigationBarNameLabel.font = UIFont.boldSystemFont(ofSize: 17) // We want semibold, which this (apparently) returns.
|
||||||
|
self.navigationBarNameLabel.text = self.title
|
||||||
|
self.navigationBarNameLabel.sizeToFit()
|
||||||
|
|
||||||
|
self.navigationBarIconView = UIImageView(frame: .zero)
|
||||||
|
self.navigationBarIconView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.navigationBarTitleView = UIStackView(arrangedSubviews: [self.navigationBarIconView, self.navigationBarNameLabel])
|
||||||
|
self.navigationBarTitleView.axis = .horizontal
|
||||||
|
self.navigationBarTitleView.spacing = 8
|
||||||
|
|
||||||
|
self.navigationBarButton = PillButton(type: .system)
|
||||||
|
self.navigationBarButton.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 9000), for: .horizontal) // Prioritize over title length.
|
||||||
|
|
||||||
|
// Embed navigationBarButton in container view with Auto Layout to ensure it can automatically update its size.
|
||||||
|
let buttonContainerView = UIView()
|
||||||
|
buttonContainerView.addSubview(self.navigationBarButton, pinningEdgesWith: .zero)
|
||||||
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: buttonContainerView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
self.navigationBarIconView.widthAnchor.constraint(equalToConstant: 35),
|
||||||
|
self.navigationBarIconView.heightAnchor.constraint(equalTo: self.navigationBarIconView.widthAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
let size = self.navigationBarTitleView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
|
self.navigationBarTitleView.bounds.size = size
|
||||||
|
self.navigationItem.titleView = self.navigationBarTitleView
|
||||||
|
|
||||||
|
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
||||||
|
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
||||||
|
|
||||||
|
self.contentSizeObservation = self.contentViewController.scrollView.observe(\.contentSize, options: [.new, .old]) { [weak self] (scrollView, change) in
|
||||||
|
guard let size = change.newValue, let previousSize = change.oldValue, size != previousSize else { return }
|
||||||
|
self?.view.setNeedsLayout()
|
||||||
|
self?.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't call update() before subclasses have finished viewDidLoad().
|
||||||
|
// self.update()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(HeaderContentViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(HeaderContentViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||||
|
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
// Fix navigation bar + tab bar appearance on iOS 15.
|
||||||
|
self.setContentScrollView(self.scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with navigation bar hidden.
|
||||||
|
self.hideNavigationBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.prepareBlur()
|
||||||
|
|
||||||
|
// Update blur immediately.
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
self.headerScrollView.flashScrollIndicators()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
if self._shouldResetLayout
|
||||||
|
{
|
||||||
|
// Various events can cause UI to mess up, so reset affected components now.
|
||||||
|
|
||||||
|
self.prepareBlur()
|
||||||
|
|
||||||
|
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
|
||||||
|
self._shouldResetLayout = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Dynamically calculate status bar height.
|
||||||
|
let statusBarHeight = 20.0 //self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||||
|
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
|
||||||
|
let inset = 15 as CGFloat
|
||||||
|
let padding = 20 as CGFloat
|
||||||
|
|
||||||
|
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
|
||||||
|
let largestBackButtonDimension = max(backButtonSize.width, backButtonSize.height) // Enforce 1:1 aspect ratio.
|
||||||
|
var backButtonFrame = CGRect(x: inset, y: statusBarHeight, width: largestBackButtonDimension, height: largestBackButtonDimension)
|
||||||
|
|
||||||
|
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.headerView.bounds.height)
|
||||||
|
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||||
|
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
|
||||||
|
|
||||||
|
let backButtonPadding = 8.0
|
||||||
|
let minimumHeaderY = backButtonFrame.maxY + backButtonPadding
|
||||||
|
|
||||||
|
let minimumContentHeight = minimumHeaderY + headerFrame.height + padding // Minimum height for header + back button + spacing.
|
||||||
|
let maximumContentY = max(self.view.bounds.width * 0.667, minimumContentHeight) // Initial Y-value of content view.
|
||||||
|
|
||||||
|
contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
|
||||||
|
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
|
||||||
|
|
||||||
|
// Stretch the app icon image to fill additional vertical space if necessary.
|
||||||
|
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
|
||||||
|
backgroundIconFrame.size.height = height
|
||||||
|
|
||||||
|
// Update blur.
|
||||||
|
self.updateBlur()
|
||||||
|
|
||||||
|
// Animate navigation bar.
|
||||||
|
let showNavigationBarThreshold = (maximumContentY - minimumContentHeight) + backButtonFrame.origin.y
|
||||||
|
if self.scrollView.contentOffset.y > showNavigationBarThreshold
|
||||||
|
{
|
||||||
|
if self.navigationBarAnimator == nil
|
||||||
|
{
|
||||||
|
self.prepareNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
|
||||||
|
let range = maximumContentY - (maximumContentY - padding - headerFrame.height) - inset
|
||||||
|
|
||||||
|
let fractionComplete = min(difference, range) / range
|
||||||
|
self.navigationBarAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.navigationBarAnimator?.fractionComplete = 0.0
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentHeight)
|
||||||
|
if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold
|
||||||
|
{
|
||||||
|
let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold
|
||||||
|
backButtonFrame.origin.y -= difference
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinContentToTopThreshold = maximumContentY
|
||||||
|
if self.scrollView.contentOffset.y > pinContentToTopThreshold
|
||||||
|
{
|
||||||
|
contentFrame.origin.y = 0
|
||||||
|
backgroundIconFrame.origin.y = 0
|
||||||
|
|
||||||
|
let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold
|
||||||
|
self.contentViewController.scrollView.contentOffset.y = -self.contentViewController.scrollView.contentInset.top + difference
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Keep content table view's content offset at the top.
|
||||||
|
self.contentViewController.scrollView.contentOffset.y = -self.contentViewController.scrollView.contentInset.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep background app icon centered in gap between top of content and top of screen.
|
||||||
|
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
|
||||||
|
|
||||||
|
// Set frames.
|
||||||
|
self.contentViewController.view.frame = contentFrame
|
||||||
|
self.contentViewControllerShadowView.frame = contentFrame
|
||||||
|
self.backgroundImageView.frame = backgroundIconFrame
|
||||||
|
|
||||||
|
self.backButton.frame = backButtonFrame
|
||||||
|
self.backButton.layer.cornerRadius = backButtonFrame.height / 2
|
||||||
|
|
||||||
|
// Adjust header scroll view content size for paging
|
||||||
|
self.headerView.frame = CGRect(origin: .zero, size: headerFrame.size)
|
||||||
|
self.headerScrollView.frame = headerFrame
|
||||||
|
self.headerScrollView.contentSize = CGSize(width: headerFrame.width * 2, height: headerFrame.height)
|
||||||
|
|
||||||
|
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
|
||||||
|
self.headerScrollView.horizontalScrollIndicatorInsets.bottom = -12
|
||||||
|
|
||||||
|
// Adjust content offset + size.
|
||||||
|
let contentOffset = self.scrollView.contentOffset
|
||||||
|
|
||||||
|
var contentSize = self.contentViewController.scrollView.contentSize
|
||||||
|
contentSize.height += self.contentViewController.scrollView.contentInset.top + self.contentViewController.scrollView.contentInset.bottom
|
||||||
|
contentSize.height += maximumContentY
|
||||||
|
contentSize.height = max(contentSize.height, self.view.bounds.height + maximumContentY - (self.navigationController?.navigationBar.bounds.height ?? 0))
|
||||||
|
self.scrollView.contentSize = contentSize
|
||||||
|
|
||||||
|
self.scrollView.contentOffset = contentOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
// Overridden by subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cannot add @objc functions in extensions of generic types, so include them in main definition instead.
|
||||||
|
|
||||||
|
//MARK: Notifications
|
||||||
|
|
||||||
|
@objc private func willEnterForeground(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||||
|
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func didBecomeActive(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||||
|
|
||||||
|
// Fixes incorrect blur after app becomes inactive -> active again.
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UIAdaptivePresentationControllerDelegate
|
||||||
|
|
||||||
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UIScrollViewDelegate
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
||||||
|
{
|
||||||
|
switch scrollView
|
||||||
|
{
|
||||||
|
case self.scrollView: self.view.setNeedsLayout()
|
||||||
|
case self.headerScrollView:
|
||||||
|
// Do NOT call setNeedsLayout(), or else it will mess with scrolling.
|
||||||
|
self.headerScrollView.showsHorizontalScrollIndicator = false
|
||||||
|
self.updateBlur()
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HeaderContentViewController
|
||||||
|
{
|
||||||
|
func showNavigationBar()
|
||||||
|
{
|
||||||
|
self.navigationBarIconView.alpha = 1.0
|
||||||
|
self.navigationBarNameLabel.alpha = 1.0
|
||||||
|
self.navigationBarButton.alpha = 1.0
|
||||||
|
|
||||||
|
self.updateNavigationBarAppearance(isHidden: false)
|
||||||
|
|
||||||
|
if self.traitCollection.userInterfaceStyle == .dark
|
||||||
|
{
|
||||||
|
self._preferredStatusBarStyle = .lightContent
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self._preferredStatusBarStyle = .default
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideNavigationBar()
|
||||||
|
{
|
||||||
|
self.navigationBarIconView.alpha = 0.0
|
||||||
|
self.navigationBarNameLabel.alpha = 0.0
|
||||||
|
self.navigationBarButton.alpha = 0.0
|
||||||
|
|
||||||
|
self.updateNavigationBarAppearance(isHidden: true)
|
||||||
|
|
||||||
|
self._preferredStatusBarStyle = .lightContent
|
||||||
|
|
||||||
|
self.navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNavigationBarAppearance(isHidden: Bool)
|
||||||
|
{
|
||||||
|
let barAppearance = self.navigationItem.standardAppearance ?? UINavigationBarAppearance()
|
||||||
|
|
||||||
|
if isHidden
|
||||||
|
{
|
||||||
|
barAppearance.configureWithTransparentBackground()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
barAppearance.configureWithDefaultBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
barAppearance.titleTextAttributes = [.foregroundColor: UIColor.clear]
|
||||||
|
|
||||||
|
let tintColor = isHidden ? UIColor.clear : self.tintColor ?? .altPrimary
|
||||||
|
|
||||||
|
let buttonAppearance = UIBarButtonItemAppearance(style: .plain)
|
||||||
|
buttonAppearance.normal.titleTextAttributes = [.foregroundColor: tintColor]
|
||||||
|
barAppearance.buttonAppearance = buttonAppearance
|
||||||
|
|
||||||
|
let backButtonImage = UIImage(systemName: "chevron.backward")?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
|
||||||
|
barAppearance.setBackIndicatorImage(backButtonImage, transitionMaskImage: backButtonImage)
|
||||||
|
|
||||||
|
self.navigationItem.standardAppearance = barAppearance
|
||||||
|
self.navigationItem.scrollEdgeAppearance = barAppearance
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareBlur()
|
||||||
|
{
|
||||||
|
if let animator = self.blurAnimator
|
||||||
|
{
|
||||||
|
animator.stopAnimation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundBlurView.effect = self._backgroundBlurEffect
|
||||||
|
self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor
|
||||||
|
|
||||||
|
self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||||
|
self?.backgroundBlurView.effect = nil
|
||||||
|
self?.backgroundBlurView.contentView.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
self.blurAnimator?.startAnimation()
|
||||||
|
self.blurAnimator?.pauseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBlur()
|
||||||
|
{
|
||||||
|
// A full blur is too much for header, so we reduce the visible blur by 0.3, resulting in 70% blur.
|
||||||
|
let minimumBlurFraction = 0.3 as CGFloat
|
||||||
|
|
||||||
|
if self.isViewingHeader
|
||||||
|
{
|
||||||
|
let maximumX = self.headerScrollView.bounds.width
|
||||||
|
let fraction = self.headerScrollView.contentOffset.x / maximumX
|
||||||
|
|
||||||
|
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
|
||||||
|
self.blurAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else if self.scrollView.contentOffset.y < 0
|
||||||
|
{
|
||||||
|
// Determine how much to lessen blur by.
|
||||||
|
|
||||||
|
let range = 75 as CGFloat
|
||||||
|
let difference = -self.scrollView.contentOffset.y
|
||||||
|
|
||||||
|
let fraction = min(difference, range) / range
|
||||||
|
|
||||||
|
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
|
||||||
|
self.blurAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set blur to default.
|
||||||
|
self.blurAnimator?.fractionComplete = minimumBlurFraction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareNavigationBarAnimation()
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
|
||||||
|
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||||
|
self?.showNavigationBar()
|
||||||
|
|
||||||
|
// Must call layoutIfNeeded() to animate appearance change.
|
||||||
|
self?.navigationController?.navigationBar.layoutIfNeeded()
|
||||||
|
|
||||||
|
self?.contentViewController.view.layer.cornerRadius = 0
|
||||||
|
}
|
||||||
|
self.navigationBarAnimator?.startAnimation()
|
||||||
|
self.navigationBarAnimator?.pauseAnimation()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetNavigationBarAnimation()
|
||||||
|
{
|
||||||
|
guard self.navigationBarAnimator != nil else { return }
|
||||||
|
|
||||||
|
self.navigationBarAnimator?.stopAnimation(true)
|
||||||
|
self.navigationBarAnimator = nil
|
||||||
|
|
||||||
|
self.hideNavigationBar()
|
||||||
|
|
||||||
|
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,10 +85,27 @@ final class PillButton: UIButton
|
|||||||
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
|
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
override func awakeFromNib()
|
override func awakeFromNib()
|
||||||
{
|
{
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
self.layer.masksToBounds = true
|
self.layer.masksToBounds = true
|
||||||
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
||||||
|
|
||||||
@@ -153,6 +170,9 @@ private extension PillButton
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.progressView.progressTintColor = self.tintColor
|
self.progressView.progressTintColor = self.tintColor
|
||||||
|
|
||||||
|
// Update font after init because the original titleLabel is replaced.
|
||||||
|
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func updateCountdown()
|
@objc func updateCountdown()
|
||||||
|
|||||||
150
AltStore/Components/VibrantButton.swift
Normal file
150
AltStore/Components/VibrantButton.swift
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
//
|
||||||
|
// VibrantButton.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/22/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
private let preferredFont = UIFont.boldSystemFont(ofSize: 14)
|
||||||
|
|
||||||
|
class VibrantButton: UIButton
|
||||||
|
{
|
||||||
|
var title: String? {
|
||||||
|
didSet {
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
self.configuration?.title = self.title
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.setTitle(self.title, for: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: UIImage? {
|
||||||
|
didSet {
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
self.configuration?.image = self.image
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.setImage(self.image, for: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentInsets: NSDirectionalEdgeInsets = .zero {
|
||||||
|
didSet {
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
self.configuration?.contentInsets = self.contentInsets
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.contentEdgeInsets = UIEdgeInsets(top: self.contentInsets.top, left: self.contentInsets.leading, bottom: self.contentInsets.bottom, right: self.contentInsets.trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isIndicatingActivity: Bool {
|
||||||
|
didSet {
|
||||||
|
guard #available(iOS 15, *) else { return }
|
||||||
|
self.updateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let vibrancyView = UIVisualEffectView(effect: nil)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
let blurEffect = UIBlurEffect(style: .systemThinMaterial)
|
||||||
|
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: .fill) // .fill is more vibrant than .secondaryLabel
|
||||||
|
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
var backgroundConfig = UIBackgroundConfiguration.clear()
|
||||||
|
backgroundConfig.visualEffect = blurEffect
|
||||||
|
|
||||||
|
var config = UIButton.Configuration.plain()
|
||||||
|
config.cornerStyle = .capsule
|
||||||
|
config.background = backgroundConfig
|
||||||
|
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { [weak self] (attributes) in
|
||||||
|
var attributes = attributes
|
||||||
|
attributes.font = preferredFont
|
||||||
|
|
||||||
|
if let self, self.isIndicatingActivity
|
||||||
|
{
|
||||||
|
// Hide title when indicating activity, but without changing intrinsicContentSize.
|
||||||
|
attributes.foregroundColor = UIColor.clear
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
self.configuration = config
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) // Add padding.
|
||||||
|
|
||||||
|
let blurView = UIVisualEffectView(effect: blurEffect)
|
||||||
|
blurView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(blurView, pinningEdgesWith: .zero)
|
||||||
|
self.insertSubview(blurView, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.vibrancyView.effect = vibrancyEffect
|
||||||
|
self.vibrancyView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(self.vibrancyView, pinningEdgesWith: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.layer.cornerRadius = self.bounds.midY
|
||||||
|
|
||||||
|
// Make sure content subviews are inside self.vibrancyView.contentView.
|
||||||
|
|
||||||
|
if let titleLabel = self.titleLabel, titleLabel.superview != self.vibrancyView.contentView
|
||||||
|
{
|
||||||
|
self.vibrancyView.contentView.addSubview(titleLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let imageView = self.imageView, imageView.superview != self.vibrancyView.contentView
|
||||||
|
{
|
||||||
|
self.vibrancyView.contentView.addSubview(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.activityIndicatorView.superview != self.vibrancyView.contentView
|
||||||
|
{
|
||||||
|
self.vibrancyView.contentView.addSubview(self.activityIndicatorView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if #unavailable(iOS 15)
|
||||||
|
{
|
||||||
|
// Update font after init because the original titleLabel is replaced.
|
||||||
|
self.titleLabel?.font = preferredFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
341
AltStore/Sources/SourceDetailContentViewController.swift
Normal file
341
AltStore/Sources/SourceDetailContentViewController.swift
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
//
|
||||||
|
// SourcesDetailContentViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/8/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
private let sectionInset = 20.0
|
||||||
|
|
||||||
|
extension SourceDetailContentViewController
|
||||||
|
{
|
||||||
|
private enum Section: Int
|
||||||
|
{
|
||||||
|
case news
|
||||||
|
case featuredApps
|
||||||
|
case about
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ElementKind: String
|
||||||
|
{
|
||||||
|
case title
|
||||||
|
case button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceDetailContentViewController: UICollectionViewController
|
||||||
|
{
|
||||||
|
let source: Source
|
||||||
|
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
private lazy var newsDataSource = self.makeNewsDataSource()
|
||||||
|
private lazy var appsDataSource = self.makeAppsDataSource()
|
||||||
|
private lazy var aboutDataSource = self.makeAboutDataSource()
|
||||||
|
|
||||||
|
override var collectionViewLayout: UICollectionViewCompositionalLayout {
|
||||||
|
return self.collectionView.collectionViewLayout as! UICollectionViewCompositionalLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(source: Source, coder: NSCoder)
|
||||||
|
{
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.view.tintColor = self.source.effectiveTintColor
|
||||||
|
|
||||||
|
let collectionViewLayout = self.makeLayout(source: self.source)
|
||||||
|
self.collectionView.collectionViewLayout = collectionViewLayout
|
||||||
|
|
||||||
|
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: "NewsCell")
|
||||||
|
self.collectionView.register(TitleCollectionReusableView.self, forSupplementaryViewOfKind: ElementKind.title.rawValue, withReuseIdentifier: ElementKind.title.rawValue)
|
||||||
|
self.collectionView.register(ButtonCollectionReusableView.self, forSupplementaryViewOfKind: ElementKind.button.rawValue, withReuseIdentifier: ElementKind.button.rawValue)
|
||||||
|
|
||||||
|
self.dataSource.proxy = self
|
||||||
|
self.collectionView.dataSource = self.dataSource
|
||||||
|
self.collectionView.prefetchDataSource = self.dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewSafeAreaInsetsDidChange()
|
||||||
|
{
|
||||||
|
super.viewSafeAreaInsetsDidChange()
|
||||||
|
|
||||||
|
// Add sectionInset to safeAreaInsets.bottom.
|
||||||
|
self.collectionView.contentInset = UIEdgeInsets(top: sectionInset, left: 0, bottom: self.view.safeAreaInsets.bottom + sectionInset, right: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SourceDetailContentViewController
|
||||||
|
{
|
||||||
|
func makeLayout(source: Source) -> UICollectionViewCompositionalLayout
|
||||||
|
{
|
||||||
|
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
|
||||||
|
layoutConfig.interSectionSpacing = 10
|
||||||
|
|
||||||
|
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||||
|
guard let section = Section(rawValue: sectionIndex) else { return nil }
|
||||||
|
|
||||||
|
switch section
|
||||||
|
{
|
||||||
|
case .news:
|
||||||
|
guard !source.newsItems.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) // Underestimate height to prevent jumping size abruptly.
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
|
||||||
|
let groupWidth = layoutEnvironment.container.contentSize.width - sectionInset * 2
|
||||||
|
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: .estimated(50))
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
|
|
||||||
|
let buttonSize = NSCollectionLayoutSize(widthDimension: .estimated(60), heightDimension: .estimated(20))
|
||||||
|
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: buttonSize, elementKind: ElementKind.button.rawValue, alignment: .bottomTrailing)
|
||||||
|
|
||||||
|
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||||
|
layoutSection.interGroupSpacing = 10
|
||||||
|
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: sectionInset, bottom: 4, trailing: sectionInset)
|
||||||
|
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
|
||||||
|
layoutSection.boundarySupplementaryItems = [sectionFooter]
|
||||||
|
return layoutSection
|
||||||
|
|
||||||
|
case .featuredApps:
|
||||||
|
// Always show Featured Apps section, even if there are no apps.
|
||||||
|
// guard !source.effectiveFeaturedApps.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(88))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
|
||||||
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
|
||||||
|
|
||||||
|
let titleSize = NSCollectionLayoutSize(widthDimension: .estimated(75), heightDimension: .estimated(40))
|
||||||
|
let titleHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.title.rawValue, alignment: .topLeading)
|
||||||
|
|
||||||
|
let buttonSize = NSCollectionLayoutSize(widthDimension: .estimated(60), heightDimension: .estimated(20))
|
||||||
|
let buttonHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: buttonSize, elementKind: ElementKind.button.rawValue, alignment: .bottomTrailing)
|
||||||
|
|
||||||
|
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||||
|
layoutSection.interGroupSpacing = 15
|
||||||
|
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 15 /* independent of sectionInset */, leading: sectionInset, bottom: 4, trailing: sectionInset)
|
||||||
|
layoutSection.orthogonalScrollingBehavior = .none
|
||||||
|
layoutSection.boundarySupplementaryItems = [titleHeader, buttonHeader]
|
||||||
|
return layoutSection
|
||||||
|
|
||||||
|
case .about:
|
||||||
|
guard source.localizedDescription != nil else { return nil }
|
||||||
|
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
|
||||||
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
|
||||||
|
|
||||||
|
let titleSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(40))
|
||||||
|
let titleHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.title.rawValue, alignment: .topLeading)
|
||||||
|
|
||||||
|
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||||
|
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 15 /* independent of sectionInset */, leading: sectionInset, bottom: 0, trailing: sectionInset)
|
||||||
|
layoutSection.orthogonalScrollingBehavior = .none
|
||||||
|
layoutSection.boundarySupplementaryItems = [titleHeader]
|
||||||
|
return layoutSection
|
||||||
|
}
|
||||||
|
}, configuration: layoutConfig)
|
||||||
|
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<NSManagedObject, UIImage>
|
||||||
|
{
|
||||||
|
let newsDataSource = self.newsDataSource as! RSTFetchedResultsCollectionViewDataSource<NSManagedObject>
|
||||||
|
let appsDataSource = self.appsDataSource as! RSTArrayCollectionViewPrefetchingDataSource<NSManagedObject, UIImage>
|
||||||
|
|
||||||
|
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<NSManagedObject, UIImage>(dataSources: [newsDataSource, appsDataSource, self.aboutDataSource])
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNewsDataSource() -> RSTFetchedResultsCollectionViewDataSource<NewsItem>
|
||||||
|
{
|
||||||
|
let fetchRequest = NewsItem.sortedFetchRequest(for: self.source)
|
||||||
|
|
||||||
|
let context = self.source.managedObjectContext ?? DatabaseManager.shared.viewContext
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewDataSource(fetchRequest: fetchRequest, managedObjectContext: context)
|
||||||
|
dataSource.liveFetchLimit = 5
|
||||||
|
dataSource.cellIdentifierHandler = { _ in "NewsCell" }
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
|
||||||
|
let cell = cell as! NewsCollectionViewCell
|
||||||
|
|
||||||
|
// For some reason, setting cell.layoutMargins = .zero does not update cell.contentView.layoutMargins.
|
||||||
|
cell.layoutMargins = .zero
|
||||||
|
cell.contentView.layoutMargins = .zero
|
||||||
|
|
||||||
|
cell.titleLabel.text = newsItem.title
|
||||||
|
cell.captionLabel.text = newsItem.caption
|
||||||
|
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
|
||||||
|
|
||||||
|
cell.imageView.image = nil
|
||||||
|
cell.imageView.isHidden = true
|
||||||
|
|
||||||
|
cell.isAccessibilityElement = true
|
||||||
|
cell.accessibilityLabel = (cell.titleLabel.text ?? "") + ". " + (cell.captionLabel.text ?? "")
|
||||||
|
|
||||||
|
if newsItem.storeApp != nil || newsItem.externalURL != nil
|
||||||
|
{
|
||||||
|
cell.accessibilityTraits.insert(.button)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.accessibilityTraits.remove(.button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAppsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||||
|
{
|
||||||
|
let featuredApps = self.source.effectiveFeaturedApps
|
||||||
|
let limitedFeaturedApps = Array(featuredApps.prefix(5))
|
||||||
|
|
||||||
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<StoreApp, UIImage>(items: limitedFeaturedApps)
|
||||||
|
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||||
|
dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta)) // Never show beta apps (at least until we support betas for other sources).
|
||||||
|
dataSource.cellConfigurationHandler = { [weak self] (cell, storeApp, indexPath) in
|
||||||
|
let cell = cell as! AppBannerCollectionViewCell
|
||||||
|
cell.tintColor = storeApp.tintColor
|
||||||
|
|
||||||
|
// For some reason, setting cell.layoutMargins = .zero does not update cell.contentView.layoutMargins.
|
||||||
|
cell.layoutMargins = .zero
|
||||||
|
cell.contentView.layoutMargins = .zero
|
||||||
|
|
||||||
|
cell.bannerView.configure(for: storeApp)
|
||||||
|
|
||||||
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
cell.bannerView.buttonLabel.isHidden = true
|
||||||
|
|
||||||
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
|
cell.bannerView.button.tintColor = storeApp.tintColor
|
||||||
|
|
||||||
|
let buttonTitle = NSLocalizedString("Free", comment: "")
|
||||||
|
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
||||||
|
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), storeApp.name)
|
||||||
|
cell.bannerView.button.accessibilityValue = buttonTitle
|
||||||
|
|
||||||
|
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
|
cell.bannerView.button.progress = progress
|
||||||
|
|
||||||
|
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
|
||||||
|
{
|
||||||
|
cell.bannerView.button.countdownDate = versionDate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.countdownDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure refresh button is correct size.
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
|
||||||
|
if let progress = AppManager.shared.installationProgress(for: storeApp), progress.fractionCompleted < 1.0
|
||||||
|
{
|
||||||
|
cell.bannerView.button.progress = progress
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.progress = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (storeApp, indexPath, completion) -> Foundation.Operation? in
|
||||||
|
return RSTAsyncBlockOperation { (operation) in
|
||||||
|
storeApp.managedObjectContext?.perform {
|
||||||
|
ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { result in
|
||||||
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success(let response): completion(response.image, nil)
|
||||||
|
case .failure(let error): completion(nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! AppBannerCollectionViewCell
|
||||||
|
cell.bannerView.iconImageView.image = image
|
||||||
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
|
|
||||||
|
if let error
|
||||||
|
{
|
||||||
|
print("[ALTLog] Error loading source icon:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAboutDataSource() -> RSTDynamicCollectionViewDataSource<NSManagedObject>
|
||||||
|
{
|
||||||
|
let dataSource = RSTDynamicCollectionViewDataSource<NSManagedObject>()
|
||||||
|
dataSource.numberOfSectionsHandler = { 1 }
|
||||||
|
dataSource.numberOfItemsHandler = { _ in self.source.localizedDescription == nil ? 0 : 1 }
|
||||||
|
dataSource.cellIdentifierHandler = { _ in "AboutCell" }
|
||||||
|
dataSource.cellConfigurationHandler = { [weak self] (cell, _, indexPath) in
|
||||||
|
let cell = cell as! TextViewCollectionViewCell
|
||||||
|
cell.contentView.layoutMargins = .zero // Fixes incorrect margins if not initially on screen.
|
||||||
|
cell.textView.text = self?.source.localizedDescription
|
||||||
|
cell.textView.isCollapsed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SourceDetailContentViewController
|
||||||
|
{
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||||
|
{
|
||||||
|
let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath)
|
||||||
|
|
||||||
|
let section = Section(rawValue: indexPath.section)!
|
||||||
|
let kind = ElementKind(rawValue: kind)!
|
||||||
|
switch (section, kind)
|
||||||
|
{
|
||||||
|
case (.news, _):
|
||||||
|
let buttonView = supplementaryView as! ButtonCollectionReusableView
|
||||||
|
buttonView.button.setTitle(NSLocalizedString("View All", comment: ""), for: .normal)
|
||||||
|
|
||||||
|
case (.featuredApps, .title):
|
||||||
|
let titleView = supplementaryView as! TitleCollectionReusableView
|
||||||
|
titleView.label.text = NSLocalizedString("Featured Apps", comment: "")
|
||||||
|
|
||||||
|
case (.featuredApps, .button):
|
||||||
|
let buttonView = supplementaryView as! ButtonCollectionReusableView
|
||||||
|
buttonView.button.setTitle(NSLocalizedString("View All Apps", comment: ""), for: .normal)
|
||||||
|
|
||||||
|
case (.about, _):
|
||||||
|
let titleView = supplementaryView as! TitleCollectionReusableView
|
||||||
|
titleView.label.text = NSLocalizedString("About", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return supplementaryView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SourceDetailContentViewController: ScrollableContentViewController
|
||||||
|
{
|
||||||
|
var scrollView: UIScrollView { self.collectionView }
|
||||||
|
}
|
||||||
108
AltStore/Sources/SourceDetailViewController.swift
Normal file
108
AltStore/Sources/SourceDetailViewController.swift
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// SourceDetailViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/15/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
class SourceDetailViewController: HeaderContentViewController<SourceHeaderView, SourceDetailContentViewController>
|
||||||
|
{
|
||||||
|
@Managed private(set) var source: Source
|
||||||
|
|
||||||
|
private var addButton: VibrantButton!
|
||||||
|
|
||||||
|
private var previousBounds: CGRect?
|
||||||
|
|
||||||
|
init?(source: Source, coder: NSCoder)
|
||||||
|
{
|
||||||
|
self.source = source
|
||||||
|
super.init(coder: coder)
|
||||||
|
|
||||||
|
self.title = source.name
|
||||||
|
self.tintColor = source.effectiveTintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.addButton = VibrantButton(type: .system)
|
||||||
|
self.addButton.title = NSLocalizedString("ADD", comment: "")
|
||||||
|
self.addButton.contentInsets = PillButton.contentInsets
|
||||||
|
self.addButton.sizeToFit()
|
||||||
|
self.view.addSubview(self.addButton)
|
||||||
|
|
||||||
|
Nuke.loadImage(with: self.source.effectiveIconURL, into: self.navigationBarIconView)
|
||||||
|
Nuke.loadImage(with: self.source.effectiveHeaderImageURL, into: self.backgroundImageView)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
self.addButton.layer.cornerRadius = self.addButton.bounds.midY
|
||||||
|
self.navigationBarIconView.layer.cornerRadius = self.navigationBarIconView.bounds.midY
|
||||||
|
|
||||||
|
var addButtonSize = self.addButton.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
|
||||||
|
addButtonSize.width = max(addButtonSize.width, PillButton.minimumSize.width)
|
||||||
|
addButtonSize.height = max(addButtonSize.height, PillButton.minimumSize.height)
|
||||||
|
self.addButton.frame.size = addButtonSize
|
||||||
|
|
||||||
|
// Place in top-right corner.
|
||||||
|
let inset = 15.0
|
||||||
|
self.addButton.center.y = self.backButton.center.y
|
||||||
|
self.addButton.frame.origin.x = self.view.bounds.width - inset - self.addButton.bounds.width
|
||||||
|
|
||||||
|
guard self.view.bounds != self.previousBounds else { return }
|
||||||
|
self.previousBounds = self.view.bounds
|
||||||
|
|
||||||
|
let headerSize = self.headerView.systemLayoutSizeFitting(CGSize(width: self.view.bounds.width - inset * 2, height: UIView.layoutFittingCompressedSize.height))
|
||||||
|
self.headerView.frame.size.height = headerSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: Override
|
||||||
|
|
||||||
|
override func makeContentViewController() -> SourceDetailContentViewController
|
||||||
|
{
|
||||||
|
guard let storyboard = self.storyboard else { fatalError("SourceDetailViewController must be initialized via UIStoryboard.") }
|
||||||
|
|
||||||
|
let contentViewController = storyboard.instantiateViewController(identifier: "sourceDetailContentViewController") { coder in
|
||||||
|
SourceDetailContentViewController(source: self.source, coder: coder)
|
||||||
|
}
|
||||||
|
return contentViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
override func makeHeaderView() -> SourceHeaderView
|
||||||
|
{
|
||||||
|
let sourceAboutView = SourceHeaderView(frame: CGRect(x: 0, y: 0, width: 375, height: 200))
|
||||||
|
sourceAboutView.configure(for: self.source)
|
||||||
|
return sourceAboutView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func update()
|
||||||
|
{
|
||||||
|
super.update()
|
||||||
|
|
||||||
|
if self.source.identifier == Source.altStoreIdentifier
|
||||||
|
{
|
||||||
|
// Users can't remove default AltStore source, so hide buttons.
|
||||||
|
self.addButton.isHidden = true
|
||||||
|
self.navigationBarButton.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
AltStore/Sources/SourceDetailsComponents.swift
Normal file
89
AltStore/Sources/SourceDetailsComponents.swift
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// SourceDetailsComponents.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/16/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class TitleCollectionReusableView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let label: UILabel
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .largeTitle).withSymbolicTraits(.traitBold)!
|
||||||
|
let font = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||||
|
|
||||||
|
self.label = UILabel(frame: .zero)
|
||||||
|
self.label.font = font
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.label, pinningEdgesWith: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ButtonCollectionReusableView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let button: UIButton
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
self.button = UIButton(type: .system)
|
||||||
|
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.button, pinningEdgesWith: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextViewCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
let textView = CollapsingTextView(frame: .zero)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
self.textView.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
self.textView.isScrollEnabled = false
|
||||||
|
self.textView.isEditable = false
|
||||||
|
self.textView.isSelectable = true
|
||||||
|
self.textView.dataDetectorTypes = [.link]
|
||||||
|
self.contentView.addSubview(self.textView, pinningEdgesWith: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutMarginsDidChange()
|
||||||
|
{
|
||||||
|
super.layoutMarginsDidChange()
|
||||||
|
|
||||||
|
self.textView.textContainerInset.left = self.contentView.layoutMargins.left
|
||||||
|
self.textView.textContainerInset.right = self.contentView.layoutMargins.right
|
||||||
|
}
|
||||||
|
}
|
||||||
106
AltStore/Sources/SourceHeaderView.swift
Normal file
106
AltStore/Sources/SourceHeaderView.swift
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// SourceHeaderView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/9/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
class SourceHeaderView: RSTNibView
|
||||||
|
{
|
||||||
|
@IBOutlet private(set) var titleLabel: UILabel!
|
||||||
|
@IBOutlet private(set) var subtitleLabel: UILabel!
|
||||||
|
@IBOutlet private(set) var iconImageView: UIImageView!
|
||||||
|
@IBOutlet private(set) var websiteButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet private var websiteContentView: UIView!
|
||||||
|
@IBOutlet private var websiteButtonContainerView: UIView!
|
||||||
|
@IBOutlet private var websiteImageView: UIImageView!
|
||||||
|
|
||||||
|
@IBOutlet private var widthConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.layer.cornerRadius = 22
|
||||||
|
|
||||||
|
self.iconImageView.clipsToBounds = true
|
||||||
|
|
||||||
|
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).withSymbolicTraits(.traitBold)!
|
||||||
|
let titleFont = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||||
|
self.titleLabel.font = titleFont
|
||||||
|
|
||||||
|
self.websiteButton.setTitle(nil, for: .normal)
|
||||||
|
self.websiteButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||||
|
|
||||||
|
let imageConfiguration = UIImage.SymbolConfiguration(scale: .medium)
|
||||||
|
let websiteImage = UIImage(systemName: "link", withConfiguration: imageConfiguration)
|
||||||
|
self.websiteImageView.image = websiteImage
|
||||||
|
|
||||||
|
self.websiteButtonContainerView.clipsToBounds = true
|
||||||
|
self.websiteButtonContainerView.layer.cornerRadius = 14 // 22 - inset (8)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.iconImageView.layer.cornerRadius = self.iconImageView.bounds.midY
|
||||||
|
|
||||||
|
if let titleLabel = self.websiteButton.titleLabel, self.widthConstraint.constant == 0
|
||||||
|
{
|
||||||
|
// Left-align website button text with subtitle by increasing width by label inset.
|
||||||
|
let frame = self.websiteButton.convert(titleLabel.frame, from: titleLabel.superview)
|
||||||
|
self.widthConstraint.constant = frame.minX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SourceHeaderView
|
||||||
|
{
|
||||||
|
func configure(for source: Source)
|
||||||
|
{
|
||||||
|
self.titleLabel.text = source.name
|
||||||
|
self.subtitleLabel.text = source.subtitle
|
||||||
|
|
||||||
|
self.websiteImageView.tintColor = source.effectiveTintColor
|
||||||
|
|
||||||
|
if let websiteURL = source.websiteURL
|
||||||
|
{
|
||||||
|
self.websiteButton.setTitle(websiteURL.absoluteString, for: .normal)
|
||||||
|
|
||||||
|
self.websiteContentView.isHidden = false
|
||||||
|
self.websiteImageView.isHidden = false
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.websiteButton.setTitle(nil, for: .normal)
|
||||||
|
|
||||||
|
self.websiteContentView.isHidden = true
|
||||||
|
self.websiteImageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Nuke.loadImage(with: source.effectiveIconURL, into: self.iconImageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
206
AltStore/Sources/SourceHeaderView.xib
Normal file
206
AltStore/Sources/SourceHeaderView.xib
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SourceHeaderView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="iconImageView" destination="rfC-wI-8JY" id="1FW-GN-WDa"/>
|
||||||
|
<outlet property="subtitleLabel" destination="IUL-qI-QAg" id="RyD-ax-OtJ"/>
|
||||||
|
<outlet property="titleLabel" destination="FsG-Wm-6xP" id="huW-re-9G0"/>
|
||||||
|
<outlet property="websiteButton" destination="cDF-t8-8Ri" id="6YC-OT-StI"/>
|
||||||
|
<outlet property="websiteButtonContainerView" destination="kyG-Ne-9eG" id="eH9-eb-iEe"/>
|
||||||
|
<outlet property="websiteContentView" destination="lLy-42-4bf" id="sUV-gS-ykd"/>
|
||||||
|
<outlet property="websiteImageView" destination="Vd9-Y3-Vhc" id="vvT-Wx-o8o"/>
|
||||||
|
<outlet property="widthConstraint" destination="KPO-2J-5Pt" id="o0i-tJ-88g"/>
|
||||||
|
</connections>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ed2-hy-JkU">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VTh-Dz-qVQ" userLabel="Blur View">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="eHD-cD-5y4">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" distribution="equalSpacing" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="Ydv-2n-m56">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="hfp-yJ-gcH" userLabel="App Info">
|
||||||
|
<rect key="frame" x="14" y="14" width="349" height="68"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="rfC-wI-8JY" userLabel="Icon Image View">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="68" height="68"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" secondItem="rfC-wI-8JY" secondAttribute="height" multiplier="1:1" id="Pec-Vt-SX1"/>
|
||||||
|
<constraint firstAttribute="height" constant="68" id="enw-jt-m0C"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="a5P-5y-U64" userLabel="Labels Stack View">
|
||||||
|
<rect key="frame" x="79" y="10.000000000000004" width="270" height="48.333333333333343"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Source Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="FsG-Wm-6xP">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="128.66666666666666" height="26.333333333333332"/>
|
||||||
|
<accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7WA-03-aKF" userLabel="Vibrancy View">
|
||||||
|
<rect key="frame" x="0.0" y="30.333333333333336" width="95" height="18"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ie4-Od-obh">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="95" height="18"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="IUL-qI-QAg">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="95" height="18"/>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="IUL-qI-QAg" firstAttribute="top" secondItem="ie4-Od-obh" secondAttribute="top" id="3YV-ax-2UB"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="IUL-qI-QAg" secondAttribute="trailing" id="PFG-Oe-76p"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="IUL-qI-QAg" secondAttribute="bottom" id="Q0z-A4-UhM"/>
|
||||||
|
<constraint firstItem="IUL-qI-QAg" firstAttribute="leading" secondItem="ie4-Od-obh" secondAttribute="leading" id="qjX-ro-UD6"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<vibrancyEffect style="secondaryLabel">
|
||||||
|
<blurEffect style="systemThinMaterial"/>
|
||||||
|
</vibrancyEffect>
|
||||||
|
</visualEffectView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lLy-42-4bf" userLabel="Website">
|
||||||
|
<rect key="frame" x="14" y="336" width="349" height="50"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bl1-oP-fLT" userLabel="Spacer View">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="349" height="50"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="44" placeholderIntrinsicHeight="44" translatesAutoresizingMaskIntoConstraints="NO" id="Vd9-Y3-Vhc">
|
||||||
|
<rect key="frame" x="12" y="3" width="44" height="44"/>
|
||||||
|
</imageView>
|
||||||
|
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kyG-Ne-9eG" userLabel="Website Button Container View">
|
||||||
|
<rect key="frame" x="79" y="0.0" width="270" height="50"/>
|
||||||
|
<subviews>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c6g-CV-IeK" userLabel="Vibrancy View">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="zhu-r0-cL8">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1lg-Ki-MOK">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||||
|
<state key="normal" title=" "/>
|
||||||
|
<buttonConfiguration key="configuration" style="plain" title=" "/>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="1lg-Ki-MOK" secondAttribute="bottom" id="2Xf-5H-MQm"/>
|
||||||
|
<constraint firstItem="1lg-Ki-MOK" firstAttribute="top" secondItem="zhu-r0-cL8" secondAttribute="top" id="PIj-oh-hQg"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="1lg-Ki-MOK" secondAttribute="trailing" id="f2H-jO-d5v"/>
|
||||||
|
<constraint firstItem="1lg-Ki-MOK" firstAttribute="leading" secondItem="zhu-r0-cL8" secondAttribute="leading" id="zdh-qf-8ow"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<vibrancyEffect style="secondaryFill">
|
||||||
|
<blurEffect style="systemThinMaterial"/>
|
||||||
|
</vibrancyEffect>
|
||||||
|
</visualEffectView>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Sgm-uO-rMs" userLabel="Vibrancy View">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="6e6-8c-O5P">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cDF-t8-8Ri">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
|
||||||
|
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||||
|
<state key="normal" title="https://rileytestut.com"/>
|
||||||
|
<buttonConfiguration key="configuration" style="plain" title="https://rileytestut.com"/>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="cDF-t8-8Ri" firstAttribute="leading" secondItem="6e6-8c-O5P" secondAttribute="leading" id="9pW-be-mEO"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="cDF-t8-8Ri" secondAttribute="bottom" id="9zM-Hq-5ea"/>
|
||||||
|
<constraint firstItem="cDF-t8-8Ri" firstAttribute="top" secondItem="6e6-8c-O5P" secondAttribute="top" id="RW7-Qu-Afy"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="cDF-t8-8Ri" secondAttribute="trailing" id="sln-aP-Czq"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<vibrancyEffect style="fill">
|
||||||
|
<blurEffect style="systemThinMaterial"/>
|
||||||
|
</vibrancyEffect>
|
||||||
|
</visualEffectView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="c6g-CV-IeK" secondAttribute="trailing" id="2Db-8d-4ta"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Sgm-uO-rMs" secondAttribute="bottom" id="CuL-oW-Tka"/>
|
||||||
|
<constraint firstItem="c6g-CV-IeK" firstAttribute="leading" secondItem="kyG-Ne-9eG" secondAttribute="leading" id="Wey-l1-VIN"/>
|
||||||
|
<constraint firstItem="Sgm-uO-rMs" firstAttribute="leading" secondItem="kyG-Ne-9eG" secondAttribute="leading" id="Zca-A9-Z3v"/>
|
||||||
|
<constraint firstItem="Sgm-uO-rMs" firstAttribute="top" secondItem="kyG-Ne-9eG" secondAttribute="top" id="aLc-Sh-C2g"/>
|
||||||
|
<constraint firstItem="c6g-CV-IeK" firstAttribute="top" secondItem="kyG-Ne-9eG" secondAttribute="top" id="gL7-rP-JJQ"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="c6g-CV-IeK" secondAttribute="bottom" id="h1Q-x5-7ch"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Sgm-uO-rMs" secondAttribute="trailing" id="hgG-mP-CiI"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="kyG-Ne-9eG" firstAttribute="top" secondItem="bl1-oP-fLT" secondAttribute="top" id="4WU-ym-LUr"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="kyG-Ne-9eG" secondAttribute="bottom" id="9Md-8N-jr9"/>
|
||||||
|
<constraint firstItem="Vd9-Y3-Vhc" firstAttribute="centerY" secondItem="kyG-Ne-9eG" secondAttribute="centerY" id="kd3-l3-vD7"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="kyG-Ne-9eG" secondAttribute="trailing" id="mCi-EJ-bcd"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="hfp-yJ-gcH" secondAttribute="trailing" id="9R7-eh-OeE"/>
|
||||||
|
<constraint firstItem="kyG-Ne-9eG" firstAttribute="width" secondItem="a5P-5y-U64" secondAttribute="width" id="KPO-2J-5Pt"/>
|
||||||
|
<constraint firstItem="Vd9-Y3-Vhc" firstAttribute="centerX" secondItem="rfC-wI-8JY" secondAttribute="centerX" id="TtS-ai-grE"/>
|
||||||
|
<constraint firstAttribute="leadingMargin" secondItem="hfp-yJ-gcH" secondAttribute="leading" id="YPF-kW-MKJ"/>
|
||||||
|
</constraints>
|
||||||
|
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="BlurTint"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Ydv-2n-m56" firstAttribute="leading" secondItem="eHD-cD-5y4" secondAttribute="leading" id="FdZ-7R-70D"/>
|
||||||
|
<constraint firstItem="Ydv-2n-m56" firstAttribute="top" secondItem="eHD-cD-5y4" secondAttribute="top" id="JtX-k9-9A3"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Ydv-2n-m56" secondAttribute="bottom" id="X2R-Ab-m7o"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Ydv-2n-m56" secondAttribute="trailing" id="z0g-5Q-eso"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<blurEffect style="systemThinMaterial"/>
|
||||||
|
</visualEffectView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="jw2-Uc-PZa"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="VTh-Dz-qVQ" firstAttribute="top" secondItem="ed2-hy-JkU" secondAttribute="top" id="2oR-Up-r2e"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="VTh-Dz-qVQ" secondAttribute="bottom" id="4bb-e5-tea"/>
|
||||||
|
<constraint firstItem="VTh-Dz-qVQ" firstAttribute="leading" secondItem="ed2-hy-JkU" secondAttribute="leading" id="aZg-21-M6I"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="VTh-Dz-qVQ" secondAttribute="trailing" id="o4u-HT-CZ6"/>
|
||||||
|
</constraints>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<point key="canvasLocation" x="266" y="135"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="BlurTint">
|
||||||
|
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -4,7 +4,9 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
@@ -127,12 +129,73 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
</navigationItem>
|
</navigationItem>
|
||||||
|
<connections>
|
||||||
|
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="dLj-Tf-ZjV"/>
|
||||||
|
</connections>
|
||||||
</collectionViewController>
|
</collectionViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="69z-hg-xF8" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="69z-hg-xF8" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
<exit id="wBZ-c2-miy" userLabel="Exit" sceneMemberID="exit"/>
|
<exit id="wBZ-c2-miy" userLabel="Exit" sceneMemberID="exit"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="810" y="-13"/>
|
<point key="canvasLocation" x="810" y="-13"/>
|
||||||
</scene>
|
</scene>
|
||||||
|
<!--Source Detail View Controller-->
|
||||||
|
<scene sceneID="xbN-Mz-TtU">
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="cYc-NX-nF1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
<viewController storyboardIdentifier="sourceDetailViewController" id="7XE-Wv-lf9" customClass="SourceDetailViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="eIv-0H-ZIq">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="GND-ro-Anp"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</view>
|
||||||
|
<navigationItem key="navigationItem" id="Ocv-bj-TfG"/>
|
||||||
|
</viewController>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="1646.5648854961833" y="-13.380281690140846"/>
|
||||||
|
</scene>
|
||||||
|
<!--Source Detail Content View Controller-->
|
||||||
|
<scene sceneID="8nl-ly-jhT">
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="KEz-hK-u3f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
<collectionViewController storyboardIdentifier="sourceDetailContentViewController" id="MSh-hM-32I" customClass="SourceDetailContentViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="fiF-YD-Ing">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
<collectionViewFlowLayout key="collectionViewLayout" automaticEstimatedItemSize="YES" minimumLineSpacing="10" minimumInteritemSpacing="10" id="evc-Tb-ofk">
|
||||||
|
<size key="itemSize" width="128" height="128"/>
|
||||||
|
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||||
|
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||||
|
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||||
|
</collectionViewFlowLayout>
|
||||||
|
<cells>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="AppCell" id="ioR-1o-Qe1" customClass="AppBannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Bda-YQ-Gv5">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</collectionViewCellContentView>
|
||||||
|
</collectionViewCell>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="AboutCell" id="Bnj-xm-pBT" customClass="TextViewCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="265" y="0.0" width="128" height="128"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="lXN-gL-rhU">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</collectionViewCellContentView>
|
||||||
|
</collectionViewCell>
|
||||||
|
</cells>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dataSource" destination="MSh-hM-32I" id="iWe-66-9HQ"/>
|
||||||
|
<outlet property="delegate" destination="MSh-hM-32I" id="8SG-5v-iF2"/>
|
||||||
|
</connections>
|
||||||
|
</collectionView>
|
||||||
|
</collectionViewController>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="2509" y="-13"/>
|
||||||
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
|
|||||||
@@ -199,6 +199,15 @@ private extension SourcesViewController
|
|||||||
let dataSource = RSTArrayCollectionViewDataSource<Source>(items: [])
|
let dataSource = RSTArrayCollectionViewDataSource<Source>(items: [])
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBSegueAction
|
||||||
|
func makeSourceDetailViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
|
||||||
|
{
|
||||||
|
guard let source = sender as? Source else { return nil }
|
||||||
|
|
||||||
|
let sourceDetailViewController = SourceDetailViewController(source: source, coder: coder)
|
||||||
|
return sourceDetailViewController
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SourcesViewController
|
private extension SourcesViewController
|
||||||
@@ -229,7 +238,7 @@ private extension SourcesViewController
|
|||||||
self.present(alertController, animated: true, completion: nil)
|
self.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSource(url: URL, isTrusted: Bool = false, completionHandler: ((Result<Void, Error>) -> Void)? = nil)
|
func addSource(url: URL, completionHandler: ((Result<Void, Error>) -> Void)? = nil)
|
||||||
{
|
{
|
||||||
guard self.view.window != nil else { return }
|
guard self.view.window != nil else { return }
|
||||||
|
|
||||||
@@ -262,6 +271,7 @@ private extension SourcesViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Remove this now that trusted sources aren't necessary.
|
||||||
var dependencies: [Foundation.Operation] = []
|
var dependencies: [Foundation.Operation] = []
|
||||||
if let fetchTrustedSourcesOperation = self.fetchTrustedSourcesOperation
|
if let fetchTrustedSourcesOperation = self.fetchTrustedSourcesOperation
|
||||||
{
|
{
|
||||||
@@ -273,34 +283,13 @@ private extension SourcesViewController
|
|||||||
AppManager.shared.fetchSource(sourceURL: url, dependencies: dependencies) { (result) in
|
AppManager.shared.fetchSource(sourceURL: url, dependencies: dependencies) { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let source = try result.get()
|
@Managed var source = try result.get()
|
||||||
let sourceName = source.name
|
|
||||||
let managedObjectContext = source.managedObjectContext
|
|
||||||
|
|
||||||
// Hide warning when adding a featured trusted source.
|
|
||||||
let message = isTrusted ? nil : NSLocalizedString("Make sure to only add sources that you trust.", comment: "")
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let alertController = UIAlertController(title: String(format: NSLocalizedString("Would you like to add the source “%@”?", comment: ""), sourceName),
|
self.showSourceDetails(for: source)
|
||||||
message: message, preferredStyle: .alert)
|
|
||||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
|
||||||
finish(.failure(OperationError.cancelled))
|
|
||||||
})
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Add Source", comment: ""), style: UIAlertAction.ok.style) { _ in
|
|
||||||
managedObjectContext?.perform {
|
|
||||||
do
|
|
||||||
{
|
|
||||||
try managedObjectContext?.save()
|
|
||||||
finish(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
finish(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finish(.success(()))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -416,7 +405,7 @@ private extension SourcesViewController
|
|||||||
sender.progress = completedProgress
|
sender.progress = completedProgress
|
||||||
|
|
||||||
let source = self.dataSource.item(at: indexPath)
|
let source = self.dataSource.item(at: indexPath)
|
||||||
self.addSource(url: source.sourceURL, isTrusted: true) { _ in
|
self.addSource(url: source.sourceURL) { _ in
|
||||||
//FIXME: Handle cell reuse.
|
//FIXME: Handle cell reuse.
|
||||||
sender.progress = nil
|
sender.progress = nil
|
||||||
}
|
}
|
||||||
@@ -451,6 +440,11 @@ private extension SourcesViewController
|
|||||||
|
|
||||||
self.present(alertController, animated: true, completion: nil)
|
self.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showSourceDetails(for source: Source)
|
||||||
|
{
|
||||||
|
self.performSegue(withIdentifier: "showSourceDetails", sender: source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SourcesViewController
|
extension SourcesViewController
|
||||||
@@ -460,9 +454,7 @@ extension SourcesViewController
|
|||||||
self.collectionView.deselectItem(at: indexPath, animated: true)
|
self.collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
|
|
||||||
let source = self.dataSource.item(at: indexPath)
|
let source = self.dataSource.item(at: indexPath)
|
||||||
guard let error = source.error else { return }
|
self.showSourceDetails(for: source)
|
||||||
|
|
||||||
self.present(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +605,7 @@ extension SourcesViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
let addAction = UIAction(title: String(format: NSLocalizedString("Add “%@”", comment: ""), source.name), image: UIImage(systemName: "plus")) { (action) in
|
let addAction = UIAction(title: String(format: NSLocalizedString("Add “%@”", comment: ""), source.name), image: UIImage(systemName: "plus")) { (action) in
|
||||||
self.addSource(url: source.sourceURL, isTrusted: true)
|
self.addSource(url: source.sourceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
var actions: [UIAction] = []
|
var actions: [UIAction] = []
|
||||||
|
|||||||
@@ -88,4 +88,20 @@ public extension NewsItem
|
|||||||
{
|
{
|
||||||
return NSFetchRequest<NewsItem>(entityName: "NewsItem")
|
return NSFetchRequest<NewsItem>(entityName: "NewsItem")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class func sortedFetchRequest(for source: Source?) -> NSFetchRequest<NewsItem>
|
||||||
|
{
|
||||||
|
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
|
||||||
|
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)]
|
||||||
|
|
||||||
|
if let source
|
||||||
|
{
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(NewsItem.source), source)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchRequest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user