From d51f8e9b6cb1deee3c00453806b5a8d9c3d67096 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:03:39 -0500 Subject: [PATCH] Move Scripts to Tools tab, add bookmarks to Location Simulator, and replace Home screen with Apps tab --- StikJIT/Views/HomeView.swift | 283 ++++------------------ StikJIT/Views/InstalledAppsListView.swift | 21 +- StikJIT/Views/MainTabView.swift | 6 +- StikJIT/Views/MapSelectionView.swift | 128 ++++++++++ StikJIT/Views/ToolsView.swift | 1 + 5 files changed, 193 insertions(+), 246 deletions(-) diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 62902e2..52c1e0d 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -7,6 +7,7 @@ import SwiftUI import UniformTypeIdentifiers +import UIKit struct JITEnableConfiguration { var bundleID: String? = nil @@ -17,164 +18,35 @@ struct JITEnableConfiguration { struct HomeView: View { - @AppStorage("username") private var username = "User" @AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false - let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @AppStorage("bundleID") private var bundleID: String = "" @State private var isProcessing = false - @State private var isShowingInstalledApps = false - @State private var isShowingPairingFilePicker = false - @State private var pairingFileExists: Bool = false - @State private var showPairingFileMessage = false - @State private var pairingFileIsValid = false - @State private var isImportingFile = false - @State private var importProgress: Float = 0.0 - @State private var viewDidAppeared = false @State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil - + @State private var isShowingPairingFilePicker = false + @State var scriptViewShow = false @State private var isShowingConsole = false @AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js" @State var jsModel: RunJSViewModel? @ObservedObject private var mounting = MountingProgress.shared + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { - ZStack { - VStack(spacing: 25) { - Spacer() - VStack(spacing: 5) { - Text("Welcome to StikDebug, \(username)!") - .font(.system(.largeTitle, design: .rounded)) - .fontWeight(.bold) - .lineLimit(1) - .minimumScaleFactor(0.5) - - Text(pairingFileExists ? "Tap \"Enable JIT\" to get started" : "Pick pairing file to get started") - .font(.system(.subheadline, design: .rounded)) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } - .padding(.top, 40) - - // Main action button - changes based on whether we have a pairing file - Button(action: { - - - if pairingFileExists { - // Refresh heartbeat - pubHeartBeat = false - startHeartbeatInBackground() - - // Got a pairing file, check mount status - let mountStatus = checkMountStatus() - if mountStatus == .notMounted { - showAlert(title: "Device Not Mounted", message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.", showOk: true) { cool in - // No Need - } - return - } else if mountStatus == .unreachable { - // Don't show a separate error here — the heartbeat - // will fail and show its own connectivity error. - return - } - - isShowingInstalledApps = true - - } else { - // No pairing file yet, let's get one - isShowingPairingFilePicker = true - } - }) { - HStack { - Image(systemName: pairingFileExists ? "bolt.fill" : "doc.badge.plus") - .font(.system(size: 20)) - Text(pairingFileExists ? "Enable JIT" : "Select Pairing File") - .font(.system(.title3, design: .rounded)) - .fontWeight(.semibold) - } - .frame(maxWidth: .infinity) - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(16) - } - .padding(.horizontal, 20) - - Button(action: { - isShowingConsole = true - }) { - HStack { - Image(systemName: "terminal") - .font(.system(size: 20)) - Text("Open Console") - .font(.system(.title3, design: .rounded)) - .fontWeight(.semibold) - } - .frame(maxWidth: .infinity) - .padding() - .background(Color.gray.opacity(0.2)) - .foregroundColor(.primary) - .cornerRadius(16) - } - .padding(.horizontal, 20) - - // Status message area - keeps layout consistent - ZStack { - // Progress bar for importing file - if isImportingFile { - VStack(spacing: 8) { - HStack { - Text("Processing pairing file...") - .font(.system(.caption, design: .rounded)) - Spacer() - Text("\(Int(importProgress * 100))%") - .font(.system(.caption, design: .rounded)) - } - - GeometryReader { geometry in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 4) - .fill(Color.black.opacity(0.2)) - .frame(height: 8) - - RoundedRectangle(cornerRadius: 4) - .fill(Color.green) - .frame(width: geometry.size.width * CGFloat(importProgress), height: 8) - .animation(.linear(duration: 0.3), value: importProgress) - } - } - .frame(height: 8) - } - .padding(.horizontal, 40) - } - - // Success message - if showPairingFileMessage && pairingFileIsValid { - Text("✓ Pairing file successfully imported") - .font(.system(.callout, design: .rounded)) - .foregroundColor(.green) - .padding(.vertical, 4) - .padding(.horizontal, 12) - .background(Color.green.opacity(0.1)) - .cornerRadius(8) - .transition(.opacity) - } - - // Invisible text to reserve space - no layout jumps - Text(" ").opacity(0) - } - .frame(height: isImportingFile ? 60 : 30) // Adjust height based on what's showing - - Spacer() - } - .padding() - } + InstalledAppsListView(onSelectApp: { selectedBundle in + bundleID = selectedBundle + HapticFeedbackHelper.trigger() + startJITInBackground(bundleID: selectedBundle) + }, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true }) .onAppear { - checkPairingFileExists() startHeartbeatInBackground() MountingProgress.shared.checkforMounted() + viewDidAppeared = true + if let config = pendingJITEnableConfiguration { + startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) + pendingJITEnableConfiguration = nil + } } .onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady)) { notification in guard let model = notification.userInfo?["model"] as? RunJSViewModel else { return } @@ -185,85 +57,10 @@ struct HomeView: View { scriptViewShow = true } .onReceive(timer) { _ in - checkPairingFileExists() if mounting.mountingThread == nil && !mounting.coolisMounted { MountingProgress.shared.checkforMounted() } } - .fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!, .propertyList]) {result in - switch result { - - case .success(let url): - let fileManager = FileManager.default - let accessing = url.startAccessingSecurityScopedResource() - - if fileManager.fileExists(atPath: url.path) { - do { - if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) { - try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) - } - - try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) - print("File copied successfully!") - - // Show progress bar and initialize progress - DispatchQueue.main.async { - isImportingFile = true - importProgress = 0.0 - pairingFileExists = true - } - - startHeartbeatInBackground() - - let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in - DispatchQueue.main.async { - if importProgress < 1.0 { - importProgress += 0.25 - } else { - timer.invalidate() - isImportingFile = false - pairingFileIsValid = true - - // Show success message - withAnimation { - showPairingFileMessage = true - } - - // Hide message after delay - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - withAnimation { - showPairingFileMessage = false - } - } - } - } - } - - // Ensure timer keeps running - RunLoop.current.add(progressTimer, forMode: .common) - - } catch { - print("Error copying file: \(error)") - } - } else { - print("Source file does not exist.") - } - - if accessing { - url.stopAccessingSecurityScopedResource() - } - case .failure(let error): - print("Failed to import file: \(error)") - } - } - .sheet(isPresented: $isShowingInstalledApps) { - InstalledAppsListView { selectedBundle in - bundleID = selectedBundle - isShowingInstalledApps = false - HapticFeedbackHelper.trigger() - startJITInBackground(bundleID: selectedBundle) - } - } .onOpenURL { url in guard let host = url.host() else { return } let components = URLComponents(url: url, resolvingAgainstBaseURL: false) @@ -324,11 +121,26 @@ struct HomeView: View { break } } - .onAppear() { - viewDidAppeared = true - if let config = pendingJITEnableConfiguration { - startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) - pendingJITEnableConfiguration = nil + .fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!, .propertyList]) { result in + switch result { + case .success(let url): + let fileManager = FileManager.default + let accessing = url.startAccessingSecurityScopedResource() + if fileManager.fileExists(atPath: url.path) { + do { + let dest = URL.documentsDirectory.appendingPathComponent("pairingFile.plist") + if fileManager.fileExists(atPath: dest.path) { + try fileManager.removeItem(at: dest) + } + try fileManager.copyItem(at: url, to: dest) + startHeartbeatInBackground() + } catch { + print("Error copying pairing file: \(error)") + } + } + if accessing { url.stopAccessingSecurityScopedResource() } + case .failure(let error): + print("Failed to import pairing file: \(error)") } } .sheet(isPresented: $isShowingConsole) { @@ -358,9 +170,8 @@ struct HomeView: View { } } } - - + private func autoScript(for bundleID: String) -> (data: Data, name: String)? { guard ProcessInfo.processInfo.hasTXM else { return nil } guard #available(iOS 26, *) else { return nil } @@ -381,7 +192,7 @@ struct HomeView: View { } return (data, resource.fileName) } - + private func assignedScript(for bundleID: String) -> (data: Data, name: String)? { guard let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String], let scriptName = mapping[bundleID] else { return nil } @@ -392,20 +203,20 @@ struct HomeView: View { let data = try? Data(contentsOf: scriptURL) else { return nil } return (data, scriptName) } - + private func preferredScript(for bundleID: String) -> (data: Data, name: String)? { if let assigned = assignedScript(for: bundleID) { return assigned } return autoScript(for: bundleID) } - + private func storedFavoriteName(for bundleID: String) -> String? { let defaults = UserDefaults(suiteName: "group.com.stik.sj") let names = defaults?.dictionary(forKey: "favoriteAppNames") as? [String: String] return names?[bundleID] } - + private func autoScriptResource(for appName: String) -> (resource: String, fileName: String)? { switch appName { case "maciOS": @@ -422,30 +233,26 @@ struct HomeView: View { return nil } } - + private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback { return { pid, debugProxyHandle, remoteServerHandle, semaphore in let model = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, remoteServer: remoteServerHandle, semaphore: semaphore) - + DispatchQueue.main.async { jsModel = model scriptViewShow = true } - + DispatchQueue.global(qos: .background).async { do { try model.runScript(data: script, name: name) } catch { showAlert(title: "Error Occurred While Executing Script.".localized, message: error.localizedDescription, showOk: true) } } } } - - private func checkPairingFileExists() { - pairingFileExists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) - } - + private func startJITInBackground(bundleID: String? = nil, pid: Int? = nil, scriptData: Data? = nil, scriptName: String? = nil, triggeredByURLScheme: Bool = false) { isProcessing = true LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))") @@ -527,11 +334,11 @@ public extension ProcessInfo { } return ProcessInfo.detectLocalTXM() } - + var isTXMOverridden: Bool { UserDefaults.standard.bool(forKey: UserDefaults.Keys.txmOverride) } - + private static func detectLocalTXM() -> Bool { if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index 77f2e74..278f1c5 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -40,6 +40,8 @@ struct InstalledAppsListView: View { @Environment(\.dismiss) private var dismiss var onSelectApp: (String) -> Void + var showDoneButton: Bool = true + var onImportPairingFile: (() -> Void)? = nil private var currentSearchBinding: Binding { @@ -164,7 +166,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable { tabContent(for: selectedTab) .transition(.opacity) .transaction { t in t.disablesAnimations = true } - .navigationTitle("Installed Apps".localized) + .navigationTitle(selectedTab == .debuggable ? "Enable JIT".localized : "Launch Apps".localized) .searchable( text: currentSearchBinding, placement: .navigationBarDrawer(displayMode: .always), @@ -182,8 +184,17 @@ private enum AppListTab: Int, CaseIterable, Identifiable { .pickerStyle(.segmented) .frame(width: 220) } - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { dismiss() }.fontWeight(.semibold) + if let onImportPairingFile { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: onImportPairingFile) { + Image(systemName: "doc.badge.plus") + } + } + } + if showDoneButton { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { dismiss() }.fontWeight(.semibold) + } } } .onAppear { @@ -305,7 +316,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable { } } } - Section("All Applications".localized) { + Section("Apps with get-task-allow".localized) { ForEach(filteredDebuggableApps, id: \.key) { bundleID, appName in AppButton( bundleID: bundleID, appName: appName, @@ -342,7 +353,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable { .listRowBackground(Color.clear) } } else { - Section("Other Apps".localized) { + Section("All Apps".localized) { ForEach(filteredLaunchApps, id: \.key) { bundleID, appName in let isPinned = pinnedSystemApps.contains(bundleID) LaunchAppRow( diff --git a/StikJIT/Views/MainTabView.swift b/StikJIT/Views/MainTabView.swift index f60278a..77da521 100644 --- a/StikJIT/Views/MainTabView.swift +++ b/StikJIT/Views/MainTabView.swift @@ -27,7 +27,7 @@ struct MainTabView: View { private var configurableTabs: [TabDescriptor] { var tabs: [TabDescriptor] = [ - TabDescriptor(id: "home", title: "Home", systemImage: "house") { AnyView(HomeView()) }, + TabDescriptor(id: "home", title: "Apps", systemImage: "square.grid.2x2") { AnyView(HomeView()) }, TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) }, TabDescriptor(id: "tools", title: "Tools", systemImage: "wrench.and.screwdriver") { AnyView(ToolsView()) }, TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) }, @@ -62,10 +62,10 @@ struct MainTabView: View { } private var displayTabs: [TabDescriptor] { - var tabs = ["home", "scripts", "tools"].compactMap { id in + var tabs = ["home", "tools"].compactMap { id in configurableTabs.first(where: { $0.id == id }) } - tabs.insert(settingsTab, at: min(3, tabs.count)) + tabs.insert(settingsTab, at: min(2, tabs.count)) return tabs } diff --git a/StikJIT/Views/MapSelectionView.swift b/StikJIT/Views/MapSelectionView.swift index b555abd..f5eb595 100644 --- a/StikJIT/Views/MapSelectionView.swift +++ b/StikJIT/Views/MapSelectionView.swift @@ -15,6 +15,19 @@ extension CLLocationCoordinate2D: Equatable { } } +// MARK: - Bookmark Model + +struct LocationBookmark: Identifiable, Codable { + var id: UUID = UUID() + var name: String + var latitude: Double + var longitude: Double + + var coordinate: CLLocationCoordinate2D { + CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + } +} + // MARK: - Search Completer @MainActor @@ -61,6 +74,12 @@ struct LocationSimulationView: View { @State private var searchText = "" @StateObject private var searchCompleter = LocationSearchCompleter() + // Bookmarks + @State private var bookmarks: [LocationBookmark] = [] + @State private var showBookmarks = false + @State private var showSaveBookmark = false + @State private var newBookmarkName = "" + private var pairingFilePath: String { URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path() } @@ -166,6 +185,14 @@ struct LocationSimulationView: View { Button("Simulate Location", action: simulate) .buttonStyle(.borderedProminent) .disabled(!pairingExists || isBusy) + + Button { + showSaveBookmark = true + } label: { + Image(systemName: "bookmark") + } + .buttonStyle(.bordered) + .tint(.blue) } } else { Text("Tap map to drop pin") @@ -179,6 +206,13 @@ struct LocationSimulationView: View { } .navigationBarTitleDisplayMode(.inline) .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + showBookmarks = true + } label: { + Image(systemName: "bookmark.fill") + } + } ToolbarItem(placement: .topBarTrailing) { TextField("Search location...", text: $searchText) .padding(.leading, 6) @@ -193,6 +227,25 @@ struct LocationSimulationView: View { } message: { Text(alertMessage) } + .alert("Save Bookmark", isPresented: $showSaveBookmark) { + TextField("Name", text: $newBookmarkName) + Button("Save") { addBookmark() } + Button("Cancel", role: .cancel) { newBookmarkName = "" } + } message: { + Text("Enter a name for this location.") + } + .sheet(isPresented: $showBookmarks) { + BookmarksView(bookmarks: $bookmarks) { bookmark in + coordinate = bookmark.coordinate + showBookmarks = false + } onDelete: { offsets in + bookmarks.remove(atOffsets: offsets) + saveBookmarks() + } + } + .onAppear { + loadBookmarks() + } .onDisappear { stopResendLoop() if backgroundTaskID != .invalid { @@ -202,6 +255,35 @@ struct LocationSimulationView: View { } } + // MARK: - Bookmarks + + private func loadBookmarks() { + guard let data = UserDefaults.standard.data(forKey: "locationBookmarks"), + let decoded = try? JSONDecoder().decode([LocationBookmark].self, from: data) else { return } + bookmarks = decoded + } + + private func saveBookmarks() { + if let data = try? JSONEncoder().encode(bookmarks) { + UserDefaults.standard.set(data, forKey: "locationBookmarks") + } + } + + private func addBookmark() { + guard let coord = coordinate else { return } + let name = newBookmarkName.trimmingCharacters(in: .whitespacesAndNewlines) + let bookmark = LocationBookmark( + name: name.isEmpty ? String(format: "%.4f, %.4f", coord.latitude, coord.longitude) : name, + latitude: coord.latitude, + longitude: coord.longitude + ) + bookmarks.append(bookmark) + saveBookmarks() + newBookmarkName = "" + } + + // MARK: - Location + private func selectSearchResult(_ result: MKLocalSearchCompletion) { searchText = "" searchCompleter.results = [] @@ -289,3 +371,49 @@ struct LocationSimulationView: View { resendTimer = nil } } + +// MARK: - Bookmarks Sheet + +struct BookmarksView: View { + @Binding var bookmarks: [LocationBookmark] + let onSelect: (LocationBookmark) -> Void + let onDelete: (IndexSet) -> Void + + var body: some View { + NavigationStack { + Group { + if bookmarks.isEmpty { + ContentUnavailableView( + "No Bookmarks", + systemImage: "bookmark.slash", + description: Text("Drop a pin on the map and tap the bookmark icon to save a location.") + ) + } else { + List { + ForEach(bookmarks) { bookmark in + Button { + onSelect(bookmark) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(bookmark.name) + .foregroundStyle(.primary) + Text(String(format: "%.6f, %.6f", bookmark.latitude, bookmark.longitude)) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + } + } + .onDelete(perform: onDelete) + } + } + } + .navigationTitle("Bookmarks") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if !bookmarks.isEmpty { + EditButton() + } + } + } + } +} diff --git a/StikJIT/Views/ToolsView.swift b/StikJIT/Views/ToolsView.swift index f198556..bf274bb 100644 --- a/StikJIT/Views/ToolsView.swift +++ b/StikJIT/Views/ToolsView.swift @@ -18,6 +18,7 @@ struct ToolsView: View { private var tools: [ToolItem] { [ + ToolItem(id: "scripts", title: "Scripts", detail: "Manage and run JS scripts", systemImage: "scroll", destination: AnyView(ScriptListView())), ToolItem(id: "console", title: "Console", detail: "Live device logs", systemImage: "terminal", destination: AnyView(ConsoleLogsView())), ToolItem(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", systemImage: "iphone.and.arrow.forward", destination: AnyView(DeviceInfoView())), ToolItem(id: "profiles", title: "App Expiry", detail: "Check app expiration dates", systemImage: "calendar.badge.clock", destination: AnyView(ProfileView())),