diff --git a/DebugWidget/DebugWidgetBundle.swift b/DebugWidget/DebugWidgetBundle.swift index 77bf06e..10b39be 100644 --- a/DebugWidget/DebugWidgetBundle.swift +++ b/DebugWidget/DebugWidgetBundle.swift @@ -11,7 +11,7 @@ import SwiftUI @main struct StikDebugWidgetBundle: WidgetBundle { var body: some Widget { - // Both widgets enabled: Favorites uses enable-jit URL scheme (with PiP/script handled in-app), + // Both widgets enabled: Favorites uses enable-jit URL scheme (with continued processing handled in-app), // System Apps uses launch-app URL scheme for non-debug launch behavior. FavoritesWidget() SystemAppsWidget() diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj index 507ead8..f3caf5c 100644 --- a/StikDebug.xcodeproj/project.pbxproj +++ b/StikDebug.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 17C744F02E20BED000834F17 /* Pipify in Frameworks */ = {isa = PBXBuildFile; productRef = 17C744EF2E20BED000834F17 /* Pipify */; }; 68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */; }; 68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BD2E1B415700A5BA36 /* CodeEditorView */; }; 68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BF2E1B415700A5BA36 /* LanguageSupport */; }; @@ -155,7 +154,6 @@ buildActionMask = 2147483647; files = ( DCBA85862E3897BD00E88C06 /* StikImporter in Frameworks */, - 17C744F02E20BED000834F17 /* Pipify in Frameworks */, 68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */, 68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */, 68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */, @@ -272,7 +270,6 @@ packageProductDependencies = ( 68D569BD2E1B415700A5BA36 /* CodeEditorView */, 68D569BF2E1B415700A5BA36 /* LanguageSupport */, - 17C744EF2E20BED000834F17 /* Pipify */, DCBA85852E3897BD00E88C06 /* StikImporter */, 68E714E52E6AA2B00025610F /* ZIPFoundation */, ); @@ -390,7 +387,6 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */, - 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */, DCBA85842E3897BD00E88C06 /* XCRemoteSwiftPackageReference "StikImporter" */, 68E714E42E6AA2B00025610F /* XCRemoteSwiftPackageReference "ZIPFoundation" */, ); @@ -997,14 +993,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/hugeBlack/swiftui-pipify"; - requirement = { - branch = main; - kind = branch; - }; - }; 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mchakravarty/CodeEditorView"; @@ -1032,11 +1020,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 17C744EF2E20BED000834F17 /* Pipify */ = { - isa = XCSwiftPackageProductDependency; - package = 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */; - productName = Pipify; - }; 68D569BD2E1B415700A5BA36 /* CodeEditorView */ = { isa = XCSwiftPackageProductDependency; package = 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */; diff --git a/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7da08b0..ce50db7 100644 --- a/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ac6355f0d394d08e98cd8bc09669552151ca717b83a414b5708b301c9ba767e4", + "originHash" : "0c22ab9d76f71ac04ee3ae6420154b1551511330120649c0ec71bbeb02f33fd5", "pins" : [ { "identity" : "codeeditorview", @@ -28,15 +28,6 @@ "version" : "1.0.2" } }, - { - "identity" : "swiftui-pipify", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hugeBlack/swiftui-pipify", - "state" : { - "branch" : "main", - "revision" : "a1ec2fd1781c8289bff1a8b3f664dcf21c910efb" - } - }, { "identity" : "zipfoundation", "kind" : "remoteSourceControl", diff --git a/StikJIT/Info.plist b/StikJIT/Info.plist index ece1afd..d3c8eff 100644 --- a/StikJIT/Info.plist +++ b/StikJIT/Info.plist @@ -15,9 +15,14 @@ + BGTaskSchedulerPermittedIdentifiers + + $(PRODUCT_BUNDLE_IDENTIFIER).continuedProcessingTask.script + UIBackgroundModes audio + processing UIFileSharingEnabled diff --git a/StikJIT/JSSupport/RunJSView.swift b/StikJIT/JSSupport/RunJSView.swift index 5c97a92..d09d713 100644 --- a/StikJIT/JSSupport/RunJSView.swift +++ b/StikJIT/JSSupport/RunJSView.swift @@ -16,6 +16,8 @@ class RunJSViewModel: ObservableObject { var pid: Int var debugProxy: OpaquePointer? var semaphore: dispatch_semaphore_t? + private var progressTimer: DispatchSourceTimer? + private var reportedProgress: Double = 0 init(pid: Int, debugProxy: OpaquePointer?, semaphore: dispatch_semaphore_t?) { self.pid = pid @@ -30,6 +32,7 @@ class RunJSViewModel: ObservableObject { func runScript(data: Data, name: String? = nil) throws { let scriptContent = String(data: data, encoding: .utf8) scriptName = name ?? "Script" + startContinuedProcessing(withTitle: scriptName) let getPidFunction: @convention(block) () -> Int = { return self.pid @@ -73,37 +76,48 @@ class RunJSViewModel: ObservableObject { if let semaphore { semaphore.signal() } - + DispatchQueue.main.async { if let exception = self.context?.exception { self.logs.append(exception.debugDescription) } - + let success = self.context?.exception == nil && !self.executionInterrupted + self.stopContinuedProcessing(success: success) self.logs.append("Script Execution Completed") - self.logs.append("You are safe to close the PIP Window.") + self.logs.append("Background processing finished. You can dismiss this view.") } } -} - -struct RunJSViewPiP: View { - @Binding var model: RunJSViewModel? - @State var logs: [String] = [] - let timer = Timer.publish(every: 0.034, on: .main, in: .common).autoconnect() - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(logs.suffix(6).indices, id: \.self) { index in - Text(logs.suffix(6)[index]) - .font(.system(size: 12)) - .foregroundStyle(.white) + private func startContinuedProcessing(withTitle title: String) { + guard ContinuedProcessingManager.shared.isSupported, + UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return } + stopProgressTimer() + reportedProgress = 0.05 + ContinuedProcessingManager.shared.begin(title: title, subtitle: "Script execution in progress") + ContinuedProcessingManager.shared.updateProgress(reportedProgress) + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background)) + timer.schedule(deadline: .now() + 5, repeating: 5) + timer.setEventHandler { [weak self] in + guard let self else { return } + self.reportedProgress = min(0.9, self.reportedProgress + 0.1) + ContinuedProcessingManager.shared.updateProgress(self.reportedProgress) + if self.reportedProgress >= 0.9 { + self.stopProgressTimer() } } - .padding() - .onReceive(timer) { _ in - self.logs = model?.logs ?? [] - } - .frame(width: 300, height: 150) + timer.resume() + progressTimer = timer + } + + private func stopContinuedProcessing(success: Bool) { + stopProgressTimer() + ContinuedProcessingManager.shared.updateProgress(1.0) + ContinuedProcessingManager.shared.finish(success: success) + } + + private func stopProgressTimer() { + progressTimer?.cancel() + progressTimer = nil } } diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index 9861f2c..d0e62f1 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -15,9 +15,17 @@ private func registerAdvancedOptionsDefault() { let os = ProcessInfo.processInfo.operatingSystemVersion // Enable advanced options by default on iOS 19/26 and above let enabled = os.majorVersion >= 19 - UserDefaults.standard.register(defaults: ["enableAdvancedOptions": enabled]) - UserDefaults.standard.register(defaults: ["enablePiP": enabled]) - UserDefaults.standard.register(defaults: [UserDefaults.Keys.txmOverride: false]) + let defaults = UserDefaults.standard + defaults.register(defaults: ["enableAdvancedOptions": enabled]) + defaults.register(defaults: [UserDefaults.Keys.txmOverride: false]) + if defaults.object(forKey: UserDefaults.Keys.enableContinuedProcessing) == nil { + let legacyPiP = defaults.object(forKey: "enablePiP") as? Bool + if let legacyPiP { + defaults.set(legacyPiP, forKey: UserDefaults.Keys.enableContinuedProcessing) + } else { + defaults.register(defaults: [UserDefaults.Keys.enableContinuedProcessing: enabled]) + } + } } // MARK: - Welcome Sheet @@ -519,6 +527,7 @@ struct HeartbeatApp: App { init() { registerAdvancedOptionsDefault() + ContinuedProcessingManager.shared.configureIfNeeded() newVerCheck() let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))! let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))! diff --git a/StikJIT/Utilities/ContinuedProcessingManager.swift b/StikJIT/Utilities/ContinuedProcessingManager.swift new file mode 100644 index 0000000..b358020 --- /dev/null +++ b/StikJIT/Utilities/ContinuedProcessingManager.swift @@ -0,0 +1,157 @@ +// +// ContinuedProcessingManager.swift +// StikJIT +// +// Created by Codex on 11/20/24. +// + +import Foundation +import BackgroundTasks + +final class ContinuedProcessingManager { + static let shared = ContinuedProcessingManager() + private let handler: ContinuedProcessingHandling + + private init() { + if #available(iOS 26.0, *) { + handler = ModernContinuedProcessingHandler() + } else { + handler = NoopContinuedProcessingHandler() + } + } + + var isSupported: Bool { handler.isSupported } + + func configureIfNeeded() { + handler.configureIfNeeded() + } + + func begin(title: String, subtitle: String) { + handler.begin(title: title, subtitle: subtitle) + } + + func updateProgress(_ fraction: Double) { + handler.updateProgress(fraction) + } + + func finish(success: Bool) { + handler.finish(success: success) + } +} + +private protocol ContinuedProcessingHandling: AnyObject { + var isSupported: Bool { get } + func configureIfNeeded() + func begin(title: String, subtitle: String) + func updateProgress(_ fraction: Double) + func finish(success: Bool) +} + +private final class NoopContinuedProcessingHandler: ContinuedProcessingHandling { + var isSupported: Bool { false } + func configureIfNeeded() {} + func begin(title: String, subtitle: String) {} + func updateProgress(_ fraction: Double) {} + func finish(success: Bool) {} +} + +@available(iOS 26.0, *) +private final class ModernContinuedProcessingHandler: ContinuedProcessingHandling { + private let scheduler = BGTaskScheduler.shared + private let taskIdentifier: String + private var didRegister = false + private var activeTask: BGContinuedProcessingTask? + private let queue = DispatchQueue(label: "com.stikdebug.continuedProcessing", + qos: .utility) + private var pendingMetadata: (title: String, subtitle: String)? + + init() { + let bundleID = Bundle.main.bundleIdentifier ?? "com.stik.sj" + taskIdentifier = "\(bundleID).continuedProcessingTask.script" + } + + var isSupported: Bool { true } + + func configureIfNeeded() { + guard !didRegister else { return } + scheduler.register(forTaskWithIdentifier: taskIdentifier, using: nil) { [weak self] task in + guard let continuedTask = task as? BGContinuedProcessingTask else { + task.setTaskCompleted(success: false) + return + } + self?.handle(task: continuedTask) + } + didRegister = true + } + + func begin(title: String, subtitle: String) { + guard UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return } + configureIfNeeded() + var reserved = false + queue.sync { + if activeTask == nil && pendingMetadata == nil { + pendingMetadata = (title: title, subtitle: subtitle) + reserved = true + } + } + guard reserved else { return } + let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, + title: title, + subtitle: subtitle) + request.strategy = .queue + do { + try scheduler.submit(request) + LogManager.shared.addInfoLog("Requested continued processing: \(title)") + } catch { + LogManager.shared.addWarningLog("Unable to request continued processing: \(error.localizedDescription)") + queue.async { [weak self] in + self?.pendingMetadata = nil + } + } + } + + func updateProgress(_ fraction: Double) { + queue.async { [weak self] in + guard let task = self?.activeTask else { return } + let clamped = max(0.0, min(1.0, fraction)) + task.progress.totalUnitCount = max(task.progress.totalUnitCount, 100) + task.progress.completedUnitCount = Int64(Double(task.progress.totalUnitCount) * clamped) + } + } + + func finish(success: Bool) { + queue.async { [weak self] in + guard let self else { return } + if let task = self.activeTask { + task.progress.completedUnitCount = task.progress.totalUnitCount + task.setTaskCompleted(success: success) + self.activeTask = nil + } else if pendingMetadata != nil { + scheduler.cancel(taskRequestWithIdentifier: taskIdentifier) + } + pendingMetadata = nil + } + } + + private func handle(task: BGContinuedProcessingTask) { + queue.async { [weak self] in + guard let self else { return } + activeTask = task + if let metadata = pendingMetadata { + task.updateTitle(metadata.title, subtitle: metadata.subtitle) + } + if task.progress.totalUnitCount == 0 { + task.progress.totalUnitCount = 100 + } + task.progress.completedUnitCount = 1 + task.expirationHandler = { [weak self] in + self?.handleExpiration() + } + } + } + + private func handleExpiration() { + LogManager.shared.addWarningLog("Continued processing expired early") + finish(success: false) + } +} diff --git a/StikJIT/Utilities/Extensions.swift b/StikJIT/Utilities/Extensions.swift index 31e66a5..55e50ff 100644 --- a/StikJIT/Utilities/Extensions.swift +++ b/StikJIT/Utilities/Extensions.swift @@ -23,5 +23,7 @@ extension UserDefaults { enum Keys { /// Forces the app to treat the current device as TXM-capable so scripts always run. static let txmOverride = "overrideTXMForScripts" + /// Controls whether BGContinuedProcessingTask should be used to keep scripts alive in the background. + static let enableContinuedProcessing = "enableContinuedProcessing" } } diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 90ed630..153218b 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -7,7 +7,6 @@ import SwiftUI import UniformTypeIdentifiers -import Pipify import UIKit import WidgetKit import Combine @@ -51,9 +50,7 @@ struct HomeView: View { @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("useDefaultScript") private var useDefaultScript = false - @AppStorage("enablePiP") private var enablePiP = true @State var scriptViewShow = false - @State var pipRequired = false @AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js" @State var jsModel: RunJSViewModel? @@ -190,6 +187,7 @@ struct HomeView: View { } .onChange(of: favoriteApps) { _, _ in loadAppListIfNeeded() + syncFavoriteAppNamesWithCache() } .onChange(of: recentApps) { _, _ in loadAppListIfNeeded() @@ -250,42 +248,9 @@ struct HomeView: View { var autoScriptData: Data? = nil var autoScriptName: String? = nil - let appName: String? = (try? JITEnableContext.shared.getAppList()[selectedBundle]) - - if #available(iOS 26, *) { - if ProcessInfo.processInfo.hasTXM, let appName { - if appName == "maciOS" { - if let url = Bundle.main.url(forResource: "script1", withExtension: "js"), - let data = try? Data(contentsOf: url) { - autoScriptData = data - autoScriptName = "script1.js" - } - } else if appName == "Amethyst" { - if let url = Bundle.main.url(forResource: "script2", withExtension: "js"), - let data = try? Data(contentsOf: url) { - autoScriptData = data - autoScriptName = "script2.js" - } - } else if appName == "Geode" { - if let url = Bundle.main.url(forResource: "Geode", withExtension: "js"), - let data = try? Data(contentsOf: url) { - autoScriptData = data - autoScriptName = "Geode.js" - } - } else if appName == "MeloNX" { - if let url = Bundle.main.url(forResource: "melo", withExtension: "js"), - let data = try? Data(contentsOf: url) { - autoScriptData = data - autoScriptName = "melo.js" - } - } else if appName == "UTM" || appName == "DolphiniOS" { - if let url = Bundle.main.url(forResource: "utmjit", withExtension: "js"), - let data = try? Data(contentsOf: url) { - autoScriptData = data - autoScriptName = "utmjit.js" - } - } - } + if let scriptInfo = autoScript(for: selectedBundle) { + autoScriptData = scriptInfo.data + autoScriptName = scriptInfo.name } startJITInBackground(bundleID: selectedBundle, @@ -295,12 +260,6 @@ struct HomeView: View { triggeredByURLScheme: false) } } - .pipify(isPresented: Binding( - get: { pipRequired && enablePiP }, - set: { newValue in pipRequired = newValue } - )) { - RunJSViewPiP(model: $jsModel) - } .sheet(isPresented: $scriptViewShow) { NavigationView { if let jsModel { @@ -346,6 +305,11 @@ struct HomeView: View { if let scriptName = components?.queryItems?.first(where: { $0.name == "script-name" })?.value { config.scriptName = scriptName } + if config.scriptData == nil, let bundleID = config.bundleID, + let scriptInfo = autoScript(for: bundleID) { + config.scriptData = scriptInfo.data + config.scriptName = scriptInfo.name + } if viewDidAppeared { startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) } else { @@ -1033,6 +997,7 @@ struct HomeView: View { if let restoredApps, cachedAppNames.isEmpty { cachedAppNames = restoredApps + syncFavoriteAppNamesWithCache() } refreshBackground() @@ -1062,6 +1027,7 @@ struct HomeView: View { let encoded = try? JSONEncoder().encode(result) DispatchQueue.main.async { cachedAppNames = result + syncFavoriteAppNamesWithCache() cachedAppNamesData = encoded } } @@ -1253,6 +1219,65 @@ struct HomeView: View { } private func refreshBackground() { } + private func autoScript(for bundleID: String) -> (data: Data, name: String)? { + guard ProcessInfo.processInfo.hasTXM else { return nil } + 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 { + return nil + } + return (data, resource.fileName) + } + + 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 syncFavoriteAppNamesWithCache() { + guard let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj") else { return } + let favorites = sharedDefaults.stringArray(forKey: "favoriteApps") ?? [] + guard !favorites.isEmpty else { return } + + var storedNames = (sharedDefaults.dictionary(forKey: "favoriteAppNames") as? [String: String]) ?? [:] + var changed = false + + for bundle in favorites { + guard let rawName = cachedAppNames[bundle], !rawName.isEmpty else { continue } + let display = shortDisplayName(from: rawName) + if storedNames[bundle] != display { + storedNames[bundle] = display + changed = true + } + } + + if changed { + sharedDefaults.set(storedNames, forKey: "favoriteAppNames") + WidgetCenter.shared.reloadTimelines(ofKind: "FavoritesWidget") + } + } + + private func autoScriptResource(for appName: String) -> (resource: String, fileName: String)? { + switch appName { + case "maciOS": + return ("script1", "script1.js") + case "Amethyst": + return ("script2", "script2.js") + case "Geode": + return ("Geode", "Geode.js") + case "MeloNX": + return ("melo", "melo.js") + case "UTM", "DolphiniOS": + return ("utmjit", "utmjit.js") + default: + return nil + } + } + private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback { return { pid, debugProxyHandle, semaphore in jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore) @@ -1291,9 +1316,6 @@ struct HomeView: View { if ProcessInfo.processInfo.hasTXM, let sd = scriptData { callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script") if triggeredByURLScheme { usleep(500000) } - pipRequired = true - } else { - pipRequired = false } let logger: LogFunc = { message in if let message { LogManager.shared.addInfoLog(message) } } @@ -1316,7 +1338,6 @@ struct HomeView: View { } } isProcessing = false - pipRequired = false } } diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 4e58f08..05b44e0 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -14,7 +14,7 @@ struct SettingsView: View { @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("enableAdvancedBetaOptions") private var enableAdvancedBetaOptions = false @AppStorage("enableTesting") private var enableTesting = false - @AppStorage("enablePiP") private var enablePiP = false + @AppStorage(UserDefaults.Keys.enableContinuedProcessing) private var enableContinuedProcessing = false @AppStorage(UserDefaults.Keys.txmOverride) private var overrideTXMDetection = false @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @@ -332,8 +332,7 @@ struct SettingsView: View { Toggle("Run Default Script After Connecting", isOn: $useDefaultScript) .tint(accentColor) - Toggle("Picture in Picture", isOn: $enablePiP) - .tint(accentColor) + continuedProcessingToggle Toggle(isOn: $overrideTXMDetection) { VStack(alignment: .leading, spacing: 2) { Text("Always Run Scripts") @@ -349,7 +348,7 @@ struct SettingsView: View { .onChange(of: enableAdvancedOptions) { _, newValue in if !newValue { useDefaultScript = false - enablePiP = false + enableContinuedProcessing = false enableAdvancedBetaOptions = false enableTesting = false } @@ -397,6 +396,24 @@ struct SettingsView: View { } } } + + private var continuedProcessingToggle: some View { + Group { + if ContinuedProcessingManager.shared.isSupported { + Toggle("Allow Continued Processing", isOn: $enableContinuedProcessing) + .tint(accentColor) + } else { + VStack(alignment: .leading, spacing: 4) { + Toggle("Allow Continued Processing", isOn: .constant(false)) + .tint(accentColor) + .disabled(true) + Text("Requires iOS 26 or later.") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + } private var helpCard: some View { glassCard {