From 81409227d61c5092da7712662c2108c94c7a4603 Mon Sep 17 00:00:00 2001 From: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com> Date: Sun, 19 Feb 2023 08:06:33 -0800 Subject: [PATCH] Add developer mode --- AltStore.xcodeproj/project.pbxproj | 33 ++ .../xcshareddata/swiftpm/Package.resolved | 9 + AltStore/AppDelegate.swift | 3 + AltStore/Extensions/View+SideStore.swift | 22 ++ AltStore/Generated/Assets.swift | 7 + AltStore/Generated/Localizations.swift | 41 ++ AltStore/Info.plist | 2 + AltStore/Operations/ResignAppOperation.swift | 22 ++ .../Resources/en.lproj/Localizable.strings | 27 +- .../View Components/AsyncFallibleButton.swift | 118 ++++++ AltStore/View Components/FileExplorer.swift | 366 ++++++++++++++++++ AltStore/Views/Settings/AppIconsView.swift | 4 +- AltStore/Views/Settings/DevModeView.swift | 147 +++++++ AltStore/Views/Settings/SettingsView.swift | 24 +- .../Extensions/UserDefaults+AltStore.swift | 5 + 15 files changed, 822 insertions(+), 8 deletions(-) create mode 100644 AltStore/Extensions/View+SideStore.swift create mode 100644 AltStore/View Components/AsyncFallibleButton.swift create mode 100644 AltStore/View Components/FileExplorer.swift create mode 100644 AltStore/Views/Settings/DevModeView.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 7ce6832e..0eced414 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -83,7 +83,12 @@ 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; 99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; }; + 99D87A60299F1B1100ED09A9 /* DevModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D87A5F299F1B1100ED09A9 /* DevModeView.swift */; }; + 99D87A62299F3EC300ED09A9 /* FileExplorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D87A61299F3EC300ED09A9 /* FileExplorer.swift */; }; 99D87A6529A04D5E00ED09A9 /* Inject in Frameworks */ = {isa = PBXBuildFile; productRef = 99D87A6429A04D5E00ED09A9 /* Inject */; }; + 99DE640129A1271100B920BF /* AsyncFallibleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99DE640029A1271100B920BF /* AsyncFallibleButton.swift */; }; + 99DE640329A1624500B920BF /* View+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99DE640229A1624500B920BF /* View+SideStore.swift */; }; + 99DE640629A1753800B920BF /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 99DE640529A1753800B920BF /* ZIPFoundation */; }; 99E59E1D299BFE5D00FAF33D /* AppIconsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E59E1C299BFE5D00FAF33D /* AppIconsView.swift */; }; 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, ); }; }; @@ -633,6 +638,10 @@ 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonProgressViewStyle.swift; sourceTree = ""; }; 1FFA56C1299994390011B6F5 /* OutputCapturer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputCapturer.swift; sourceTree = ""; }; 1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = ""; }; + 99D87A5F299F1B1100ED09A9 /* DevModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevModeView.swift; sourceTree = ""; }; + 99D87A61299F3EC300ED09A9 /* FileExplorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorer.swift; sourceTree = ""; }; + 99DE640029A1271100B920BF /* AsyncFallibleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFallibleButton.swift; sourceTree = ""; }; + 99DE640229A1624500B920BF /* View+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+SideStore.swift"; sourceTree = ""; }; 99E59E1C299BFE5D00FAF33D /* AppIconsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconsView.swift; sourceTree = ""; }; B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = ""; }; B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = ""; }; @@ -1056,6 +1065,7 @@ 1F07F5672955D16A00F7BE95 /* SFSafeSymbols in Frameworks */, 1FFA56C52999978C0011B6F5 /* LocalConsole in Frameworks */, BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */, + 99DE640629A1753800B920BF /* ZIPFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1215,6 +1225,7 @@ 1F545E82298D79E400589F68 /* ErrorLogView.swift */, 1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */, 99E59E1C299BFE5D00FAF33D /* AppIconsView.swift */, + 99D87A5F299F1B1100ED09A9 /* DevModeView.swift */, ); path = Settings; sourceTree = ""; @@ -1241,6 +1252,8 @@ 1F6E08E529280F4B005059C0 /* RatingStars.swift */, 1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */, 1F545E86298D86D800589F68 /* ModalNavigationLink.swift */, + 99D87A61299F3EC300ED09A9 /* FileExplorer.swift */, + 99DE640029A1271100B920BF /* AsyncFallibleButton.swift */, ); path = "View Components"; sourceTree = ""; @@ -1951,6 +1964,7 @@ 1F66F5BD2938F06100A910CA /* StoreApp+Filterable.swift */, 1F180F91298E7A1B00D1C98B /* StoreApp+Trusted.swift */, 1F180F93298E7A2500D1C98B /* Source+Trusted.swift */, + 99DE640229A1624500B920BF /* View+SideStore.swift */, ); path = Extensions; sourceTree = ""; @@ -2312,6 +2326,7 @@ 1F1295802989B51F0048FCB9 /* ExpandableText */, 1FFA56C42999978C0011B6F5 /* LocalConsole */, 99D87A6429A04D5E00ED09A9 /* Inject */, + 99DE640529A1753800B920BF /* ZIPFoundation */, ); productName = AltStore; productReference = BFD2476A2284B9A500981D42 /* SideStore.app */; @@ -2389,6 +2404,7 @@ 1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */, 1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */, 99D87A6329A04D5E00ED09A9 /* XCRemoteSwiftPackageReference "Inject" */, + 99DE640429A1753800B920BF /* XCRemoteSwiftPackageReference "ZIPFoundation" */, ); productRefGroup = BFD2476B2284B9A500981D42 /* Products */; projectDirPath = ""; @@ -2758,12 +2774,14 @@ buildActionMask = 2147483647; files = ( BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, + 99DE640129A1271100B920BF /* AsyncFallibleButton.swift in Sources */, 1F6E08E029280B12005059C0 /* SafariView.swift in Sources */, 1F943C6C2927F90400ABE095 /* SettingsView.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, 1F66F5C02938F07C00A910CA /* Filterable.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, + 99D87A60299F1B1100ED09A9 /* DevModeView.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, 1F07F556295458D800F7BE95 /* Assets.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, @@ -2835,6 +2853,7 @@ BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */, BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */, + 99D87A62299F3EC300ED09A9 /* FileExplorer.swift in Sources */, BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */, BF41B808233433C100C593A3 /* LoadingState.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, @@ -2871,6 +2890,7 @@ BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, 1F66F5BC2938F03700A910CA /* Modifiers.swift in Sources */, + 99DE640329A1624500B920BF /* View+SideStore.swift in Sources */, 1FA5A6CA298E8B2F007BA946 /* RefreshAttemptsView.swift in Sources */, 1F5DF9D82974426300DDAA47 /* AppScreenshot.swift in Sources */, 1F66F5BA2938CA5700A910CA /* VisualEffectView.swift in Sources */, @@ -3809,6 +3829,14 @@ minimumVersion = 1.0.0; }; }; + 99DE640429A1753800B920BF /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/weichsel/ZIPFoundation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.9; + }; + }; B3C395EF284F2DE700DA9E2F /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; @@ -3920,6 +3948,11 @@ package = 99D87A6329A04D5E00ED09A9 /* XCRemoteSwiftPackageReference "Inject" */; productName = Inject; }; + 99DE640529A1753800B920BF /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = 99DE640429A1753800B920BF /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; B3C395F0284F2DE700DA9E2F /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = B3C395EF284F2DE700DA9E2F /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9fab500f..a4ebbd6f 100644 --- a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -134,6 +134,15 @@ "branch" : "master", "revision" : "10a9150ef32d444af326beba76356ae9af95a3e7" } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "43ec568034b3731101dbf7670765d671c30f54f3", + "version" : "0.9.16" + } } ], "version" : 2 diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index f9fb90c1..ce1db769 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import UserNotifications import AVFoundation import Intents +import LocalConsole import AltStoreCore import AltSign @@ -64,6 +65,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Register default settings before doing anything else. UserDefaults.registerDefaults() + LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled + DatabaseManager.shared.start { (error) in if let error = error { diff --git a/AltStore/Extensions/View+SideStore.swift b/AltStore/Extensions/View+SideStore.swift new file mode 100644 index 00000000..dc58b51f --- /dev/null +++ b/AltStore/Extensions/View+SideStore.swift @@ -0,0 +1,22 @@ +// +// View+SideStore.swift +// SideStore +// +// Created by naturecodevoid on 2/18/23. +// Copyright © 2023 SideStore. All rights reserved. +// + +import SwiftUI + +// https://stackoverflow.com/a/59228385 (modified) +extension View { + @ViewBuilder func isHidden(_ hidden: Binding, remove: Bool = false) -> some View { + if hidden.wrappedValue { + if !remove { + self.hidden() + } + } else { + self + } + } +} diff --git a/AltStore/Generated/Assets.swift b/AltStore/Generated/Assets.swift index feb370d5..1529b056 100644 --- a/AltStore/Generated/Assets.swift +++ b/AltStore/Generated/Assets.swift @@ -31,7 +31,14 @@ internal enum Asset { internal static let blurTint = ColorAsset(name: "BlurTint") internal static let settingsBackground = ColorAsset(name: "SettingsBackground") internal static let settingsHighlighted = ColorAsset(name: "SettingsHighlighted") + internal static let honeydewImage = ImageAsset(name: "Honeydew-image") + internal static let midnightImage = ImageAsset(name: "Midnight-image") + internal static let neonImage = ImageAsset(name: "Neon-image") internal static let next = ImageAsset(name: "Next") + internal static let skyImage = ImageAsset(name: "Sky-image") + internal static let starburstImage = ImageAsset(name: "Starburst-image") + internal static let steelImage = ImageAsset(name: "Steel-image") + internal static let stormImage = ImageAsset(name: "Storm-image") internal static let browse = ImageAsset(name: "Browse") internal static let myApps = ImageAsset(name: "MyApps") internal static let news = ImageAsset(name: "News") diff --git a/AltStore/Generated/Localizations.swift b/AltStore/Generated/Localizations.swift index dfd19929..28631eee 100644 --- a/AltStore/Generated/Localizations.swift +++ b/AltStore/Generated/Localizations.swift @@ -15,6 +15,8 @@ internal enum L10n { internal static let close = L10n.tr("Localizable", "Action.close", fallback: "Close") /// General Actions internal static let done = L10n.tr("Localizable", "Action.done", fallback: "Done") + /// Enable + internal static let enable = L10n.tr("Localizable", "Action.enable", fallback: "Enable") } internal enum AddSourceView { /// Continue @@ -150,6 +152,10 @@ internal enum L10n { /// AppRowView internal static let sideloaded = L10n.tr("Localizable", "AppRowView.sideloaded", fallback: "Sideloaded") } + internal enum AsyncFallibleButton { + /// AsyncFallibleButton + internal static let error = L10n.tr("Localizable", "AsyncFallibleButton.error", fallback: "An error occurred") + } internal enum BrowseView { /// Search internal static let search = L10n.tr("Localizable", "BrowseView.search", fallback: "Search") @@ -223,6 +229,41 @@ internal enum L10n { /// Why do we need this? internal static let whyDoWeNeedThis = L10n.tr("Localizable", "ConnectAppleIDView.whyDoWeNeedThis", fallback: "Why do we need this?") } + internal enum DevModeView { + /// Console + internal static let console = L10n.tr("Localizable", "DevModeView.console", fallback: "Console") + /// Data File Explorer + internal static let dataExplorer = L10n.tr("Localizable", "DevModeView.dataExplorer", fallback: "Data File Explorer") + /// minimuxer debug actions + internal static let minimuxer = L10n.tr("Localizable", "DevModeView.minimuxer", fallback: "minimuxer debug actions") + /// SideStore's Developer Mode gives access to a menu with some debugging actions commonly used by developers. **However, some of them can break SideStore if used in the wrong way.** + /// + /// You should only enable Developer Mode if you meet one of the following requirements: + /// - You are a SideStore developer or contributor + /// - When getting support, you were asked to do this by a helper + /// - You were asked to do this when you reported a bug or helped a developer test a change + /// + /// **_We will not provide support if you break SideStore with Developer Mode._** + internal static let prompt = L10n.tr("Localizable", "DevModeView.prompt", fallback: "SideStore's Developer Mode gives access to a menu with some debugging actions commonly used by developers. **However, some of them can break SideStore if used in the wrong way.**\n\nYou should only enable Developer Mode if you meet one of the following requirements:\n- You are a SideStore developer or contributor\n- When getting support, you were asked to do this by a helper\n- You were asked to do this when you reported a bug or helped a developer test a change\n\n**_We will not provide support if you break SideStore with Developer Mode._**") + /// Read the text! + internal static let read = L10n.tr("Localizable", "DevModeView.read", fallback: "Read the text!") + /// Skip Resign + internal static let skipResign = L10n.tr("Localizable", "DevModeView.skipResign", fallback: "Skip Resign") + /// Skip Resign should only be used when you have an IPA that you have self signed. Otherwise, it will break things, and might make SideStore crash (there is absolutely no error handling and everything is expected to work). Useful for debugging ApplicationVerificationError + internal static let skipResignInfo = L10n.tr("Localizable", "DevModeView.skipResignInfo", fallback: "Skip Resign should only be used when you have an IPA that you have self signed. Otherwise, it will break things, and might make SideStore crash (there is absolutely no error handling and everything is expected to work). Useful for debugging ApplicationVerificationError") + /// DevModeView + internal static let title = L10n.tr("Localizable", "DevModeView.title", fallback: "Developer Mode") + /// Temporary File Explorer + internal static let tmpExplorer = L10n.tr("Localizable", "DevModeView.tmpExplorer", fallback: "Temporary File Explorer") + internal enum Minimuxer { + /// Dump provisioning profiles to Documents directory + internal static let dumpProfiles = L10n.tr("Localizable", "DevModeView.Minimuxer.dumpProfiles", fallback: "Dump provisioning profiles to Documents directory") + /// PublicStaging File Explorer + internal static let stagingExplorer = L10n.tr("Localizable", "DevModeView.Minimuxer.stagingExplorer", fallback: "PublicStaging File Explorer") + /// View provisioning profiles + internal static let viewProfiles = L10n.tr("Localizable", "DevModeView.Minimuxer.viewProfiles", fallback: "View provisioning profiles") + } + } internal enum MyAppsView { /// MyAppsView internal static let active = L10n.tr("Localizable", "MyAppsView.active", fallback: "Active") diff --git a/AltStore/Info.plist b/AltStore/Info.plist index 2b4b48de..e1f98490 100644 --- a/AltStore/Info.plist +++ b/AltStore/Info.plist @@ -204,6 +204,8 @@ + UISupportsDocumentBrowser + UIFileSharingEnabled diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 8be28221..33e6e907 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -8,6 +8,8 @@ import Foundation import Roxas +import SwiftUI +import ZIPFoundation import AltStoreCore import AltSign @@ -15,6 +17,9 @@ import AltSign @objc(ResignAppOperation) final class ResignAppOperation: ResultOperation { + static var skipResign: Bool = false + static var skipResignBinding: Binding { Binding(get: { skipResign }, set: { skipResign = $0 }) } + let context: InstallAppOperationContext init(context: InstallAppOperationContext) @@ -50,6 +55,23 @@ final class ResignAppOperation: ResultOperation let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in guard let appBundleURL = self.process(result) else { return } + if ResignAppOperation.skipResign { + print("⚠️ WARNING: Skipping resign. Unless you correctly resigned the IPA before installing it, things will not work! Also, this might crash SideStore. You have been warned!") + let ipaFile = self.context.temporaryDirectory.appendingPathComponent("App.ipa") + let archive = Archive(url: ipaFile, accessMode: .create)! + for case let fileURL as URL in FileManager.default.enumerator(at: appBundleURL, includingPropertiesForKeys: [])! { + let relative = fileURL.description.replacingOccurrences(of: appBundleURL.description, with: "").removingPercentEncoding! + try! archive.addEntry(with: "Payload/App.app\(relative)", fileURL: fileURL) + } + let destinationURL = InstalledApp.refreshedIPAURL(for: app) + try! FileManager.default.copyItem(at: ipaFile, to: destinationURL, shouldReplace: true) + + // Use appBundleURL since we need an app bundle, not .ipa. + let resignedApplication = ALTApplication(fileURL: appBundleURL)! + self.finish(.success(resignedApplication)) + return + } + print("Resigning App:", self.context.bundleIdentifier) // Resign app bundle diff --git a/AltStore/Resources/en.lproj/Localizable.strings b/AltStore/Resources/en.lproj/Localizable.strings index 53c541b6..1a653f07 100644 --- a/AltStore/Resources/en.lproj/Localizable.strings +++ b/AltStore/Resources/en.lproj/Localizable.strings @@ -10,13 +10,12 @@ /* General Actions */ "Action.done" = "Done"; "Action.close" = "Close"; - +"Action.enable" = "Enable"; /* NewsView */ "NewsView.title" = "News"; "NewsView.Section.FromSources.title" = "From your Sources"; - /* BrowseView */ "BrowseView.title" = "Browse"; "BrowseView.search" = "Search"; @@ -166,3 +165,27 @@ /* AppIconsView */ "AppIconsView.title" = "App Icon"; + +/* DevModeView */ +"DevModeView.title" = "Developer Mode"; +"DevModeView.prompt" = "SideStore's Developer Mode gives access to a menu with some debugging actions commonly used by developers. **However, some of them can break SideStore if used in the wrong way.** + +You should only enable Developer Mode if you meet one of the following requirements: +- You are a SideStore developer or contributor +- When getting support, you were asked to do this by a helper +- You were asked to do this when you reported a bug or helped a developer test a change + +**_We will not provide support if you break SideStore with Developer Mode._**"; +"DevModeView.read" = "Read the text!"; +"DevModeView.console" = "Console"; +"DevModeView.dataExplorer" = "Data File Explorer"; +"DevModeView.tmpExplorer" = "Temporary File Explorer"; +"DevModeView.skipResign" = "Skip Resign"; +"DevModeView.skipResignInfo" = "Skip Resign should only be used when you have an IPA that you have self signed. Otherwise, it will break things, and might make SideStore crash (there is absolutely no error handling and everything is expected to work). Useful for debugging ApplicationVerificationError"; +"DevModeView.minimuxer" = "minimuxer debug actions"; +"DevModeView.Minimuxer.stagingExplorer" = "PublicStaging File Explorer"; +"DevModeView.Minimuxer.viewProfiles" = "View provisioning profiles"; +"DevModeView.Minimuxer.dumpProfiles" = "Dump provisioning profiles to Documents directory"; + +/* AsyncFallibleButton */ +"AsyncFallibleButton.error" = "An error occurred"; diff --git a/AltStore/View Components/AsyncFallibleButton.swift b/AltStore/View Components/AsyncFallibleButton.swift new file mode 100644 index 00000000..9d5c9d1c --- /dev/null +++ b/AltStore/View Components/AsyncFallibleButton.swift @@ -0,0 +1,118 @@ +// +// AsyncFallibleButton.swift +// SideStore +// +// Created by naturecodevoid on 2/18/23. +// Copyright © 2023 SideStore. All rights reserved. +// + +import SwiftUI + +private enum AsyncFallibleButtonState { + case none + case loading + case success + case error +} + +struct AsyncFallibleButton: View { + @ObservedObject private var iO = Inject.observer + + let action: () throws -> Void + let label: (_ execute: @escaping () -> Void) -> Label + + var afterFinish: (_ success: Bool) -> Void = { success in } // runs after the checkmark/X has disappeared + var wrapInButton = true + var secondsToDisplayResultIcon: Double = 3 + + @State private var state: AsyncFallibleButtonState = .none + @State private var showErrorAlert = false + @State private var errorAlertMessage = "" + + private var inside: some View { + HStack { + label(execute) + if state != .none { + if wrapInButton { + Spacer() + } + switch (state) { + case .loading: + ProgressView() + case .success: + Image(systemSymbol: .checkmark) + .foregroundColor(Color.green) + case .error: + Image(systemSymbol: .xmark) + .foregroundColor(Color.red) + default: + Image(systemSymbol: .questionmark) + .foregroundColor(Color.yellow) + } + } + } + } + + private var wrapped: some View { + if wrapInButton { + return AnyView(SwiftUI.Button(action: { + execute() + }) { + inside + }) + } else { + return AnyView(inside) + } + } + + var body: some View { + wrapped + .alert(isPresented: $showErrorAlert) { + Alert( + title: Text(L10n.AsyncFallibleButton.error), + message: Text(errorAlertMessage) + ) + } + .disabled(state != .none) + .animation(.default, value: state) + .enableInjection() + } + + func execute() { + if state != .none { return } + state = .loading + DispatchQueue.global().async { + do { + try action() + DispatchQueue.main.async { state = .success } + } catch { + DispatchQueue.main.async { + state = .error + errorAlertMessage = error.localizedDescription + showErrorAlert = true + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + secondsToDisplayResultIcon) { + let lastState = state + state = .none + afterFinish(lastState == .success) + } + } + } +} + +struct AsyncFallibleButton_Previews: PreviewProvider { + static var previews: some View { + AsyncFallibleButton(action: { + print("Start") + for index in 0...5000000 { + _ = index + index + } + throw NSError(domain: "TestError", code: -1) + //print("Finish") + }) { execute in + Text("Hello World") + } + } +} diff --git a/AltStore/View Components/FileExplorer.swift b/AltStore/View Components/FileExplorer.swift new file mode 100644 index 00000000..5d6a690f --- /dev/null +++ b/AltStore/View Components/FileExplorer.swift @@ -0,0 +1,366 @@ +// +// FileExplorer.swift +// SideStore +// +// Created by naturecodevoid on 2/16/23. +// Copyright © 2023 SideStore. All rights reserved. +// + +import SwiftUI +import ZIPFoundation +import UniformTypeIdentifiers + +// https://stackoverflow.com/a/72165424 +func allUTITypes() -> [UTType] { + let types: [UTType] = + [.item, + .content, + .compositeContent, + .diskImage, + .data, + .directory, + .resolvable, + .symbolicLink, + .executable, + .mountPoint, + .aliasFile, + .urlBookmarkData, + .url, + .fileURL, + .text, + .plainText, + .utf8PlainText, + .utf16ExternalPlainText, + .utf16PlainText, + .delimitedText, + .commaSeparatedText, + .tabSeparatedText, + .utf8TabSeparatedText, + .rtf, + .html, + .xml, + .yaml, + .sourceCode, + .assemblyLanguageSource, + .cSource, + .objectiveCSource, + .swiftSource, + .cPlusPlusSource, + .objectiveCPlusPlusSource, + .cHeader, + .cPlusPlusHeader] + + let types_1: [UTType] = + [.script, + .appleScript, + .osaScript, + .osaScriptBundle, + .javaScript, + .shellScript, + .perlScript, + .pythonScript, + .rubyScript, + .phpScript, + .json, + .propertyList, + .xmlPropertyList, + .binaryPropertyList, + .pdf, + .rtfd, + .flatRTFD, + .webArchive, + .image, + .jpeg, + .tiff, + .gif, + .png, + .icns, + .bmp, + .ico, + .rawImage, + .svg, + .livePhoto, + .heif, + .heic, + .webP, + .threeDContent, + .usd, + .usdz, + .realityFile, + .sceneKitScene, + .arReferenceObject, + .audiovisualContent] + + let types_2: [UTType] = + [.movie, + .video, + .audio, + .quickTimeMovie, + UTType("com.apple.quicktime-image"), + .mpeg, + .mpeg2Video, + .mpeg2TransportStream, + .mp3, + .mpeg4Movie, + .mpeg4Audio, + .appleProtectedMPEG4Audio, + .appleProtectedMPEG4Video, + .avi, + .aiff, + .wav, + .midi, + .playlist, + .m3uPlaylist, + .folder, + .volume, + .package, + .bundle, + .pluginBundle, + .spotlightImporter, + .quickLookGenerator, + .xpcService, + .framework, + .application, + .applicationBundle, + .applicationExtension, + .unixExecutable, + .exe, + .systemPreferencesPane, + .archive, + .gzip, + .bz2, + .zip, + .appleArchive, + .spreadsheet, + .presentation, + .database, + .message, + .contact, + .vCard, + .toDoItem, + .calendarEvent, + .emailMessage, + .internetLocation, + .internetShortcut, + .font, + .bookmark, + .pkcs12, + .x509Certificate, + .epub, + .log] + .compactMap({ $0 }) + + return types + types_1 + types_2 +} + +extension Binding: Equatable { + public static func == (lhs: Binding, rhs: Binding) -> Bool { + return lhs.wrappedValue == rhs.wrappedValue + } +} + +private struct DirectoryEntry: Identifiable { + var id = UUID() + var path: URL + var parent: URL + var isFile = false + var childFiles = [URL]() + var childDirectories: [DirectoryEntry]? + var filesAndDirectories: [DirectoryEntry]? { + if childFiles.count <= 0 { return childDirectories } + + var filesAndDirectories = childDirectories ?? [] + for file in childFiles { + filesAndDirectories.insert(DirectoryEntry(path: file, parent: path, isFile: true), at: 0) + } + + return filesAndDirectories.sorted(by: { $0.asString < $1.asString }) + } + var asString: String { + let str = path.description.replacingOccurrences(of: parent.description, with: "").removingPercentEncoding! + if str.count <= 0 { + return "/" + } + return str + } +} + +private enum FileExplorerAction { + case delete + case zip + case insert +} + +private struct File: View { + @ObservedObject private var iO = Inject.observer + + var item: DirectoryEntry + @Binding var explorerHidden: Bool + + @State var quickLookURL: URL? + @State var fileExplorerAction: FileExplorerAction? + @State var hidden = false + @State var isShowingFilePicker = false + @State var selectedFile: URL? + + var body: some View { + AsyncFallibleButton(action: { + switch (fileExplorerAction) { + case .delete: + print("deleting \(item.path.description)") + try FileManager.default.removeItem(at: item.path) + + case .zip: + print("zipping \(item.path.description)") + let dest = FileManager.default.documentsDirectory.appendingPathComponent(item.path.pathComponents.last! + ".zip") + do { + try FileManager.default.removeItem(at: dest) + } catch {} + + try FileManager.default.zipItem(at: item.path, to: dest) + + case .insert: + print("inserting \(selectedFile!.description) to \(item.path.description)") + + try FileManager.default.copyItem(at: selectedFile!, to: item.path.appendingPathComponent(selectedFile!.pathComponents.last!)) + explorerHidden = true + explorerHidden = false + + default: + print("unknown action for \(item.path.description): \(String(describing: fileExplorerAction))") + } + }, label: { execute in + HStack { + Text(item.asString) + if item.isFile { + Text(getFileSize(file: item.path)).foregroundColor(.secondary) + } + Spacer() + Menu { + if item.isFile { + SwiftUI.Button(action: { quickLookURL = item.path }) { + Label("View/Share", systemSymbol: .eye) + } + } else { + SwiftUI.Button(action: { + fileExplorerAction = .zip + execute() + }) { + Label("Save to ZIP file", systemSymbol: .squareAndArrowDown) + } + + SwiftUI.Button { + isShowingFilePicker = true + } label: { + Label("Insert file", systemSymbol: .plus) + } + } + + if item.asString != "/" { + SwiftUI.Button(action: { + fileExplorerAction = .delete + execute() + }) { + Label("Delete", systemSymbol: .trash) + } + } + } label: { + Image(systemSymbol: .ellipsis) + .frame(width: 20, height: 20) // Make it easier to tap + } + } + .onChange(of: $selectedFile) { file in + guard file.wrappedValue != nil else { return } + + fileExplorerAction = .insert + execute() + } + }, afterFinish: { success in + switch (fileExplorerAction) { + case .delete: + if success { hidden = true } + + case .zip: + UIApplication.shared.open(URL(string: "shareddocuments://" + FileManager.default.documentsDirectory.description.replacingOccurrences(of: "file://", with: ""))!, options: [:], completionHandler: nil) + + default: break + } + }, wrapInButton: false) + .quickLookPreview($quickLookURL) + .sheet(isPresented: $isShowingFilePicker) { + DocumentPicker(selectedUrl: $selectedFile, supportedTypes: allUTITypes().map({ $0.identifier })) + .ignoresSafeArea() + } + .isHidden($hidden) + .enableInjection() + } + + func getFileSize(file: URL) -> String { + guard let attributes = try? FileManager.default.attributesOfItem(atPath: file.description.replacingOccurrences(of: "file://", with: "")) else { return "Unknown file size" } + var bytes = attributes[FileAttributeKey.size] as! Double + + // https://stackoverflow.com/a/14919494 (ported to swift) + let thresh = 1024.0; + + if (bytes < thresh) { + return String(describing: bytes) + " B"; + } + + let units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + var u = -1; + + while (bytes >= thresh && u < units.count - 1) { + bytes /= thresh; + u += 1; + } + + return String(format: "%.2f", bytes) + " " + units[u]; + } +} + +struct FileExplorer: View { + @ObservedObject private var iO = Inject.observer + + var url: URL? + + @State var hidden = false + + var body: some View { + List([iterateOverDirectory(directory: url!, parent: url!)], children: \.filesAndDirectories) { item in + File(item: item, explorerHidden: $hidden) + } + .toolbar { + ToolbarItem { + SwiftUI.Button { + hidden = true + hidden = false + } label: { + Image(systemSymbol: .arrowClockwise) + } + } + } + .isHidden($hidden) + .enableInjection() + } + + private func iterateOverDirectory(directory: URL, parent: URL) -> DirectoryEntry { + var directoryEntry = DirectoryEntry(path: directory, parent: parent) + if let contents = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) { + for entry in contents { + if entry.hasDirectoryPath { + if directoryEntry.childDirectories == nil { directoryEntry.childDirectories = [] } + directoryEntry.childDirectories!.append(iterateOverDirectory(directory: entry, parent: directory)) + } else { + directoryEntry.childFiles.append(entry) + } + } + } + return directoryEntry + } +} + +struct FileExplorer_Previews: PreviewProvider { + static var previews: some View { + FileExplorer(url: FileManager.default.altstoreSharedDirectory) + } +} diff --git a/AltStore/Views/Settings/AppIconsView.swift b/AltStore/Views/Settings/AppIconsView.swift index 356c635d..6d1829ca 100644 --- a/AltStore/Views/Settings/AppIconsView.swift +++ b/AltStore/Views/Settings/AppIconsView.swift @@ -9,13 +9,13 @@ import SwiftUI import SFSafeSymbols -struct Icon: Identifiable { +private struct Icon: Identifiable { var id: String { assetName } var displayName: String let assetName: String } -struct SpecialIcon { +private struct SpecialIcon { let assetName: String let suffix: String? let forceIndex: Int? diff --git a/AltStore/Views/Settings/DevModeView.swift b/AltStore/Views/Settings/DevModeView.swift new file mode 100644 index 00000000..84631baa --- /dev/null +++ b/AltStore/Views/Settings/DevModeView.swift @@ -0,0 +1,147 @@ +// +// DevModeView.swift +// SideStore +// +// Created by naturecodevoid on 2/16/23. +// Copyright © 2023 SideStore. All rights reserved. +// + +import SwiftUI +import LocalConsole + +struct DevModePrompt: View { + @Binding var isShowingDevModePrompt: Bool + @Binding var isShowingDevModeMenu: Bool + + @State var countdown = 0 + + var button: some View { + SwiftUI.Button(action: { + UserDefaults.standard.isDevModeEnabled = true + isShowingDevModePrompt = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + isShowingDevModeMenu = true + } + }) { + Text(countdown <= 0 ? L10n.Action.enable + " " + L10n.DevModeView.title : L10n.DevModeView.read + " (\(countdown))") + .foregroundColor(.red) + } + .disabled(countdown > 0) + } + + var text: some View { + if #available(iOS 15.0, *) { + do { + return Text(try AttributedString(markdown: L10n.DevModeView.prompt, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))) + } catch { + return Text(L10n.DevModeView.prompt) + } + } else { + return Text(L10n.DevModeView.prompt) + } + } + + var body: some View { + NavigationView { + ScrollView { + VStack { + text + .foregroundColor(.primary) + .padding(.bottom) + + if #available(iOS 15.0, *) { + button.buttonStyle(.bordered) + } else { + button + } + } + .padding(.horizontal) + } + .frame(maxWidth: .infinity) + .navigationTitle(L10n.DevModeView.title) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + SwiftUI.Button(action: { isShowingDevModePrompt = false }) { + Text(L10n.Action.close) + } + } + } + .onAppear { + countdown = 20 + tickCountdown() + } + } + } + + func tickCountdown() { + if countdown <= 0 { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + countdown -= 1 + tickCountdown() + } + } +} + +struct DevModeMenu: View { + @ObservedObject private var iO = Inject.observer + + @AppStorage("isConsoleEnabled") + var isConsoleEnabled: Bool = false + + var body: some View { + List { + Section { + Toggle(L10n.DevModeView.console, isOn: self.$isConsoleEnabled) + .onChange(of: self.isConsoleEnabled) { value in + LCManager.shared.isVisible = value + } + + NavigationLink(L10n.DevModeView.dataExplorer) { + FileExplorer(url: FileManager.default.altstoreSharedDirectory) + .navigationTitle(L10n.DevModeView.dataExplorer) + }.foregroundColor(.red) + + NavigationLink(L10n.DevModeView.tmpExplorer) { + FileExplorer(url: FileManager.default.temporaryDirectory) + .navigationTitle(L10n.DevModeView.tmpExplorer) + }.foregroundColor(.red) + + Toggle(L10n.DevModeView.skipResign, isOn: ResignAppOperation.skipResignBinding) + .foregroundColor(.red) + } footer: { + Text(L10n.DevModeView.skipResignInfo) + } + + Section { + NavigationLink(L10n.DevModeView.Minimuxer.stagingExplorer + " (Coming soon, needs minimuxer additions)") { + FileExplorer(url: FileManager.default.altstoreSharedDirectory) + .navigationTitle(L10n.DevModeView.Minimuxer.stagingExplorer) + }.foregroundColor(.red).disabled(true) + + NavigationLink(L10n.DevModeView.Minimuxer.viewProfiles + " (Coming soon, needs minimuxer additions)") { + + }.disabled(true) + + SwiftUI.Button(L10n.DevModeView.Minimuxer.dumpProfiles + " (Coming soon, needs minimuxer additions)", action: { + // TODO: dump profiles to Documents/ProfileDump/[current time] + }).disabled(true) + } header: { + Text(L10n.DevModeView.minimuxer) + } + } + .navigationTitle(L10n.DevModeView.title) + .enableInjection() + } +} + +struct DevModeView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + List { + NavigationLink("DevModeMenu") { + DevModeMenu() + } + } + } + } +} diff --git a/AltStore/Views/Settings/SettingsView.swift b/AltStore/Views/Settings/SettingsView.swift index 715616c0..682687a7 100644 --- a/AltStore/Views/Settings/SettingsView.swift +++ b/AltStore/Views/Settings/SettingsView.swift @@ -26,10 +26,15 @@ struct SettingsView: View { @AppStorage("isBackgroundRefreshEnabled") var isBackgroundRefreshEnabled: Bool = true + @AppStorage("isDevModeEnabled") + var isDevModeEnabled: Bool = false + @State var isShowingConnectAppleIDView = false @State var isShowingAddShortcutView = false @State var isShowingFeedbackMailView = false @State var isShowingResetPairingFileConfirmation = false + @State var isShowingDevModePrompt = false + @State var isShowingDevModeMenu = false @State var externalURLToShow: URL? @@ -163,10 +168,6 @@ struct SettingsView: View { RefreshAttemptsView() } - SwiftUI.Button("Toggle Console") { - LCManager.shared.isVisible.toggle() - } - if MailComposeView.canSendMail { SwiftUI.Button("Send Feedback") { self.isShowingFeedbackMailView = true @@ -199,6 +200,21 @@ struct SettingsView: View { .cancel() ]) } + + if isDevModeEnabled { + NavigationLink(L10n.DevModeView.title, isActive: self.$isShowingDevModeMenu) { + DevModeMenu() + } + .foregroundColor(.red) + } else { + SwiftUI.Button(L10n.DevModeView.title) { + self.isShowingDevModePrompt = true + } + .foregroundColor(.red) + .sheet(isPresented: self.$isShowingDevModePrompt) { + DevModePrompt(isShowingDevModePrompt: self.$isShowingDevModePrompt, isShowingDevModeMenu: self.$isShowingDevModeMenu) + } + } } header: { Text(L10n.SettingsView.debug) } diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift index d780073f..c87b36da 100644 --- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift +++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift @@ -26,6 +26,9 @@ public extension UserDefaults @NSManaged var customAnisetteURL: String? @NSManaged var preferredServerID: String? + @NSManaged var isDevModeEnabled: Bool + @NSManaged var isConsoleEnabled: Bool + @NSManaged var isBackgroundRefreshEnabled: Bool @NSManaged var isDebugModeEnabled: Bool @NSManaged var presentedLaunchReminderNotification: Bool @@ -70,6 +73,8 @@ public extension UserDefaults let localServerSupportsRefreshing = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14) let defaults = [ + #keyPath(UserDefaults.isDevModeEnabled): false, + #keyPath(UserDefaults.isConsoleEnabled): false, #keyPath(UserDefaults.isBackgroundRefreshEnabled): true, #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,