mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Merge pull request #202 from SideStore/pullrequests/jawshoeadan/develop
Merge all of Riley's new error handling stuff REBASED
This commit is contained in:
@@ -23,6 +23,9 @@
|
||||
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
|
||||
B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; };
|
||||
B3146ED3284F581E00BBC3FD /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F894295F7F9B002B1159 /* libfragmentzip.a */; };
|
||||
B33FFBAA295F8F78002259E6 /* preboard.c in Sources */ = {isa = PBXBuildFile; fileRef = B33FFBA9295F8F78002259E6 /* preboard.c */; };
|
||||
B33FFBAC295F8F98002259E6 /* companion_proxy.c in Sources */ = {isa = PBXBuildFile; fileRef = B33FFBAB295F8F98002259E6 /* companion_proxy.c */; };
|
||||
B343F858295F6331002B1159 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F84C295F6321002B1159 /* libminimuxer_static.a */; };
|
||||
B343F859295F6335002B1159 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F853295F6323002B1159 /* libem_proxy_static.a */; };
|
||||
B343F86D295F759E002B1159 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F86C295F759E002B1159 /* libresolv.tbd */; };
|
||||
@@ -35,9 +38,7 @@
|
||||
B343F883295F7C5D002B1159 /* thread.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F879295F7C5D002B1159 /* thread.c */; };
|
||||
B343F884295F7C5D002B1159 /* utils.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F87A295F7C5D002B1159 /* utils.c */; };
|
||||
B343F885295F7C5D002B1159 /* tlv.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F87B295F7C5D002B1159 /* tlv.c */; };
|
||||
B343F895295F7FAC002B1159 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F894295F7F9B002B1159 /* libfragmentzip.a */; };
|
||||
B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */; };
|
||||
B3919A52292DBE5400519575 /* ProgressRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D504F42528AD72C50014BB5D /* ProgressRing.swift */; };
|
||||
B39575F5284F29E20080B4FF /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B39575F4284F29E20080B4FF /* Roxas.framework */; };
|
||||
B39F16132918D7C5002E9404 /* Consts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39F16122918D7C5002E9404 /* Consts.swift */; };
|
||||
B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39F16142918D7DA002E9404 /* Consts+Proxy.swift */; };
|
||||
@@ -320,13 +321,17 @@
|
||||
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */; };
|
||||
BFF435D8255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */; };
|
||||
BFF615A82510042B00484D3B /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; };
|
||||
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
|
||||
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; };
|
||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; };
|
||||
D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; };
|
||||
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
|
||||
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; };
|
||||
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; };
|
||||
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; };
|
||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */; };
|
||||
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; };
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
|
||||
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; };
|
||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
|
||||
@@ -334,6 +339,8 @@
|
||||
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
||||
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */; };
|
||||
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; };
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
|
||||
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -506,6 +513,8 @@
|
||||
1920B04E2924AC8300744F60 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
|
||||
B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = "<group>"; };
|
||||
B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = "<group>"; };
|
||||
B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = "<group>"; };
|
||||
B343F847295F6321002B1159 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = minimuxer.xcodeproj; path = Dependencies/minimuxer.xcodeproj; sourceTree = SOURCE_ROOT; };
|
||||
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = em_proxy.xcodeproj; path = Dependencies/em_proxy.xcodeproj; sourceTree = SOURCE_ROOT; };
|
||||
B343F86C295F759E002B1159 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/usr/lib/libresolv.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -814,17 +823,21 @@
|
||||
BFF7C906257844C900E55F36 /* AltXPCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltXPCProtocol.h; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
D504F42528AD72C50014BB5D /* ProgressRing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressRing.swift; sourceTree = "<group>"; };
|
||||
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
|
||||
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; };
|
||||
D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = "<group>"; };
|
||||
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; };
|
||||
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
|
||||
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
|
||||
D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = "<group>"; };
|
||||
D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = "<group>"; };
|
||||
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = "<group>"; };
|
||||
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = "<group>"; };
|
||||
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogViewController.swift; sourceTree = "<group>"; };
|
||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; };
|
||||
D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; };
|
||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; };
|
||||
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = "<group>"; };
|
||||
@@ -833,6 +846,8 @@
|
||||
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
|
||||
D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTrustedSourcesOperation.swift; sourceTree = "<group>"; };
|
||||
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -898,7 +913,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B343F895295F7FAC002B1159 /* libfragmentzip.a in Frameworks */,
|
||||
B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */,
|
||||
191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */,
|
||||
191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */,
|
||||
19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */,
|
||||
@@ -965,6 +980,13 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B33FFB8F295F8CF2002259E6 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B343F848295F6321002B1159 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1084,6 +1106,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF4587D72298D3A800BD7491 /* afc.c */,
|
||||
B33FFBAB295F8F98002259E6 /* companion_proxy.c */,
|
||||
BF4587DF2298D3A900BD7491 /* debugserver.c */,
|
||||
BF4587E42298D3A900BD7491 /* device_link_service.c */,
|
||||
BF4587C92298D3A800BD7491 /* diagnostics_relay.c */,
|
||||
@@ -1100,6 +1123,7 @@
|
||||
BF4587DC2298D3A900BD7491 /* mobilebackup2.c */,
|
||||
BF4587F32298D3AA00BD7491 /* mobilesync.c */,
|
||||
BF4587CB2298D3A800BD7491 /* notification_proxy.c */,
|
||||
B33FFBA9295F8F78002259E6 /* preboard.c */,
|
||||
BF4587F42298D3AA00BD7491 /* property_list_service.c */,
|
||||
BF4587E62298D3A900BD7491 /* restore.c */,
|
||||
BF4587CC2298D3A800BD7491 /* sbservices.c */,
|
||||
@@ -1293,9 +1317,11 @@
|
||||
BF66EEC92501AECA007EE018 /* Account.swift */,
|
||||
BF66EEC72501AECA007EE018 /* AppID.swift */,
|
||||
BF66EEC62501AECA007EE018 /* AppPermission.swift */,
|
||||
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */,
|
||||
BF66EECA2501AECA007EE018 /* DatabaseManager.swift */,
|
||||
BF66EEC02501AECA007EE018 /* InstalledApp.swift */,
|
||||
BF66EECB2501AECA007EE018 /* InstalledExtension.swift */,
|
||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */,
|
||||
BF66EEC52501AECA007EE018 /* MergePolicy.swift */,
|
||||
BF66EEBF2501AECA007EE018 /* NewsItem.swift */,
|
||||
BF66EEC82501AECA007EE018 /* PatreonAccount.swift */,
|
||||
@@ -1323,6 +1349,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EEAE2501AECA007EE018 /* StoreAppPolicy.swift */,
|
||||
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */,
|
||||
BF66EEAF2501AECA007EE018 /* InstalledAppPolicy.swift */,
|
||||
);
|
||||
path = Policies;
|
||||
@@ -1339,6 +1366,7 @@
|
||||
BF66EEB62501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel */,
|
||||
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */,
|
||||
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */,
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */,
|
||||
);
|
||||
path = "Mapping Models";
|
||||
sourceTree = "<group>";
|
||||
@@ -1397,7 +1425,6 @@
|
||||
BF42345825101C1D006D1EB2 /* WidgetView.swift */,
|
||||
BF98917C250AAC4F002ACF50 /* Countdown.swift */,
|
||||
D55E163528776CB000A627A1 /* ComplicationView.swift */,
|
||||
D504F42528AD72C50014BB5D /* ProgressRing.swift */,
|
||||
BF989170250AABF4002ACF50 /* Assets.xcassets */,
|
||||
BF989172250AABF4002ACF50 /* Info.plist */,
|
||||
);
|
||||
@@ -1486,6 +1513,7 @@
|
||||
BFD247852284BB3300981D42 /* Frameworks */,
|
||||
B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */,
|
||||
BFD2476B2284B9A500981D42 /* Products */,
|
||||
B33FFB8F295F8CF2002259E6 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -1626,6 +1654,7 @@
|
||||
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */,
|
||||
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */,
|
||||
BFF0B695232242D3007A79E1 /* LicensesViewController.swift */,
|
||||
D589170128C7D93500E39C8B /* Error Log */,
|
||||
B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
@@ -1727,6 +1756,15 @@
|
||||
path = XPC;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D589170128C7D93500E39C8B /* Error Log */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
|
||||
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
|
||||
);
|
||||
path = "Error Log";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -2260,6 +2298,8 @@
|
||||
B343F881295F7C5D002B1159 /* termcolors.c in Sources */,
|
||||
B343F87E295F7C5D002B1159 /* collection.c in Sources */,
|
||||
BFD52C0F22A1A9CB000B7ED1 /* Real.cpp in Sources */,
|
||||
B33FFBAA295F8F78002259E6 /* preboard.c in Sources */,
|
||||
B33FFBAC295F8F98002259E6 /* companion_proxy.c in Sources */,
|
||||
BF4587FB2298D3AB00BD7491 /* notification_proxy.c in Sources */,
|
||||
BF4588352298D3C100BD7491 /* userpref.c in Sources */,
|
||||
BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */,
|
||||
@@ -2333,11 +2373,13 @@
|
||||
BF66EECF2501AECA007EE018 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||
BF66EEA82501AEC5007EE018 /* Patron.swift in Sources */,
|
||||
BF66EEDD2501AECA007EE018 /* AppPermission.swift in Sources */,
|
||||
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */,
|
||||
BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */,
|
||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
|
||||
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
|
||||
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */,
|
||||
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
|
||||
BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */,
|
||||
BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */,
|
||||
BF66EED92501AECA007EE018 /* Team.swift in Sources */,
|
||||
@@ -2348,10 +2390,12 @@
|
||||
BF66EEE02501AECA007EE018 /* Account.swift in Sources */,
|
||||
BF66EED52501AECA007EE018 /* AltStore.xcdatamodeld in Sources */,
|
||||
BFAECC582501B0A400528F27 /* ALTConstants.m in Sources */,
|
||||
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
|
||||
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
|
||||
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
|
||||
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
|
||||
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,
|
||||
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */,
|
||||
BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */,
|
||||
BF66EECC2501AECA007EE018 /* Source.swift in Sources */,
|
||||
BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */,
|
||||
@@ -2376,7 +2420,6 @@
|
||||
BF42345A25101C35006D1EB2 /* WidgetView.swift in Sources */,
|
||||
D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */,
|
||||
BF98917F250AAC4F002ACF50 /* AltWidget.swift in Sources */,
|
||||
B3919A52292DBE5400519575 /* ProgressRing.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2397,6 +2440,7 @@
|
||||
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
|
||||
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */,
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
|
||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
||||
@@ -2446,6 +2490,7 @@
|
||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
||||
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
|
||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */,
|
||||
@@ -3398,6 +3443,7 @@
|
||||
BF66EEB72501AECA007EE018 /* AltStore.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */,
|
||||
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */,
|
||||
BFBF33142526754700B7B8C9 /* AltStore 9.xcdatamodel */,
|
||||
BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */,
|
||||
@@ -3409,7 +3455,7 @@
|
||||
BF66EEBD2501AECA007EE018 /* AltStore 2.xcdatamodel */,
|
||||
BF66EEBE2501AECA007EE018 /* AltStore 4.xcdatamodel */,
|
||||
);
|
||||
currentVersion = D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */;
|
||||
currentVersion = D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */;
|
||||
path = AltStore.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
||||
@@ -14,13 +14,7 @@ import AppCenter
|
||||
import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
#if DEBUG
|
||||
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
|
||||
#elseif RELEASE
|
||||
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
|
||||
#else
|
||||
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
|
||||
#endif
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
|
||||
extension AnalyticsManager
|
||||
{
|
||||
|
||||
@@ -80,10 +80,21 @@ class AppContentViewController: UITableViewController
|
||||
|
||||
self.subtitleLabel.text = self.app.subtitle
|
||||
self.descriptionTextView.text = self.app.localizedDescription
|
||||
self.versionDescriptionTextView.text = self.app.versionDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
|
||||
|
||||
if let version = self.app.latestVersion
|
||||
{
|
||||
self.versionDescriptionTextView.text = version.localizedDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.versionDescriptionTextView.text = nil
|
||||
self.versionLabel.text = nil
|
||||
self.versionDateLabel.text = nil
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
||||
}
|
||||
|
||||
self.descriptionTextView.maximumNumberOfLines = 5
|
||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
|
||||
@@ -384,10 +384,10 @@ private extension AppViewController
|
||||
button.progress = progress
|
||||
}
|
||||
|
||||
if Date() < self.app.versionDate
|
||||
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
|
||||
{
|
||||
self.bannerView.button.countdownDate = self.app.versionDate
|
||||
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
||||
self.bannerView.button.countdownDate = versionDate
|
||||
self.navigationBarDownloadButton.countdownDate = versionDate
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -33,34 +33,34 @@ extension AppDelegate
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var intentHandler: IntentHandler {
|
||||
get { _intentHandler as! IntentHandler }
|
||||
set { _intentHandler = newValue }
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
private var viewAppIntentHandler: ViewAppIntentHandler {
|
||||
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
||||
set { _viewAppIntentHandler = newValue }
|
||||
}
|
||||
|
||||
|
||||
private lazy var _intentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return IntentHandler()
|
||||
}()
|
||||
|
||||
|
||||
private lazy var _viewAppIntentHandler: Any = {
|
||||
guard #available(iOS 14, *) else { fatalError() }
|
||||
return ViewAppIntentHandler()
|
||||
}()
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Register default settings before doing anything else.
|
||||
UserDefaults.registerDefaults()
|
||||
|
||||
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
@@ -71,50 +71,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
print("Started DatabaseManager.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AnalyticsManager.shared.start()
|
||||
|
||||
|
||||
self.setTintColor()
|
||||
|
||||
SecureValueTransformer.register()
|
||||
|
||||
|
||||
SecureValueTransformer.register()
|
||||
|
||||
if UserDefaults.standard.firstLaunch == nil
|
||||
{
|
||||
Keychain.shared.reset()
|
||||
UserDefaults.standard.firstLaunch = Date()
|
||||
}
|
||||
|
||||
|
||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||
|
||||
|
||||
#if DEBUG || BETA
|
||||
UserDefaults.standard.isDebugModeEnabled = true
|
||||
#endif
|
||||
|
||||
|
||||
self.prepareForBackgroundFetch()
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication)
|
||||
{
|
||||
{
|
||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication)
|
||||
{
|
||||
AppManager.shared.update()
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
}
|
||||
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
||||
{
|
||||
guard #available(iOS 14, *) else { return nil }
|
||||
|
||||
|
||||
switch intent
|
||||
{
|
||||
case is RefreshAllIntent: return self.intentHandler
|
||||
@@ -133,7 +145,7 @@ extension AppDelegate
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
||||
{
|
||||
// Called when the user discards a scene session.
|
||||
@@ -148,36 +160,36 @@ private extension AppDelegate
|
||||
{
|
||||
self.window?.tintColor = .altPrimary
|
||||
}
|
||||
|
||||
|
||||
func open(_ url: URL) -> Bool
|
||||
{
|
||||
if url.isFileURL
|
||||
{
|
||||
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let host = components.host?.lowercased() else { return false }
|
||||
|
||||
|
||||
switch host
|
||||
{
|
||||
case "patreon":
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "appbackupresponse":
|
||||
let result: Result<Void, Error>
|
||||
|
||||
|
||||
switch url.path.lowercased()
|
||||
{
|
||||
case "/success": result = .success(())
|
||||
@@ -188,37 +200,37 @@ private extension AppDelegate
|
||||
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||
let errorDescription = queryItems["errorDescription"]
|
||||
else { return false }
|
||||
|
||||
|
||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||
result = .failure(error)
|
||||
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "install":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
case "source":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -231,47 +243,47 @@ extension AppDelegate
|
||||
{
|
||||
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
||||
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
let tokenParts = deviceToken.map { data -> String in
|
||||
return String(format: "%02.2hhx", data)
|
||||
}
|
||||
|
||||
|
||||
let token = tokenParts.joined()
|
||||
print("Push Token:", token)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||
{
|
||||
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||
{
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
||||
{
|
||||
let threeHours: TimeInterval = 3 * 60 * 60
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
|
||||
|
||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||
if let error = taskResult.error
|
||||
{
|
||||
@@ -280,7 +292,7 @@ extension AppDelegate
|
||||
taskCompletionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if !DatabaseManager.shared.isStarted
|
||||
{
|
||||
DatabaseManager.shared.start() { (error) in
|
||||
@@ -309,7 +321,7 @@ extension AppDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
{
|
||||
@@ -319,15 +331,15 @@ extension AppDelegate
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
}
|
||||
|
||||
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
refreshAppsCompletionHandler(.success([:]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
||||
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
||||
@@ -343,49 +355,49 @@ private extension AppDelegate
|
||||
do
|
||||
{
|
||||
let (sources, context) = try result.get()
|
||||
|
||||
|
||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
||||
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
||||
|
||||
|
||||
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
||||
previousNewsItemsFetchRequest.includesPendingChanges = false
|
||||
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
||||
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
||||
|
||||
|
||||
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
||||
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
||||
|
||||
|
||||
try context.save()
|
||||
|
||||
|
||||
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
||||
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
|
||||
|
||||
let updates = try context.fetch(updatesFetchRequest)
|
||||
let newsItems = try context.fetch(newsItemsFetchRequest)
|
||||
|
||||
|
||||
for update in updates
|
||||
{
|
||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||
guard let storeApp = update.storeApp else { continue }
|
||||
|
||||
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
|
||||
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
|
||||
content.sound = .default
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
|
||||
for newsItem in newsItems
|
||||
{
|
||||
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
||||
guard !newsItem.isSilent else { continue }
|
||||
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
|
||||
if let app = newsItem.storeApp
|
||||
{
|
||||
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
||||
@@ -394,10 +406,10 @@ private extension AppDelegate
|
||||
{
|
||||
content.title = NSLocalizedString("SideStore News", comment: "")
|
||||
}
|
||||
|
||||
|
||||
content.body = newsItem.title
|
||||
content.sound = .default
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
@@ -405,7 +417,7 @@ private extension AppDelegate
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||
}
|
||||
|
||||
|
||||
completionHandler(.success(sources))
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -113,7 +113,7 @@ private extension BrowseViewController
|
||||
let progress = AppManager.shared.installationProgress(for: app)
|
||||
cell.bannerView.button.progress = progress
|
||||
|
||||
if Date() < app.versionDate
|
||||
if let versionDate = app.latestVersion?.date, versionDate > Date()
|
||||
{
|
||||
cell.bannerView.button.countdownDate = app.versionDate
|
||||
}
|
||||
|
||||
@@ -71,8 +71,10 @@ class CollapsingTextView: UITextView
|
||||
{
|
||||
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||
|
||||
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
|
||||
if self.intrinsicContentSize.height > maximumCollapsedHeight
|
||||
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
|
||||
|
||||
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||
{
|
||||
var exclusionFrame = moreButtonFrame
|
||||
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>http://ani.sidestore.io/</string>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
@@ -9,12 +11,10 @@
|
||||
</array>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>00008101-000129D63698001E</string>
|
||||
<key>ALTServerID</key>
|
||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
||||
<key>ALTServerID</key>
|
||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
@@ -93,6 +93,11 @@
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_altserver._tcp</string>
|
||||
@@ -133,11 +138,6 @@
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
||||
@@ -86,6 +86,7 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
||||
types.append(.xml)
|
||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||
documentPickerController.shouldShowFileExtensions = true
|
||||
documentPickerController.delegate = self
|
||||
self.present(documentPickerController, animated: true, completion: nil)
|
||||
})
|
||||
@@ -172,7 +173,19 @@ extension LaunchViewController
|
||||
{
|
||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
||||
let errorDescription: String
|
||||
|
||||
if #available(iOS 14.5, *)
|
||||
{
|
||||
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
||||
errorDescription = errorMessages.joined(separator: "\n\n")
|
||||
}
|
||||
else
|
||||
{
|
||||
errorDescription = error.debugDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
|
||||
@@ -1121,7 +1121,7 @@ private extension AppManager
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
presentingViewController.present(navigationController, animated: true, completion: nil)
|
||||
presentingViewController.present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -1223,7 +1223,7 @@ private extension AppManager
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
context.app = ALTApplication(fileURL: app.url)
|
||||
context.app = ALTApplication(fileURL: app.fileURL)
|
||||
|
||||
/* Fetch Provisioning Profiles */
|
||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||
@@ -1662,13 +1662,8 @@ private extension AppManager
|
||||
}
|
||||
|
||||
if #available(iOS 14, *)
|
||||
{
|
||||
WidgetCenter.shared.getCurrentConfigurations { (result) in
|
||||
guard case .success(let widgets) = result else { return }
|
||||
|
||||
guard let widget = widgets.first(where: { $0.configuration is ViewAppIntent }) else { return }
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
|
||||
}
|
||||
{
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
do { try installedApp.managedObjectContext?.save() }
|
||||
@@ -1677,6 +1672,8 @@ private extension AppManager
|
||||
catch
|
||||
{
|
||||
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
||||
|
||||
self.log(error, for: operation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1701,6 +1698,43 @@ private extension AppManager
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
func log(_ error: Error, for operation: AppOperation)
|
||||
{
|
||||
// Sanitize NSError on same thread before performing background task.
|
||||
let sanitizedError = (error as NSError).sanitizedForCoreData()
|
||||
|
||||
let loggedErrorOperation: LoggedError.Operation = {
|
||||
switch operation
|
||||
{
|
||||
case .install: return .install
|
||||
case .update: return .update
|
||||
case .refresh: return .refresh
|
||||
case .activate: return .activate
|
||||
case .deactivate: return .deactivate
|
||||
case .backup: return .backup
|
||||
case .restore: return .restore
|
||||
}
|
||||
}()
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
var app = operation.app
|
||||
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
|
||||
{
|
||||
app = tempApp
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
_ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context)
|
||||
try context.save()
|
||||
}
|
||||
catch let saveError
|
||||
{
|
||||
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
|
||||
{
|
||||
// Find "Install AltStore" operation if it already exists in `context`
|
||||
|
||||
@@ -186,7 +186,7 @@ private extension MyAppsViewController
|
||||
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
||||
{
|
||||
let fetchRequest = InstalledApp.updatesFetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, ascending: true),
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
|
||||
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
@@ -195,7 +195,7 @@ private extension MyAppsViewController
|
||||
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
|
||||
guard let self = self else { return }
|
||||
guard let app = installedApp.storeApp else { return }
|
||||
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
|
||||
|
||||
let cell = cell as! UpdateCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
@@ -209,7 +209,7 @@ private extension MyAppsViewController
|
||||
|
||||
cell.bannerView.configure(for: app)
|
||||
|
||||
let versionDate = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
|
||||
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
|
||||
cell.bannerView.subtitleLabel.text = versionDate
|
||||
|
||||
let appName: String
|
||||
@@ -223,7 +223,7 @@ private extension MyAppsViewController
|
||||
appName = app.name
|
||||
}
|
||||
|
||||
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, app.version, versionDate)
|
||||
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
|
||||
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||
|
||||
@@ -391,7 +391,7 @@ extension NewsViewController
|
||||
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||
footerView.bannerView.button.progress = progress
|
||||
|
||||
if Date() < storeApp.versionDate
|
||||
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
|
||||
{
|
||||
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
let context: AppOperationContext
|
||||
|
||||
private let bundleIdentifier: String
|
||||
private let sourceURL: URL
|
||||
private var sourceURL: URL?
|
||||
private let destinationURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
@@ -69,7 +69,9 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
self.downloadApp(from: self.sourceURL) { result in
|
||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
|
||||
self.downloadApp(from: sourceURL) { result in
|
||||
do
|
||||
{
|
||||
let application = try result.get()
|
||||
@@ -165,7 +167,7 @@ private extension DownloadAppOperation
|
||||
}
|
||||
}
|
||||
|
||||
if self.sourceURL.isFileURL
|
||||
if sourceURL.isFileURL
|
||||
{
|
||||
finishOperation(.success(sourceURL))
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
|
||||
|
||||
if let anisette = ALTAnisetteData(json: formattedJSON) {
|
||||
DLOG("Anisette used: %@", formattedJSON)
|
||||
self.finish(.success(anisette))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
let resignedBundleID = appExtension.bundleIdentifier
|
||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
||||
|
||||
print("`parentBundleID`: \(parentBundleID)")
|
||||
print("`resignedParentBundleID`: \(resignedParentBundleID)")
|
||||
print("`resignedBundleID`: \(resignedBundleID)")
|
||||
print("`originalBundleID`: \(originalBundleID)")
|
||||
|
||||
let installedExtension: InstalledExtension
|
||||
|
||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||
|
||||
@@ -11,6 +11,8 @@ import AltSign
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
{
|
||||
static let domain = OperationError.unknown._domain
|
||||
|
||||
case unknown
|
||||
case unknownResult
|
||||
case cancelled
|
||||
|
||||
@@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation<()>
|
||||
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url)
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||
|
||||
let ns_bundle = NSString(string: app.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
@@ -59,6 +60,7 @@ class SendAppOperation: ResultOperation<()>
|
||||
attempts -= 1
|
||||
}
|
||||
if res == 0 {
|
||||
print("minimuxer_yeet_app_afc `res` == \(res)")
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
|
||||
@@ -36,14 +36,14 @@
|
||||
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "An alternative App Store for iOS.",
|
||||
"version": "1.5.1b",
|
||||
"versionDate": "2022-05-27T12:00:00-07:00",
|
||||
"versionDescription": "This beta fixes the following issues:\n\n• Using Apple IDs that contain capital letters\n• Using Apple IDs with 2FA enabled without any trusted devices\n• Repeatedly asking some users to sign in every refresh\n• \"Incorrect Apple ID or password\" error after changing Apple ID email address\n• “Application is missing application-identifier” error when sideloading or (de-)activating certain apps",
|
||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_5_1_b.ipa",
|
||||
"version": "1.6b2",
|
||||
"versionDate": "2022-09-21T13:00:00-05:00",
|
||||
"versionDescription": "• Fixed “error migrating persistent store” issue on launch\n\nPREVIOUS VERSION\n\nLock Screen Widget (iOS 16+)\n• Counts down days until AltStore expires\n• Comes in 2 different styles: “icon” and “text”\n\nError Log\n• View past errors in more detail\n• Tap an error to copy the error message or error code\n• Search for error code directly in AltStore FAQ",
|
||||
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_6_b2.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 5464776,
|
||||
"size": 5465933,
|
||||
"beta": true,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
||||
|
||||
@@ -53,6 +53,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate
|
||||
|
||||
guard UIApplication.shared.applicationState == .background else { return }
|
||||
|
||||
// Make sure to update AppDelegate.applicationDidEnterBackground() as well.
|
||||
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
52
AltStore/Settings/Error Log/ErrorLogTableViewCell.swift
Normal file
52
AltStore/Settings/Error Log/ErrorLogTableViewCell.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// ErrorLogTableViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/9/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc(ErrorLogTableViewCell)
|
||||
class ErrorLogTableViewCell: UITableViewCell
|
||||
{
|
||||
@IBOutlet var appIconImageView: AppIconImageView!
|
||||
|
||||
@IBOutlet var dateLabel: UILabel!
|
||||
@IBOutlet var errorFailureLabel: UILabel!
|
||||
@IBOutlet var errorCodeLabel: UILabel!
|
||||
@IBOutlet var errorDescriptionTextView: CollapsingTextView!
|
||||
|
||||
@IBOutlet var menuButton: UIButton!
|
||||
|
||||
private var didLayoutSubviews = false
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
|
||||
{
|
||||
let moreButtonFrame = self.convert(self.errorDescriptionTextView.moreButton.frame, from: self.errorDescriptionTextView)
|
||||
guard moreButtonFrame.contains(point) else { return super.hitTest(point, with: event) }
|
||||
|
||||
// Pass touches through menuButton so user can press moreButton.
|
||||
return self.errorDescriptionTextView.moreButton
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
self.didLayoutSubviews = true
|
||||
}
|
||||
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||
{
|
||||
if !self.didLayoutSubviews
|
||||
{
|
||||
// Ensure cell is laid out so it will report correct size.
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
|
||||
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||
return size
|
||||
}
|
||||
}
|
||||
301
AltStore/Settings/Error Log/ErrorLogViewController.swift
Normal file
301
AltStore/Settings/Error Log/ErrorLogViewController.swift
Normal file
@@ -0,0 +1,301 @@
|
||||
//
|
||||
// ErrorLogViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class ErrorLogViewController: UITableViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private var expandedErrorIDs = Set<NSManagedObjectID>()
|
||||
|
||||
private lazy var timeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .none
|
||||
dateFormatter.timeStyle = .short
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.prefetchDataSource = self.dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension ErrorLogViewController
|
||||
{
|
||||
func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<LoggedError, UIImage>
|
||||
{
|
||||
let fetchRequest = LoggedError.fetchRequest() as NSFetchRequest<LoggedError>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)]
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(LoggedError.localizedDateString), cacheName: nil)
|
||||
|
||||
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<LoggedError, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.proxy = self
|
||||
dataSource.rowAnimation = .fade
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, loggedError, indexPath) in
|
||||
guard let self else { return }
|
||||
|
||||
let cell = cell as! ErrorLogTableViewCell
|
||||
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
|
||||
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
|
||||
|
||||
switch loggedError.domain
|
||||
{
|
||||
case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code))
|
||||
case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code))
|
||||
default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode
|
||||
}
|
||||
|
||||
let nsError = loggedError.error as NSError
|
||||
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||
cell.errorDescriptionTextView.text = errorDescription
|
||||
cell.errorDescriptionTextView.maximumNumberOfLines = 5
|
||||
cell.errorDescriptionTextView.isCollapsed = !self.expandedErrorIDs.contains(loggedError.objectID)
|
||||
cell.errorDescriptionTextView.moreButton.addTarget(self, action: #selector(ErrorLogViewController.toggleCollapsingCell(_:)), for: .primaryActionTriggered)
|
||||
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.appIconImageView.layer.borderColor = UIColor.gray.cgColor
|
||||
|
||||
let displayScale = (self.traitCollection.displayScale == 0.0) ? 1.0 : self.traitCollection.displayScale // 0.0 == "unspecified"
|
||||
cell.appIconImageView.layer.borderWidth = 1.0 / displayScale
|
||||
|
||||
if #available(iOS 14, *)
|
||||
{
|
||||
let menu = UIMenu(title: "", children: [
|
||||
UIAction(title: NSLocalizedString("Copy Error Message", comment: ""), image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in
|
||||
self?.copyErrorMessage(for: loggedError)
|
||||
},
|
||||
UIAction(title: NSLocalizedString("Copy Error Code", comment: ""), image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in
|
||||
self?.copyErrorCode(for: loggedError)
|
||||
},
|
||||
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
|
||||
self?.searchFAQ(for: loggedError)
|
||||
}
|
||||
])
|
||||
|
||||
cell.menuButton.menu = menu
|
||||
}
|
||||
|
||||
// Include errorDescriptionTextView's text in cell summary.
|
||||
cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ")
|
||||
|
||||
// Group all paragraphs together into single accessibility element (otherwise, each paragraph is independently selectable).
|
||||
cell.errorDescriptionTextView.accessibilityLabel = cell.errorDescriptionTextView.text
|
||||
}
|
||||
dataSource.prefetchHandler = { (loggedError, indexPath, completion) in
|
||||
RSTAsyncBlockOperation { (operation) in
|
||||
loggedError.managedObjectContext?.perform {
|
||||
if let installedApp = loggedError.installedApp
|
||||
{
|
||||
installedApp.loadIcon { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(nil, error)
|
||||
case .success(let image): completion(image, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let storeApp = loggedError.storeApp
|
||||
{
|
||||
ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completion(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(nil, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! ErrorLogTableViewCell
|
||||
cell.appIconImageView.image = image
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.text = NSLocalizedString("No Errors", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("Errors that occur when sideloading or refreshing apps will appear here.", comment: "")
|
||||
dataSource.placeholderView = placeholderView
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension ErrorLogViewController
|
||||
{
|
||||
@IBAction func toggleCollapsingCell(_ sender: UIButton)
|
||||
{
|
||||
let point = self.tableView.convert(sender.center, from: sender.superview)
|
||||
guard let indexPath = self.tableView.indexPathForRow(at: point), let cell = self.tableView.cellForRow(at: indexPath) as? ErrorLogTableViewCell else { return }
|
||||
|
||||
let loggedError = self.dataSource.item(at: indexPath)
|
||||
|
||||
if cell.errorDescriptionTextView.isCollapsed
|
||||
{
|
||||
self.expandedErrorIDs.remove(loggedError.objectID)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.expandedErrorIDs.insert(loggedError.objectID)
|
||||
}
|
||||
|
||||
self.tableView.performBatchUpdates {
|
||||
cell.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem)
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Error Log", comment: ""), style: .destructive) { _ in
|
||||
self.clearLoggedErrors()
|
||||
})
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
func clearLoggedErrors()
|
||||
{
|
||||
DatabaseManager.shared.purgeLoggedErrors { result in
|
||||
do
|
||||
{
|
||||
try result.get()
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Clear Error Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyErrorMessage(for loggedError: LoggedError)
|
||||
{
|
||||
let nsError = loggedError.error as NSError
|
||||
let errorMessage = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||
|
||||
UIPasteboard.general.string = errorMessage
|
||||
}
|
||||
|
||||
func copyErrorCode(for loggedError: LoggedError)
|
||||
{
|
||||
let errorCode = loggedError.error.localizedErrorCode
|
||||
UIPasteboard.general.string = errorCode
|
||||
}
|
||||
|
||||
func searchFAQ(for loggedError: LoggedError)
|
||||
{
|
||||
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
|
||||
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||
|
||||
let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+")
|
||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
||||
|
||||
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
|
||||
safariViewController.preferredControlTintColor = .altPrimary
|
||||
self.present(safariViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension ErrorLogViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let loggedError = self.dataSource.item(at: indexPath)
|
||||
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy Error Message", comment: ""), style: .default) { [weak self] _ in
|
||||
self?.copyErrorMessage(for: loggedError)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy Error Code", comment: ""), style: .default) { [weak self] _ in
|
||||
self?.copyErrorCode(for: loggedError)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Search FAQ", comment: ""), style: .default) { [weak self] _ in
|
||||
self?.searchFAQ(for: loggedError)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
})
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
|
||||
{
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { _, _, completion in
|
||||
let loggedError = self.dataSource.item(at: indexPath)
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
do
|
||||
{
|
||||
let loggedError = context.object(with: loggedError.objectID) as! LoggedError
|
||||
context.delete(loggedError)
|
||||
|
||||
try context.save()
|
||||
completion(true)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("[ALTLog] Failed to delete LoggedError \(loggedError.objectID):", error)
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
|
||||
configuration.performsFirstActionWithFullSwipe = false
|
||||
return configuration
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
let indexPath = IndexPath(row: 0, section: section)
|
||||
let loggedError = self.dataSource.item(at: indexPath)
|
||||
|
||||
if Calendar.current.isDateInToday(loggedError.date)
|
||||
{
|
||||
return NSLocalizedString("Today", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
return loggedError.localizedDateString
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,8 @@ private extension PatreonViewController
|
||||
|
||||
@objc func didUpdatePatrons(_ notification: Notification)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
// Wait short delay before reloading or else footer won't properly update if it's already visible 🤷♂️
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
@@ -270,17 +271,27 @@ extension PatreonViewController
|
||||
footerView.button.isHidden = false
|
||||
//footerView.button.addTarget(self, action: #selector(PatreonViewController.fetchPatrons), for: .primaryActionTriggered)
|
||||
|
||||
if self.patronsDataSource.itemCount > 0
|
||||
switch AppManager.shared.updatePatronsResult
|
||||
{
|
||||
footerView.button.isHidden = true
|
||||
}
|
||||
else
|
||||
{
|
||||
switch AppManager.shared.updatePatronsResult
|
||||
case .none: footerView.button.isIndicatingActivity = true
|
||||
case .success?: footerView.button.isHidden = true
|
||||
case .failure?:
|
||||
#if DEBUG
|
||||
let debug = true
|
||||
#else
|
||||
let debug = false
|
||||
#endif
|
||||
|
||||
if self.patronsDataSource.itemCount == 0 || debug
|
||||
{
|
||||
case .none: footerView.button.isIndicatingActivity = true
|
||||
case .success?: footerView.button.isHidden = true
|
||||
case .failure?: footerView.button.setTitle(NSLocalizedString("Error Loading Patrons", comment: ""), for: .normal)
|
||||
// Only show error message if there aren't any cached Patrons (or if this is a debug build).
|
||||
|
||||
footerView.button.isHidden = false
|
||||
footerView.button.setTitle(NSLocalizedString("Error Loading Patrons", comment: ""), for: .normal)
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.button.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17503.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17502"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
|
||||
<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"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -540,7 +541,7 @@
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="3"/>
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
@@ -548,6 +549,42 @@
|
||||
<segue destination="GBh-rB-juy" kind="show" identifier="showRefreshAttempts" id="K2i-nF-6qa"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="972" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWC-OG-5jx">
|
||||
<rect key="frame" x="30" y="15.5" width="119" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="PWC-OG-5jx" firstAttribute="leading" secondItem="qIT-rz-ZUb" secondAttribute="leadingMargin" id="BQr-Nx-fIq"/>
|
||||
<constraint firstItem="PWC-OG-5jx" firstAttribute="centerY" secondItem="qIT-rz-ZUb" secondAttribute="centerY" id="IDa-ov-tmK"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="VfB-c5-5wG" secondAttribute="trailing" id="Q6c-iP-6bi"/>
|
||||
<constraint firstItem="VfB-c5-5wG" firstAttribute="centerY" secondItem="qIT-rz-ZUb" secondAttribute="centerY" id="WuL-ax-fFw"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="3"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="SSW-vL-86I"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
@@ -842,6 +879,123 @@ Settings by i cons from the Noun Project</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1697" y="-199"/>
|
||||
</scene>
|
||||
<!--Error Log-->
|
||||
<scene sceneID="Htu-2V-dbE">
|
||||
<objects>
|
||||
<tableViewController id="g8a-Rf-zWa" customClass="ErrorLogViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="BBn-tI-e0e">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="HAm-mA-O78" customClass="ErrorLogTableViewCell">
|
||||
<rect key="frame" x="16" y="55.5" width="343" height="107.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="HAm-mA-O78" id="swa-et-rfA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="mtw-JM-T70">
|
||||
<rect key="frame" x="16" y="11" width="311" height="85.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="bjU-TX-4lm" userLabel="Compact">
|
||||
<rect key="frame" x="0.0" y="0.0" width="311" height="43.5"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sDZ-ZN-NT1" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="43" height="43"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="sDZ-ZN-NT1" secondAttribute="height" multiplier="1:1" id="M8a-Wh-6wd"/>
|
||||
<constraint firstAttribute="width" constant="43" id="dgV-jc-GET"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="82d-v0-RCp">
|
||||
<rect key="frame" x="51" y="0.0" width="260" height="39"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Q2j-Tc-bp2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="260" height="18"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Success" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Na7-uj-XYZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SGf-pP-RL0">
|
||||
<rect key="frame" x="229.5" y="0.0" width="30.5" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Error Code" textAlignment="natural" lineBreakMode="headTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="R5a-wv-xHd">
|
||||
<rect key="frame" x="0.0" y="22" width="260" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Error Description" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1df-ri-hKN" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="51.5" width="311" height="34"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES"/>
|
||||
<bool key="isElement" value="NO"/>
|
||||
</accessibility>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="NO"/>
|
||||
</accessibility>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="plain" title=" "/>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ba2-EY-tf5" firstAttribute="leading" secondItem="swa-et-rfA" secondAttribute="leading" id="70b-ce-vg1"/>
|
||||
<constraint firstItem="ba2-EY-tf5" firstAttribute="top" secondItem="swa-et-rfA" secondAttribute="top" id="98S-pF-R8J"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="mtw-JM-T70" secondAttribute="bottom" id="fmH-Tj-9iY"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="mtw-JM-T70" secondAttribute="trailing" id="fu1-uu-mXb"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ba2-EY-tf5" secondAttribute="bottom" id="kvX-B0-v1x"/>
|
||||
<constraint firstAttribute="leadingMargin" secondItem="mtw-JM-T70" secondAttribute="leading" id="mIY-lM-64i"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ba2-EY-tf5" secondAttribute="trailing" id="qnx-qR-VAH"/>
|
||||
<constraint firstAttribute="topMargin" secondItem="mtw-JM-T70" secondAttribute="top" id="wOS-QI-w47"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="appIconImageView" destination="sDZ-ZN-NT1" id="5Fb-6X-vNV"/>
|
||||
<outlet property="dateLabel" destination="SGf-pP-RL0" id="jCW-Ib-QGv"/>
|
||||
<outlet property="errorCodeLabel" destination="R5a-wv-xHd" id="AeF-Yh-OVe"/>
|
||||
<outlet property="errorDescriptionTextView" destination="1df-ri-hKN" id="s4Z-Id-iS8"/>
|
||||
<outlet property="errorFailureLabel" destination="Na7-uj-XYZ" id="c6r-XP-oIL"/>
|
||||
<outlet property="menuButton" destination="ba2-EY-tf5" id="GjL-NZ-KuX"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="g8a-Rf-zWa" id="3Tb-tm-jjW"/>
|
||||
<outlet property="delegate" destination="g8a-Rf-zWa" id="mbI-k7-Dqq"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Error Log" largeTitleDisplayMode="never" id="a1p-3W-bSi">
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="trash" id="BnQ-Eh-1gC">
|
||||
<connections>
|
||||
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1697" y="1774"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Next" width="18" height="18"/>
|
||||
@@ -852,5 +1006,8 @@ Settings by i cons from the Noun Project</string>
|
||||
<systemColor name="darkTextColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="labelColor">
|
||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -52,6 +52,7 @@ extension SettingsViewController
|
||||
{
|
||||
case sendFeedback
|
||||
case refreshAttempts
|
||||
case errorLog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +503,7 @@ extension SettingsViewController
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
case .refreshAttempts: break
|
||||
case .refreshAttempts, .errorLog: break
|
||||
}
|
||||
|
||||
default: break
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>AltStore 10.xcdatamodel</string>
|
||||
<string>AltStore 11.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
|
||||
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="version"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="appName" attributeType="String"/>
|
||||
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="operation" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
116
AltStoreCore/Model/AppVersion.swift
Normal file
116
AltStoreCore/Model/AppVersion.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// AppVersion.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 8/18/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(AppVersion)
|
||||
public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public var version: String
|
||||
@NSManaged public var date: Date
|
||||
@NSManaged public var localizedDescription: String?
|
||||
|
||||
@NSManaged public var downloadURL: URL
|
||||
@NSManaged public var size: Int64
|
||||
|
||||
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = self._minOSVersion else { return nil }
|
||||
|
||||
let osVersion = OperatingSystemVersion(string: osVersionString)
|
||||
return osVersion
|
||||
}
|
||||
@NSManaged @objc(minOSVersion) private var _minOSVersion: String?
|
||||
|
||||
@nonobjc public var maxOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = self._maxOSVersion else { return nil }
|
||||
|
||||
let osVersion = OperatingSystemVersion(string: osVersionString)
|
||||
return osVersion
|
||||
}
|
||||
@NSManaged @objc(maxOSVersion) private var _maxOSVersion: String?
|
||||
|
||||
@NSManaged public var appBundleID: String
|
||||
@NSManaged public var sourceID: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var app: StoreApp?
|
||||
@NSManaged public private(set) var latestVersionApp: StoreApp?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case version
|
||||
case date
|
||||
case localizedDescription
|
||||
case downloadURL
|
||||
case size
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: AppVersion.entity(), insertInto: context)
|
||||
|
||||
do
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.version = try container.decode(String.self, forKey: .version)
|
||||
self.date = try container.decode(Date.self, forKey: .date)
|
||||
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
|
||||
|
||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
self.size = try container.decode(Int64.self, forKey: .size)
|
||||
}
|
||||
catch
|
||||
{
|
||||
if let context = self.managedObjectContext
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppVersion
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppVersion>
|
||||
{
|
||||
return NSFetchRequest<AppVersion>(entityName: "AppVersion")
|
||||
}
|
||||
|
||||
class func makeAppVersion(
|
||||
version: String,
|
||||
date: Date,
|
||||
localizedDescription: String? = nil,
|
||||
downloadURL: URL,
|
||||
size: Int64,
|
||||
appBundleID: String,
|
||||
sourceID: String? = nil,
|
||||
in context: NSManagedObjectContext) -> AppVersion
|
||||
{
|
||||
let appVersion = AppVersion(context: context)
|
||||
appVersion.version = version
|
||||
appVersion.date = date
|
||||
appVersion.localizedDescription = localizedDescription
|
||||
appVersion.downloadURL = downloadURL
|
||||
appVersion.size = size
|
||||
appVersion.appBundleID = appBundleID
|
||||
appVersion.sourceID = sourceID
|
||||
|
||||
return appVersion
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,15 @@ import CoreData
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
extension CFNotificationName
|
||||
{
|
||||
fileprivate static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
|
||||
}
|
||||
|
||||
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
|
||||
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
|
||||
}
|
||||
|
||||
fileprivate class PersistentContainer: RSTPersistentContainer
|
||||
{
|
||||
override class func defaultDirectoryURL() -> URL
|
||||
@@ -43,10 +52,15 @@ public class DatabaseManager
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
private var ignoreWillAccessDatabaseNotification = false
|
||||
|
||||
private init()
|
||||
{
|
||||
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
||||
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
||||
|
||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +87,10 @@ public extension DatabaseManager
|
||||
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
|
||||
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
||||
self.ignoreWillAccessDatabaseNotification = true
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
|
||||
|
||||
self.migrateDatabaseToAppGroupIfNeeded { (result) in
|
||||
switch result
|
||||
{
|
||||
@@ -122,6 +140,27 @@ public extension DatabaseManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.persistentContainer.performBackgroundTask { context in
|
||||
do
|
||||
{
|
||||
let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) }
|
||||
|
||||
let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true])
|
||||
loggedErrors.forEach { context.delete($0) }
|
||||
|
||||
try context.save()
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
@@ -129,10 +168,7 @@ public extension DatabaseManager
|
||||
var viewContext: NSManagedObjectContext {
|
||||
return self.persistentContainer.viewContext
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
|
||||
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
|
||||
@@ -193,7 +229,7 @@ private extension DatabaseManager
|
||||
else
|
||||
{
|
||||
storeApp = StoreApp.makeAltStoreApp(in: context)
|
||||
storeApp.version = localApp.version
|
||||
storeApp.latestVersion?.version = localApp.version
|
||||
storeApp.source = altStoreSource
|
||||
}
|
||||
|
||||
@@ -380,4 +416,14 @@ private extension DatabaseManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivedWillAccessDatabaseNotification()
|
||||
{
|
||||
defer { self.ignoreWillAccessDatabaseNotification = false }
|
||||
|
||||
// Ignore notifications sent by the current process.
|
||||
guard !self.ignoreWillAccessDatabaseNotification else { return }
|
||||
|
||||
exit(104)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
@NSManaged public var team: Team?
|
||||
@NSManaged public var appExtensions: Set<InstalledExtension>
|
||||
|
||||
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
||||
|
||||
public var isSideloaded: Bool {
|
||||
return self.storeApp == nil
|
||||
}
|
||||
@@ -77,6 +79,8 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
|
||||
self.bundleIdentifier = originalBundleIdentifier
|
||||
|
||||
print("InstalledApp `self.bundleIdentifier`: \(self.bundleIdentifier)")
|
||||
|
||||
self.refreshedDate = Date()
|
||||
self.installedDate = Date()
|
||||
|
||||
@@ -144,7 +148,7 @@ public extension InstalledApp
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestVersion.version))
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
@@ -152,13 +156,14 @@ public extension InstalledApp
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
|
||||
print("Active Apps Fetch Request: \(String(describing: fetchRequest.predicate))")
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
print("Fetch 'AltStore' Predicate: \(String(describing: predicate))")
|
||||
let altStore = InstalledApp.first(satisfying: predicate, in: context)
|
||||
return altStore
|
||||
}
|
||||
@@ -172,6 +177,7 @@ public extension InstalledApp
|
||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||
{
|
||||
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
|
||||
|
||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
// {
|
||||
@@ -205,6 +211,7 @@ public extension InstalledApp
|
||||
#keyPath(InstalledApp.isActive),
|
||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
print("Active Apps For Background Refresh 'AltStore' predicate: \(String(describing: predicate))")
|
||||
|
||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
// {
|
||||
@@ -251,14 +258,15 @@ public extension InstalledApp
|
||||
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
||||
|
||||
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { print(error) }
|
||||
|
||||
catch { print("Creating App Directory Error: \(error)") }
|
||||
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
|
||||
return appsDirectoryURL
|
||||
}
|
||||
|
||||
class var legacyAppsDirectoryURL: URL {
|
||||
let baseDirectory = FileManager.default.applicationSupportDirectory
|
||||
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
||||
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
|
||||
return appsDirectoryURL
|
||||
}
|
||||
|
||||
@@ -271,6 +279,7 @@ public extension InstalledApp
|
||||
class func refreshedIPAURL(for app: AppProtocol) -> URL
|
||||
{
|
||||
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
||||
print("`ipaURL`: \(ipaURL.absoluteString)")
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
|
||||
126
AltStoreCore/Model/LoggedError.swift
Normal file
126
AltStoreCore/Model/LoggedError.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// LoggedError.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
extension LoggedError
|
||||
{
|
||||
public enum Operation: String
|
||||
{
|
||||
case install
|
||||
case update
|
||||
case refresh
|
||||
case activate
|
||||
case deactivate
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
@objc(LoggedError)
|
||||
public class LoggedError: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var date: Date
|
||||
|
||||
@nonobjc public var operation: Operation? {
|
||||
guard let rawOperation = self._operation else { return nil }
|
||||
|
||||
let operation = Operation(rawValue: rawOperation)
|
||||
return operation
|
||||
}
|
||||
@NSManaged @objc(operation) private var _operation: String?
|
||||
|
||||
@NSManaged public private(set) var domain: String
|
||||
@NSManaged public private(set) var code: Int32
|
||||
@NSManaged public private(set) var userInfo: [String: Any]
|
||||
|
||||
@NSManaged public private(set) var appName: String
|
||||
@NSManaged public private(set) var appBundleID: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var storeApp: StoreApp?
|
||||
@NSManaged public private(set) var installedApp: InstalledApp?
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .long
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
public init(error: Error, app: AppProtocol, date: Date = Date(), operation: Operation? = nil, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: LoggedError.entity(), insertInto: context)
|
||||
|
||||
self.date = date
|
||||
self._operation = operation?.rawValue
|
||||
|
||||
let nsError = error as NSError
|
||||
self.domain = nsError.domain
|
||||
self.code = Int32(nsError.code)
|
||||
self.userInfo = nsError.userInfo
|
||||
|
||||
self.appName = app.name
|
||||
self.appBundleID = app.bundleIdentifier
|
||||
|
||||
switch app
|
||||
{
|
||||
case let storeApp as StoreApp: self.storeApp = storeApp
|
||||
case let installedApp as InstalledApp: self.installedApp = installedApp
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension LoggedError
|
||||
{
|
||||
var app: AppProtocol {
|
||||
// `as AppProtocol` needed to fix "cannot convert AnyApp to StoreApp" compiler error with Xcode 14.
|
||||
let app = self.installedApp ?? self.storeApp ?? AnyApp(name: self.appName, bundleIdentifier: self.appBundleID, url: nil) as AppProtocol
|
||||
return app
|
||||
}
|
||||
|
||||
var error: Error {
|
||||
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
|
||||
return nsError
|
||||
}
|
||||
|
||||
@objc
|
||||
var localizedDateString: String {
|
||||
let localizedDateString = LoggedError.dateFormatter.string(from: self.date)
|
||||
return localizedDateString
|
||||
}
|
||||
|
||||
var localizedFailure: String? {
|
||||
guard let operation = self.operation else { return nil }
|
||||
switch operation
|
||||
{
|
||||
case .install: return String(format: NSLocalizedString("Install %@ Failed", comment: ""), self.appName)
|
||||
case .update: return String(format: NSLocalizedString("Update %@ Failed", comment: ""), self.appName)
|
||||
case .refresh: return String(format: NSLocalizedString("Refresh %@ Failed", comment: ""), self.appName)
|
||||
case .activate: return String(format: NSLocalizedString("Activate %@ Failed", comment: ""), self.appName)
|
||||
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName)
|
||||
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName)
|
||||
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension LoggedError
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<LoggedError>
|
||||
{
|
||||
return NSFetchRequest<LoggedError>(entityName: "LoggedError")
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,24 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
// Delete previous versions (different than below).
|
||||
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil
|
||||
{
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
}
|
||||
|
||||
case is AppVersion where conflict.conflictingObjects.count == 2:
|
||||
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
|
||||
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
||||
|
||||
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
||||
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
|
||||
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
|
||||
{
|
||||
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
||||
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -55,6 +73,16 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
if let contextApp = conflict.conflictingObjects.first as? StoreApp
|
||||
{
|
||||
let contextVersions = Set(contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
|
||||
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
|
||||
{
|
||||
print("[ALTLog] Deleting cached app version: \(appVersion.appBundleID + "_" + appVersion.version), not in:", contextApp.versions.map { $0.appBundleID + "_" + $0.version })
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
}
|
||||
|
||||
case let databaseObject as Source:
|
||||
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// StoreApp10ToStoreApp11Policy.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 9/13/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
// Can't use NSManagedObject subclasses, so add convenience accessors for KVC.
|
||||
fileprivate extension NSManagedObject
|
||||
{
|
||||
var storeAppBundleID: String? {
|
||||
let bundleID = self.value(forKey: #keyPath(StoreApp.bundleIdentifier)) as? String
|
||||
return bundleID
|
||||
}
|
||||
|
||||
var storeAppSourceID: String? {
|
||||
let sourceID = self.value(forKey: #keyPath(StoreApp.sourceIdentifier)) as? String
|
||||
return sourceID
|
||||
}
|
||||
|
||||
var storeAppVersion: String? {
|
||||
let version = self.value(forKey: #keyPath(StoreApp._version)) as? String
|
||||
return version
|
||||
}
|
||||
|
||||
var storeAppVersionDate: Date? {
|
||||
let versionDate = self.value(forKey: #keyPath(StoreApp._versionDate)) as? Date
|
||||
return versionDate
|
||||
}
|
||||
|
||||
var storeAppVersionDescription: String? {
|
||||
let versionDescription = self.value(forKey: #keyPath(StoreApp._versionDescription)) as? String
|
||||
return versionDescription
|
||||
}
|
||||
|
||||
var storeAppSize: NSNumber? {
|
||||
let size = self.value(forKey: #keyPath(StoreApp._size)) as? NSNumber
|
||||
return size
|
||||
}
|
||||
|
||||
var storeAppDownloadURL: URL? {
|
||||
let downloadURL = self.value(forKey: #keyPath(StoreApp._downloadURL)) as? URL
|
||||
return downloadURL
|
||||
}
|
||||
|
||||
func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
|
||||
{
|
||||
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion))
|
||||
|
||||
let versions = NSOrderedSet(array: [appVersion])
|
||||
self.setValue(versions, forKey: #keyPath(StoreApp._versions))
|
||||
}
|
||||
|
||||
class func makeAppVersion(version: String,
|
||||
date: Date,
|
||||
localizedDescription: String?,
|
||||
downloadURL: URL,
|
||||
size: Int64,
|
||||
appBundleID: String,
|
||||
sourceID: String,
|
||||
in context: NSManagedObjectContext) -> NSManagedObject
|
||||
{
|
||||
let appVersion = NSEntityDescription.insertNewObject(forEntityName: AppVersion.entity().name!, into: context)
|
||||
appVersion.setValue(version, forKey: #keyPath(AppVersion.version))
|
||||
appVersion.setValue(date, forKey: #keyPath(AppVersion.date))
|
||||
appVersion.setValue(localizedDescription, forKey: #keyPath(AppVersion.localizedDescription))
|
||||
appVersion.setValue(downloadURL, forKey: #keyPath(AppVersion.downloadURL))
|
||||
appVersion.setValue(size, forKey: #keyPath(AppVersion.size))
|
||||
appVersion.setValue(appBundleID, forKey: #keyPath(AppVersion.appBundleID))
|
||||
appVersion.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
|
||||
return appVersion
|
||||
}
|
||||
}
|
||||
|
||||
@objc(StoreApp10ToStoreApp11Policy)
|
||||
class StoreApp10ToStoreApp11Policy: NSEntityMigrationPolicy
|
||||
{
|
||||
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
|
||||
{
|
||||
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
|
||||
|
||||
guard let appBundleID = sInstance.storeAppBundleID,
|
||||
let sourceID = sInstance.storeAppSourceID,
|
||||
let version = sInstance.storeAppVersion,
|
||||
let versionDate = sInstance.storeAppVersionDate,
|
||||
// let versionDescription = sInstance.storeAppVersionDescription, // Optional
|
||||
let downloadURL = sInstance.storeAppDownloadURL,
|
||||
let size = sInstance.storeAppSize as? Int64
|
||||
else { return }
|
||||
|
||||
guard
|
||||
let destinationStoreApp = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first,
|
||||
let context = destinationStoreApp.managedObjectContext
|
||||
else { fatalError("A destination StoreApp and its managedObjectContext must exist.") }
|
||||
|
||||
let appVersion = NSManagedObject.makeAppVersion(
|
||||
version: version,
|
||||
date: versionDate,
|
||||
localizedDescription: sInstance.storeAppVersionDescription,
|
||||
downloadURL: downloadURL,
|
||||
size: Int64(size),
|
||||
appBundleID: appBundleID,
|
||||
sourceID: sourceID,
|
||||
in: context)
|
||||
|
||||
destinationStoreApp.setStoreAppLatestVersion(appVersion)
|
||||
}
|
||||
}
|
||||
@@ -103,22 +103,41 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
@NSManaged public private(set) var developerName: String
|
||||
@NSManaged public private(set) var localizedDescription: String
|
||||
@NSManaged public private(set) var size: Int32
|
||||
@NSManaged @objc(size) internal var _size: Int32
|
||||
|
||||
@NSManaged public private(set) var iconURL: URL
|
||||
@NSManaged public private(set) var screenshotURLs: [URL]
|
||||
|
||||
@NSManaged public var version: String
|
||||
@NSManaged public private(set) var versionDate: Date
|
||||
@NSManaged public private(set) var versionDescription: String?
|
||||
@NSManaged @objc(version) internal var _version: String
|
||||
@NSManaged @objc(versionDate) internal var _versionDate: Date
|
||||
@NSManaged @objc(versionDescription) internal var _versionDescription: String?
|
||||
|
||||
@NSManaged public private(set) var downloadURL: URL
|
||||
@NSManaged @objc(downloadURL) internal var _downloadURL: URL
|
||||
@NSManaged public private(set) var platformURLs: PlatformURLs?
|
||||
|
||||
@NSManaged public private(set) var tintColor: UIColor?
|
||||
@NSManaged public private(set) var isBeta: Bool
|
||||
|
||||
@NSManaged public var sourceIdentifier: String?
|
||||
@objc public internal(set) var sourceIdentifier: String? {
|
||||
get {
|
||||
self.willAccessValue(forKey: #keyPath(sourceIdentifier))
|
||||
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
|
||||
|
||||
let sourceIdentifier = self.primitiveSourceIdentifier
|
||||
return sourceIdentifier
|
||||
}
|
||||
set {
|
||||
self.willChangeValue(forKey: #keyPath(sourceIdentifier))
|
||||
self.primitiveSourceIdentifier = newValue
|
||||
self.didChangeValue(forKey: #keyPath(sourceIdentifier))
|
||||
|
||||
for version in self.versions
|
||||
{
|
||||
version.sourceID = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@NSManaged private var primitiveSourceIdentifier: String?
|
||||
|
||||
@NSManaged public var sortIndex: Int32
|
||||
|
||||
@@ -129,6 +148,11 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged @objc(source) public var _source: Source?
|
||||
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
|
||||
|
||||
@NSManaged public private(set) var latestVersion: AppVersion?
|
||||
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
||||
|
||||
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
||||
|
||||
@nonobjc public var source: Source? {
|
||||
set {
|
||||
self._source = newValue
|
||||
@@ -143,6 +167,35 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
return self._permissions.array as! [AppPermission]
|
||||
}
|
||||
|
||||
@nonobjc public var versions: [AppVersion] {
|
||||
return self._versions.array as! [AppVersion]
|
||||
}
|
||||
|
||||
@nonobjc public var size: Int64? {
|
||||
guard let version = self.latestVersion else { return nil }
|
||||
return version.size
|
||||
}
|
||||
|
||||
@nonobjc public var version: String? {
|
||||
guard let version = self.latestVersion else { return nil }
|
||||
return version.version
|
||||
}
|
||||
|
||||
@nonobjc public var versionDescription: String? {
|
||||
guard let version = self.latestVersion else { return nil }
|
||||
return version.localizedDescription
|
||||
}
|
||||
|
||||
@nonobjc public var versionDate: Date? {
|
||||
guard let version = self.latestVersion else { return nil }
|
||||
return version.date
|
||||
}
|
||||
|
||||
@nonobjc public var downloadURL: URL? {
|
||||
guard let version = self.latestVersion else { return nil }
|
||||
return version.downloadURL
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
@@ -166,6 +219,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case permissions
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
case versions
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
@@ -185,10 +239,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
|
||||
self.version = try container.decode(String.self, forKey: .version)
|
||||
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||
|
||||
@@ -198,14 +248,14 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
self.platformURLs = platformURLs
|
||||
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
|
||||
if let first = platformURLs.sorted().first {
|
||||
self.downloadURL = first.downloadURL
|
||||
self._downloadURL = first.downloadURL
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
|
||||
|
||||
}
|
||||
|
||||
} else if let downloadURL = downloadURL {
|
||||
self.downloadURL = downloadURL
|
||||
self._downloadURL = downloadURL
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
||||
}
|
||||
@@ -219,11 +269,40 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
self.tintColor = tintColor
|
||||
}
|
||||
|
||||
self.size = try container.decode(Int32.self, forKey: .size)
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||
self._permissions = NSOrderedSet(array: permissions)
|
||||
|
||||
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
||||
{
|
||||
//TODO: Throw error if there isn't at least one version.
|
||||
|
||||
for version in versions
|
||||
{
|
||||
version.appBundleID = self.bundleIdentifier
|
||||
}
|
||||
|
||||
self.setVersions(versions)
|
||||
}
|
||||
else
|
||||
{
|
||||
let version = try container.decode(String.self, forKey: .version)
|
||||
let versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
let size = try container.decode(Int32.self, forKey: .size)
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: version,
|
||||
date: versionDate,
|
||||
localizedDescription: versionDescription,
|
||||
downloadURL: downloadURL,
|
||||
size: Int64(size),
|
||||
appBundleID: self.bundleIdentifier,
|
||||
in: context)
|
||||
self.setVersions([appVersion])
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -237,6 +316,24 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
}
|
||||
}
|
||||
|
||||
private extension StoreApp
|
||||
{
|
||||
func setVersions(_ versions: [AppVersion])
|
||||
{
|
||||
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
|
||||
|
||||
self.latestVersion = latestVersion
|
||||
self._versions = NSOrderedSet(array: versions)
|
||||
|
||||
// Preserve backwards compatibility by assigning legacy property values.
|
||||
self._version = latestVersion.version
|
||||
self._versionDate = latestVersion.date
|
||||
self._versionDescription = latestVersion.localizedDescription
|
||||
self._downloadURL = latestVersion.downloadURL
|
||||
self._size = Int32(latestVersion.size)
|
||||
}
|
||||
}
|
||||
|
||||
public extension StoreApp
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
||||
@@ -253,9 +350,18 @@ public extension StoreApp
|
||||
app.localizedDescription = "SideStore is an alternative App Store."
|
||||
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
|
||||
app.screenshotURLs = []
|
||||
app.version = "1.0"
|
||||
app.versionDate = Date()
|
||||
app.downloadURL = URL(string: "http://rileytestut.com")!
|
||||
app.sourceIdentifier = Source.altStoreIdentifier
|
||||
|
||||
let appVersion = AppVersion.makeAppVersion(version: "0.1.2",
|
||||
date: Date(),
|
||||
downloadURL: URL(string: "http://rileytestut.com")!,
|
||||
size: 0,
|
||||
appBundleID: app.bundleIdentifier,
|
||||
sourceID: Source.altStoreIdentifier,
|
||||
in: context)
|
||||
app.setVersions([appVersion])
|
||||
|
||||
print("makeAltStoreApp StoreApp: \(String(describing: app))")
|
||||
|
||||
#if BETA
|
||||
app.isBeta = true
|
||||
|
||||
@@ -13,16 +13,16 @@ public protocol AppProtocol
|
||||
{
|
||||
var name: String { get }
|
||||
var bundleIdentifier: String { get }
|
||||
var url: URL { get }
|
||||
var url: URL? { get }
|
||||
}
|
||||
|
||||
public struct AnyApp: AppProtocol
|
||||
{
|
||||
public var name: String
|
||||
public var bundleIdentifier: String
|
||||
public var url: URL
|
||||
public var url: URL?
|
||||
|
||||
public init(name: String, bundleIdentifier: String, url: URL)
|
||||
public init(name: String, bundleIdentifier: String, url: URL?)
|
||||
{
|
||||
self.name = name
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
@@ -32,21 +32,21 @@ public struct AnyApp: AppProtocol
|
||||
|
||||
extension ALTApplication: AppProtocol
|
||||
{
|
||||
public var url: URL {
|
||||
public var url: URL? {
|
||||
return self.fileURL
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreApp: AppProtocol
|
||||
{
|
||||
public var url: URL {
|
||||
public var url: URL? {
|
||||
return self.downloadURL
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp: AppProtocol
|
||||
{
|
||||
public var url: URL {
|
||||
public var url: URL? {
|
||||
return self.fileURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +189,9 @@ struct HomeScreenWidget: Widget
|
||||
}
|
||||
}
|
||||
|
||||
struct LockScreenWidget: Widget
|
||||
struct TextLockScreenWidget: Widget
|
||||
{
|
||||
private let kind: String = "LockAppDetail"
|
||||
private let kind: String = "TextLockAppDetail"
|
||||
|
||||
public var body: some WidgetConfiguration {
|
||||
if #available(iOSApplicationExtension 16, *)
|
||||
@@ -199,10 +199,10 @@ struct LockScreenWidget: Widget
|
||||
return IntentConfiguration(kind: kind,
|
||||
intent: ViewAppIntent.self,
|
||||
provider: Provider()) { (entry) in
|
||||
ComplicationView(entry: entry)
|
||||
ComplicationView(entry: entry, style: .text)
|
||||
}
|
||||
.supportedFamilies([.accessoryCircular])
|
||||
.configurationDisplayName("AltWidget")
|
||||
.configurationDisplayName("AltWidget (Text)")
|
||||
.description("View remaining days until SideStore expires.")
|
||||
}
|
||||
else
|
||||
@@ -212,11 +212,58 @@ struct LockScreenWidget: Widget
|
||||
}
|
||||
}
|
||||
|
||||
struct IconLockScreenWidget: Widget
|
||||
{
|
||||
private let kind: String = "IconLockAppDetail"
|
||||
|
||||
public var body: some WidgetConfiguration {
|
||||
if #available(iOSApplicationExtension 16, *)
|
||||
{
|
||||
return IntentConfiguration(kind: kind,
|
||||
intent: ViewAppIntent.self,
|
||||
provider: Provider()) { (entry) in
|
||||
ComplicationView(entry: entry, style: .icon)
|
||||
}
|
||||
.supportedFamilies([.accessoryCircular])
|
||||
.configurationDisplayName("AltWidget (Icon)")
|
||||
.description("View remaining days until SideStore expires.")
|
||||
}
|
||||
else
|
||||
{
|
||||
return EmptyWidgetConfiguration()
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//struct LockScreenWidget: Widget
|
||||
//{
|
||||
// private let kind: String = "LockAppDetail"
|
||||
//
|
||||
// public var body: some WidgetConfiguration {
|
||||
// if #available(iOSApplicationExtension 16, *)
|
||||
// {
|
||||
// return IntentConfiguration(kind: kind,
|
||||
// intent: ViewAppIntent.self,
|
||||
// provider: Provider()) { (entry) in
|
||||
// ComplicationView(entry: entry, style: .icon)
|
||||
// }
|
||||
// .supportedFamilies([.accessoryCircular])
|
||||
// .configurationDisplayName("AltWidget")
|
||||
// .description("View remaining days until SideStore expires.")
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return EmptyWidgetConfiguration()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@main
|
||||
struct AltWidgets: WidgetBundle
|
||||
{
|
||||
var body: some Widget {
|
||||
HomeScreenWidget()
|
||||
LockScreenWidget()
|
||||
IconLockScreenWidget()
|
||||
TextLockScreenWidget()
|
||||
}
|
||||
}
|
||||
|
||||
16
AltWidget/Assets.xcassets/SmallIcon.imageset/Contents.json
vendored
Normal file
16
AltWidget/Assets.xcassets/SmallIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "altminicon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
AltWidget/Assets.xcassets/SmallIcon.imageset/altminicon.pdf
vendored
Normal file
BIN
AltWidget/Assets.xcassets/SmallIcon.imageset/altminicon.pdf
vendored
Normal file
Binary file not shown.
@@ -9,10 +9,21 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@available(iOS 16, *)
|
||||
extension ComplicationView
|
||||
{
|
||||
enum Style
|
||||
{
|
||||
case text
|
||||
case icon
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
struct ComplicationView: View
|
||||
{
|
||||
let entry: AppEntry
|
||||
let style: Style
|
||||
|
||||
var body: some View {
|
||||
let refreshedDate = self.entry.app?.refreshedDate ?? .now
|
||||
@@ -23,26 +34,53 @@ struct ComplicationView: View
|
||||
|
||||
let progress = Double(daysRemaining) / Double(totalDays)
|
||||
|
||||
ZStack(alignment: .center) {
|
||||
ProgressRing(progress: progress) {
|
||||
if daysRemaining < 0
|
||||
{
|
||||
Text("Expired")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
}
|
||||
else
|
||||
Gauge(value: progress) {
|
||||
if daysRemaining < 0
|
||||
{
|
||||
Text("Expired")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
}
|
||||
else
|
||||
{
|
||||
switch self.style
|
||||
{
|
||||
case .text:
|
||||
VStack(spacing: -1) {
|
||||
let fontSize = daysRemaining > 99 ? 18.0 : 20.0
|
||||
Text("\(daysRemaining)")
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.font(.system(size: fontSize, weight: .bold, design: .rounded))
|
||||
|
||||
Text(daysRemaining == 1 ? "DAY" : "DAYS")
|
||||
.font(.caption)
|
||||
}
|
||||
.fixedSize()
|
||||
.offset(y: -1)
|
||||
|
||||
case .icon:
|
||||
ZStack {
|
||||
// Destination
|
||||
Image("SmallIcon")
|
||||
.resizable()
|
||||
.aspectRatio(1.0, contentMode: .fill)
|
||||
.scaleEffect(x: 0.8, y: 0.8)
|
||||
|
||||
// Source
|
||||
(
|
||||
daysRemaining > 7 ?
|
||||
Text("7+")
|
||||
.font(.system(size: 18, weight: .bold, design: .rounded))
|
||||
.kerning(-2) :
|
||||
|
||||
Text("\(daysRemaining)")
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
)
|
||||
.foregroundColor(Color.black)
|
||||
.blendMode(.destinationOut) // Clip text out of image.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.gaugeStyle(.accessoryCircularCapacity)
|
||||
.unredacted()
|
||||
}
|
||||
}
|
||||
@@ -73,14 +111,23 @@ struct ComplicationView_Previews: PreviewProvider {
|
||||
icon: UIImage(named: "AltStore"))
|
||||
|
||||
return Group {
|
||||
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore))
|
||||
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore), style: .icon)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
|
||||
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore))
|
||||
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore), style: .icon)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
|
||||
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore))
|
||||
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore), style: .icon)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
|
||||
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore), style: .text)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
|
||||
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore), style: .text)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
|
||||
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore), style: .text)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// ProgressRing.swift
|
||||
// AltWidgetExtension
|
||||
//
|
||||
// Created by Riley Testut on 8/17/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct ProgressRing<Content: View>: View
|
||||
{
|
||||
let progress: Double
|
||||
|
||||
private let content: Content
|
||||
|
||||
init(progress: Double, @ViewBuilder content: () -> Content)
|
||||
{
|
||||
self.progress = progress
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
ring(progress: 1.0)
|
||||
.opacity(0.3)
|
||||
|
||||
ring(progress: self.progress)
|
||||
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func ring(progress: Double) -> some View {
|
||||
let strokeStyle = StrokeStyle(lineWidth: 4.0, lineCap: .round, lineJoin: .round)
|
||||
|
||||
Circle()
|
||||
.inset(by: 2.0)
|
||||
.trim(from: 0.0, to: progress)
|
||||
.rotation(Angle(degrees: -90), anchor: .center)
|
||||
.stroke(style: strokeStyle)
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressRing_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProgressRing(progress: 0.5) {
|
||||
EmptyView()
|
||||
}
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
#include "Build.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(EXTENSION_PREFIX).AltWidget
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltWidget
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
MARKETING_VERSION = 0.1.1
|
||||
CURRENT_PROJECT_VERSION = 3011
|
||||
MARKETING_VERSION = 0.1.2
|
||||
CURRENT_PROJECT_VERSION = 3012
|
||||
|
||||
// Vars to be overwritten by `CodeSigning.xcconfig` if exists
|
||||
DEVELOPMENT_TEAM = S32Z3HMYVQ
|
||||
@@ -17,8 +17,8 @@ PRODUCT_NAME = SideStore
|
||||
EXTENSION_PREFIX = $(ORG_PREFIX).SideStore
|
||||
//PRODUCT_NAME[configuration=Debug] = Prov Debug
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).SideStore
|
||||
//PRODUCT_BUNDLE_IDENTIFIER[configuration=Debug] = $(ORG_PREFIX).$(PROJECT_NAME:lower)-debug
|
||||
|
||||
APP_GROUP_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME)
|
||||
APP_GROUP_IDENTIFIER = $(ORG_PREFIX).SideStore
|
||||
ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME)
|
||||
|
||||
2
Dependencies/libimobiledevice
vendored
2
Dependencies/libimobiledevice
vendored
Submodule Dependencies/libimobiledevice updated: 7a8e432e9b...b314f04bd7
2
Dependencies/libplist
vendored
2
Dependencies/libplist
vendored
Submodule Dependencies/libplist updated: 307fcc5f95...c3af449543
@@ -41,6 +41,7 @@ public class ConnectionManager<RequestHandlerType: RequestHandler>
|
||||
public var isStarted = false
|
||||
|
||||
private var connections = [Connection]()
|
||||
private let connectionsLock = NSLock()
|
||||
|
||||
public init(requestHandler: RequestHandlerType, connectionHandlers: [ConnectionHandler])
|
||||
{
|
||||
@@ -88,6 +89,9 @@ private extension ConnectionManager
|
||||
{
|
||||
func prepare(_ connection: Connection)
|
||||
{
|
||||
self.connectionsLock.lock()
|
||||
defer { self.connectionsLock.unlock() }
|
||||
|
||||
guard !self.connections.contains(where: { $0 === connection }) else { return }
|
||||
self.connections.append(connection)
|
||||
|
||||
@@ -96,6 +100,9 @@ private extension ConnectionManager
|
||||
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
self.connectionsLock.lock()
|
||||
defer { self.connectionsLock.unlock() }
|
||||
|
||||
guard let index = self.connections.firstIndex(where: { $0 === connection }) else { return }
|
||||
self.connections.remove(at: index)
|
||||
}
|
||||
|
||||
@@ -305,6 +305,8 @@ public struct BeginInstallationRequest: ServerMessageProtocol
|
||||
{
|
||||
self.activeProfiles = activeProfiles
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
print("BeginInstallationRequest `activeProfiles`: \(String(describing: activeProfiles))")
|
||||
print("BeginInstallationRequest `bundleIdentifier`: \(String(describing: bundleIdentifier))")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +348,9 @@ public struct InstallProvisioningProfilesRequest: ServerMessageProtocol
|
||||
self.udid = udid
|
||||
self.provisioningProfiles = provisioningProfiles
|
||||
self.activeProfiles = activeProfiles
|
||||
print("InstallProvisioningProfilesRequest `self.udid`: \(self.udid)")
|
||||
print("InstallProvisioningProfilesRequest `self.provisioningProfiles`: \(self.provisioningProfiles)")
|
||||
print("InstallProvisioningProfilesRequest `self.activeProfiles`: \(String(describing: self.activeProfiles))")
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
|
||||
Reference in New Issue
Block a user