feat(dev mode): add AFC file explorer and dump profiles

This commit is contained in:
naturecodevoid
2023-04-09 13:38:44 -07:00
parent 423ac28ba3
commit 994b2318a9
5 changed files with 408 additions and 293 deletions

View File

@@ -11,12 +11,18 @@ import Foundation
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum L10n {
internal enum Action {
/// Cancel
internal static let cancel = L10n.tr("Localizable", "Action.cancel", fallback: "Cancel")
/// Close
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")
/// Submit
internal static let submit = L10n.tr("Localizable", "Action.submit", fallback: "Submit")
/// Try Again
internal static let tryAgain = L10n.tr("Localizable", "Action.tryAgain", fallback: "Try Again")
}
internal enum AddSourceView {
/// Continue
@@ -250,6 +256,8 @@ internal enum L10n {
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")
/// 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).
internal static let footer = L10n.tr("Localizable", "DevModeView.footer", 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).")
/// Incorrect password.
internal static let incorrectPassword = L10n.tr("Localizable", "DevModeView.incorrectPassword", fallback: "Incorrect password.")
/// minimuxer debug actions
@@ -260,28 +268,31 @@ internal enum L10n {
///
/// 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 by a helper when getting support
/// - 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._**")
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- You were asked to do this by a helper when getting support\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 {
/// AFC File Explorer (check footer for notes)
internal static let afcExplorer = L10n.tr("Localizable", "DevModeView.Minimuxer.afcExplorer", fallback: "AFC File Explorer (check footer for notes)")
/// 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")
/// Notes on AFC File Explorer:
/// - If nothing shows up, check minimuxer logs for error
/// - It is currently extremely very unoptimized and may be very slow; a new AFC client is created for every action
/// - It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it
/// - Very buggy
/// - There are multiple unimplemented actions
internal static let footer = L10n.tr("Localizable", "DevModeView.Minimuxer.footer", fallback: "Notes on AFC File Explorer:\n- If nothing shows up, check minimuxer logs for error\n- It is currently extremely very unoptimized and may be very slow; a new AFC client is created for every action\n- It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it\n- Very buggy\n- There are multiple unimplemented actions")
}
}
internal enum MyAppsView {

View File

@@ -123,50 +123,53 @@ enum OperationError: LocalizedError
}
}
/// crashes if error is not a MinimuxerError
/// crashes if error is not a MinimuxerError or OperationError
func minimuxerToOperationError(_ error: Error) -> OperationError {
switch error as! MinimuxerError {
case .NoDevice:
return OperationError.noDevice
case .NoConnection:
return OperationError.noConnection
case .PairingFile:
return OperationError.invalidPairingFile
case .CreateDebug:
return OperationError.createService(name: "debug")
case .CreateInstproxy:
return OperationError.createService(name: "instproxy")
case .LookupApps:
return OperationError.getFromDevice(name: "installed apps")
case .FindApp:
return OperationError.getFromDevice(name: "path to the app")
case .BundlePath:
return OperationError.getFromDevice(name: "bundle path")
case .MaxPacket:
return OperationError.setArgument(name: "max packet")
case .WorkingDirectory:
return OperationError.setArgument(name: "working directory")
case .Argv:
return OperationError.setArgument(name: "argv")
case .LaunchSuccess:
return OperationError.getFromDevice(name: "launch success")
case .Detach:
return OperationError.detach
case .Attach:
return OperationError.attach
case .CreateAfc:
return OperationError.createService(name: "AFC")
case .RwAfc:
return OperationError.afc
case .InstallApp:
return OperationError.install
case .UninstallApp:
return OperationError.uninstall
case .CreateMisagent:
return OperationError.createService(name: "misagent")
case .ProfileInstall:
return OperationError.profileManage
case .ProfileRemove:
return OperationError.profileManage
if let error = error as? MinimuxerError {
switch error {
case .NoDevice:
return OperationError.noDevice
case .NoConnection:
return OperationError.noConnection
case .PairingFile:
return OperationError.invalidPairingFile
case .CreateDebug:
return OperationError.createService(name: "debug")
case .CreateInstproxy:
return OperationError.createService(name: "instproxy")
case .LookupApps:
return OperationError.getFromDevice(name: "installed apps")
case .FindApp:
return OperationError.getFromDevice(name: "path to the app")
case .BundlePath:
return OperationError.getFromDevice(name: "bundle path")
case .MaxPacket:
return OperationError.setArgument(name: "max packet")
case .WorkingDirectory:
return OperationError.setArgument(name: "working directory")
case .Argv:
return OperationError.setArgument(name: "argv")
case .LaunchSuccess:
return OperationError.getFromDevice(name: "launch success")
case .Detach:
return OperationError.detach
case .Attach:
return OperationError.attach
case .CreateAfc:
return OperationError.createService(name: "AFC")
case .RwAfc:
return OperationError.afc
case .InstallApp:
return OperationError.install
case .UninstallApp:
return OperationError.uninstall
case .CreateMisagent:
return OperationError.createService(name: "misagent")
case .ProfileInstall:
return OperationError.profileManage
case .ProfileRemove:
return OperationError.profileManage
}
}
return error as! OperationError
}

View File

@@ -11,6 +11,9 @@
"Action.done" = "Done";
"Action.close" = "Close";
"Action.enable" = "Enable";
"Action.submit" = "Submit";
"Action.cancel" = "Cancel";
"Action.tryAgain" = "Try Again";
/* NewsView */
"NewsView.title" = "News";
@@ -174,7 +177,7 @@
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 by a helper when getting support
- 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._**";
@@ -185,11 +188,16 @@ You should only enable Developer Mode if you meet one of the following requireme
"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.footer" = "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).";
"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";
"DevModeView.Minimuxer.afcExplorer" = "AFC File Explorer (check footer for notes)";
"DevModeView.Minimuxer.footer" = "Notes on AFC File Explorer:
- If nothing shows up, check minimuxer logs for error
- It is currently extremely very unoptimized and may be very slow; a new AFC client is created for every action
- It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it
- Very buggy
- There are multiple unimplemented actions";
/* AsyncFallibleButton */
"AsyncFallibleButton.error" = "An error occurred";

View File

@@ -9,6 +9,309 @@
import SwiftUI
import ZIPFoundation
import UniformTypeIdentifiers
import minimuxer
extension Binding<URL?>: Equatable {
public static func == (lhs: Binding<URL?>, rhs: Binding<URL?>) -> Bool {
return lhs.wrappedValue == rhs.wrappedValue
}
}
private protocol FileExplorerBackend {
func delete(_ path: URL) throws
func zip(_ path: URL) throws
func insert(file: URL, to: URL) throws
func iterate(_ directory: URL) -> DirectoryEntry
func getQuickLookURL(_ path: URL) throws -> URL
}
private class NormalFileExplorerBackend: FileExplorerBackend {
func delete(_ path: URL) throws {
try FileManager.default.removeItem(at: path)
}
func zip(_ path: URL) throws {
let dest = FileManager.default.documentsDirectory.appendingPathComponent(path.pathComponents.last! + ".zip")
do {
try FileManager.default.removeItem(at: dest)
} catch {}
try FileManager.default.zipItem(at: path, to: dest)
}
func insert(file: URL, to: URL) throws {
try FileManager.default.copyItem(at: file, to: to.appendingPathComponent(file.pathComponents.last!), shouldReplace: true)
}
private func _iterate(directory: URL, parent: URL) -> DirectoryEntry {
var directoryEntry = DirectoryEntry(path: directory, parent: parent, isFile: false)
if let contents = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) {
for entry in contents {
if entry.hasDirectoryPath {
directoryEntry.children!.append(_iterate(directory: entry, parent: directory))
} else {
directoryEntry.children!.append(DirectoryEntry(path: entry, parent: directory, isFile: true, size: {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: entry.description.replacingOccurrences(of: "file://", with: "")) else { return nil }
return attributes[FileAttributeKey.size] as? Double
}()))
}
}
}
return directoryEntry
}
func iterate(_ directory: URL) -> DirectoryEntry {
return _iterate(directory: directory, parent: directory)
}
func getQuickLookURL(_ path: URL) throws -> URL {
path
}
}
private class AfcFileExplorerBackend: FileExplorerBackend {
func delete(_ path: URL) throws {
try AfcFileManager.remove(path.description.replacingOccurrences(of: "file://", with: "").removingPercentEncoding!)
}
func zip(_ path: URL) throws {
throw NSError(domain: "AFC currently doesn't support zipping a directory/file. however, it is possible (we should be able to copy the files outside of AFC and then zip the copied directory/file), it just hasn't been implemented", code: -1)
}
func insert(file: URL, to: URL) throws {
let data = try Data(contentsOf: file)
let rustByteSlice = data.toRustByteSlice()
let to = to.appendingPathComponent(file.lastPathComponent).description.replacingOccurrences(of: "file://", with: "").removingPercentEncoding!
print("writing to \(to)")
try AfcFileManager.writeFile(to, rustByteSlice.forRust())
}
private func _addChildren(_ rustEntry: RustDirectoryEntryRef) -> DirectoryEntry {
var entry = DirectoryEntry(
path: URL(string: rustEntry.path().toString())!,
parent: URL(string: rustEntry.parent().toString())!,
isFile: rustEntry.isFile(),
size: rustEntry.size() != nil ? Double(rustEntry.size()!) : nil
)
for child in rustEntry.children() {
entry.children!.append(_addChildren(child))
}
return entry
}
func iterate(_ directory: URL) -> DirectoryEntry {
var directoryEntry = DirectoryEntry(path: directory, parent: directory, isFile: false)
for child in AfcFileManager.contents() {
directoryEntry.children!.append(_addChildren(child))
}
return directoryEntry
}
func getQuickLookURL(_ path: URL) throws -> URL {
throw NSError(domain: "AFC currently doesn't support viewing a file. however, it is possible (we should be able to copy the file outside of AFC and then view the copied file), it just hasn't been implemented", code: -1)
}
}
private struct DirectoryEntry: Identifiable {
var id = UUID()
var path: URL
var parent: URL
var isFile: Bool
var size: Double?
var children: [DirectoryEntry]? = []
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
case quickLook
}
private struct File: View {
@ObservedObject private var iO = Inject.observer
var item: DirectoryEntry
var backend: FileExplorerBackend
@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 backend.delete(item.path)
case .zip:
print("zipping \(item.path.description)")
try backend.zip(item.path)
case .insert:
print("inserting \(selectedFile!.description) to \(item.path.description)")
try backend.insert(file: selectedFile!, to: item.path)
explorerHidden = true
explorerHidden = false
case .quickLook:
print("viewing \(item.path.description)")
quickLookURL = try backend.getQuickLookURL(item.path)
default:
print("unknown action for \(item.path.description): \(String(describing: fileExplorerAction))")
}
}, label: { execute in
HStack {
Text(item.asString)
if item.isFile {
Text(getFileSize(item.size)).foregroundColor(.secondary)
}
Spacer()
Menu {
if item.isFile {
SwiftUI.Button(action: {
fileExplorerAction = .quickLook
execute()
}) {
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(_ bytes: Double?) -> String {
guard var bytes = bytes else { return "Unknown file size" }
// 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
private var url: URL?
private var backend: FileExplorerBackend
private init(_ url: URL?, _ backend: FileExplorerBackend) {
self.url = url
self.backend = backend
}
static func normal(url: URL?) -> FileExplorer {
FileExplorer(url, NormalFileExplorerBackend())
}
static func afc() -> FileExplorer {
FileExplorer(URL(string: "/")!, AfcFileExplorerBackend())
}
@State var hidden = false
var body: some View {
List([backend.iterate(url!)], children: \.children) { item in
File(item: item, backend: backend, explorerHidden: $hidden)
}
.toolbar {
ToolbarItem {
SwiftUI.Button {
hidden = true
hidden = false
} label: {
Image(systemSymbol: .arrowClockwise)
}
}
}
.isHidden($hidden)
.enableInjection()
}
}
struct FileExplorer_Previews: PreviewProvider {
static var previews: some View {
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
}
}
// https://stackoverflow.com/a/72165424
func allUTITypes() -> [UTType] {
@@ -152,215 +455,3 @@ func allUTITypes() -> [UTType] {
return types + types_1 + types_2
}
extension Binding<URL?>: Equatable {
public static func == (lhs: Binding<URL?>, rhs: Binding<URL?>) -> 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!), shouldReplace: true)
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)
}
}

View File

@@ -8,8 +8,9 @@
import SwiftUI
import LocalConsole
import minimuxer
// Yes, we know the password is right here. It's also in CONTRIBUTING.md. It's not supposed to be a secret, just something to hopefully prevent people breaking SideStore with dev mode and then complaining to us.
// Yes, we know the password is right here. It's not supposed to be a secret, just something to hopefully prevent people breaking SideStore with dev mode and then complaining to us.
let DEV_MODE_PASSWORD = "devmode"
struct DevModePrompt: View {
@@ -23,16 +24,18 @@ struct DevModePrompt: View {
var button: some View {
SwiftUI.Button(action: {
if #available(iOS 15.0, *) {
if #available(iOS 16.0, *) {
isShowingPasswordAlert = true
} else {
// iOS 14 doesn't support .alert, so just go straight to dev mode without asking for a password
// iOS 15 also doesn't seem to support TextField in an alert (the text field was nonexistent)
enableDevMode()
}
}) {
Text(countdown <= 0 ? L10n.Action.enable + " " + L10n.DevModeView.title : L10n.DevModeView.read + " (\(countdown))")
.foregroundColor(.red)
}
.buttonStyle(FilledButtonStyle()) // TODO: set tintColor so text is more readable
.disabled(countdown > 0)
}
@@ -53,11 +56,7 @@ struct DevModePrompt: View {
.foregroundColor(.primary)
.padding(.bottom)
if #available(iOS 15.0, *) {
button.buttonStyle(.bordered)
} else {
button
}
button
}
.padding(.horizontal)
}
@@ -81,10 +80,10 @@ struct DevModePrompt: View {
if #available(iOS 15.0, *) {
view
.alert(L10n.DevModeView.password, isPresented: $isShowingPasswordAlert) {
TextField("Password", text: $password)
TextField(L10n.DevModeView.password, text: $password)
.autocapitalization(.none)
.autocorrectionDisabled(true)
SwiftUI.Button("Submit", action: {
SwiftUI.Button(L10n.Action.submit, action: {
if password == DEV_MODE_PASSWORD {
enableDevMode()
} else {
@@ -93,11 +92,11 @@ struct DevModePrompt: View {
})
}
.alert(L10n.DevModeView.incorrectPassword, isPresented: $isShowingIncorrectPasswordAlert) {
SwiftUI.Button("Try again", action: {
SwiftUI.Button(L10n.Action.tryAgain, action: {
isShowingIncorrectPasswordAlert = false
isShowingPasswordAlert = true
})
SwiftUI.Button("Cancel", action: {
SwiftUI.Button(L10n.Action.cancel, action: {
isShowingIncorrectPasswordAlert = false
isShowingDevModePrompt = false
})
@@ -138,36 +137,39 @@ struct DevModeMenu: View {
}
NavigationLink(L10n.DevModeView.dataExplorer) {
FileExplorer(url: FileManager.default.altstoreSharedDirectory)
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
.navigationTitle(L10n.DevModeView.dataExplorer)
}.foregroundColor(.red)
NavigationLink(L10n.DevModeView.tmpExplorer) {
FileExplorer(url: FileManager.default.temporaryDirectory)
FileExplorer.normal(url: FileManager.default.temporaryDirectory)
.navigationTitle(L10n.DevModeView.tmpExplorer)
}.foregroundColor(.red)
Toggle(L10n.DevModeView.skipResign, isOn: ResignAppOperation.skipResignBinding)
.foregroundColor(.red)
} footer: {
Text(L10n.DevModeView.skipResignInfo)
Text(L10n.DevModeView.footer)
}
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)
AsyncFallibleButton(action: {
let dir = try dump_profiles(FileManager.default.documentsDirectory.absoluteString)
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: "shareddocuments://" + dir.toString())!, options: [:], completionHandler: nil)
}
}) { execute in
Text(L10n.DevModeView.Minimuxer.dumpProfiles)
}
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)
NavigationLink(L10n.DevModeView.Minimuxer.afcExplorer) {
FileExplorer.afc()
.navigationTitle(L10n.DevModeView.Minimuxer.afcExplorer)
}.foregroundColor(.red)
} header: {
Text(L10n.DevModeView.minimuxer)
} footer: {
Text(L10n.DevModeView.Minimuxer.footer)
}
}
.navigationTitle(L10n.DevModeView.title)