mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-30 23:35:39 +02:00
Merge branch 'sources_tab'
# Conflicts: # AltStore.xcodeproj/project.pbxproj
This commit is contained in:
@@ -343,6 +343,7 @@
|
||||
BFF7C90F257844C900E55F36 /* AltXPC.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = BFF7C904257844C900E55F36 /* AltXPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
BFF7C920257844FA00E55F36 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
|
||||
BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; };
|
||||
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50107EB2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift */; };
|
||||
D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCD2A02FA7400B24B63 /* SourceError.swift */; };
|
||||
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */; };
|
||||
D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */; };
|
||||
@@ -364,6 +365,7 @@
|
||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; };
|
||||
D537C8592AA94D94009A1E08 /* altjit in Embed AltJIT */ = {isa = PBXBuildFile; fileRef = D5FB7A132AA284BE00EF863D /* altjit */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
D537C85B2AA9507A009A1E08 /* libcorecrypto.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D537C85A2AA95066009A1E08 /* libcorecrypto.tbd */; platformFilters = (macos, ); };
|
||||
D5390C3C2AC3A43900D17E62 /* AddSourceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5390C3B2AC3A43900D17E62 /* AddSourceViewController.swift */; };
|
||||
D53D84022A2158FC00543C3B /* Permissions.plist in Resources */ = {isa = PBXBuildFile; fileRef = D53D84012A2158FC00543C3B /* Permissions.plist */; };
|
||||
D54058B92A1D6269008CCC58 /* AppPermissionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54058B82A1D6269008CCC58 /* AppPermissionProtocol.swift */; };
|
||||
D54058BB2A1D8FE3008CCC58 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */; };
|
||||
@@ -391,13 +393,12 @@
|
||||
D586D39B28EF58B0000E101F /* AltTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586D39A28EF58B0000E101F /* AltTests.swift */; };
|
||||
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; };
|
||||
D5893F802A1419E800E767CD /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */; };
|
||||
D5893F822A141E4900E767CD /* KnownSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F812A141E4900E767CD /* KnownSource.swift */; };
|
||||
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 */; };
|
||||
D5927D6629DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */; };
|
||||
D5927D6929DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */; };
|
||||
D5935AED29C39DE300C157EF /* SourceDetailsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */; };
|
||||
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5935AEC29C39DE300C157EF /* SourceComponents.swift */; };
|
||||
D5935AEF29C3B23600C157EF /* Sources.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5935AEE29C3B23600C157EF /* Sources.storyboard */; };
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
|
||||
D59A6B7B2AA91B8E00F61259 /* PythonCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7A2AA91B8E00F61259 /* PythonCommand.swift */; };
|
||||
@@ -415,6 +416,7 @@
|
||||
D5A299882AAB9E4E00A3988D /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; };
|
||||
D5A299892AAB9E5900A3988D /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.swift */; };
|
||||
D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; };
|
||||
D5B6F6A92AD75D01007EED5A /* ProcessInfo+Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */; };
|
||||
D5BA9E9B2A9FE1E8007C0661 /* JITManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BA9E9A2A9FE1E8007C0661 /* JITManager.swift */; };
|
||||
D5C8ACDB2A956B2B00669F92 /* Process+STPrivilegedTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C8ACDA2A956B2B00669F92 /* Process+STPrivilegedTask.swift */; };
|
||||
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; };
|
||||
@@ -435,6 +437,8 @@
|
||||
D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */; };
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
|
||||
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
|
||||
D5FB28EC2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */; };
|
||||
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F812A141E4900E767CD /* KnownSource.swift */; };
|
||||
D5FB7A0E2AA25A4E00EF863D /* Previews.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */; };
|
||||
D5FB7A212AA284ED00EF863D /* EnableJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */; };
|
||||
D5FB7A242AA284ED00EF863D /* Logger+AltJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1D2AA284ED00EF863D /* Logger+AltJIT.swift */; };
|
||||
@@ -950,6 +954,7 @@
|
||||
BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = "<group>"; };
|
||||
C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D50107EB2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSourceTextFieldCell.swift; sourceTree = "<group>"; };
|
||||
D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsIntent.swift; sourceTree = "<group>"; };
|
||||
D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsWidgetIntent.swift; sourceTree = "<group>"; };
|
||||
D5151BE52A90391900C96F28 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; };
|
||||
@@ -966,6 +971,7 @@
|
||||
D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; };
|
||||
D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; };
|
||||
D537C85A2AA95066009A1E08 /* libcorecrypto.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcorecrypto.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/lib/system/libcorecrypto.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
D5390C3B2AC3A43900D17E62 /* AddSourceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSourceViewController.swift; sourceTree = "<group>"; };
|
||||
D53D84012A2158FC00543C3B /* Permissions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Permissions.plist; sourceTree = "<group>"; };
|
||||
D54058B82A1D6269008CCC58 /* AppPermissionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionProtocol.swift; sourceTree = "<group>"; };
|
||||
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; };
|
||||
@@ -1000,7 +1006,7 @@
|
||||
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBarAppearance+TintColor.swift"; sourceTree = "<group>"; };
|
||||
D5927D6729DCE1FE00D6898E /* AltStore 12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 12.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D5927D6829DCE28700D6898E /* AltStore11ToAltStore12.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore12.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailsComponents.swift; sourceTree = "<group>"; };
|
||||
D5935AEC29C39DE300C157EF /* SourceComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceComponents.swift; 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>"; };
|
||||
D59A6B7A2AA91B8E00F61259 /* PythonCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PythonCommand.swift; sourceTree = "<group>"; };
|
||||
@@ -1013,6 +1019,7 @@
|
||||
D5A1D2EA2AA513410066CACC /* URL+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Tools.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>"; };
|
||||
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+Previews.swift"; sourceTree = "<group>"; };
|
||||
D5BA9E9A2A9FE1E8007C0661 /* JITManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JITManager.swift; sourceTree = "<group>"; };
|
||||
D5C8ACDA2A956B2B00669F92 /* Process+STPrivilegedTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Process+STPrivilegedTask.swift"; sourceTree = "<group>"; };
|
||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; };
|
||||
@@ -1032,6 +1039,7 @@
|
||||
D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
|
||||
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFontDescriptor+Bold.swift"; sourceTree = "<group>"; };
|
||||
D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Previews.xcassets; sourceTree = "<group>"; };
|
||||
D5FB7A132AA284BE00EF863D /* altjit */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = altjit; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnableJIT.swift; path = AltJIT/Commands/EnableJIT.swift; sourceTree = SOURCE_ROOT; };
|
||||
@@ -1230,7 +1238,6 @@
|
||||
BF41B807233433C100C593A3 /* LoadingState.swift */,
|
||||
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */,
|
||||
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */,
|
||||
D5893F812A141E4900E767CD /* KnownSource.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
@@ -1473,6 +1480,7 @@
|
||||
children = (
|
||||
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
|
||||
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
|
||||
D5893F812A141E4900E767CD /* KnownSource.swift */,
|
||||
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
|
||||
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
|
||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */,
|
||||
@@ -1583,6 +1591,7 @@
|
||||
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */,
|
||||
D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */,
|
||||
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */,
|
||||
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -1734,9 +1743,8 @@
|
||||
BFC84A4C2421A19100853474 /* SourcesViewController.swift */,
|
||||
D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */,
|
||||
D5A0537229B91DB400997551 /* SourceDetailContentViewController.swift */,
|
||||
D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */,
|
||||
D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */,
|
||||
D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */,
|
||||
D5390C3B2AC3A43900D17E62 /* AddSourceViewController.swift */,
|
||||
D50107ED2ADF2E310069F2A1 /* Components */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -1898,6 +1906,7 @@
|
||||
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */,
|
||||
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
|
||||
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
||||
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -2059,6 +2068,17 @@
|
||||
path = XPC;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D50107ED2ADF2E310069F2A1 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5935AEC29C39DE300C157EF /* SourceComponents.swift */,
|
||||
D59162AA29BA60A9005CBF47 /* SourceHeaderView.swift */,
|
||||
D59162AC29BA616A005CBF47 /* SourceHeaderView.xib */,
|
||||
D50107EB2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D50C29F22A8ECD71009AB488 /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2987,6 +3007,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
|
||||
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
||||
BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */,
|
||||
BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */,
|
||||
@@ -3028,6 +3049,7 @@
|
||||
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
|
||||
BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */,
|
||||
D5B6F6A92AD75D01007EED5A /* ProcessInfo+Previews.swift in Sources */,
|
||||
BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */,
|
||||
D5F48B4829CCF21B002B52A4 /* AltStore+Async.swift in Sources */,
|
||||
BF66EED92501AECA007EE018 /* Team.swift in Sources */,
|
||||
@@ -3096,7 +3118,6 @@
|
||||
D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */,
|
||||
D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */,
|
||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
||||
D5893F822A141E4900E767CD /* KnownSource.swift in Sources */,
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
|
||||
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,
|
||||
@@ -3104,18 +3125,20 @@
|
||||
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
||||
D5390C3C2AC3A43900D17E62 /* AddSourceViewController.swift in Sources */,
|
||||
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */,
|
||||
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */,
|
||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
|
||||
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */,
|
||||
D5FB28EC2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift in Sources */,
|
||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
||||
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
|
||||
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,
|
||||
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||
D5935AED29C39DE300C157EF /* SourceDetailsComponents.swift in Sources */,
|
||||
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
||||
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
||||
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||
@@ -3159,6 +3182,7 @@
|
||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
|
||||
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
|
||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
|
||||
@@ -31,13 +31,6 @@ class AppContentViewController: UITableViewController
|
||||
|
||||
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private lazy var byteCountFormatter: ByteCountFormatter = {
|
||||
let formatter = ByteCountFormatter()
|
||||
return formatter
|
||||
@@ -84,7 +77,7 @@ class AppContentViewController: UITableViewController
|
||||
{
|
||||
self.versionDescriptionTextView.text = version.localizedDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||
}
|
||||
else
|
||||
|
||||
@@ -191,7 +191,21 @@ class AppViewController: UIViewController
|
||||
self._shouldResetLayout = false
|
||||
}
|
||||
|
||||
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||
let statusBarHeight: Double
|
||||
|
||||
if let navigationController, navigationController.presentingViewController != nil, navigationController.modalPresentationStyle != .fullScreen
|
||||
{
|
||||
statusBarHeight = 20
|
||||
}
|
||||
else if let statusBarManager = self.view.window?.windowScene?.statusBarManager
|
||||
{
|
||||
statusBarHeight = statusBarManager.statusBarFrame.height
|
||||
}
|
||||
else
|
||||
{
|
||||
statusBarHeight = 0
|
||||
}
|
||||
|
||||
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||
|
||||
let inset = 12 as CGFloat
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
@@ -36,11 +35,11 @@
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/>
|
||||
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="dXz-Tu-hW8"/>
|
||||
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="zii-dF-qEt"/>
|
||||
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="4Nf-rL-P4c"/>
|
||||
<segue destination="bTL-bY-9Yq" kind="presentation" identifier="finishJailbreak" id="cIc-Ta-uNk"/>
|
||||
<segue destination="HCK-G6-KdY" kind="presentation" identifier="presentSources" id="SRd-VL-5nP"/>
|
||||
<segue destination="HCK-G6-KdY" kind="relationship" relationship="viewControllers" id="X0t-T6-JeA"/>
|
||||
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="OLu-kM-z1J"/>
|
||||
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="phQ-Pc-pqw"/>
|
||||
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="cQE-Az-fdo"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
|
||||
@@ -67,16 +66,7 @@
|
||||
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr">
|
||||
<barButtonItem key="rightBarButtonItem" title="Sources" id="6Ul-JW-TMT">
|
||||
<connections>
|
||||
<segue destination="HCK-G6-KdY" kind="presentation" id="hBK-Tt-naZ"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="sourcesBarButtonItem" destination="6Ul-JW-TMT" id="99s-O4-OpX"/>
|
||||
</connections>
|
||||
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"/>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -238,7 +228,7 @@
|
||||
<tableViewSection id="rfR-32-T0h">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="57" id="xef-ko-Qp1">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="57"/>
|
||||
<rect key="frame" x="0.0" y="50" width="375" height="57"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="xef-ko-Qp1" id="8PX-jQ-nHd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="57"/>
|
||||
@@ -262,7 +252,7 @@
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d">
|
||||
<rect key="frame" x="0.0" y="85" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="107" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nI6-wC-H2d" id="Z4y-vb-Z4Q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
@@ -300,10 +290,10 @@
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="129" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="151" width="375" height="98"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EL5-UC-RIw" id="D1G-nK-G0Z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EL5-UC-RIw" id="D1G-nK-G0Z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -324,29 +314,29 @@
|
||||
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="173" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="249" width="375" height="137.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="47M-El-a4G" id="f9D-OR-oGE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="137.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" distribution="equalSpacing" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="n9R-39-Glq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="137.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="y3w-4S-e64">
|
||||
<rect key="frame" x="20" y="0.0" width="335" height="4"/>
|
||||
<rect key="frame" x="20" y="0.0" width="335" height="47.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="PFD-gZ-77F">
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="26.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="What's New" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="obM-TM-y2E">
|
||||
<rect key="frame" x="0.0" y="0.0" width="124" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="124" height="26.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="2w ago" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wGD-mS-8fO">
|
||||
<rect key="frame" x="285" y="0.0" width="50" height="0.0"/>
|
||||
<rect key="frame" x="285" y="0.0" width="50" height="26.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -354,16 +344,16 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
|
||||
<rect key="frame" x="0.0" y="4" width="335" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 4.4.2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
|
||||
<rect key="frame" x="0.0" y="0.0" width="84.5" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="84.5" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="50.4 MB" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DgM-bD-bBY">
|
||||
<rect key="frame" x="280.5" y="0.0" width="54.5" height="0.0"/>
|
||||
<rect key="frame" x="280.5" y="0.0" width="54.5" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -373,7 +363,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="16" width="335" height="0.0"/>
|
||||
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
@@ -488,7 +478,7 @@
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HgE-PD-dC2" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="313" y="544"/>
|
||||
<point key="canvasLocation" x="233" y="550"/>
|
||||
</scene>
|
||||
<!--News-->
|
||||
<scene sceneID="bqw-wB-hyB">
|
||||
@@ -520,10 +510,10 @@
|
||||
<scene sceneID="VHa-uP-bFU">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
||||
<tabBarItem key="tabBarItem" title="Browse" id="Uwh-Bg-Ymq"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
</navigationBar>
|
||||
@@ -542,7 +532,7 @@
|
||||
<viewControllerPlaceholder storyboardName="PatchApp" id="bTL-bY-9Yq" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="NyZ-z6-R2q" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1" y="545"/>
|
||||
<point key="canvasLocation" x="-228" y="551"/>
|
||||
</scene>
|
||||
<!--My Apps-->
|
||||
<scene sceneID="nhh-BJ-XiT">
|
||||
@@ -553,7 +543,7 @@
|
||||
</tabBarItem>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
@@ -828,7 +818,7 @@
|
||||
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||
</navigationBar>
|
||||
@@ -862,20 +852,20 @@
|
||||
<!--Sources-->
|
||||
<scene sceneID="Vzf-tb-LIH">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Sources" id="HCK-G6-KdY" sceneMemberID="viewController"/>
|
||||
<viewControllerPlaceholder storyboardName="Sources" id="HCK-G6-KdY" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Item" id="Q7y-bi-ncT"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="VTd-he-VYb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2" y="-553"/>
|
||||
<point key="canvasLocation" x="-2" y="550"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="hBK-Tt-naZ"/>
|
||||
<segue reference="cnd-KK-o60"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
<resources>
|
||||
<image name="Back" width="18" height="18"/>
|
||||
<image name="Browse" width="20" height="20"/>
|
||||
<image name="MyApps" width="20" height="20"/>
|
||||
<image name="News" width="19" height="20"/>
|
||||
<image name="Settings" width="20" height="20"/>
|
||||
|
||||
@@ -49,6 +49,8 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.collectionView.backgroundColor = .altBackground
|
||||
|
||||
#if BETA
|
||||
self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
|
||||
self.navigationItem.searchController = self.dataSource.searchController
|
||||
@@ -91,11 +93,6 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
self.fetchSource()
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrowseViewController
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class AppBannerCollectionViewCell: UICollectionViewCell
|
||||
class AppBannerCollectionViewCell: UICollectionViewListCell
|
||||
{
|
||||
let bannerView = AppBannerView(frame: .zero)
|
||||
|
||||
private(set) var errorBadge: UIView!
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
@@ -30,43 +28,24 @@ class AppBannerCollectionViewCell: UICollectionViewCell
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
// Prevent content "squishing" when scrolling offscreen.
|
||||
self.insetsLayoutMarginsFromSafeArea = false
|
||||
self.contentView.insetsLayoutMarginsFromSafeArea = false
|
||||
self.bannerView.insetsLayoutMarginsFromSafeArea = false
|
||||
|
||||
self.selectedBackgroundView = UIView() // Disable selection highlighting.
|
||||
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.contentView.addSubview(self.bannerView)
|
||||
|
||||
let errorBadge = UIView()
|
||||
errorBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||
errorBadge.isHidden = true
|
||||
self.addSubview(errorBadge)
|
||||
|
||||
// Solid background to make the X opaque white.
|
||||
let backgroundView = UIView()
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundView.backgroundColor = .white
|
||||
errorBadge.addSubview(backgroundView)
|
||||
|
||||
let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill"))
|
||||
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
|
||||
badgeView.tintColor = .systemRed
|
||||
errorBadge.addSubview(badgeView, pinningEdgesWith: .zero)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.bannerView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
|
||||
self.bannerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
|
||||
self.bannerView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
|
||||
self.bannerView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor),
|
||||
self.bannerView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
|
||||
self.bannerView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
|
||||
|
||||
errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5),
|
||||
errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5),
|
||||
|
||||
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
|
||||
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
|
||||
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
|
||||
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
|
||||
])
|
||||
|
||||
self.errorBadge = errorBadge
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ import UIKit
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
extension AppBannerView
|
||||
{
|
||||
enum Style
|
||||
{
|
||||
case app
|
||||
case source
|
||||
}
|
||||
}
|
||||
|
||||
class AppBannerView: RSTNibView
|
||||
{
|
||||
override var accessibilityLabel: String? {
|
||||
@@ -38,6 +47,8 @@ class AppBannerView: RSTNibView
|
||||
set { self.accessibilityView?.accessibilityTraits = newValue }
|
||||
}
|
||||
|
||||
var style: Style = .app
|
||||
|
||||
private var originalTintColor: UIColor?
|
||||
|
||||
@IBOutlet var titleLabel: UILabel!
|
||||
@@ -50,8 +61,11 @@ class AppBannerView: RSTNibView
|
||||
@IBOutlet var backgroundEffectView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var vibrancyView: UIVisualEffectView!
|
||||
@IBOutlet private var stackView: UIStackView!
|
||||
@IBOutlet private var accessibilityView: UIView!
|
||||
|
||||
@IBOutlet private var iconImageViewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
@@ -74,6 +88,10 @@ class AppBannerView: RSTNibView
|
||||
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
|
||||
|
||||
self.betaBadgeView.isHidden = true
|
||||
|
||||
self.layoutMargins = self.stackView.layoutMargins
|
||||
self.stackView.preservesSuperviewLayoutMargins = true
|
||||
self.stackView.isLayoutMarginsRelativeArrangement = true
|
||||
}
|
||||
|
||||
override func tintColorDidChange()
|
||||
@@ -113,6 +131,8 @@ extension AppBannerView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.style = .app
|
||||
|
||||
let values = AppValues(app: app)
|
||||
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
|
||||
@@ -129,6 +149,34 @@ extension AppBannerView
|
||||
self.accessibilityLabel = values.name
|
||||
}
|
||||
}
|
||||
|
||||
func configure(for source: Source)
|
||||
{
|
||||
self.style = .source
|
||||
|
||||
let subtitle: String
|
||||
if let text = source.subtitle
|
||||
{
|
||||
subtitle = text
|
||||
}
|
||||
else if let scheme = source.sourceURL.scheme
|
||||
{
|
||||
subtitle = source.sourceURL.absoluteString.replacingOccurrences(of: scheme + "://", with: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
subtitle = source.sourceURL.absoluteString
|
||||
}
|
||||
|
||||
self.titleLabel.text = source.name
|
||||
self.subtitleLabel.text = subtitle
|
||||
|
||||
let tintColor = source.effectiveTintColor ?? .altPrimary
|
||||
self.tintColor = tintColor
|
||||
|
||||
let accessibilityLabel = source.name + "\n" + subtitle
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppBannerView
|
||||
@@ -138,7 +186,48 @@ private extension AppBannerView
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 22
|
||||
|
||||
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||
let tintColor = self.originalTintColor ?? self.tintColor
|
||||
self.subtitleLabel.textColor = tintColor
|
||||
|
||||
switch self.style
|
||||
{
|
||||
case .app:
|
||||
self.directionalLayoutMargins.trailing = self.stackView.directionalLayoutMargins.trailing
|
||||
|
||||
self.iconImageViewHeightConstraint.constant = 60
|
||||
self.iconImageView.style = .icon
|
||||
|
||||
self.titleLabel.textColor = .label
|
||||
|
||||
self.button.style = .pill
|
||||
|
||||
self.backgroundEffectView.contentView.backgroundColor = UIColor(resource: .blurTint)
|
||||
self.backgroundEffectView.backgroundColor = tintColor
|
||||
|
||||
case .source:
|
||||
self.directionalLayoutMargins.trailing = 20
|
||||
|
||||
self.iconImageViewHeightConstraint.constant = 44
|
||||
self.iconImageView.style = .circular
|
||||
|
||||
self.titleLabel.textColor = .white
|
||||
|
||||
self.button.style = .custom
|
||||
|
||||
self.backgroundEffectView.contentView.backgroundColor = tintColor?.adjustedForDisplay
|
||||
self.backgroundEffectView.backgroundColor = nil
|
||||
|
||||
if let tintColor, tintColor.isTooBright
|
||||
{
|
||||
let textVibrancyEffect = UIVibrancyEffect(blurEffect: .init(style: .systemChromeMaterialLight), style: .fill)
|
||||
self.vibrancyView.effect = textVibrancyEffect
|
||||
}
|
||||
else
|
||||
{
|
||||
// Thinner == more dull
|
||||
let textVibrancyEffect = UIVibrancyEffect(blurEffect: .init(style: .systemThinMaterialDark), style: .secondaryLabel)
|
||||
self.vibrancyView.effect = textVibrancyEffect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -17,6 +17,8 @@
|
||||
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
|
||||
<outlet property="buttonLabel" destination="Yd9-jw-faD" id="o7g-Gb-CIt"/>
|
||||
<outlet property="iconImageView" destination="avS-dx-4iy" id="TQs-Ej-gin"/>
|
||||
<outlet property="iconImageViewHeightConstraint" destination="6lU-H8-nEw" id="PSt-Xa-lQT"/>
|
||||
<outlet property="stackView" destination="d1T-UD-gWG" id="E7N-Zb-lm1"/>
|
||||
<outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/>
|
||||
<outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/>
|
||||
<outlet property="vibrancyView" destination="fC4-1V-iMn" id="PXE-2B-A7w"/>
|
||||
@@ -43,18 +45,18 @@
|
||||
</view>
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</visualEffectView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="14" y="14" width="60" height="60"/>
|
||||
<rect key="frame" x="16" y="14" width="60" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
|
||||
<constraint firstAttribute="width" secondItem="avS-dx-4iy" secondAttribute="height" multiplier="1:1" id="AYT-3g-wcV"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
|
||||
<rect key="frame" x="85" y="25.5" width="190" height="37.5"/>
|
||||
<rect key="frame" x="87" y="25.5" width="184" height="37.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="126" height="19.5"/>
|
||||
@@ -76,13 +78,13 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
|
||||
<rect key="frame" x="0.0" y="21.5" width="190" height="16"/>
|
||||
<rect key="frame" x="0.0" y="21.5" width="184" height="16"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="184" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="184" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -102,7 +104,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3ox-Q2-Rnd" userLabel="Button Stack View">
|
||||
<rect key="frame" x="286" y="28.5" width="77" height="31"/>
|
||||
<rect key="frame" x="282" y="28.5" width="77" height="31"/>
|
||||
<subviews>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yd9-jw-faD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="77" height="0.0"/>
|
||||
@@ -110,12 +112,12 @@
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" placeholderIntrinsicWidth="77" placeholderIntrinsicHeight="31" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="tVx-3G-dcu" secondAttribute="height" priority="999" id="Vbk-VH-5eU"/>
|
||||
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="77" id="eGc-Dk-QbL"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="FREE"/>
|
||||
@@ -133,7 +135,7 @@
|
||||
<constraint firstItem="bJL-Yw-i4u" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="h6T-q1-YV9"/>
|
||||
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="mlG-w3-Ly6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="rZk-be-tiI" secondAttribute="trailing" id="nEy-pm-Fcs"/>
|
||||
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="nJo-To-LmX"/>
|
||||
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" priority="999" id="nJo-To-LmX"/>
|
||||
<constraint firstItem="bJL-Yw-i4u" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="oLt-2z-QoJ"/>
|
||||
<constraint firstItem="rZk-be-tiI" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="pGD-Tl-U4c"/>
|
||||
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="q2p-0S-Nv5"/>
|
||||
|
||||
@@ -8,8 +8,23 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
extension AppIconImageView
|
||||
{
|
||||
enum Style
|
||||
{
|
||||
case icon
|
||||
case circular
|
||||
}
|
||||
}
|
||||
|
||||
class AppIconImageView: UIImageView
|
||||
{
|
||||
var style: Style = .icon {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
@@ -25,8 +40,16 @@ class AppIconImageView: UIImageView
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
// Based off of 60pt icon having 12pt radius.
|
||||
let radius = self.bounds.height / 5
|
||||
self.layer.cornerRadius = radius
|
||||
switch self.style
|
||||
{
|
||||
case .icon:
|
||||
// Based off of 60pt icon having 12pt radius.
|
||||
let radius = self.bounds.height / 5
|
||||
self.layer.cornerRadius = radius
|
||||
|
||||
case .circular:
|
||||
let radius = self.bounds.height / 2
|
||||
self.layer.cornerRadius = radius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class HeaderContentViewController<Header: UIView, Content: ScrollableContentView
|
||||
didSet {
|
||||
guard self.isViewLoaded else { return }
|
||||
|
||||
self.view.tintColor = self.tintColor
|
||||
self.view.tintColor = self.tintColor?.adjustedForDisplay
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
@@ -233,6 +233,8 @@ class HeaderContentViewController<Header: UIView, Content: ScrollableContentView
|
||||
|
||||
// Start with navigation bar hidden.
|
||||
self.hideNavigationBar()
|
||||
|
||||
self.view.tintColor = self.tintColor?.adjustedForDisplay
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
@@ -275,8 +277,21 @@ class HeaderContentViewController<Header: UIView, Content: ScrollableContentView
|
||||
self._shouldResetLayout = false
|
||||
}
|
||||
|
||||
//TODO: Dynamically calculate status bar height.
|
||||
let statusBarHeight = 20.0 //self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||
let statusBarHeight: Double
|
||||
|
||||
if let navigationController, navigationController.presentingViewController != nil, navigationController.modalPresentationStyle != .fullScreen
|
||||
{
|
||||
statusBarHeight = 20
|
||||
}
|
||||
else if let statusBarManager = self.view.window?.windowScene?.statusBarManager
|
||||
{
|
||||
statusBarHeight = statusBarManager.statusBarFrame.height
|
||||
}
|
||||
else
|
||||
{
|
||||
statusBarHeight = 0
|
||||
}
|
||||
|
||||
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||
|
||||
let inset = 15 as CGFloat
|
||||
@@ -496,7 +511,22 @@ private extension HeaderContentViewController
|
||||
|
||||
barAppearance.titleTextAttributes = [.foregroundColor: UIColor.clear]
|
||||
|
||||
let tintColor = isHidden ? UIColor.clear : self.tintColor ?? .altPrimary
|
||||
let dynamicColor = UIColor { traitCollection in
|
||||
var tintColor = self.tintColor ?? .altPrimary
|
||||
|
||||
if traitCollection.userInterfaceStyle == .dark && tintColor.isTooDark
|
||||
{
|
||||
tintColor = .white
|
||||
}
|
||||
else
|
||||
{
|
||||
tintColor = tintColor.adjustedForDisplay
|
||||
}
|
||||
|
||||
return tintColor
|
||||
}
|
||||
|
||||
let tintColor = isHidden ? UIColor.clear : dynamicColor
|
||||
barAppearance.configureWithTintColor(tintColor)
|
||||
|
||||
self.navigationItem.standardAppearance = barAppearance
|
||||
|
||||
@@ -14,6 +14,15 @@ extension PillButton
|
||||
static let contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 13, bottom: 7, trailing: 13)
|
||||
}
|
||||
|
||||
extension PillButton
|
||||
{
|
||||
enum Style
|
||||
{
|
||||
case pill
|
||||
case custom
|
||||
}
|
||||
}
|
||||
|
||||
class PillButton: UIButton
|
||||
{
|
||||
override var accessibilityValue: String? {
|
||||
@@ -55,6 +64,20 @@ class PillButton: UIButton
|
||||
}
|
||||
}
|
||||
|
||||
var style: Style = .pill {
|
||||
didSet {
|
||||
guard self.style != oldValue else { return }
|
||||
|
||||
if self.style == .custom
|
||||
{
|
||||
// Reset insets for custom style.
|
||||
self.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private let progressView = UIProgressView(progressViewStyle: .default)
|
||||
|
||||
private lazy var displayLink: CADisplayLink = {
|
||||
@@ -106,8 +129,6 @@ class PillButton: UIButton
|
||||
self.layer.masksToBounds = true
|
||||
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
|
||||
|
||||
self.contentEdgeInsets = UIEdgeInsets(top: Self.contentInsets.top, left: Self.contentInsets.leading, bottom: Self.contentInsets.bottom, right: Self.contentInsets.trailing)
|
||||
|
||||
self.activityIndicatorView.style = .medium
|
||||
self.activityIndicatorView.color = .white
|
||||
self.activityIndicatorView.isUserInteractionEnabled = false
|
||||
@@ -144,8 +165,16 @@ class PillButton: UIButton
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize
|
||||
{
|
||||
var size = super.sizeThatFits(size)
|
||||
size.width = max(size.width, PillButton.minimumSize.width)
|
||||
size.height = max(size.height, PillButton.minimumSize.height)
|
||||
|
||||
switch self.style
|
||||
{
|
||||
case .pill:
|
||||
// Enforce minimum size for pill style.
|
||||
size.width = max(size.width, PillButton.minimumSize.width)
|
||||
size.height = max(size.height, PillButton.minimumSize.height)
|
||||
|
||||
case .custom: break
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
@@ -170,6 +199,13 @@ private extension PillButton
|
||||
|
||||
// Update font after init because the original titleLabel is replaced.
|
||||
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
|
||||
|
||||
switch self.style
|
||||
{
|
||||
case .custom: break // Don't update insets in case client has updated them.
|
||||
case .pill:
|
||||
self.contentEdgeInsets = UIEdgeInsets(top: Self.contentInsets.top, left: Self.contentInsets.leading, bottom: Self.contentInsets.bottom, right: Self.contentInsets.trailing)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateCountdown()
|
||||
|
||||
@@ -12,3 +12,51 @@ extension UIColor
|
||||
{
|
||||
static let altBackground = UIColor(named: "Background")!
|
||||
}
|
||||
|
||||
extension UIColor
|
||||
{
|
||||
private static let brightnessMaxThreshold = 0.85
|
||||
private static let brightnessMinThreshold = 0.35
|
||||
|
||||
private static let saturationBrightnessThreshold = 0.5
|
||||
|
||||
var adjustedForDisplay: UIColor {
|
||||
guard self.isTooBright || self.isTooDark else { return self }
|
||||
|
||||
return UIColor { traits in
|
||||
var hue: CGFloat = 0
|
||||
var saturation: CGFloat = 0
|
||||
var brightness: CGFloat = 0
|
||||
guard self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) else { return self }
|
||||
|
||||
brightness = min(brightness, UIColor.brightnessMaxThreshold)
|
||||
|
||||
if traits.userInterfaceStyle == .dark
|
||||
{
|
||||
// Only raise brightness when in dark mode.
|
||||
brightness = max(brightness, UIColor.brightnessMinThreshold)
|
||||
}
|
||||
|
||||
let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
var isTooBright: Bool {
|
||||
var saturation: CGFloat = 0
|
||||
var brightness: CGFloat = 0
|
||||
|
||||
guard self.getHue(nil, saturation: &saturation, brightness: &brightness, alpha: nil) else { return false }
|
||||
|
||||
let isTooBright = (brightness >= UIColor.brightnessMaxThreshold && saturation <= UIColor.saturationBrightnessThreshold)
|
||||
return isTooBright
|
||||
}
|
||||
|
||||
var isTooDark: Bool {
|
||||
var brightness: CGFloat = 0
|
||||
guard self.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil) else { return false }
|
||||
|
||||
let isTooDark = brightness <= UIColor.brightnessMinThreshold
|
||||
return isTooDark
|
||||
}
|
||||
}
|
||||
|
||||
18
AltStore/Extensions/UIFontDescriptor+Bold.swift
Normal file
18
AltStore/Extensions/UIFontDescriptor+Bold.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIFontDescriptor+Bold.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/16/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIFontDescriptor
|
||||
{
|
||||
func bolded() -> UIFontDescriptor
|
||||
{
|
||||
guard let descriptor = self.withSymbolicTraits(.traitBold) else { return self }
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ extension AppManager
|
||||
{
|
||||
static let didFetchSourceNotification = Notification.Name("io.altstore.AppManager.didFetchSource")
|
||||
static let didUpdatePatronsNotification = Notification.Name("io.altstore.AppManager.didUpdatePatrons")
|
||||
static let didAddSourceNotification = Notification.Name("io.altstore.AppManager.didAddSource")
|
||||
static let didRemoveSourceNotification = Notification.Name("io.altstore.AppManager.didRemoveSource")
|
||||
|
||||
static let expirationWarningNotificationID = "altstore-expiration-warning"
|
||||
static let enableJITResultNotificationID = "altstore-enable-jit"
|
||||
@@ -344,7 +346,7 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func add(@AsyncManaged _ source: Source, message: String? = nil, presentingViewController: UIViewController) async throws
|
||||
func add(@AsyncManaged _ source: Source, message: String? = NSLocalizedString("Make sure to only add sources that you trust.", comment: ""), presentingViewController: UIViewController) async throws
|
||||
{
|
||||
let (sourceName, sourceURL) = await $source.perform { ($0.name, $0.sourceURL) }
|
||||
|
||||
@@ -352,9 +354,8 @@ extension AppManager
|
||||
async let fetchedSource = try await self.fetchSource(sourceURL: sourceURL, managedObjectContext: context) // Fetch source async while showing alert.
|
||||
|
||||
let title = String(format: NSLocalizedString("Would you like to add the source “%@”?", comment: ""), sourceName)
|
||||
let message = message ?? NSLocalizedString("Make sure to only add sources that you trust.", comment: "")
|
||||
let action = await UIAlertAction(title: NSLocalizedString("Add Source", comment: ""), style: .default)
|
||||
try await presentingViewController.presentConfirmationAlert(title: title, message: message, primaryAction: action)
|
||||
try await presentingViewController.presentConfirmationAlert(title: title, message: message ?? "", primaryAction: action)
|
||||
|
||||
// Wait for fetch to finish before saving context to make
|
||||
// sure there isn't already a source with this identifier.
|
||||
@@ -366,6 +367,8 @@ extension AppManager
|
||||
try await context.performAsync {
|
||||
try context.save()
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppManager.didAddSourceNotification, object: source)
|
||||
}
|
||||
|
||||
func remove(@AsyncManaged _ source: Source, presentingViewController: UIViewController) async throws
|
||||
@@ -388,16 +391,19 @@ extension AppManager
|
||||
context.delete(source)
|
||||
try context.save()
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppManager.didRemoveSourceNotification, object: source)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
@available(*, renamed: "fetchSource(sourceURL:managedObjectContext:)")
|
||||
@discardableResult
|
||||
func fetchSource(sourceURL: URL,
|
||||
managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(),
|
||||
dependencies: [Foundation.Operation] = [],
|
||||
completionHandler: @escaping (Result<Source, Error>) -> Void)
|
||||
completionHandler: @escaping (Result<Source, Error>) -> Void) -> FetchSourceOperation
|
||||
{
|
||||
let fetchSourceOperation = FetchSourceOperation(sourceURL: sourceURL, managedObjectContext: managedObjectContext)
|
||||
fetchSourceOperation.resultHandler = { (result) in
|
||||
@@ -417,6 +423,8 @@ extension AppManager
|
||||
}
|
||||
|
||||
self.run([fetchSourceOperation], context: nil)
|
||||
|
||||
return fetchSourceOperation
|
||||
}
|
||||
|
||||
@available(*, renamed: "fetchSources")
|
||||
|
||||
@@ -62,13 +62,6 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing
|
||||
// Cache
|
||||
private var cachedUpdateSizes = [String: CGSize]()
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
@@ -236,7 +229,7 @@ private extension MyAppsViewController
|
||||
|
||||
cell.bannerView.configure(for: app)
|
||||
|
||||
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter)
|
||||
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date)
|
||||
cell.bannerView.subtitleLabel.text = versionDate
|
||||
|
||||
let appName: String
|
||||
|
||||
@@ -85,6 +85,8 @@ class NewsViewController: UICollectionViewController, PeekPopPreviewing
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.collectionView.backgroundColor = .altBackground
|
||||
|
||||
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
|
||||
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@@ -133,11 +135,6 @@ class NewsViewController: UICollectionViewController, PeekPopPreviewing
|
||||
self.collectionView.contentInset.bottom = 20
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
self.fetchSource()
|
||||
}
|
||||
}
|
||||
|
||||
private extension NewsViewController
|
||||
|
||||
@@ -23,6 +23,7 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
private var source: Source?
|
||||
|
||||
private let session: URLSession
|
||||
private weak var dataTask: URLSessionDataTask?
|
||||
|
||||
private lazy var dateFormatter: ISO8601DateFormatter = {
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
@@ -54,6 +55,13 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
self.session = URLSession(configuration: configuration)
|
||||
}
|
||||
|
||||
override func cancel()
|
||||
{
|
||||
super.cancel()
|
||||
|
||||
self.dataTask?.cancel()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
@@ -144,6 +152,8 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
self.progress.addChild(dataTask.progress, withPendingUnitCount: 1)
|
||||
|
||||
dataTask.resume()
|
||||
|
||||
self.dataTask = dataTask
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
private extension URL
|
||||
{
|
||||
#if STAGING
|
||||
@@ -66,7 +68,7 @@ class UpdateKnownSourcesOperation: ResultOperation<([KnownSource], [KnownSource]
|
||||
let sources = (trusted: response.trusted ?? [], blocked: response.blocked ?? [])
|
||||
|
||||
// Cache sources
|
||||
UserDefaults.shared.trustedSources = sources.trusted
|
||||
UserDefaults.shared.recommendedSources = sources.trusted
|
||||
UserDefaults.shared.blockedSources = sources.blocked
|
||||
|
||||
self.finish(.success(sources))
|
||||
|
||||
@@ -96,11 +96,11 @@ class VerifyAppOperation: ResultOperation<Void>
|
||||
throw error
|
||||
#endif
|
||||
|
||||
if let trustedSources = UserDefaults.shared.trustedSources, let sourceID = await self.context.$appVersion.sourceID
|
||||
if let recommendedSources = UserDefaults.shared.recommendedSources, let sourceID = await self.context.$appVersion.sourceID
|
||||
{
|
||||
let isTrusted = trustedSources.contains { $0.identifier == sourceID }
|
||||
guard !isTrusted else {
|
||||
// Don't enforce permission checking for Trusted Sources while 2.0 is in beta.
|
||||
let isRecommended = recommendedSources.contains { $0.identifier == sourceID }
|
||||
guard !isRecommended else {
|
||||
// Don't enforce permission checking for Recommended Sources while 2.0 is in beta.
|
||||
return self.finish(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 436 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
816
AltStore/Sources/AddSourceViewController.swift
Normal file
816
AltStore/Sources/AddSourceViewController.swift
Normal file
@@ -0,0 +1,816 @@
|
||||
//
|
||||
// AddSourceViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/26/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
private extension UIAction.Identifier
|
||||
{
|
||||
static let addSource = UIAction.Identifier("io.altstore.AddSource")
|
||||
}
|
||||
|
||||
private typealias SourcePreviewResult = (sourceURL: URL, result: Result<Managed<Source>, Error>)
|
||||
|
||||
extension AddSourceViewController
|
||||
{
|
||||
private enum Section: Int
|
||||
{
|
||||
case add
|
||||
case preview
|
||||
case recommended
|
||||
}
|
||||
|
||||
private enum ReuseID: String
|
||||
{
|
||||
case textFieldCell = "TextFieldCell"
|
||||
case placeholderFooter = "PlaceholderFooter"
|
||||
}
|
||||
|
||||
private class ViewModel: ObservableObject
|
||||
{
|
||||
/* Pipeline */
|
||||
@Published
|
||||
var sourceAddress: String = ""
|
||||
|
||||
@Published
|
||||
var sourceURL: URL?
|
||||
|
||||
@Published
|
||||
var sourcePreviewResult: SourcePreviewResult?
|
||||
|
||||
|
||||
/* State */
|
||||
@Published
|
||||
var isLoadingPreview: Bool = false
|
||||
|
||||
@Published
|
||||
var isShowingPreviewStatus: Bool = false
|
||||
}
|
||||
}
|
||||
|
||||
class AddSourceViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var addSourceDataSource = self.makeAddSourceDataSource()
|
||||
private lazy var sourcePreviewDataSource = self.makeSourcePreviewDataSource()
|
||||
private lazy var recommendedSourcesDataSource = self.makeRecommendedSourcesDataSource()
|
||||
|
||||
private var fetchRecommendedSourcesOperation: UpdateKnownSourcesOperation?
|
||||
private var fetchRecommendedSourcesResult: Result<Void, Error>?
|
||||
private var _fetchRecommendedSourcesContext: NSManagedObjectContext?
|
||||
|
||||
private let viewModel = ViewModel()
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.navigationController?.isModalInPresentation = true
|
||||
self.navigationController?.view.tintColor = .altPrimary
|
||||
|
||||
let layout = self.makeLayout()
|
||||
self.collectionView.collectionViewLayout = layout
|
||||
|
||||
self.collectionView.register(AppBannerCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
self.collectionView.register(AddSourceTextFieldCell.self, forCellWithReuseIdentifier: ReuseID.textFieldCell.rawValue)
|
||||
|
||||
self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: UICollectionView.elementKindSectionHeader)
|
||||
self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: UICollectionView.elementKindSectionFooter)
|
||||
self.collectionView.register(PlaceholderCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: ReuseID.placeholderFooter.rawValue)
|
||||
|
||||
self.collectionView.backgroundColor = .altBackground
|
||||
self.collectionView.keyboardDismissMode = .onDrag
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.startPipeline()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if self.fetchRecommendedSourcesOperation == nil
|
||||
{
|
||||
self.fetchRecommendedSources()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AddSourceViewController
|
||||
{
|
||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||
{
|
||||
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
|
||||
layoutConfig.contentInsetsReference = .safeArea
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
guard let self, let section = Section(rawValue: sectionIndex) else { return nil }
|
||||
switch section
|
||||
{
|
||||
case .add:
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
|
||||
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(20))
|
||||
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.interGroupSpacing = 10
|
||||
layoutSection.boundarySupplementaryItems = [headerItem]
|
||||
return layoutSection
|
||||
|
||||
case .preview:
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .clear
|
||||
|
||||
if self.viewModel.sourceURL != nil && self.viewModel.isShowingPreviewStatus
|
||||
{
|
||||
switch self.viewModel.sourcePreviewResult
|
||||
{
|
||||
case (_, .success)?: configuration.footerMode = .none
|
||||
case (_, .failure)?: configuration.footerMode = .supplementary
|
||||
case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
||||
default: configuration.footerMode = .none
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
configuration.footerMode = .none
|
||||
}
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
|
||||
return layoutSection
|
||||
|
||||
case .recommended:
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .clear
|
||||
|
||||
switch self.fetchRecommendedSourcesResult
|
||||
{
|
||||
case nil:
|
||||
configuration.headerMode = .supplementary
|
||||
configuration.footerMode = .supplementary
|
||||
|
||||
case .failure: configuration.footerMode = .supplementary
|
||||
case .success: configuration.headerMode = .supplementary
|
||||
}
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
|
||||
return layoutSection
|
||||
}
|
||||
}, configuration: layoutConfig)
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<Source, UIImage>
|
||||
{
|
||||
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<Source, UIImage>(dataSources: [self.addSourceDataSource,
|
||||
self.sourcePreviewDataSource,
|
||||
self.recommendedSourcesDataSource])
|
||||
dataSource.proxy = self
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeAddSourceDataSource() -> RSTDynamicCollectionViewPrefetchingDataSource<Source, UIImage>
|
||||
{
|
||||
let dataSource = RSTDynamicCollectionViewPrefetchingDataSource<Source, UIImage>()
|
||||
dataSource.numberOfSectionsHandler = { 1 }
|
||||
dataSource.numberOfItemsHandler = { _ in 1 }
|
||||
dataSource.cellIdentifierHandler = { _ in ReuseID.textFieldCell.rawValue }
|
||||
dataSource.cellConfigurationHandler = { [weak self] cell, source, indexPath in
|
||||
guard let self else { return }
|
||||
|
||||
let cell = cell as! AddSourceTextFieldCell
|
||||
cell.contentView.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.contentView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.textField.delegate = self
|
||||
|
||||
cell.setNeedsLayout()
|
||||
cell.layoutIfNeeded()
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.map { ($0.object as? UITextField)?.text ?? "" }
|
||||
.assign(to: &self.viewModel.$sourceAddress)
|
||||
|
||||
// Results in memory leak
|
||||
// .assign(to: \.viewModel.sourceAddress, on: self)
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeSourcePreviewDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<Source, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<Source, UIImage>(items: [])
|
||||
dataSource.cellConfigurationHandler = { [weak self] cell, source, indexPath in
|
||||
guard let self else { return }
|
||||
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
self.configure(cell, with: source)
|
||||
}
|
||||
dataSource.prefetchHandler = { (source, indexPath, completionHandler) in
|
||||
guard let imageURL = source.effectiveIconURL else { return nil }
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL, progress: nil) { result in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success(let response): completionHandler(response.image, nil)
|
||||
case .failure(let error): completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeRecommendedSourcesDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<Source, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<Source, UIImage>(items: [])
|
||||
dataSource.cellConfigurationHandler = { [weak self] cell, source, indexPath in
|
||||
guard let self else { return }
|
||||
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
self.configure(cell, with: source)
|
||||
}
|
||||
dataSource.prefetchHandler = { (source, indexPath, completionHandler) in
|
||||
guard let imageURL = source.effectiveIconURL else { return nil }
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL, progress: nil) { result in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success(let response): completionHandler(response.image, nil)
|
||||
case .failure(let error): completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension AddSourceViewController
|
||||
{
|
||||
func startPipeline()
|
||||
{
|
||||
/* Pipeline */
|
||||
|
||||
// Map UITextField text -> URL
|
||||
self.viewModel.$sourceAddress
|
||||
.map { [weak self] in self?.sourceURL(from: $0) }
|
||||
.assign(to: &self.viewModel.$sourceURL)
|
||||
|
||||
let showPreviewStatusPublisher = self.viewModel.$isShowingPreviewStatus
|
||||
.filter { $0 == true }
|
||||
|
||||
let sourceURLPublisher = self.viewModel.$sourceURL
|
||||
.removeDuplicates()
|
||||
.debounce(for: 0.2, scheduler: RunLoop.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.map { [weak self] sourceURL in
|
||||
// Only set sourcePreviewResult to nil if sourceURL actually changes.
|
||||
self?.viewModel.sourcePreviewResult = nil
|
||||
return sourceURL
|
||||
}
|
||||
|
||||
// Map URL -> Source Preview
|
||||
Publishers.CombineLatest(sourceURLPublisher, showPreviewStatusPublisher.prepend(false))
|
||||
.receive(on: RunLoop.main)
|
||||
.map { $0.0 }
|
||||
.compactMap { [weak self] (sourceURL: URL?) -> AnyPublisher<SourcePreviewResult?, Never>? in
|
||||
guard let self else { return nil }
|
||||
|
||||
guard let sourceURL else {
|
||||
// Unlike above guard, this continues the pipeline with nil value.
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
self.viewModel.isLoadingPreview = true
|
||||
return self.fetchSourcePreview(sourceURL: sourceURL).eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest() // Cancels previous publisher
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] sourcePreviewResult in
|
||||
self?.viewModel.isLoadingPreview = false
|
||||
self?.viewModel.sourcePreviewResult = sourcePreviewResult
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
|
||||
/* Update UI */
|
||||
|
||||
Publishers.CombineLatest(self.viewModel.$isLoadingPreview.removeDuplicates(),
|
||||
self.viewModel.$isShowingPreviewStatus.removeDuplicates())
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
// @Published fires _before_ property is updated, so wait until next run loop.
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.performBatchUpdates {
|
||||
let indexPath = IndexPath(item: 0, section: Section.preview.rawValue)
|
||||
|
||||
if let footerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath) as? PlaceholderCollectionReusableView
|
||||
{
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
||||
}
|
||||
|
||||
let context = UICollectionViewLayoutInvalidationContext()
|
||||
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter, at: [indexPath])
|
||||
self.collectionView.collectionViewLayout.invalidateLayout(with: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
self.viewModel.$sourcePreviewResult
|
||||
.map { $0?.1 }
|
||||
.map { result -> Managed<Source>? in
|
||||
switch result
|
||||
{
|
||||
case .success(let source): return source
|
||||
case .failure, nil: return nil
|
||||
}
|
||||
}
|
||||
.removeDuplicates { (sourceA: Managed<Source>?, sourceB: Managed<Source>?) in
|
||||
sourceA?.identifier == sourceB?.identifier
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] source in
|
||||
self?.updateSourcePreview(for: source?.wrappedValue)
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
let addPublisher = NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification)
|
||||
let removePublisher = NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
||||
Publishers.Merge(addPublisher, removePublisher)
|
||||
.compactMap { notification -> String? in
|
||||
guard let source = notification.object as? Source,
|
||||
let context = source.managedObjectContext
|
||||
else { return nil }
|
||||
|
||||
let sourceID = context.performAndWait { source.identifier }
|
||||
return sourceID
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.compactMap { [dataSource = recommendedSourcesDataSource] sourceID -> IndexPath? in
|
||||
guard let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID }) else { return nil }
|
||||
|
||||
let indexPath = IndexPath(item: index, section: Section.recommended.rawValue)
|
||||
return indexPath
|
||||
}
|
||||
.sink { [weak self] indexPath in
|
||||
// Added or removed a recommended source, so make sure to update its state.
|
||||
self?.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
||||
func sourceURL(from address: String) -> URL?
|
||||
{
|
||||
guard let sourceURL = URL(string: address) else { return nil }
|
||||
|
||||
// URLs without hosts are OK (e.g. localhost:8000)
|
||||
// guard sourceURL.host != nil else { return }
|
||||
|
||||
guard let scheme = sourceURL.scheme else {
|
||||
let sanitizedURL = URL(string: "https://" + address)
|
||||
return sanitizedURL
|
||||
}
|
||||
|
||||
guard scheme.lowercased() != "localhost" else {
|
||||
let sanitizedURL = URL(string: "http://" + address)
|
||||
return sanitizedURL
|
||||
}
|
||||
|
||||
return sourceURL
|
||||
}
|
||||
|
||||
func fetchSourcePreview(sourceURL: URL) -> some Publisher<SourcePreviewResult?, Never>
|
||||
{
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
|
||||
var fetchOperation: FetchSourceOperation?
|
||||
return Future<Source, Error> { promise in
|
||||
fetchOperation = AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
|
||||
promise(result)
|
||||
}
|
||||
}
|
||||
.map { source in
|
||||
let result = SourcePreviewResult(sourceURL, .success(Managed(wrappedValue: source)))
|
||||
return result
|
||||
}
|
||||
.catch { error in
|
||||
print("Failed to fetch source for URL \(sourceURL).", error.localizedDescription)
|
||||
|
||||
let result = SourcePreviewResult(sourceURL, .failure(error))
|
||||
return Just<SourcePreviewResult?>(result)
|
||||
}
|
||||
.handleEvents(receiveCancel: {
|
||||
fetchOperation?.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
func updateSourcePreview(for source: Source?)
|
||||
{
|
||||
let items = [source].compactMap { $0 }
|
||||
|
||||
// Have to provide changes in terms of sourcePreviewDataSource.
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
|
||||
if !items.isEmpty && self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: indexPath)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
else if items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .delete, currentIndexPath: indexPath, destinationIndexPath: nil)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
else if !items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .update, currentIndexPath: indexPath, destinationIndexPath: indexPath)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
|
||||
if source == nil
|
||||
{
|
||||
self.collectionView.reloadSections([Section.preview.rawValue])
|
||||
}
|
||||
else
|
||||
{
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AddSourceViewController
|
||||
{
|
||||
func configure(_ cell: AppBannerCollectionViewCell, with source: Source)
|
||||
{
|
||||
cell.bannerView.style = .source
|
||||
cell.layoutMargins.top = 5
|
||||
cell.layoutMargins.bottom = 5
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
cell.contentView.backgroundColor = .altBackground
|
||||
|
||||
cell.bannerView.configure(for: source)
|
||||
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
let config = UIImage.SymbolConfiguration(scale: .medium)
|
||||
let image = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
cell.bannerView.button.setImage(image, for: .normal)
|
||||
cell.bannerView.button.setImage(image, for: .highlighted)
|
||||
cell.bannerView.button.setTitle(nil, for: .normal)
|
||||
cell.bannerView.button.imageView?.contentMode = .scaleAspectFit
|
||||
cell.bannerView.button.contentHorizontalAlignment = .fill // Fill entire button with imageView
|
||||
cell.bannerView.button.contentVerticalAlignment = .fill
|
||||
cell.bannerView.button.contentEdgeInsets = .zero
|
||||
cell.bannerView.button.tintColor = .clear
|
||||
cell.bannerView.button.isHidden = false
|
||||
|
||||
let action = UIAction(identifier: .addSource) { [weak self] _ in
|
||||
self?.add(source)
|
||||
}
|
||||
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
||||
|
||||
Task<Void, Never>(priority: .userInitiated) {
|
||||
do
|
||||
{
|
||||
let isAdded = try await source.isAdded
|
||||
if isAdded
|
||||
{
|
||||
cell.bannerView.button.isHidden = true
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to determine if source is added.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResult: SourcePreviewResult?)
|
||||
{
|
||||
footerView.placeholderView.stackView.isLayoutMarginsRelativeArrangement = false
|
||||
|
||||
footerView.placeholderView.textLabel.textColor = .secondaryLabel
|
||||
footerView.placeholderView.textLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
footerView.placeholderView.textLabel.textAlignment = .center
|
||||
|
||||
footerView.placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
switch sourcePreviewResult
|
||||
{
|
||||
case (let sourceURL, .failure(let previewError))? where self.viewModel.sourceURL == sourceURL && !self.viewModel.isLoadingPreview:
|
||||
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
||||
|
||||
footerView.placeholderView.textLabel.text = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
||||
footerView.placeholderView.textLabel.isHidden = false
|
||||
|
||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||
|
||||
default:
|
||||
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
||||
|
||||
footerView.placeholderView.textLabel.text = nil
|
||||
footerView.placeholderView.textLabel.isHidden = true
|
||||
|
||||
footerView.placeholderView.activityIndicatorView.startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRecommendedSources()
|
||||
{
|
||||
// Closure instead of local function so we can capture `self` weakly.
|
||||
let finish: (Result<[Source], Error>) -> Void = { [weak self] result in
|
||||
self?.fetchRecommendedSourcesResult = result.map { _ in () }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
do
|
||||
{
|
||||
let sources = try result.get()
|
||||
print("Fetched recommended sources:", sources.map { $0.identifier })
|
||||
|
||||
let sectionUpdate = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||
self?.recommendedSourcesDataSource.setItems(sources, with: [sectionUpdate])
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error fetching recommended sources:", error)
|
||||
|
||||
let sectionUpdate = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||
self?.recommendedSourcesDataSource.setItems([], with: [sectionUpdate])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.fetchRecommendedSourcesOperation = AppManager.shared.updateKnownSources { [weak self] result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success((let trustedSources, _)):
|
||||
|
||||
// Don't show sources without a sourceURL.
|
||||
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
|
||||
|
||||
// This context is never saved, but keeps the managed sources alive.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
self?._fetchRecommendedSourcesContext = context
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
var sourcesByURL = [URL: Source]()
|
||||
var fetchError: Error?
|
||||
|
||||
for sourceURL in featuredSourceURLs
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
|
||||
// Serialize access to sourcesByURL.
|
||||
context.performAndWait {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to load recommended source \(sourceURL.absoluteString):", error.localizedDescription)
|
||||
fetchError = error
|
||||
|
||||
case .success(let source): sourcesByURL[source.sourceURL] = source
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
|
||||
|
||||
if let error = fetchError, sources.isEmpty
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
finish(.success(sources))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func add(@AsyncManaged _ source: Source)
|
||||
{
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
let isRecommended = await $source.isRecommended
|
||||
if isRecommended
|
||||
{
|
||||
try await AppManager.shared.add(source, message: nil, presentingViewController: self)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use default message
|
||||
try await AppManager.shared.add(source, presentingViewController: self)
|
||||
}
|
||||
|
||||
self.dismiss()
|
||||
|
||||
}
|
||||
catch is CancellationError {}
|
||||
catch
|
||||
{
|
||||
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss()
|
||||
{
|
||||
guard
|
||||
let navigationController = self.navigationController, let presentingViewController = navigationController.presentingViewController
|
||||
else { return }
|
||||
|
||||
presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AddSourceViewController
|
||||
{
|
||||
@IBSegueAction
|
||||
func makeSourceDetailViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
|
||||
{
|
||||
guard let source = sender as? Source else { return nil }
|
||||
|
||||
let sourceDetailViewController = SourceDetailViewController(source: source, coder: coder)
|
||||
sourceDetailViewController?.addedSourceHandler = { [weak self] _ in
|
||||
self?.dismiss()
|
||||
}
|
||||
return sourceDetailViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension AddSourceViewController
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
{
|
||||
guard Section(rawValue: indexPath.section) != .add else { return }
|
||||
|
||||
let source = self.dataSource.item(at: indexPath)
|
||||
self.performSegue(withIdentifier: "showSourceDetails", sender: source)
|
||||
}
|
||||
}
|
||||
|
||||
extension AddSourceViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
{
|
||||
let section = Section(rawValue: indexPath.section)!
|
||||
switch (section, kind)
|
||||
{
|
||||
case (.add, UICollectionView.elementKindSectionHeader):
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! UICollectionViewListCell
|
||||
|
||||
var configuation = UIListContentConfiguration.cell()
|
||||
configuation.text = NSLocalizedString("Enter a source's URL below, or add one of the recommended sources.", comment: "")
|
||||
configuation.textProperties.color = .secondaryLabel
|
||||
|
||||
headerView.contentConfiguration = configuation
|
||||
|
||||
return headerView
|
||||
|
||||
case (.preview, UICollectionView.elementKindSectionFooter):
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseID.placeholderFooter.rawValue, for: indexPath) as! PlaceholderCollectionReusableView
|
||||
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
||||
|
||||
return footerView
|
||||
|
||||
case (.recommended, UICollectionView.elementKindSectionHeader):
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! UICollectionViewListCell
|
||||
|
||||
var configuation = UIListContentConfiguration.groupedHeader()
|
||||
configuation.text = NSLocalizedString("Recommended Sources", comment: "")
|
||||
configuation.textProperties.color = .secondaryLabel
|
||||
|
||||
headerView.contentConfiguration = configuation
|
||||
|
||||
return headerView
|
||||
|
||||
case (.recommended, UICollectionView.elementKindSectionFooter):
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseID.placeholderFooter.rawValue, for: indexPath) as! PlaceholderCollectionReusableView
|
||||
|
||||
footerView.placeholderView.stackView.spacing = 15
|
||||
footerView.placeholderView.stackView.directionalLayoutMargins.top = 20
|
||||
footerView.placeholderView.stackView.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
if let result = self.fetchRecommendedSourcesResult, case .failure(let error) = result
|
||||
{
|
||||
footerView.placeholderView.textLabel.isHidden = false
|
||||
footerView.placeholderView.textLabel.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||
footerView.placeholderView.textLabel.text = NSLocalizedString("Unable to Load Recommended Sources", comment: "")
|
||||
|
||||
footerView.placeholderView.detailTextLabel.isHidden = false
|
||||
footerView.placeholderView.detailTextLabel.text = error.localizedDescription
|
||||
|
||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.placeholderView.textLabel.isHidden = true
|
||||
footerView.placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
footerView.placeholderView.activityIndicatorView.startAnimating()
|
||||
}
|
||||
|
||||
return footerView
|
||||
|
||||
default: fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AddSourceViewController: UITextFieldDelegate
|
||||
{
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
|
||||
{
|
||||
self.viewModel.isShowingPreviewStatus = false
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool
|
||||
{
|
||||
textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
func textFieldDidEndEditing(_ textField: UITextField)
|
||||
{
|
||||
self.viewModel.isShowingPreviewStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
#Preview(traits: .portrait) {
|
||||
DatabaseManager.shared.startForPreview()
|
||||
|
||||
let storyboard = UIStoryboard(name: "Sources", bundle: .main)
|
||||
|
||||
let addSourceNavigationController = storyboard.instantiateViewController(withIdentifier: "addSourceNavigationController")
|
||||
return addSourceNavigationController
|
||||
}
|
||||
93
AltStore/Sources/Components/AddSourceTextFieldCell.swift
Normal file
93
AltStore/Sources/Components/AddSourceTextFieldCell.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// AddSourceTextFieldCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/17/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class AddSourceTextFieldCell: UICollectionViewCell
|
||||
{
|
||||
let textField: UITextField
|
||||
|
||||
private let backgroundEffectView: UIVisualEffectView
|
||||
private let imageView: UIImageView
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
self.textField = UITextField(frame: frame)
|
||||
self.textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textField.placeholder = "apps.altstore.io"
|
||||
self.textField.textContentType = .URL
|
||||
self.textField.keyboardType = .URL
|
||||
self.textField.returnKeyType = .done
|
||||
self.textField.autocapitalizationType = .none
|
||||
self.textField.autocorrectionType = .no
|
||||
self.textField.spellCheckingType = .no
|
||||
self.textField.enablesReturnKeyAutomatically = true
|
||||
self.textField.tintColor = .altPrimary
|
||||
self.textField.textColor = UIColor { traits in
|
||||
if traits.userInterfaceStyle == .dark
|
||||
{
|
||||
//TODO: Change once we update UIColor.altPrimary to match 2.0 icon.
|
||||
return UIColor(resource: .gradientTop)
|
||||
}
|
||||
else
|
||||
{
|
||||
return UIColor.altPrimary
|
||||
}
|
||||
}
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
|
||||
self.backgroundEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
self.backgroundEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.backgroundEffectView.clipsToBounds = true
|
||||
self.backgroundEffectView.backgroundColor = .altPrimary
|
||||
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold)
|
||||
let image = UIImage(systemName: "link", withConfiguration: config)?.withRenderingMode(.alwaysTemplate)
|
||||
self.imageView = UIImageView(image: image)
|
||||
self.imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.imageView.contentMode = .center
|
||||
self.imageView.tintColor = .altPrimary
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.backgroundEffectView.contentView.addSubview(self.imageView)
|
||||
self.backgroundEffectView.contentView.addSubview(self.textField)
|
||||
self.contentView.addSubview(self.backgroundEffectView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.backgroundEffectView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
|
||||
self.backgroundEffectView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
|
||||
self.backgroundEffectView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
|
||||
self.backgroundEffectView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
|
||||
|
||||
self.imageView.widthAnchor.constraint(equalToConstant: 44),
|
||||
self.imageView.heightAnchor.constraint(equalToConstant: 44),
|
||||
self.imageView.centerYAnchor.constraint(equalTo: self.backgroundEffectView.centerYAnchor),
|
||||
|
||||
self.textField.topAnchor.constraint(equalTo: self.backgroundEffectView.topAnchor, constant: 15),
|
||||
self.textField.bottomAnchor.constraint(equalTo: self.backgroundEffectView.bottomAnchor, constant: -15),
|
||||
self.textField.trailingAnchor.constraint(equalTo: self.backgroundEffectView.trailingAnchor, constant: -15),
|
||||
|
||||
self.imageView.leadingAnchor.constraint(equalTo: self.backgroundEffectView.leadingAnchor, constant: 15),
|
||||
self.textField.leadingAnchor.constraint(equalToSystemSpacingAfter: self.imageView.trailingAnchor, multiplier: 1.0),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
self.backgroundEffectView.layer.cornerRadius = self.backgroundEffectView.bounds.midY
|
||||
}
|
||||
}
|
||||
@@ -87,3 +87,27 @@ class TextViewCollectionViewCell: UICollectionViewCell
|
||||
self.textView.textContainerInset.right = self.contentView.layoutMargins.right
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderCollectionReusableView: UICollectionReusableView
|
||||
{
|
||||
let placeholderView: RSTPlaceholderView
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
self.placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
self.placeholderView.activityIndicatorView.style = .medium
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.placeholderView, pinningEdgesWith: .zero)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.placeholderView.stackView.topAnchor.constraint(equalTo: self.placeholderView.topAnchor),
|
||||
self.placeholderView.stackView.bottomAnchor.constraint(equalTo: self.placeholderView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class SourceDetailContentViewController: UICollectionViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.tintColor = self.source.effectiveTintColor
|
||||
self.collectionView.backgroundColor = .altBackground
|
||||
|
||||
let collectionViewLayout = self.makeLayout(source: self.source)
|
||||
self.collectionView.collectionViewLayout = collectionViewLayout
|
||||
@@ -98,11 +98,13 @@ private extension SourceDetailContentViewController
|
||||
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.
|
||||
// Underestimate height to prevent jumping size abruptly.
|
||||
let heightDimension: NSCollectionLayoutDimension = if #available(iOS 17, *) { .uniformAcrossSiblings(estimate: 50) } else { .estimated(50) }
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: heightDimension)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupWidth = layoutEnvironment.container.contentSize.width - sectionInset * 2
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: .estimated(50))
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: heightDimension)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
|
||||
let buttonSize = NSCollectionLayoutSize(widthDimension: .estimated(60), heightDimension: .estimated(20))
|
||||
@@ -221,6 +223,7 @@ private extension SourceDetailContentViewController
|
||||
// For some reason, setting cell.layoutMargins = .zero does not update cell.contentView.layoutMargins.
|
||||
cell.layoutMargins = .zero
|
||||
cell.contentView.layoutMargins = .zero
|
||||
cell.contentView.backgroundColor = .altBackground
|
||||
|
||||
cell.bannerView.configure(for: storeApp)
|
||||
|
||||
@@ -300,6 +303,7 @@ private extension SourceDetailContentViewController
|
||||
cell.contentView.layoutMargins = .zero // Fixes incorrect margins if not initially on screen.
|
||||
cell.textView.text = source.localizedDescription
|
||||
cell.textView.isCollapsed = false
|
||||
cell.textView.backgroundColor = .altBackground
|
||||
}
|
||||
|
||||
return dataSource
|
||||
|
||||
@@ -31,6 +31,30 @@ extension SourceDetailViewController
|
||||
{
|
||||
self.source = source
|
||||
|
||||
let sourceID = source.identifier
|
||||
|
||||
let addedPublisher = NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification, object: nil)
|
||||
let removedPublisher = NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification, object: nil)
|
||||
|
||||
Publishers.Merge(addedPublisher, removedPublisher)
|
||||
.filter { notification -> Bool in
|
||||
guard let source = notification.object as? Source, let context = source.managedObjectContext else { return false }
|
||||
|
||||
let updatedSourceID = context.performAndWait { source.identifier }
|
||||
return sourceID == updatedSourceID
|
||||
}
|
||||
.compactMap { notification in
|
||||
switch notification.name
|
||||
{
|
||||
case AppManager.didAddSourceNotification: return true
|
||||
case AppManager.didRemoveSourceNotification: return false
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
.filter { $0 != nil }
|
||||
.receive(on: RunLoop.main)
|
||||
.assign(to: &self.$isSourceAdded)
|
||||
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
@@ -49,6 +73,8 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
||||
{
|
||||
@Managed private(set) var source: Source
|
||||
|
||||
var addedSourceHandler: ((Source) -> Void)?
|
||||
|
||||
private let viewModel: ViewModel
|
||||
|
||||
private var addButton: VibrantButton!
|
||||
@@ -195,7 +221,7 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
||||
|
||||
case false?:
|
||||
title = NSLocalizedString("ADD", comment: "")
|
||||
self.navigationBarButton.tintColor = self.source.effectiveTintColor ?? .altPrimary
|
||||
self.navigationBarButton.tintColor = self.source.effectiveTintColor?.adjustedForDisplay ?? .altPrimary
|
||||
|
||||
self.addButton.isHidden = false
|
||||
self.navigationBarButton.isHidden = false
|
||||
@@ -207,9 +233,13 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
||||
self.navigationBarButton.isHidden = true
|
||||
}
|
||||
|
||||
if self.addButton.title != title
|
||||
if title != self.addButton.title
|
||||
{
|
||||
self.addButton.title = title
|
||||
}
|
||||
|
||||
if title != self.navigationBarButton.title(for: .normal) && !self.navigationBarButton.isIndicatingActivity
|
||||
{
|
||||
self.navigationBarButton.setTitle(title, for: .normal)
|
||||
}
|
||||
|
||||
@@ -224,7 +254,6 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
||||
self.viewModel.isAddingSource = true
|
||||
|
||||
Task<Void, Never> { /* @MainActor in */ // Already on MainActor, even though this function wasn't called from async context.
|
||||
var isSourceAdded: Bool?
|
||||
var errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||
|
||||
do
|
||||
@@ -238,9 +267,9 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
||||
else
|
||||
{
|
||||
try await AppManager.shared.add(self.source, presentingViewController: self)
|
||||
|
||||
self.addedSourceHandler?(self.source)
|
||||
}
|
||||
|
||||
isSourceAdded = try await self.source.isAdded
|
||||
}
|
||||
catch is CancellationError {}
|
||||
catch
|
||||
@@ -249,11 +278,6 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
||||
}
|
||||
|
||||
self.viewModel.isAddingSource = false
|
||||
|
||||
if let isSourceAdded
|
||||
{
|
||||
self.viewModel.isSourceAdded = isSourceAdded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,150 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.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"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Forwarding Navigation Controller-->
|
||||
<!--Sources-->
|
||||
<scene sceneID="QxB-Dd-1xC">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7We-99-yEv" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Sources" image="Sources" id="xPv-dc-X4v"/>
|
||||
<toolbarItems/>
|
||||
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xWh-1U-u0q" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="59" width="393" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="5vR-Su-j54" kind="relationship" relationship="rootViewController" id="Kt6-Nl-WsS"/>
|
||||
<segue destination="Wm7-1O-FkD" kind="relationship" relationship="rootViewController" id="B9x-Lz-fg2"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ULL-gB-Cpt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-18" y="-13"/>
|
||||
</scene>
|
||||
<!--Sources-->
|
||||
<scene sceneID="hR0-Xj-lcc">
|
||||
<objects>
|
||||
<collectionViewController title="Sources" id="5vR-Su-j54" customClass="SourcesViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="M2N-lD-Q3M">
|
||||
<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" minimumLineSpacing="15" minimumInteritemSpacing="10" id="ldV-TM-Jx1">
|
||||
<size key="itemSize" width="375" height="80"/>
|
||||
<size key="headerReferenceSize" width="50" height="200"/>
|
||||
<size key="footerReferenceSize" width="50" height="50"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="mkn-CU-TaQ" customClass="AppBannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="9" y="200" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="w5e-xs-D45" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Manage sources to control which apps are available to download through AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KCm-fD-Jy0">
|
||||
<rect key="frame" x="8" y="14" width="377" height="171"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="KCm-fD-Jy0" secondAttribute="bottom" priority="999" constant="15" id="1M7-ad-U2f"/>
|
||||
<constraint firstItem="KCm-fD-Jy0" firstAttribute="top" secondItem="w5e-xs-D45" secondAttribute="top" priority="999" constant="14" id="5h0-b0-UWE"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="KCm-fD-Jy0" secondAttribute="trailing" priority="999" id="K04-Ud-iGz"/>
|
||||
<constraint firstAttribute="leadingMargin" secondItem="KCm-fD-Jy0" secondAttribute="leading" priority="999" id="MN6-lr-3tF"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="bottomLayoutConstraint" destination="1M7-ad-U2f" id="tej-O7-Lyh"/>
|
||||
<outlet property="leadingLayoutConstraint" destination="MN6-lr-3tF" id="Deq-Tk-7Lc"/>
|
||||
<outlet property="textLabel" destination="KCm-fD-Jy0" id="alm-sb-NAa"/>
|
||||
<outlet property="topLayoutConstraint" destination="5h0-b0-UWE" id="kHq-up-pCk"/>
|
||||
<outlet property="trailingLayoutConstraint" destination="K04-Ud-iGz" id="rS5-c5-EkL"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="H5R-Ci-5aX" customClass="SourcesFooterView">
|
||||
<rect key="frame" x="0.0" y="280" width="393" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="oeg-V0-knE">
|
||||
<rect key="frame" x="8" y="0.0" width="377" height="50"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="hnb-2l-24w">
|
||||
<rect key="frame" x="0.0" y="0.0" width="377" height="0.0"/>
|
||||
</activityIndicatorView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="800" scrollEnabled="NO" contentInsetAdjustmentBehavior="never" editable="NO" text="Test" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="4Mf-Ge-exp">
|
||||
<rect key="frame" x="0.0" y="15" width="377" height="35"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="oeg-V0-knE" firstAttribute="top" secondItem="H5R-Ci-5aX" secondAttribute="top" priority="999" id="KK7-2G-rL0" propertyAccessControl="none"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="oeg-V0-knE" secondAttribute="trailing" priority="999" id="aG4-4x-ACP"/>
|
||||
<constraint firstAttribute="bottom" secondItem="oeg-V0-knE" secondAttribute="bottom" priority="999" id="ueD-zU-eSQ"/>
|
||||
<constraint firstItem="oeg-V0-knE" firstAttribute="leading" secondItem="H5R-Ci-5aX" secondAttribute="leadingMargin" priority="999" id="w2y-e8-rJG"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="activityIndicatorView" destination="hnb-2l-24w" id="6Dp-gp-nlC"/>
|
||||
<outlet property="bottomLayoutConstraint" destination="ueD-zU-eSQ" id="ChX-d2-sRT"/>
|
||||
<outlet property="leadingLayoutConstraint" destination="w2y-e8-rJG" id="9j3-ao-bfA"/>
|
||||
<outlet property="textView" destination="4Mf-Ge-exp" id="XqS-CB-3ek"/>
|
||||
<outlet property="topLayoutConstraint" destination="KK7-2G-rL0" id="oFe-fV-BgO"/>
|
||||
<outlet property="trailingLayoutConstraint" destination="aG4-4x-ACP" id="YOc-HZ-1pw"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="5vR-Su-j54" id="lGW-bH-xYZ"/>
|
||||
<outlet property="delegate" destination="5vR-Su-j54" id="bj3-kR-erl"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Sources" id="UQX-GH-OMC">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="add" id="ox5-Bu-RLr">
|
||||
<connections>
|
||||
<action selector="addSource" destination="5vR-Su-j54" id="4s7-KQ-ume"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="QYt-Dn-SKf">
|
||||
<connections>
|
||||
<segue destination="wBZ-c2-miy" kind="unwind" unwindAction="unwindFromSourcesViewController:" id="Dc6-tD-Z2I"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="dLj-Tf-ZjV"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="69z-hg-xF8" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<exit id="wBZ-c2-miy" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="810" y="-13"/>
|
||||
</scene>
|
||||
<!--All Apps-->
|
||||
<scene sceneID="d8a-U8-CPc">
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ih5-9R-QX7" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<collectionViewController storyboardIdentifier="browseViewController" id="Nhf-Gw-Ukx" customClass="BrowseViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="oBI-6P-Lm3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="783"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="10" id="pSh-Xl-aNg">
|
||||
@@ -161,13 +52,13 @@
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="All Apps" id="rUb-ON-AON"/>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ih5-9R-QX7" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3404" y="-13"/>
|
||||
</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"/>
|
||||
@@ -177,13 +68,13 @@
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="Ocv-bj-TfG"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cYc-NX-nF1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</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"/>
|
||||
@@ -218,21 +109,22 @@
|
||||
<outlet property="delegate" destination="MSh-hM-32I" id="8SG-5v-iF2"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" id="SsY-RM-EFx"/>
|
||||
<connections>
|
||||
<segue destination="MVH-oB-c8m" kind="show" identifier="showAllNews" destinationCreationSelector="makeNewsViewController:" id="txA-ay-P7p"/>
|
||||
<segue destination="Nhf-Gw-Ukx" kind="show" identifier="showAllApps" destinationCreationSelector="makeBrowseViewController:" id="On0-GP-kaE"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="KEz-hK-u3f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2509" y="-13"/>
|
||||
</scene>
|
||||
<!--All News-->
|
||||
<scene sceneID="avV-5f-uNE">
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="7f5-vn-JrS" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<collectionViewController storyboardIdentifier="newsViewController" id="MVH-oB-c8m" customClass="NewsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="p9p-rr-fbF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="783"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="3cD-ax-3h6">
|
||||
@@ -249,11 +141,125 @@
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="All News" id="FGB-cd-Vkd"/>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="7f5-vn-JrS" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3404" y="-711"/>
|
||||
</scene>
|
||||
<!--Sources-->
|
||||
<scene sceneID="w2v-ek-6dY">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" id="Wm7-1O-FkD" customClass="SourcesViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="VJr-nx-7Vh">
|
||||
<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" minimumLineSpacing="15" minimumInteritemSpacing="10" id="Rlv-wJ-9Ef">
|
||||
<size key="itemSize" width="375" height="80"/>
|
||||
<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" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="1Rm-Gf-VDt" customClass="AppBannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="9" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Wm7-1O-FkD" id="239-Tq-YUw"/>
|
||||
<outlet property="delegate" destination="Wm7-1O-FkD" id="UvE-ta-8ir"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Sources" largeTitleDisplayMode="always" id="Noh-fc-wch">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="add" id="y96-Ve-1gW">
|
||||
<connections>
|
||||
<segue destination="i15-Jk-F75" kind="presentation" identifier="addSource" id="FGe-k6-H7c"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="eVl-NI-lj3"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cR1-aE-0KX" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="810" y="-13"/>
|
||||
</scene>
|
||||
<!--Add Source-->
|
||||
<scene sceneID="oCv-by-94o">
|
||||
<objects>
|
||||
<collectionViewController id="bbz-wy-kaK" customClass="AddSourceViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="lXl-N1-6cT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" automaticEstimatedItemSize="YES" minimumLineSpacing="10" minimumInteritemSpacing="10" id="Xgg-0U-PlW">
|
||||
<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" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="" id="W0l-zW-MjJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="bbz-wy-kaK" id="Fcm-4b-KZC"/>
|
||||
<outlet property="delegate" destination="bbz-wy-kaK" id="RRO-hx-gbl"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Add Source" id="v46-dC-g7e">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="ozl-rj-JhC">
|
||||
<connections>
|
||||
<segue destination="qr8-ss-Ghz" kind="unwind" unwindAction="unwindFromAddSource:" id="Pba-Kh-qfH"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<segue destination="qr8-ss-Ghz" kind="unwind" unwindAction="unwindFromAddSource:" id="nj3-eB-DML"/>
|
||||
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="LO9-iP-zZC"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="RWS-LV-vih" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<exit id="qr8-ss-Ghz" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1647" y="-711"/>
|
||||
</scene>
|
||||
<!--Forwarding Navigation Controller-->
|
||||
<scene sceneID="NAl-mP-f8p">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="addSourceNavigationController" automaticallyAdjustsScrollViewInsets="NO" id="i15-Jk-F75" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationItem key="navigationItem" id="Opx-Pr-Tyy"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="HLe-3g-P8I" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="108"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="bbz-wy-kaK" kind="relationship" relationship="rootViewController" id="uVn-Xo-Kjy"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="WTH-Dd-NJP" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="810" y="-711"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="LO9-iP-zZC"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="Sources" width="20" height="20"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
|
||||
@@ -12,158 +12,170 @@ import CoreData
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
@objc(SourcesFooterView)
|
||||
private class SourcesFooterView: TextCollectionReusableView
|
||||
{
|
||||
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
|
||||
@IBOutlet var textView: UITextView!
|
||||
}
|
||||
import Nuke
|
||||
|
||||
extension SourcesViewController
|
||||
private extension UIAction.Identifier
|
||||
{
|
||||
private enum Section: Int, CaseIterable
|
||||
{
|
||||
case added
|
||||
case trusted
|
||||
}
|
||||
static let showDetails = UIAction.Identifier("io.altstore.showDetails")
|
||||
static let showError = UIAction.Identifier("io.altstore.showError")
|
||||
}
|
||||
|
||||
class SourcesViewController: UICollectionViewController
|
||||
{
|
||||
var deepLinkSourceURL: URL? {
|
||||
didSet {
|
||||
guard let sourceURL = self.deepLinkSourceURL else { return }
|
||||
self.addSource(url: sourceURL)
|
||||
self.handleAddSourceDeepLink()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var addedSourcesDataSource = self.makeAddedSourcesDataSource()
|
||||
private lazy var trustedSourcesDataSource = self.makeTrustedSourcesDataSource()
|
||||
|
||||
private var fetchTrustedSourcesOperation: UpdateKnownSourcesOperation?
|
||||
private var fetchTrustedSourcesResult: Result<Void, Error>?
|
||||
private var _fetchTrustedSourcesContext: NSManagedObjectContext?
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private var placeholderView: RSTPlaceholderView!
|
||||
private var placeholderViewButton: UIButton!
|
||||
private var placeholderViewCenterYConstraint: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.tintColor = .altPrimary
|
||||
let layout = self.makeLayout()
|
||||
self.collectionView.collectionViewLayout = layout
|
||||
|
||||
self.navigationController?.view.tintColor = .altPrimary
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar as? NavigationBar
|
||||
{
|
||||
// Don't automatically adjust item positions when being presented non-full screen,
|
||||
// or else the navigation bar content won't be vertically centered.
|
||||
navigationBar.automaticallyAdjustsItemPositions = false
|
||||
}
|
||||
self.collectionView.register(AppBannerCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: UICollectionView.elementKindSectionHeader)
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
self.collectionView.allowsSelectionDuringEditing = false
|
||||
|
||||
#if !BETA
|
||||
// Hide "Add Source" button for public version while in beta.
|
||||
self.navigationItem.leftBarButtonItem = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
let backgroundView = UIView(frame: .zero)
|
||||
backgroundView.backgroundColor = .altBackground
|
||||
self.collectionView.backgroundView = backgroundView
|
||||
|
||||
if self.deepLinkSourceURL != nil
|
||||
{
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
|
||||
}
|
||||
self.placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
self.placeholderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Add More Sources!", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis massa tortor, tempor vel est vitae, consequat luctus arcu."
|
||||
backgroundView.addSubview(self.placeholderView)
|
||||
|
||||
if self.fetchTrustedSourcesOperation == nil
|
||||
{
|
||||
self.fetchTrustedSources()
|
||||
}
|
||||
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).bolded()
|
||||
self.placeholderView.textLabel.font = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||
self.placeholderView.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
self.placeholderView.detailTextLabel.textAlignment = .natural
|
||||
|
||||
self.placeholderViewButton = UIButton(type: .system, primaryAction: UIAction(title: NSLocalizedString("View Recommended Sources", comment: "")) { [weak self] _ in
|
||||
self?.performSegue(withIdentifier: "addSource", sender: nil)
|
||||
})
|
||||
self.placeholderViewButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
self.placeholderView.stackView.spacing = 15
|
||||
self.placeholderView.stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15)
|
||||
self.placeholderView.stackView.isLayoutMarginsRelativeArrangement = true
|
||||
self.placeholderView.stackView.addArrangedSubview(self.placeholderViewButton)
|
||||
|
||||
self.placeholderViewCenterYConstraint = self.placeholderView.safeAreaLayoutGuide.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor, constant: 0)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.placeholderViewCenterYConstraint,
|
||||
self.placeholderView.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor),
|
||||
self.placeholderView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
|
||||
self.placeholderView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
|
||||
|
||||
self.placeholderView.topAnchor.constraint(equalTo: self.placeholderView.stackView.topAnchor),
|
||||
self.placeholderView.bottomAnchor.constraint(equalTo: self.placeholderView.stackView.bottomAnchor),
|
||||
])
|
||||
|
||||
self.navigationItem.rightBarButtonItem = self.editButtonItem
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if let sourceURL = self.deepLinkSourceURL
|
||||
self.handleAddSourceDeepLink()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// Vertically center placeholder view in gap below first item.
|
||||
|
||||
let indexPath = IndexPath(item: 0, section: 0)
|
||||
guard let layoutAttributes = self.collectionView.layoutAttributesForItem(at: indexPath) else { return }
|
||||
|
||||
let maxY = layoutAttributes.frame.maxY
|
||||
|
||||
let constant = maxY / 2
|
||||
if self.placeholderViewCenterYConstraint.constant != constant
|
||||
{
|
||||
self.addSource(url: sourceURL)
|
||||
self.placeholderViewCenterYConstraint.constant = constant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SourcesViewController
|
||||
{
|
||||
func makeDataSource() -> RSTCompositeCollectionViewDataSource<Source>
|
||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||
{
|
||||
let dataSource = RSTCompositeCollectionViewDataSource<Source>(dataSources: [self.addedSourcesDataSource, self.trustedSourcesDataSource])
|
||||
dataSource.proxy = self
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, source, indexPath) in
|
||||
guard let self else { return }
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||
configuration.headerMode = .supplementary
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .clear
|
||||
|
||||
configuration.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
|
||||
guard let self else { return UISwipeActionsConfiguration(actions: []) }
|
||||
|
||||
let tintColor = UIColor.altPrimary
|
||||
let source = self.dataSource.item(at: indexPath)
|
||||
var actions: [UIContextualAction] = []
|
||||
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
cell.tintColor = tintColor
|
||||
|
||||
cell.bannerView.iconImageView.isHidden = true
|
||||
cell.bannerView.buttonLabel.isHidden = true
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
|
||||
switch Section.allCases[indexPath.section]
|
||||
if source.identifier != Source.altStoreIdentifier
|
||||
{
|
||||
case .added:
|
||||
cell.bannerView.button.isHidden = true
|
||||
// Prevent users from removing AltStore source.
|
||||
|
||||
case .trusted:
|
||||
// Quicker way to determine whether a source is already added than by reading from disk.
|
||||
if (self.addedSourcesDataSource.fetchedResultsController.fetchedObjects ?? []).contains(where: { $0.identifier == source.identifier })
|
||||
{
|
||||
// Source exists in .added section, so hide the button.
|
||||
cell.bannerView.button.isHidden = true
|
||||
|
||||
let configuation = UIImage.SymbolConfiguration(pointSize: 24)
|
||||
|
||||
let imageAttachment = NSTextAttachment()
|
||||
imageAttachment.image = UIImage(systemName: "checkmark.circle", withConfiguration: configuation)?.withTintColor(.altPrimary)
|
||||
|
||||
let attributedText = NSAttributedString(attachment: imageAttachment)
|
||||
cell.bannerView.buttonLabel.attributedText = attributedText
|
||||
cell.bannerView.buttonLabel.textAlignment = .center
|
||||
cell.bannerView.buttonLabel.isHidden = false
|
||||
}
|
||||
else
|
||||
{
|
||||
// Source does not exist in .added section, so show the button.
|
||||
cell.bannerView.button.isHidden = false
|
||||
cell.bannerView.buttonLabel.attributedText = nil
|
||||
let removeAction = UIContextualAction(style: .destructive,
|
||||
title: NSLocalizedString("Remove", comment: "")) { _, _, completion in
|
||||
self.remove(source, completionHandler: completion)
|
||||
}
|
||||
removeAction.image = UIImage(systemName: "trash.fill")
|
||||
|
||||
cell.bannerView.button.setTitle(NSLocalizedString("ADD", comment: ""), for: .normal)
|
||||
cell.bannerView.button.addTarget(self, action: #selector(SourcesViewController.addTrustedSource(_:)), for: .primaryActionTriggered)
|
||||
actions.append(removeAction)
|
||||
}
|
||||
|
||||
cell.bannerView.titleLabel.text = source.name
|
||||
cell.bannerView.subtitleLabel.text = source.sourceURL.absoluteString
|
||||
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||
|
||||
cell.errorBadge?.isHidden = (source.error == nil)
|
||||
if let error = source.error
|
||||
{
|
||||
let viewErrorAction = UIContextualAction(style: .normal,
|
||||
title: NSLocalizedString("View Error", comment: "")) { _, _, completion in
|
||||
self.present(error)
|
||||
completion(true)
|
||||
}
|
||||
viewErrorAction.backgroundColor = .systemYellow
|
||||
viewErrorAction.image = UIImage(systemName: "exclamationmark.circle.fill")
|
||||
|
||||
actions.append(viewErrorAction)
|
||||
}
|
||||
|
||||
let attributedLabel = NSAttributedString(string: source.name + "\n" + source.sourceURL.absoluteString, attributes: [.accessibilitySpeechPunctuation: true])
|
||||
cell.bannerView.accessibilityAttributedLabel = attributedLabel
|
||||
cell.bannerView.accessibilityTraits.remove(.button)
|
||||
let config = UISwipeActionsConfiguration(actions: actions)
|
||||
config.performsFirstActionWithFullSwipe = false
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
return config
|
||||
}
|
||||
|
||||
return dataSource
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
return layout
|
||||
}
|
||||
|
||||
func makeAddedSourcesDataSource() -> RSTFetchedResultsCollectionViewDataSource<Source>
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<Source, UIImage>
|
||||
{
|
||||
let fetchRequest = Source.fetchRequest() as NSFetchRequest<Source>
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
@@ -174,13 +186,129 @@ private extension SourcesViewController
|
||||
|
||||
NSSortDescriptor(keyPath: \Source.identifier, ascending: true)]
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewDataSource<Source>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeTrustedSourcesDataSource() -> RSTArrayCollectionViewDataSource<Source>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource<Source>(items: [])
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||
fetchedResultsController.delegate = self
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<Source, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.proxy = self
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, source, indexPath) in
|
||||
guard let self else { return }
|
||||
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
cell.layoutMargins.top = 5
|
||||
cell.layoutMargins.bottom = 5
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.bannerView.configure(for: source)
|
||||
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
let numberOfApps: Int
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
{
|
||||
numberOfApps = source.apps.count
|
||||
}
|
||||
else
|
||||
{
|
||||
numberOfApps = source.apps.filter { !$0.isBeta }.count
|
||||
}
|
||||
|
||||
if let error = source.error
|
||||
{
|
||||
let image = UIImage(systemName: "exclamationmark")?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
|
||||
cell.bannerView.button.setImage(image, for: .normal)
|
||||
cell.bannerView.button.setTitle(nil, for: .normal)
|
||||
cell.bannerView.button.tintColor = .systemYellow.withAlphaComponent(0.75)
|
||||
|
||||
let action = UIAction(identifier: .showError) { _ in
|
||||
self.present(error)
|
||||
}
|
||||
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
||||
cell.bannerView.button.removeAction(identifiedBy: .showDetails, for: .primaryActionTriggered)
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.setImage(nil, for: .normal)
|
||||
cell.bannerView.button.setTitle(numberOfApps.description, for: .normal)
|
||||
cell.bannerView.button.tintColor = .white.withAlphaComponent(0.2)
|
||||
|
||||
let action = UIAction(identifier: .showDetails) { _ in
|
||||
self.showSourceDetails(for: source)
|
||||
}
|
||||
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
||||
cell.bannerView.button.removeAction(identifiedBy: .showError, for: .primaryActionTriggered)
|
||||
}
|
||||
|
||||
let dateText: String
|
||||
if let lastUpdatedDate = source.lastUpdatedDate
|
||||
{
|
||||
dateText = Date().relativeDateString(since: lastUpdatedDate, dateFormatter: self.dateFormatter)
|
||||
}
|
||||
else
|
||||
{
|
||||
dateText = NSLocalizedString("Never", comment: "")
|
||||
}
|
||||
|
||||
let text = String(format: NSLocalizedString("Last Updated: %@", comment: ""), dateText)
|
||||
cell.bannerView.subtitleLabel.text = text
|
||||
cell.bannerView.subtitleLabel.numberOfLines = 1
|
||||
|
||||
let numberOfAppsText: String
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
let attributedOutput = AttributedString(localized: "^[\(numberOfApps) app](inflect: true)")
|
||||
numberOfAppsText = String(attributedOutput.characters)
|
||||
}
|
||||
else
|
||||
{
|
||||
numberOfAppsText = ""
|
||||
}
|
||||
|
||||
let accessibilityLabel = source.name + "\n" + text + ".\n" + numberOfAppsText
|
||||
cell.bannerView.accessibilityLabel = accessibilityLabel
|
||||
|
||||
if source.identifier != Source.altStoreIdentifier
|
||||
{
|
||||
cell.accessories = [.delete(displayed: .whenEditing)]
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.accessories = []
|
||||
}
|
||||
|
||||
cell.bannerView.accessibilityTraits.remove(.button)
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
}
|
||||
dataSource.prefetchHandler = { (source, indexPath, completionHandler) in
|
||||
guard let imageURL = source.effectiveIconURL else { return nil }
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL, progress: nil) { result in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success(let response): completionHandler(response.image, nil)
|
||||
case .failure(let error): completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! AppBannerCollectionViewCell
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
@@ -192,45 +320,23 @@ private extension SourcesViewController
|
||||
let sourceDetailViewController = SourceDetailViewController(source: source, coder: coder)
|
||||
return sourceDetailViewController
|
||||
}
|
||||
|
||||
@IBAction
|
||||
func unwindFromAddSource(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private extension SourcesViewController
|
||||
{
|
||||
@IBAction func addSource()
|
||||
func handleAddSourceDeepLink()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField { (textField) in
|
||||
textField.placeholder = "https://apps.altstore.io"
|
||||
textField.textContentType = .URL
|
||||
}
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Add", comment: ""), style: .default) { (action) in
|
||||
guard let text = alertController.textFields![0].text else { return }
|
||||
guard var sourceURL = URL(string: text) else { return }
|
||||
if sourceURL.scheme == nil {
|
||||
guard let httpsSourceURL = URL(string: "https://" + text) else { return }
|
||||
sourceURL = httpsSourceURL
|
||||
}
|
||||
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
self.addSource(url: sourceURL) { _ in
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
}
|
||||
})
|
||||
guard let url = self.deepLinkSourceURL, self.view.window != nil else { return }
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func addSource(url: URL, completionHandler: ((Result<Void, Error>) -> Void)? = nil)
|
||||
{
|
||||
guard self.view.window != nil else { return }
|
||||
// Only handle deep link once.
|
||||
self.deepLinkSourceURL = nil
|
||||
|
||||
if url == self.deepLinkSourceURL
|
||||
{
|
||||
// Only handle deep link once.
|
||||
self.deepLinkSourceURL = nil
|
||||
}
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
@@ -249,48 +355,22 @@ private extension SourcesViewController
|
||||
self.present(error.withLocalizedTitle(NSLocalizedString("Unable to Add Source", comment: "")))
|
||||
}
|
||||
|
||||
self.collectionView.reloadSections([Section.trusted.rawValue])
|
||||
|
||||
completionHandler?(result)
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
|
||||
var dependencies: [Foundation.Operation] = []
|
||||
if let fetchTrustedSourcesOperation = self.fetchTrustedSourcesOperation
|
||||
{
|
||||
// Must fetch trusted sources first to determine whether this is a trusted source.
|
||||
// We assume fetchTrustedSources() has already been called before this method.
|
||||
dependencies = [fetchTrustedSourcesOperation]
|
||||
}
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: url, dependencies: dependencies) { (result) in
|
||||
AppManager.shared.fetchSource(sourceURL: url) { (result) in
|
||||
do
|
||||
{
|
||||
// Use @Managed before calling perform() to keep
|
||||
// strong reference to source.managedObjectContext.
|
||||
@Managed var source = try result.get()
|
||||
|
||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
backgroundContext.perform {
|
||||
do
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(Source.identifier), $source.identifier)
|
||||
if let existingSource = Source.first(satisfying: predicate, in: backgroundContext)
|
||||
{
|
||||
throw SourceError.duplicate(source, existingSource: existingSource)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.showSourceDetails(for: source)
|
||||
}
|
||||
|
||||
finish(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.showSourceDetails(for: source)
|
||||
}
|
||||
|
||||
finish(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -319,131 +399,53 @@ private extension SourcesViewController
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func fetchTrustedSources()
|
||||
func remove(_ source: Source, completionHandler: ((Bool) -> Void)? = nil)
|
||||
{
|
||||
// Closure instead of local function so we can capture `self` weakly.
|
||||
let finish: (Result<[Source], Error>) -> Void = { [weak self] result in
|
||||
self?.fetchTrustedSourcesResult = result.map { _ in () }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
do
|
||||
{
|
||||
let sources = try result.get()
|
||||
print("Fetched trusted sources:", sources.map { $0.identifier })
|
||||
|
||||
let sectionUpdate = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||
self?.trustedSourcesDataSource.setItems(sources, with: [sectionUpdate])
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error fetching trusted sources:", error)
|
||||
|
||||
let sectionUpdate = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||
self?.trustedSourcesDataSource.setItems([], with: [sectionUpdate])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.fetchTrustedSourcesOperation = AppManager.shared.updateKnownSources { [weak self] result in
|
||||
switch result
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success((let trustedSources, _)):
|
||||
// Don't show sources without a sourceURL.
|
||||
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
|
||||
try await AppManager.shared.remove(source, presentingViewController: self)
|
||||
|
||||
// This context is never saved, but keeps the managed sources alive.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
self?._fetchTrustedSourcesContext = context
|
||||
completionHandler?(true)
|
||||
}
|
||||
catch is CancellationError
|
||||
{
|
||||
completionHandler?(false)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler?(false)
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
var sourcesByURL = [URL: Source]()
|
||||
var fetchError: Error?
|
||||
|
||||
for sourceURL in featuredSourceURLs
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
|
||||
// Serialize access to sourcesByURL.
|
||||
context.performAndWait {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): fetchError = error
|
||||
case .success(let source): sourcesByURL[source.sourceURL] = source
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let error = fetchError
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
|
||||
finish(.success(sources))
|
||||
}
|
||||
}
|
||||
self.present(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func addTrustedSource(_ sender: PillButton)
|
||||
{
|
||||
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||
|
||||
let completedProgress = Progress(totalUnitCount: 1)
|
||||
completedProgress.completedUnitCount = 1
|
||||
sender.progress = completedProgress
|
||||
|
||||
let source = self.dataSource.item(at: indexPath)
|
||||
self.addSource(url: source.sourceURL) { _ in
|
||||
//FIXME: Handle cell reuse.
|
||||
sender.progress = nil
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ source: Source)
|
||||
{
|
||||
let alertController = UIAlertController(title: String(format: NSLocalizedString("Are you sure you want to remove the source “%@”?", comment: ""), source.name),
|
||||
message: NSLocalizedString("Any apps you've installed from this source will remain, but they'll no longer receive any app updates.", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: nil))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove Source", comment: ""), style: .destructive) { _ in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let source = context.object(with: source.objectID) as! Source
|
||||
context.delete(source)
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.reloadSections([Section.trusted.rawValue])
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.present(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func showSourceDetails(for source: Source)
|
||||
{
|
||||
self.performSegue(withIdentifier: "showSourceDetails", sender: source)
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
if self.dataSource.itemCount < 2
|
||||
{
|
||||
// Show placeholder view
|
||||
|
||||
self.placeholderView.isHidden = false
|
||||
self.collectionView.alwaysBounceVertical = false
|
||||
|
||||
self.setEditing(false, animated: true)
|
||||
self.editButtonItem.isEnabled = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.placeholderView.isHidden = true
|
||||
self.collectionView.alwaysBounceVertical = true
|
||||
|
||||
self.editButtonItem.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SourcesViewController
|
||||
@@ -455,210 +457,95 @@ extension SourcesViewController
|
||||
let source = self.dataSource.item(at: indexPath)
|
||||
self.showSourceDetails(for: source)
|
||||
}
|
||||
}
|
||||
|
||||
extension SourcesViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
return CGSize(width: collectionView.bounds.width, height: 80)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
|
||||
{
|
||||
let indexPath = IndexPath(row: 0, section: section)
|
||||
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
|
||||
|
||||
// Use this view to calculate the optimal size based on the collection view's width
|
||||
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||
withHorizontalFittingPriority: .required, // Width is fixed
|
||||
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
||||
return size
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||
{
|
||||
guard Section(rawValue: section) == .trusted else { return .zero }
|
||||
|
||||
let indexPath = IndexPath(row: 0, section: section)
|
||||
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||
|
||||
// Use this view to calculate the optimal size based on the collection view's width
|
||||
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||
withHorizontalFittingPriority: .required, // Width is fixed
|
||||
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
||||
return size
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
{
|
||||
let reuseIdentifier = (kind == UICollectionView.elementKindSectionHeader) ? "Header" : "Footer"
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! UICollectionViewListCell
|
||||
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifier, for: indexPath) as! TextCollectionReusableView
|
||||
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
var configuation = UIListContentConfiguration.cell()
|
||||
configuation.text = NSLocalizedString("Sources control what apps are available to download through AltStore.", comment: "")
|
||||
configuation.textProperties.color = .secondaryLabel
|
||||
configuation.textProperties.alignment = .natural
|
||||
|
||||
/* Changing NSLayoutConstraint priorities from required to optional (and vice versa) isn’t supported, and crashes on iOS 12. */
|
||||
// let almostRequiredPriority = UILayoutPriority(UILayoutPriority.required.rawValue - 1) // Can't be required or else we can't satisfy constraints when hidden (size = 0).
|
||||
// headerView.leadingLayoutConstraint?.priority = almostRequiredPriority
|
||||
// headerView.trailingLayoutConstraint?.priority = almostRequiredPriority
|
||||
// headerView.topLayoutConstraint?.priority = almostRequiredPriority
|
||||
// headerView.bottomLayoutConstraint?.priority = almostRequiredPriority
|
||||
headerView.contentConfiguration = configuation
|
||||
|
||||
switch kind
|
||||
{
|
||||
case UICollectionView.elementKindSectionHeader:
|
||||
switch Section.allCases[indexPath.section]
|
||||
{
|
||||
case .added:
|
||||
headerView.textLabel.text = NSLocalizedString("Sources control what apps are available to download through AltStore.", comment: "")
|
||||
headerView.textLabel.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||
headerView.textLabel.textAlignment = .natural
|
||||
|
||||
headerView.topLayoutConstraint.constant = 14
|
||||
headerView.bottomLayoutConstraint.constant = 30
|
||||
|
||||
case .trusted:
|
||||
switch self.fetchTrustedSourcesResult
|
||||
{
|
||||
case .failure: headerView.textLabel.text = NSLocalizedString("Error Loading Trusted Sources", comment: "")
|
||||
case .success, .none: headerView.textLabel.text = NSLocalizedString("Trusted Sources", comment: "")
|
||||
}
|
||||
|
||||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .callout).withSymbolicTraits(.traitBold)!
|
||||
headerView.textLabel.font = UIFont(descriptor: descriptor, size: 0)
|
||||
headerView.textLabel.textAlignment = .center
|
||||
|
||||
headerView.topLayoutConstraint.constant = 54
|
||||
headerView.bottomLayoutConstraint.constant = 15
|
||||
}
|
||||
|
||||
case UICollectionView.elementKindSectionFooter:
|
||||
let footerView = headerView as! SourcesFooterView
|
||||
let font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
|
||||
switch self.fetchTrustedSourcesResult
|
||||
{
|
||||
case .failure(let error):
|
||||
footerView.textView.font = font
|
||||
footerView.textView.text = error.localizedDescription
|
||||
|
||||
footerView.activityIndicatorView.stopAnimating()
|
||||
footerView.topLayoutConstraint.constant = 0
|
||||
footerView.textView.textAlignment = .center
|
||||
|
||||
case .success, .none:
|
||||
footerView.textView.delegate = self
|
||||
|
||||
let attributedText = NSMutableAttributedString(
|
||||
string: NSLocalizedString("AltStore has reviewed these sources to make sure they meet our safety standards.\n\nSupport for untrusted sources is currently in beta, but you can help test them out by", comment: ""),
|
||||
attributes: [.font: font, .foregroundColor: UIColor.gray]
|
||||
)
|
||||
attributedText.mutableString.append(" ")
|
||||
|
||||
let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
|
||||
let openPatreonURL = URL(string: "https://altstore.io/patreon")!
|
||||
|
||||
let joinPatreonText = NSAttributedString(
|
||||
string: NSLocalizedString("joining our Patreon.", comment: ""),
|
||||
attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
|
||||
)
|
||||
attributedText.append(joinPatreonText)
|
||||
|
||||
footerView.textView.attributedText = attributedText
|
||||
footerView.textView.textAlignment = .natural
|
||||
|
||||
if self.fetchTrustedSourcesResult != nil
|
||||
{
|
||||
footerView.activityIndicatorView.stopAnimating()
|
||||
footerView.topLayoutConstraint.constant = 20
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.activityIndicatorView.startAnimating()
|
||||
footerView.topLayoutConstraint.constant = 0
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return headerView
|
||||
}
|
||||
}
|
||||
|
||||
extension SourcesViewController
|
||||
extension SourcesViewController: NSFetchedResultsControllerDelegate
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
|
||||
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
|
||||
{
|
||||
let source = self.dataSource.item(at: indexPath)
|
||||
self.dataSource.controllerWillChangeContent(controller)
|
||||
}
|
||||
|
||||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
|
||||
{
|
||||
self.update()
|
||||
|
||||
self.dataSource.controllerDidChangeContent(controller)
|
||||
}
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
|
||||
{
|
||||
self.dataSource.controller(controller, didChange: anObject, at: indexPath, for: type, newIndexPath: newIndexPath)
|
||||
}
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType)
|
||||
{
|
||||
self.dataSource.controller(controller, didChange: sectionInfo, atSectionIndex: UInt(sectionIndex), for: type)
|
||||
}
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
|
||||
let viewErrorAction = UIAction(title: NSLocalizedString("View Error", comment: ""), image: UIImage(systemName: "exclamationmark.circle")) { (action) in
|
||||
guard let error = source.error else { return }
|
||||
self.present(error)
|
||||
}
|
||||
|
||||
let deleteAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
|
||||
self.remove(source)
|
||||
}
|
||||
|
||||
let addAction = UIAction(title: String(format: NSLocalizedString("Add “%@”", comment: ""), source.name), image: UIImage(systemName: "plus")) { (action) in
|
||||
self.addSource(url: source.sourceURL)
|
||||
}
|
||||
|
||||
var actions: [UIAction] = []
|
||||
|
||||
if source.error != nil
|
||||
{
|
||||
actions.append(viewErrorAction)
|
||||
}
|
||||
|
||||
switch Section.allCases[indexPath.section]
|
||||
{
|
||||
case .added:
|
||||
if source.identifier != Source.altStoreIdentifier
|
||||
{
|
||||
actions.append(deleteAction)
|
||||
}
|
||||
|
||||
case .trusted:
|
||||
if let cell = collectionView.cellForItem(at: indexPath) as? AppBannerCollectionViewCell, !cell.bannerView.button.isHidden
|
||||
{
|
||||
actions.append(addAction)
|
||||
}
|
||||
}
|
||||
|
||||
guard !actions.isEmpty else { return nil }
|
||||
|
||||
let menu = UIMenu(title: "", children: actions)
|
||||
return menu
|
||||
@available(iOS 17, *)
|
||||
#Preview(traits: .portrait) {
|
||||
DatabaseManager.shared.startForPreview()
|
||||
|
||||
let storyboard = UIStoryboard(name: "Sources", bundle: nil)
|
||||
let sourcesViewController = storyboard.instantiateInitialViewController()!
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
_ = Source.make(name: "OatmealDome's AltStore Source",
|
||||
identifier: "me.oatmealdome.altstore",
|
||||
sourceURL: URL(string: "https://altstore.oatmealdome.me")!,
|
||||
context: context)
|
||||
|
||||
_ = Source.make(name: "UTM Repository",
|
||||
identifier: "com.utmapp.repos.UTM",
|
||||
sourceURL: URL(string: "https://alt.getutm.app")!,
|
||||
context: context)
|
||||
|
||||
_ = Source.make(name: "Flyinghead",
|
||||
identifier: "com.flyinghead.source",
|
||||
sourceURL: URL(string: "https://flyinghead.github.io/flycast-builds/altstore.json")!,
|
||||
context: context)
|
||||
|
||||
_ = Source.make(name: "Provenance",
|
||||
identifier: "org.provenance-emu.AltStore",
|
||||
sourceURL: URL(string: "https://provenance-emu.com/apps.json")!,
|
||||
context: context)
|
||||
|
||||
_ = Source.make(name: "PojavLauncher Repository",
|
||||
identifier: "dev.crystall1ne.repos.PojavLauncher",
|
||||
sourceURL: URL(string: "http://alt.crystall1ne.dev")!,
|
||||
context: context)
|
||||
|
||||
try! context.save()
|
||||
}
|
||||
|
||||
AppManager.shared.fetchSources { result in
|
||||
do
|
||||
{
|
||||
let (sources, context) = try result.get()
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Preview failed to fetch sources:", error)
|
||||
}
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||
{
|
||||
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
|
||||
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? AppBannerCollectionViewCell else { return nil }
|
||||
|
||||
let parameters = UIPreviewParameters()
|
||||
parameters.backgroundColor = .clear
|
||||
parameters.visiblePath = UIBezierPath(roundedRect: cell.bannerView.bounds, cornerRadius: cell.bannerView.layer.cornerRadius)
|
||||
|
||||
let preview = UITargetedPreview(view: cell.bannerView, parameters: parameters)
|
||||
return preview
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||
{
|
||||
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
extension SourcesViewController: UITextViewDelegate
|
||||
{
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
return sourcesViewController
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ extension TabBarController
|
||||
private enum Tab: Int, CaseIterable
|
||||
{
|
||||
case news
|
||||
case sources
|
||||
case browse
|
||||
case myApps
|
||||
case settings
|
||||
@@ -26,6 +27,8 @@ class TabBarController: UITabBarController
|
||||
|
||||
private var _viewDidAppear = false
|
||||
|
||||
private var sourcesViewController: SourcesViewController!
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
@@ -36,6 +39,17 @@ class TabBarController: UITabBarController
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
let browseNavigationController = self.viewControllers![Tab.browse.rawValue] as! UINavigationController
|
||||
browseNavigationController.tabBarItem.image = UIImage(systemName: "bag")
|
||||
|
||||
let sourcesNavigationController = self.viewControllers![Tab.sources.rawValue] as! UINavigationController
|
||||
self.sourcesViewController = sourcesNavigationController.viewControllers.first as? SourcesViewController
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
@@ -63,15 +77,6 @@ class TabBarController: UITabBarController
|
||||
|
||||
switch identifier
|
||||
{
|
||||
case "presentSources":
|
||||
guard let notification = sender as? Notification,
|
||||
let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
|
||||
else { return }
|
||||
|
||||
let navigationController = segue.destination as! UINavigationController
|
||||
let sourcesViewController = navigationController.viewControllers.first as! SourcesViewController
|
||||
sourcesViewController.deepLinkSourceURL = sourceURL
|
||||
|
||||
case "finishJailbreak":
|
||||
guard let installedApp = sender as? InstalledApp else { return }
|
||||
|
||||
@@ -104,30 +109,19 @@ extension TabBarController
|
||||
{
|
||||
if let presentedViewController = self.presentedViewController
|
||||
{
|
||||
if let navigationController = presentedViewController as? UINavigationController,
|
||||
let sourcesViewController = navigationController.viewControllers.first as? SourcesViewController
|
||||
{
|
||||
if let notification = (sender as? Notification),
|
||||
let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
|
||||
{
|
||||
sourcesViewController.deepLinkSourceURL = sourceURL
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't dismiss SourcesViewController if it's already presented.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
presentedViewController.dismiss(animated: true) {
|
||||
self.presentSources(sender)
|
||||
}
|
||||
presentedViewController.dismiss(animated: true) {
|
||||
self.presentSources(sender)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if let notification = (sender as? Notification), let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
|
||||
{
|
||||
self.sourcesViewController?.deepLinkSourceURL = sourceURL
|
||||
}
|
||||
|
||||
self.performSegue(withIdentifier: "presentSources", sender: sender)
|
||||
self.selectedIndex = Tab.sources.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ import Foundation
|
||||
|
||||
public extension Date
|
||||
{
|
||||
private static let mediumDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
func numberOfCalendarDays(since date: Date) -> Int
|
||||
{
|
||||
let today = Calendar.current.startOfDay(for: self)
|
||||
@@ -19,15 +26,15 @@ public extension Date
|
||||
return components.day!
|
||||
}
|
||||
|
||||
func relativeDateString(since date: Date, dateFormatter: DateFormatter) -> String
|
||||
func relativeDateString(since date: Date, dateFormatter: DateFormatter? = nil) -> String
|
||||
{
|
||||
let dateFormatter = dateFormatter ?? Date.mediumDateFormatter
|
||||
let numberOfDays = self.numberOfCalendarDays(since: date)
|
||||
|
||||
switch numberOfDays
|
||||
{
|
||||
case 0: return NSLocalizedString("Today", comment: "")
|
||||
case 1: return NSLocalizedString("Yesterday", comment: "")
|
||||
case 2...7: return String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays))
|
||||
default: return dateFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
16
AltStoreCore/Extensions/ProcessInfo+Previews.swift
Normal file
16
AltStoreCore/Extensions/ProcessInfo+Previews.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// ProcessInfo+Previews.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 10/11/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension ProcessInfo
|
||||
{
|
||||
var isPreview: Bool {
|
||||
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,22 @@ public extension DatabaseManager
|
||||
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
|
||||
#if DEBUG
|
||||
// Wrap in #if DEBUG to *ensure* we never accidentally delete production databases.
|
||||
if ProcessInfo.processInfo.isPreview
|
||||
{
|
||||
do
|
||||
{
|
||||
print("!!! Purging database for preview...")
|
||||
try FileManager.default.removeItem(at: PersistentContainer.defaultDirectoryURL())
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove database directory for preview.", error)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if self.persistentContainer.isMigrationRequired
|
||||
{
|
||||
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
||||
@@ -166,6 +182,22 @@ public extension DatabaseManager
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
func startForPreview()
|
||||
{
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
self.dispatchQueue.async {
|
||||
self.startCompletionHandlers.append { error in
|
||||
semaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
_ = semaphore.wait(timeout: .now() + 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
var viewContext: NSManagedObjectContext {
|
||||
|
||||
@@ -236,6 +236,23 @@ public extension Source
|
||||
return isAdded
|
||||
}
|
||||
}
|
||||
|
||||
var isRecommended: Bool {
|
||||
guard let recommendedSources = UserDefaults.shared.recommendedSources else { return false }
|
||||
|
||||
// TODO: Support alternate URLs
|
||||
let isRecommended = recommendedSources.contains { source in
|
||||
return source.identifier == self.identifier || source.sourceURL?.absoluteString.lowercased() == self.sourceURL.absoluteString
|
||||
}
|
||||
return isRecommended
|
||||
}
|
||||
|
||||
var lastUpdatedDate: Date? {
|
||||
let allDates = self.apps.compactMap { $0.latestAvailableVersion?.date } + self.newsItems.map { $0.date }
|
||||
|
||||
let lastUpdatedDate = allDates.sorted().last
|
||||
return lastUpdatedDate
|
||||
}
|
||||
}
|
||||
|
||||
internal extension Source
|
||||
@@ -283,4 +300,14 @@ public extension Source
|
||||
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||
return source
|
||||
}
|
||||
|
||||
class func make(name: String, identifier: String, sourceURL: URL, context: NSManagedObjectContext) -> Source
|
||||
{
|
||||
let source = Source(context: context)
|
||||
source.name = name
|
||||
source.identifier = identifier
|
||||
source.sourceURL = sourceURL
|
||||
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct KnownSource: Decodable
|
||||
public struct KnownSource: Decodable
|
||||
{
|
||||
var identifier: String
|
||||
var sourceURL: URL?
|
||||
var bundleIDs: [String]?
|
||||
public var identifier: String
|
||||
public var sourceURL: URL?
|
||||
public var bundleIDs: [String]?
|
||||
}
|
||||
|
||||
private extension KnownSource
|
||||
@@ -42,19 +42,19 @@ private extension KnownSource
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults
|
||||
public extension UserDefaults
|
||||
{
|
||||
// Cache trusted sources just in case we need to check whether source is trusted or not.
|
||||
@nonobjc var trustedSources: [KnownSource]? {
|
||||
// Cache recommended sources just in case we need to check whether source is recommended or not.
|
||||
@nonobjc var recommendedSources: [KnownSource]? {
|
||||
get {
|
||||
guard let sources = _trustedSources?.compactMap({ KnownSource(dictionary: $0) }) else { return nil }
|
||||
guard let sources = _recommendedSources?.compactMap({ KnownSource(dictionary: $0) }) else { return nil }
|
||||
return sources
|
||||
}
|
||||
set {
|
||||
_trustedSources = newValue?.map { $0.dictionaryRepresentation }
|
||||
_recommendedSources = newValue?.map { $0.dictionaryRepresentation }
|
||||
}
|
||||
}
|
||||
@NSManaged @objc(trustedSources) private var _trustedSources: [[String: Any]]?
|
||||
@NSManaged @objc(recommendedSources) private var _recommendedSources: [[String: Any]]?
|
||||
|
||||
@nonobjc var blockedSources: [KnownSource]? {
|
||||
get {
|
||||
Reference in New Issue
Block a user