From 0baf631c7adb7ba6e4e5ef468730d5b7ee43942e Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:33:17 -0500 Subject: [PATCH] QOL Changes --- StikJIT/{script2.js => Amethyst.js} | 0 StikJIT/JSSupport/ScriptEditorView.swift | 74 +----- StikJIT/JSSupport/ScriptListView.swift | 264 ++++------------------ StikJIT/{melo.js => MeloNX.js} | 0 StikJIT/{utmjit.js => UTM-Dolphin.js} | 0 StikJIT/Views/HomeView.swift | 70 ++++-- StikJIT/Views/InstalledAppsListView.swift | 23 +- StikJIT/Views/SettingsView.swift | 4 - StikJIT/{script1.js => maciOS.js} | 0 9 files changed, 119 insertions(+), 316 deletions(-) rename StikJIT/{script2.js => Amethyst.js} (100%) rename StikJIT/{melo.js => MeloNX.js} (100%) rename StikJIT/{utmjit.js => UTM-Dolphin.js} (100%) rename StikJIT/{script1.js => maciOS.js} (100%) diff --git a/StikJIT/script2.js b/StikJIT/Amethyst.js similarity index 100% rename from StikJIT/script2.js rename to StikJIT/Amethyst.js diff --git a/StikJIT/JSSupport/ScriptEditorView.swift b/StikJIT/JSSupport/ScriptEditorView.swift index 40c9f44..06cd282 100644 --- a/StikJIT/JSSupport/ScriptEditorView.swift +++ b/StikJIT/JSSupport/ScriptEditorView.swift @@ -42,29 +42,24 @@ struct ScriptEditorView: View { .font(.system(.footnote, design: .monospaced)) .frame(maxWidth: .infinity, maxHeight: .infinity) .environment(\.codeEditorTheme, editorTheme) - - Divider() - - HStack(spacing: 12) { - WideGlassyButton(title: "Cancel", systemImage: "xmark") { - dismiss() - } - WideGlassyButton(title: "Save", systemImage: "checkmark") { - saveScript() - dismiss() - } - } - .padding(.horizontal) - .padding(.vertical, 12) - .frame(maxWidth: .infinity) - .background(bottomBarBackground) } .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(edges: .bottom) } .navigationTitle(scriptURL.lastPathComponent) .navigationBarTitleDisplayMode(.inline) .onAppear(perform: loadScript) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + saveScript() + dismiss() + } + } + } .preferredColorScheme(preferredScheme) + .tint(Color.white) + .toolbar(.hidden, for: .tabBar) } private func loadScript() { @@ -74,51 +69,4 @@ struct ScriptEditorView: View { private func saveScript() { try? scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8) } - - private var bottomBarBackground: some View { - Rectangle() - .fill(Color.black.opacity(colorScheme == .dark ? 0.35 : 0.08)) - .overlay( - Rectangle() - .fill(Color.white.opacity(0.08)) - .frame(height: 1), - alignment: .top - ) - } -} - -// MARK: - Equal-width rounded-rectangle button (centered content) -private struct WideGlassyButton: View { - let title: String - let systemImage: String - let action: () -> Void - - var body: some View { - Button(action: action) { - HStack(spacing: 8) { - Image(systemName: systemImage) - .imageScale(.medium) - .font(.body.weight(.semibold)) - Text(title) - .font(.body.weight(.semibold)) - .lineLimit(1) - .minimumScaleFactor(0.85) - } - .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 10) - .padding(.horizontal, 14) - } - .frame(height: 44) - .frame(maxWidth: .infinity) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(.ultraThinMaterial) - ) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .strokeBorder(Color.white.opacity(0.12), lineWidth: 1) - ) - .contentShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .buttonStyle(.plain) - } } diff --git a/StikJIT/JSSupport/ScriptListView.swift b/StikJIT/JSSupport/ScriptListView.swift index a9ffa3b..e29369a 100644 --- a/StikJIT/JSSupport/ScriptListView.swift +++ b/StikJIT/JSSupport/ScriptListView.swift @@ -26,11 +26,6 @@ struct ScriptListView: View { @State private var showDeleteConfirmation = false @State private var pendingDelete: URL? = nil - @State private var scriptToRun: URL? = nil - @State private var showRunConfiguration = false - @State private var runConfiguration = ScriptRunConfiguration() - @State private var runnerModel: RunJSViewModel? - @State private var showRunnerLogs = false var onSelectScript: ((URL?) -> Void)? = nil @@ -141,42 +136,6 @@ struct ScriptListView: View { case .failure(let error): presentError(title: "Import Failed", message: error.localizedDescription) } } - .sheet(isPresented: $showRunConfiguration) { - if let scriptToRun { - RunScriptConfigurationView( - scriptName: scriptToRun.lastPathComponent, - configuration: $runConfiguration, - onCancel: { showRunConfiguration = false }, - onRun: { - let configuration = runConfiguration - showRunConfiguration = false - startRunningScript(scriptToRun, configuration: configuration) - } - ) - } - } - .sheet(isPresented: $showRunnerLogs, onDismiss: { runnerModel = nil }) { - NavigationView { - if let runnerModel { - RunJSView(model: runnerModel) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { showRunnerLogs = false } - } - } - .navigationTitle(runnerModel.scriptName) - .navigationBarTitleDisplayMode(.inline) - } else { - Text("No script output available.") - .navigationTitle("Scripts") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { showRunnerLogs = false } - } - } - } - } - } } .preferredColorScheme(preferredScheme) } @@ -253,16 +212,6 @@ struct ScriptListView: View { .foregroundColor(.yellow) } - if !isPickerMode { - Button { - prepareToRun(script) - } label: { - Image(systemName: "play.circle.fill") - .foregroundColor(.green) - } - .buttonStyle(.borderless) - } - if showDelete { Button(role: .destructive) { pendingDelete = script @@ -368,16 +317,31 @@ struct ScriptListView: View { private func ensureDefaultScripts(in directory: URL) throws { let fm = FileManager.default - if let bundleURL = Bundle.main.url(forResource: "attachDetach", withExtension: "js") { - let dest = directory.appendingPathComponent("attachDetach.js") - if !fm.fileExists(atPath: dest.path) { - try fm.copyItem(at: bundleURL, to: dest) + let bundledScripts: [(resource: String, filename: String)] = [ + ("attachDetach", "attachDetach.js"), + ("maciOS", "maciOS.js"), + ("Amethyst", "Amethyst.js"), + ("Geode", "Geode.js"), + ("MeloNX", "MeloNX.js"), + ("UTM-Dolphin", "UTM-Dolphin.js") + ] + + for entry in bundledScripts { + if let bundleURL = Bundle.main.url(forResource: entry.resource, withExtension: "js") { + let destination = directory.appendingPathComponent(entry.filename) + if !fm.fileExists(atPath: destination.path) { + try fm.copyItem(at: bundleURL, to: destination) + } } } let screenshotURL = directory.appendingPathComponent("screenshot-demo.js") if !fm.fileExists(atPath: screenshotURL.path) { try screenshotDemoScript.write(to: screenshotURL, atomically: true, encoding: .utf8) } + let standaloneURL = directory.appendingPathComponent("screenshot-capture.js") + if !fm.fileExists(atPath: standaloneURL.path) { + try screenshotCaptureScript.write(to: standaloneURL, atomically: true, encoding: .utf8) + } } private func loadScripts() { @@ -445,90 +409,6 @@ struct ScriptListView: View { } } - // MARK: - Script Execution - - private func prepareToRun(_ script: URL) { - guard ProcessInfo.processInfo.hasTXM else { - presentError(title: "Scripts Not Supported", - message: "This device does not have a Trusted Execution Monitor. Enable the override in Settings if you want to force script execution.") - return - } - scriptToRun = script - showRunConfiguration = true - } - - private func startRunningScript(_ script: URL, configuration: ScriptRunConfiguration) { - guard ProcessInfo.processInfo.hasTXM else { - presentError(title: "Scripts Not Supported", - message: "This device does not have a Trusted Execution Monitor.") - return - } - guard configuration.isValid else { - presentError(title: "Invalid Input", message: "Enter a bundle identifier or PID to continue.") - return - } - let scriptData: Data - do { - scriptData = try Data(contentsOf: script) - } catch { - presentError(title: "Failed to Read Script", message: error.localizedDescription) - return - } - - scriptToRun = nil - isBusy = true - let scriptName = script.lastPathComponent - let callback = makeJsCallback(scriptData: scriptData, scriptName: scriptName) - let logger: LogFunc = { message in - if let message { - LogManager.shared.addInfoLog(message) - } - } - - DispatchQueue.global(qos: .userInitiated).async { - let success: Bool - switch configuration.mode { - case .bundleID: - let trimmed = configuration.bundleID.trimmingCharacters(in: .whitespacesAndNewlines) - success = JITEnableContext.shared.debugApp(withBundleID: trimmed, logger: logger, jsCallback: callback) - case .pid: - let trimmed = configuration.pid.trimmingCharacters(in: .whitespacesAndNewlines) - let pidValue = Int32(trimmed) ?? 0 - success = JITEnableContext.shared.debugApp(withPID: pidValue, logger: logger, jsCallback: callback) - } - DispatchQueue.main.async { - self.isBusy = false - if success { - self.presentSuccess(title: "Script Running", message: scriptName) - } else { - self.presentError(title: "Failed to Start Script", message: "Ensure the provider is connected and try again.") - } - } - } - } - - private func makeJsCallback(scriptData: Data, scriptName: String) -> DebugAppCallback { - return { pid, debugProxyHandle, remoteServerHandle, semaphore in - let model = RunJSViewModel(pid: Int(pid), - debugProxy: debugProxyHandle, - remoteServer: remoteServerHandle, - semaphore: semaphore) - DispatchQueue.main.async { - self.runnerModel = model - self.showRunnerLogs = true - } - DispatchQueue.global(qos: .background).async { - do { - try model.runScript(data: scriptData, name: scriptName) - } catch { - DispatchQueue.main.async { - self.presentError(title: "Script Execution Failed", message: error.localizedDescription) - } - } - } - } - } - // MARK: - Feedback helpers private func presentError(title: String, message: String) { @@ -594,90 +474,6 @@ private struct WideGlassyButton: View { } } -private struct RunScriptConfigurationView: View { - let scriptName: String - @Binding var configuration: ScriptRunConfiguration - let onCancel: () -> Void - let onRun: () -> Void - - var body: some View { - NavigationStack { - Form { - Section(header: Text("Script")) { - Text(scriptName) - .font(.body.weight(.semibold)) - .textSelection(.enabled) - Picker("Target", selection: $configuration.mode) { - ForEach(ScriptRunMode.allCases) { mode in - Text(mode.label).tag(mode) - } - } - .pickerStyle(.segmented) - } - - Section(header: Text(configuration.mode == .bundleID ? "Bundle Identifier" : "PID")) { - if configuration.mode == .bundleID { - TextField("com.example.app", text: $configuration.bundleID) - .textInputAutocapitalization(.none) - .autocorrectionDisabled() - } else { - TextField("1234", text: $configuration.pid) - .keyboardType(.numberPad) - } - } - - Section { - Text("The app will be attached, the script will run, and then it will detach automatically.") - .font(.footnote) - .foregroundColor(.secondary) - } - } - .navigationTitle("Run Script") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel", action: onCancel) - } - ToolbarItem(placement: .confirmationAction) { - Button("Run", action: onRun) - .disabled(!configuration.isValid) - } - } - } - } -} - -private enum ScriptRunMode: String, CaseIterable, Identifiable { - case bundleID - case pid - - var id: String { rawValue } - - var label: String { - switch self { - case .bundleID: return "Bundle ID" - case .pid: return "PID" - } - } -} - -private struct ScriptRunConfiguration { - var mode: ScriptRunMode = .bundleID - var bundleID: String = "" - var pid: String = "" - - var isValid: Bool { - switch mode { - case .bundleID: - return !bundleID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - case .pid: - guard let value = Int(pid.trimmingCharacters(in: .whitespacesAndNewlines)), value > 0 else { - return false - } - return true - } - } -} - private let screenshotDemoScript = """ // Screenshot Demo Script // Attaches to the target, captures a PNG screenshot, and detaches. @@ -708,3 +504,25 @@ function takeScreenshotDemo() { takeScreenshotDemo(); """ + +private let screenshotCaptureScript = """ +// Screenshot Capture Script +// Takes a screenshot without sending any debugserver commands. + +function captureScreenshot() { + log("[ScreenshotCapture] Requesting screenshot without attaching…"); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const fileName = `standalone-${timestamp}.png`; + const savedPath = take_screenshot(fileName); + + if (savedPath && savedPath.length > 0) { + log(`[ScreenshotCapture] Screenshot saved to ${savedPath}`); + } else { + log("[ScreenshotCapture] Device did not report a saved path."); + } + + log("[ScreenshotCapture] Done."); +} + +captureScreenshot(); +""" diff --git a/StikJIT/melo.js b/StikJIT/MeloNX.js similarity index 100% rename from StikJIT/melo.js rename to StikJIT/MeloNX.js diff --git a/StikJIT/utmjit.js b/StikJIT/UTM-Dolphin.js similarity index 100% rename from StikJIT/utmjit.js rename to StikJIT/UTM-Dolphin.js diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 3e61571..66db583 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -49,7 +49,6 @@ struct HomeView: View { @State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false - @AppStorage("useDefaultScript") private var useDefaultScript = false @State var scriptViewShow = false @AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js" @State var jsModel: RunJSViewModel? @@ -282,7 +281,7 @@ struct HomeView: View { var autoScriptData: Data? = nil var autoScriptName: String? = nil - if let scriptInfo = autoScript(for: selectedBundle) { + if let scriptInfo = preferredScript(for: selectedBundle) { autoScriptData = scriptInfo.data autoScriptName = scriptInfo.name } @@ -340,7 +339,7 @@ struct HomeView: View { config.scriptName = scriptName } if config.scriptData == nil, let bundleID = config.bundleID, - let scriptInfo = autoScript(for: bundleID) { + let scriptInfo = preferredScript(for: bundleID) { config.scriptData = scriptInfo.data config.scriptName = scriptInfo.name } @@ -946,10 +945,11 @@ struct HomeView: View { isEnabled: canConnectByApp && !isProcessing, action: { HapticFeedbackHelper.trigger() + let scriptInfo = preferredScript(for: item.bundleID) startJITInBackground(bundleID: item.bundleID, pid: nil, - scriptData: nil, - scriptName: nil, + scriptData: scriptInfo?.data, + scriptName: scriptInfo?.name, triggeredByURLScheme: false) } ) @@ -1306,14 +1306,40 @@ struct HomeView: View { guard #available(iOS 26, *) else { return nil } let appName = (try? JITEnableContext.shared.getAppList()[bundleID]) ?? storedFavoriteName(for: bundleID) guard let appName, - let resource = autoScriptResource(for: appName), - let url = Bundle.main.url(forResource: resource.resource, withExtension: "js"), - let data = try? Data(contentsOf: url) else { + let resource = autoScriptResource(for: appName) else { + return nil + } + let scriptsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("scripts") + let documentsURL = scriptsDir.appendingPathComponent(resource.fileName) + if let data = try? Data(contentsOf: documentsURL) { + return (data, resource.fileName) + } + guard let bundleURL = Bundle.main.url(forResource: resource.resource, withExtension: "js"), + let data = try? Data(contentsOf: bundleURL) else { return nil } 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 } + let scriptsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("scripts") + let scriptURL = scriptsDir.appendingPathComponent(scriptName) + guard FileManager.default.fileExists(atPath: scriptURL.path), + 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] @@ -1346,15 +1372,15 @@ struct HomeView: View { private func autoScriptResource(for appName: String) -> (resource: String, fileName: String)? { switch appName { case "maciOS": - return ("script1", "script1.js") + return ("maciOS", "maciOS.js") case "Amethyst": - return ("script2", "script2.js") + return ("Amethyst", "Amethyst.js") case "Geode": return ("Geode", "Geode.js") case "MeloNX": - return ("melo", "melo.js") + return ("MeloNX", "MeloNX.js") case "UTM", "DolphiniOS": - return ("utmjit", "utmjit.js") + return ("UTM-Dolphin", "UTM-Dolphin.js") default: return nil } @@ -1369,7 +1395,7 @@ struct HomeView: View { scriptViewShow = true DispatchQueue.global(qos: .background).async { do { try jsModel?.runScript(data: script, name: name) } - catch { showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true) } + catch { showAlert(title: "Error Occurred While Executing Script.".localized, message: error.localizedDescription, showOk: true) } } } } @@ -1381,18 +1407,12 @@ struct HomeView: View { DispatchQueue.global(qos: .background).async { var scriptData = scriptData var scriptName = scriptName - if enableAdvancedOptions && scriptData == nil { - if scriptName == nil, let bundleID, let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] { - scriptName = mapping[bundleID] - } - if useDefaultScript && scriptName == nil { scriptName = selectedScript } - if scriptData == nil, let scriptName { - let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - .appendingPathComponent("scripts").appendingPathComponent(scriptName) - if FileManager.default.fileExists(atPath: url.path) { - do { scriptData = try Data(contentsOf: url) } catch { print("script load error: \(error)") } - } - } + if enableAdvancedOptions, + scriptData == nil, + let bundleID, + let assigned = assignedScript(for: bundleID) { + scriptName = assigned.name + scriptData = assigned.data } else { // keep passed-in auto script if provided; otherwise nil } diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index 7a35699..13cdcfb 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -783,6 +783,7 @@ struct AppButton: View { let performanceMode: Bool @State private var showScriptPicker = false + @State private var assignedScriptName: String? @StateObject private var iconLoader: IconLoader private var rowBackgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } @@ -804,6 +805,7 @@ struct AppButton: View { self.sharedDefaults = sharedDefaults self.performanceMode = performanceMode _iconLoader = StateObject(wrappedValue: IconLoader(bundleID: bundleID)) + _assignedScriptName = State(initialValue: AppButton.currentAssignment(for: bundleID)) } var body: some View { @@ -857,6 +859,13 @@ struct AppButton: View { Button { showScriptPicker = true } label: { Label("Assign Script", systemImage: "chevron.left.slash.chevron.right") } + if assignedScriptName != nil { + Button { + resetScriptAssignment() + } label: { + Label("Reset Script", systemImage: "arrow.uturn.left") + } + } } } .swipeActions(edge: .trailing, allowsFullSwipe: false) { @@ -954,14 +963,26 @@ struct AppButton: View { private func assignScript(_ url: URL?) { var mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] ?? [:] if let url { - mapping[bundleID] = url.lastPathComponent + let filename = url.lastPathComponent + mapping[bundleID] = filename + assignedScriptName = filename } else { mapping.removeValue(forKey: bundleID) + assignedScriptName = nil } UserDefaults.standard.set(mapping, forKey: "BundleScriptMap") Haptics.light() } + private func resetScriptAssignment() { + assignScript(nil) + } + + private static func currentAssignment(for bundleID: String) -> String? { + let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] + return mapping?[bundleID] + } + private func persistIfChanged() { var touched = false let prevR = (sharedDefaults.array(forKey: "recentApps") as? [String]) ?? [] diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 41686f6..c2f4321 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -10,7 +10,6 @@ import UIKit struct SettingsView: View { @AppStorage("username") private var username = "User" @AppStorage("selectedAppIcon") private var selectedAppIcon: String = "AppIcon" - @AppStorage("useDefaultScript") private var useDefaultScript = false @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("enableAdvancedBetaOptions") private var enableAdvancedBetaOptions = false @AppStorage("enableTesting") private var enableTesting = false @@ -426,8 +425,6 @@ struct SettingsView: View { .font(.headline) .foregroundColor(.primary) - Toggle("Run Default Script After Connecting", isOn: $useDefaultScript) - .tint(accentColor) continuedProcessingToggle Toggle(isOn: $overrideTXMDetection) { VStack(alignment: .leading, spacing: 2) { @@ -443,7 +440,6 @@ struct SettingsView: View { } .onChange(of: enableAdvancedOptions) { _, newValue in if !newValue { - useDefaultScript = false enableContinuedProcessing = false enableAdvancedBetaOptions = false enableTesting = false diff --git a/StikJIT/script1.js b/StikJIT/maciOS.js similarity index 100% rename from StikJIT/script1.js rename to StikJIT/maciOS.js