From 5134b9b0b3f1cf26ab3b228b14156f89d305ea80 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Sun, 21 Sep 2025 01:28:08 -0400 Subject: [PATCH] Revert "iOS 26 Improvements - 2.2.6" This reverts commit 5e5c3c2afac9d71301e6b5a1bad9e4bfb0384445. --- StikDebug.xcodeproj/project.pbxproj | 4 +- StikJIT/StikJITApp.swift | 13 +- StikJIT/Views/HomeView.swift | 255 +++++++++--------- StikJIT/Views/InstalledAppsListView.swift | 305 ++++------------------ StikJIT/Views/SettingsView.swift | 7 - StikJIT/idevice/JITEnableContext.h | 6 - StikJIT/idevice/JITEnableContext.m | 112 +++----- StikJIT/idevice/applist.h | 15 +- StikJIT/idevice/applist.m | 108 +++----- 9 files changed, 276 insertions(+), 549 deletions(-) diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj index e3df2d8..469e5b4 100644 --- a/StikDebug.xcodeproj/project.pbxproj +++ b/StikDebug.xcodeproj/project.pbxproj @@ -725,7 +725,7 @@ "$(PROJECT_DIR)/StikJIT/idevice", ); MACOSX_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 2.2.6; + MARKETING_VERSION = 2.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -778,7 +778,7 @@ "$(PROJECT_DIR)/StikJIT/idevice", ); MACOSX_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 2.2.6; + MARKETING_VERSION = 2.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index bf275f2..edb52b5 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -17,8 +17,6 @@ private func registerAdvancedOptionsDefault() { let enabled = os.majorVersion >= 19 UserDefaults.standard.register(defaults: ["enableAdvancedOptions": enabled]) UserDefaults.standard.register(defaults: ["enablePiP": enabled]) - // New default: VPN auto start enabled - UserDefaults.standard.register(defaults: ["vpnAutoStart": true]) } // MARK: - Welcome Sheet @@ -489,7 +487,6 @@ struct HeartbeatApp: App { @AppStorage("hasLaunchedBefore") var hasLaunchedBefore: Bool = false @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue - @AppStorage("vpnAutoStart") private var vpnAutoStart: Bool = true @State private var showWelcomeSheet: Bool = false @State private var isLoading2 = true @State private var isPairing = false @@ -720,10 +717,10 @@ struct HeartbeatApp: App { .tint(globalAccent) .onAppear { // On first launch, present the welcome sheet. - // Otherwise, start the VPN automatically (if enabled). + // Otherwise, start the VPN automatically. if !hasLaunchedBefore { showWelcomeSheet = true - } else if vpnAutoStart { + } else { TunnelManager.shared.startVPN() } // Update UIKit tint now and subscribe to changes without capturing self @@ -738,12 +735,10 @@ struct HeartbeatApp: App { } .sheet(isPresented: $showWelcomeSheet) { WelcomeSheetView { - // When the user taps "Continue", mark the app as launched and start the VPN (if enabled). + // When the user taps "Continue", mark the app as launched and start the VPN. hasLaunchedBefore = true showWelcomeSheet = false - if vpnAutoStart { - TunnelManager.shared.startVPN() - } + TunnelManager.shared.startVPN() } } } diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 5537baf..5edad43 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -41,7 +41,7 @@ struct HomeView: View { @State private var viewDidAppeared = false @State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil - @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = true + @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("useDefaultScript") private var useDefaultScript = false @AppStorage("enablePiP") private var enablePiP = true @@ -50,9 +50,7 @@ struct HomeView: View { @AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js" @State var jsModel: RunJSViewModel? - // New: allow overriding TXM requirement for scripts - @AppStorage("ignoreTXMForScripts") private var ignoreTXMForScripts = false - + @StateObject private var tunnel = TunnelManager.shared @State private var heartbeatOK = false @AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true @@ -72,11 +70,7 @@ struct HomeView: View { let v = ProcessInfo.processInfo.operatingSystemVersion return v.majorVersion >= 26 } - private var isDismissibleDisclaimer: Bool { !isOnOrAfteriOS26 } - - // Observe VPN status - @StateObject private var tunnelManager = TunnelManager.shared - + var body: some View { NavigationStack { ZStack { @@ -87,7 +81,7 @@ struct HomeView: View { VStack(spacing: 20) { if isOnOrAfteriOS26 || showiOS26Disclaimer { disclaimerCard - .transition(.opacity .combined(with: .move(edge: .top))) + .transition(.opacity.combined(with: .move(edge: .top))) } topStatusAndActionsCard @@ -136,22 +130,99 @@ struct HomeView: View { checkPairingFileExists() heartbeatOK = pubHeartBeat } - .fileImporter( - isPresented: $isShowingPairingFilePicker, - allowedContentTypes: [ - UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, - .propertyList - ] - ) { result in - if case .success(let url) = result { - importPairingFile(from: url) - } else if case .failure(let error) = result { + .fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", 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.default.fileExists(atPath: dest.path) { + try fileManager.removeItem(at: dest) + } + try fileManager.copyItem(at: url, to: dest) + + DispatchQueue.main.async { + isImportingFile = true + importProgress = 0 + pairingFileExists = true + } + + startHeartbeatInBackground() + + let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { t in + DispatchQueue.main.async { + if importProgress < 1 { + importProgress += 0.25 + } else { + t.invalidate() + isImportingFile = false + pairingFileIsValid = true + withAnimation { showPairingFileMessage = true } + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + withAnimation { showPairingFileMessage = false } + } + } + } + } + RunLoop.current.add(progressTimer, forMode: .common) + } catch { + print("Error copying file: \(error)") + } + } + if accessing { url.stopAccessingSecurityScopedResource() } + case .failure(let error): print("Failed to import file: \(error)") } } .sheet(isPresented: $isShowingInstalledApps) { InstalledAppsListView { selectedBundle in - handleAppSelection(selectedBundle) + bundleID = selectedBundle + isShowingInstalledApps = false + HapticFeedbackHelper.trigger() + + 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 == "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" { + if let url = Bundle.main.url(forResource: "utmjit", withExtension: "js"), + let data = try? Data(contentsOf: url) { + autoScriptData = data + autoScriptName = "utmjit.js" + } + } + } + } + + startJITInBackground(bundleID: selectedBundle, + pid: nil, + scriptData: autoScriptData, + scriptName: autoScriptName, + triggeredByURLScheme: false) } } .pipify(isPresented: Binding( @@ -225,7 +296,7 @@ struct HomeView: View { HStack(spacing: 12) { indicatorCapsule(ok: pairingFileExists, systemImage: "doc.badge.plus", a11y: "Pairing") indicatorCapsule(ok: ddiMounted, systemImage: "externaldrive", a11y: "Developer Disk Image") - indicatorCapsule(ok: tunnelManager.tunnelStatus == .connected, systemImage: "lock.shield", a11y: "VPN") + indicatorCapsule(ok: tunnel.tunnelStatus == .connected, systemImage: "lock.shield", a11y: "VPN") indicatorCapsule(ok: heartbeatOK, systemImage: "waveform.path.ecg", a11y: "Heartbeat") Spacer() } @@ -332,8 +403,6 @@ struct HomeView: View { private var toolsCard: some View { VStack(alignment: .leading, spacing: 12) { Text("Tools").font(.headline).foregroundColor(.secondary) - - // Console Button(action: { showingConsoleLogsView = true }) { HStack { Image(systemName: "terminal").font(.system(size: 20)) @@ -488,19 +557,54 @@ struct HomeView: View { LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))") DispatchQueue.global(qos: .background).async { - // …existing script selection logic… - + 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)") } + } + } + } else { + // keep passed-in auto script if provided; otherwise nil + } + var callback: DebugAppCallback? = nil - // Allow script if TXM is present OR user chose to ignore TXM requirement - if (ProcessInfo.processInfo.hasTXM || ignoreTXMForScripts), let sd = scriptData { + if ProcessInfo.processInfo.hasTXM, let sd = scriptData { callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script") if triggeredByURLScheme { usleep(500000) } pipRequired = true } else { pipRequired = false } - - // …rest unchanged… + + let logger: LogFunc = { message in if let message { LogManager.shared.addInfoLog(message) } } + var success: Bool + if let pid { + success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: logger, jsCallback: callback) + if success { DispatchQueue.main.async { addRecentPID(pid) } } + } else if let bundleID { + success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: logger, jsCallback: callback) + } else { + DispatchQueue.main.async { + showAlert(title: "Failed to Debug App".localized, message: "Either bundle ID or PID should be specified.".localized, showOk: true) + } + success = false + } + + if success { + DispatchQueue.main.async { + LogManager.shared.addInfoLog("Debug process completed for \(bundleID ?? String(pid ?? 0))") + } + } + isProcessing = false + pipRequired = false } } @@ -537,7 +641,7 @@ struct HomeView: View { } Spacer() - if isDismissibleDisclaimer { + if !isOnOrAfteriOS26 { Button { withAnimation { showiOS26Disclaimer = false @@ -567,94 +671,6 @@ struct HomeView: View { .accessibilityElement(children: .combine) .accessibilityLabel("Important notice for iOS 26 and later. Limited compatibility; improvements are ongoing.") } - - // MARK: - Helpers extracted to avoid heavy type-check - - private func importPairingFile(from url: URL) { - let fileManager = FileManager.default - let accessing = url.startAccessingSecurityScopedResource() - defer { if accessing { url.stopAccessingSecurityScopedResource() } } - - guard fileManager.fileExists(atPath: url.path) else { return } - 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) - - DispatchQueue.main.async { - isImportingFile = true - importProgress = 0 - pairingFileExists = true - } - - startHeartbeatInBackground() - - let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { t in - DispatchQueue.main.async { - if importProgress < 1 { - importProgress += 0.25 - } else { - t.invalidate() - isImportingFile = false - pairingFileIsValid = true - withAnimation { showPairingFileMessage = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - withAnimation { showPairingFileMessage = false } - } - } - } - } - RunLoop.current.add(progressTimer, forMode: .common) - } catch { - print("Error copying file: \(error)") - } - } - - private func handleAppSelection(_ selectedBundle: String) { - bundleID = selectedBundle - isShowingInstalledApps = false - HapticFeedbackHelper.trigger() - - let auto = computeAutoScript(for: selectedBundle) - startJITInBackground(bundleID: selectedBundle, - pid: nil, - scriptData: auto.data, - scriptName: auto.name, - triggeredByURLScheme: false) - } - - private func computeAutoScript(for bundleID: String) -> (data: Data?, name: String?) { - var autoScriptData: Data? = nil - var autoScriptName: String? = nil - - let appName: String? = (try? JITEnableContext.shared.getAppList()[bundleID]) - if #available(iOS 26, *) { - if ProcessInfo.processInfo.hasTXM, let appName { - func loadScript(resource: String, name: String) { - if let url = Bundle.main.url(forResource: resource, withExtension: "js"), - let data = try? Data(contentsOf: url) { - autoScriptData = data - autoScriptName = name - } - } - switch appName { - case "maciOS": - loadScript(resource: "script1", name: "script1.js") - case "Amethyst": - loadScript(resource: "script2", name: "script2.js") - case "MeloNX": - loadScript(resource: "melo", name: "melo.js") - case "UTM", "DolphiniOS": - loadScript(resource: "utmjit", name: "utmjit.js") - default: - break - } - } - } - return (autoScriptData, autoScriptName) - } } private struct StatusDot: View { @@ -890,4 +906,3 @@ public extension ProcessInfo { }() } } - diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index bfcd765..9bc2218 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -31,10 +31,6 @@ struct InstalledAppsListView: View { @AppStorage("performanceMode") private var performanceMode = false @State private var showPerformanceToast = false - // iOS 26 Safe Mode (default ON) - @AppStorage("ios26SafeMode") private var ios26SafeMode = true - @State private var showSafeModeToast = false - @Environment(\.dismiss) private var dismiss var onSelectApp: (String) -> Void @@ -42,83 +38,6 @@ struct InstalledAppsListView: View { recentApps.filter { !favoriteApps.contains($0) } } - // MARK: - Safe Mode filtering - - private let allowedSafeNames: Set = ["UTM", "MeloNX", "DolphiniOS", "maciOS", "Amethyst"] - - // Semantic-like version compare: returns true if lhs >= rhs - private func isVersion(_ lhs: String, greaterOrEqual rhs: String) -> Bool { - guard !lhs.isEmpty else { return false } - if rhs.isEmpty { return true } - let a = lhs.split(separator: ".").map { Int($0) ?? 0 } - let b = rhs.split(separator: ".").map { Int($0) ?? 0 } - let n = max(a.count, b.count) - for i in 0.. bv } - } - return true - } - - // Single source of truth for Safe Mode eligibility (includes DolphiniOS exclusion under iOS 26 Safe Mode) - private func isAllowedInSafeMode(bundleID: String, meta: AppMeta) -> Bool { - guard allowedSafeNames.contains(meta.name) else { return false } - switch meta.name { - case "Amethyst", "maciOS": - return true - case "MeloNX": - return isVersion(meta.version, greaterOrEqual: "2.0.1") - case "UTM": - return isVersion(meta.version, greaterOrEqual: "4.7.4") - case "DolphiniOS": - // When iOS 26 Safe Mode is ON, exclude the variant whose bundle ID starts with com.joemattiello - if ios26SafeMode && bundleID.hasPrefix("com.joemattiello") { return false } - return true - default: - return false - } - } - - // Final list of allowed bundle IDs under Safe Mode, capped to 5, deterministically ordered by (name, bundleID) - private var allowedSafeBundleIDs: [String] { - let filtered = viewModel.apps.compactMap { (bundleID, meta) -> String? in - isAllowedInSafeMode(bundleID: bundleID, meta: meta) ? bundleID : nil - } - let sorted = filtered.sorted { lhs, rhs in - let lMeta = viewModel.apps[lhs] - let rMeta = viewModel.apps[rhs] - if lMeta?.name != rMeta?.name { - return (lMeta?.name ?? "") < (rMeta?.name ?? "") - } - return lhs < rhs - } - return Array(sorted.prefix(5)) - } - - // Filter helpers for each section (when Safe Mode ON, derive from allowedSafeBundleIDs) - private var favoritesForDisplay: [String] { - guard ios26SafeMode else { return favoriteApps } - return favoriteApps.filter { allowedSafeBundleIDs.contains($0) } - } - - private var recentsForDisplay: [String] { - guard ios26SafeMode else { return filteredRecents } - return filteredRecents.filter { allowedSafeBundleIDs.contains($0) } - } - - private var allAppsForDisplay: [(String, AppMeta)] { - if ios26SafeMode { - let subset = allowedSafeBundleIDs.compactMap { bid -> (String, AppMeta)? in - guard let meta = viewModel.apps[bid] else { return nil } - return (bid, meta) - } - return subset - } else { - return viewModel.apps.sorted { $0.key < $1.key } - } - } - var body: some View { NavigationStack { ZStack { @@ -148,42 +67,10 @@ struct InstalledAppsListView: View { } .animation(.spring(response: 0.3, dampingFraction: 0.8), value: showPerformanceToast) } - - if showSafeModeToast { - VStack { - Spacer() - Text(ios26SafeMode ? "iOS 26 Safe Mode On" : "iOS 26 Safe Mode Off") - .font(.subheadline.weight(.semibold)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(.ultraThinMaterial, in: Capsule()) - .shadow(radius: 4) - .transition(.move(edge: .bottom).combined(with: .opacity)) - .padding(.bottom, 80) - } - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: showSafeModeToast) - } } - .navigationTitle("Installed Apps") + .navigationTitle("Installed Apps".localized) .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - // Safe Mode toggle button - Button { - ios26SafeMode.toggle() - Haptics.selection() - showSafeModeToast = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - withAnimation { showSafeModeToast = false } - } - } label: { - Image(systemName: ios26SafeMode ? "shield.checkered" : "shield.slash") - .imageScale(.large) - .foregroundStyle(ios26SafeMode ? .green : .secondary) - .accessibilityLabel("Toggle iOS 26 Safe Mode") - .accessibilityValue(ios26SafeMode ? "On" : "Off") - } - - // Existing Performance Mode toggle + ToolbarItem(placement: .navigationBarLeading) { Button { performanceMode.toggle() Haptics.selection() @@ -244,64 +131,52 @@ struct InstalledAppsListView: View { private var appsList: some View { ScrollView { VStack(spacing: 18) { - // Favorites - if !favoritesForDisplay.isEmpty { + if !favoriteApps.isEmpty { glassSection( - title: String(format: "Favorites (%d/4)", favoritesForDisplay.count) + title: String(format: "Favorites (%d/4)".localized, favoriteApps.count) ) { LazyVStack(spacing: 12) { - ForEach(favoritesForDisplay, id: \.self) { bundleID in - if let meta = viewModel.apps[bundleID] { - AppButton( - bundleID: bundleID, - appName: meta.name, - version: meta.displayVersion, - recentApps: $recentApps, - favoriteApps: $favoriteApps, - appIcons: $appIcons, - onSelectApp: onSelectApp, - sharedDefaults: sharedDefaults, - performanceMode: performanceMode - ) - .dolphinBadgeIfNeeded(appName: meta.name) - } + ForEach(favoriteApps, id: \.self) { bundleID in + AppButton( + bundleID: bundleID, + appName: viewModel.apps[bundleID] ?? bundleID, + recentApps: $recentApps, + favoriteApps: $favoriteApps, + appIcons: $appIcons, + onSelectApp: onSelectApp, + sharedDefaults: sharedDefaults, + performanceMode: performanceMode + ) } } } } - // Recents - if !recentsForDisplay.isEmpty { - glassSection(title: "Recents") { + if !filteredRecents.isEmpty { + glassSection(title: "Recents".localized) { LazyVStack(spacing: 12) { - ForEach(recentsForDisplay, id: \.self) { bundleID in - if let meta = viewModel.apps[bundleID] { - AppButton( - bundleID: bundleID, - appName: meta.name, - version: meta.displayVersion, - recentApps: $recentApps, - favoriteApps: $favoriteApps, - appIcons: $appIcons, - onSelectApp: onSelectApp, - sharedDefaults: sharedDefaults, - performanceMode: performanceMode - ) - .dolphinBadgeIfNeeded(appName: meta.name) - } + ForEach(filteredRecents, id: \.self) { bundleID in + AppButton( + bundleID: bundleID, + appName: viewModel.apps[bundleID] ?? bundleID, + recentApps: $recentApps, + favoriteApps: $favoriteApps, + appIcons: $appIcons, + onSelectApp: onSelectApp, + sharedDefaults: sharedDefaults, + performanceMode: performanceMode + ) } } } } - // All Applications - glassSection(title: "All Applications") { + glassSection(title: "All Applications".localized) { LazyVStack(spacing: 12) { - ForEach(allAppsForDisplay, id: \.0) { bundleID, meta in + ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in AppButton( bundleID: bundleID, - appName: meta.name, - version: meta.displayVersion, + appName: appName, recentApps: $recentApps, favoriteApps: $favoriteApps, appIcons: $appIcons, @@ -309,7 +184,6 @@ struct InstalledAppsListView: View { sharedDefaults: sharedDefaults, performanceMode: performanceMode ) - .dolphinBadgeIfNeeded(appName: meta.name) } } } @@ -337,7 +211,7 @@ struct InstalledAppsListView: View { .glassCard(material: .thinMaterial, strokeOpacity: 0.12) } - // MARK: Persistence gate + // MARK: Persistence gate (avoid redundant writes + reloads) private func persistIfChanged() { var touched = false @@ -361,7 +235,6 @@ struct InstalledAppsListView: View { struct AppButton: View { let bundleID: String let appName: String - let version: String? @Binding var recentApps: [String] @Binding var favoriteApps: [String] @@ -382,22 +255,11 @@ struct AppButton: View { iconView VStack(alignment: .leading, spacing: 3) { - // First line: App name + version (if present) - HStack(spacing: 6) { - Text(appName) - .font(.system(size: 17, weight: .semibold)) - .foregroundStyle(.primary) - .lineLimit(1) + Text(appName) + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(.primary) + .lineLimit(1) - if let v = version, !v.isEmpty { - Text(v) - .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(.secondary) - .lineLimit(1) - } - } - - // Second line: bundle ID only Text(bundleID) .font(.system(size: 14)) .foregroundStyle(.secondary) @@ -631,6 +493,16 @@ final class IconCache { // MARK: - Shared UI Bits +private struct BackgroundGradient: View { + var body: some View { + LinearGradient( + colors: [Color(UIColor.systemBackground), Color(UIColor.secondarySystemBackground)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + } +} + private struct GlassCard: ViewModifier { var cornerRadius: CGFloat = 20 var material: Material = .ultraThinMaterial @@ -659,31 +531,6 @@ private extension View { } } -// Small badge modifier for DolphiniOS -private extension View { - func dolphinBadgeIfNeeded(appName: String) -> some View { - overlay(alignment: .topTrailing) { - if appName == "DolphiniOS" { - // Yellow/gold badge - Text("Not working yet") - .font(.caption2.weight(.semibold)) - .foregroundStyle(Color(UIColor.label).opacity(0.9)) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background( - Capsule() - .fill(Color.yellow.opacity(0.85)) - ) - .overlay( - Capsule() - .stroke(Color.orange.opacity(0.9), lineWidth: 0.6) - ) - .padding(6) - } - } - } -} - fileprivate func saveIconToGroup(_ image: UIImage, bundleID: String) { guard let data = image.pngData(), let container = FileManager.default @@ -724,65 +571,11 @@ extension Array: @retroactive RawRepresentable where Element: Codable { .environment(\.colorScheme, .dark) } -// MARK: - ViewModel + Models - -struct AppMeta: Equatable { - let name: String - let version: String - let build: String - - var displayVersion: String? { - if version.isEmpty && build.isEmpty { return nil } - if version.isEmpty { return "(\(build))" } - if build.isEmpty { return "v\(version)" } - return "v\(version) (\(build))" - } -} - class InstalledAppsViewModel: ObservableObject { - @Published var apps: [String: AppMeta] = [:] // bundleID -> meta - + @Published var apps: [String: String] = [:] init() { loadApps() } - func loadApps() { - // Prefer detailed list if available at runtime; else fallback to simple list. - if let detailed = getDetailedAppListIfAvailable() { - self.apps = detailed - return - } - do { - let simple = try JITEnableContext.shared.getAppList() - self.apps = simple.reduce(into: [:]) { dict, kv in - dict[kv.key] = AppMeta(name: kv.value, version: "", build: "") - } - } catch { - print(error) - self.apps = [:] - } - } - - private func getDetailedAppListIfAvailable() -> [String: AppMeta]? { - let ctx = JITEnableContext.shared as AnyObject - let sel = NSSelectorFromString("getDetailedAppListWithError:") - guard ctx.responds(to: sel) else { return nil } - - typealias ObjCFunc = @convention(c) (AnyObject, Selector, UnsafeMutablePointer?) -> NSDictionary? - guard let imp = ctx.method(for: sel) else { return nil } - let fn = unsafeBitCast(imp, to: ObjCFunc.self) - var err: NSError? - guard let dict = fn(ctx, sel, &err) as? [String: [String: Any]] else { - if let err { print("getDetailedAppList error: \(err.localizedDescription)") } - return nil - } - - var out: [String: AppMeta] = [:] - for (bundleID, info) in dict { - let name = (info["name"] as? String) ?? "Unknown" - let version = (info["version"] as? String) ?? "" - let build = (info["build"] as? String) ?? "" - out[bundleID] = AppMeta(name: name, version: version, build: build) - } - return out + do { self.apps = try JITEnableContext.shared.getAppList() } + catch { print(error); self.apps = [:] } } } - diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 5fb4c46..ab0f51b 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -17,8 +17,6 @@ struct SettingsView: View { @AppStorage("enablePiP") private var enablePiP = false @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue - // New: allow overriding TXM requirement - @AppStorage("ignoreTXMForScripts") private var ignoreTXMForScripts = false private var currentTheme: AppTheme { AppTheme(rawValue: appThemeRaw) ?? .system } @State private var isShowingPairingFilePicker = false @@ -310,10 +308,6 @@ struct SettingsView: View { .tint(accentColor) Toggle("Picture in Picture", isOn: $enablePiP) .tint(accentColor) - // New: ignore TXM requirement toggle - Toggle("Ignore TXM requirement for scripts", isOn: $ignoreTXMForScripts) - .tint(accentColor) - .accessibilityHint("When enabled, scripts will run even if TXM is not detected. Use with caution.") } .onChange(of: enableAdvancedOptions) { _, newValue in if !newValue { @@ -582,4 +576,3 @@ class FolderViewController: UIViewController { } } } - diff --git a/StikJIT/idevice/JITEnableContext.h b/StikJIT/idevice/JITEnableContext.h index f1c3408..c85f075 100644 --- a/StikJIT/idevice/JITEnableContext.h +++ b/StikJIT/idevice/JITEnableContext.h @@ -21,10 +21,4 @@ typedef void (^LogFunc)(NSString *message); - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback; - (NSDictionary*)getAppListWithError:(NSError**)error; - (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error; - -// Detailed app list including name, version, build (and icon internally). -// Returns a dictionary keyed by bundle ID. Each value has keys: -// @"name" (NSString), @"version" (NSString), @"build" (NSString), @"icon" (UIImage or NSNull). -- (NSDictionary*>*)getDetailedAppListWithError:(NSError**)error; - @end diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 5225c64..4f4af94 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -10,7 +10,7 @@ #include "heartbeat.h" #include "jit.h" -#include "applist.h" // declares list_installed_apps_with_icons +#include "applist.h" #include "JITEnableContext.h" #import "StikDebug-Swift.h" @@ -75,18 +75,14 @@ JITEnableContext* sharedJITContext = nil; if (![fm fileExistsAtPath:pairingFileURL.path]) { NSLog(@"Pairing file not found!"); - if (error) { - *error = [self errorWithStr:@"Pairing file not found!" code:-17]; - } + *error = [self errorWithStr:@"Pairing file not found!" code:-17]; return nil; } IdevicePairingFile* pairingFile = NULL; IdeviceFfiError* err = idevice_pairing_file_read(pairingFileURL.fileSystemRepresentation, &pairingFile); if (err) { - if (error) { - *error = [self errorWithStr:@"Failed to read pairing file!" code:err->code]; - } + *error = [self errorWithStr:@"Failed to read pairing file!" code:err->code]; return nil; } return pairingFile; @@ -98,31 +94,36 @@ JITEnableContext* sharedJITContext = nil; NSError* err = nil; IdevicePairingFile* pairingFile = [self getPairingFileWithError:&err]; if (err) { - if (err.code == -17) { // silently ignore "not found" + // silently swallow “pairing file not found” (-17) + if (err.code == -17) { return; } - if (logger) { logger(err.localizedDescription); } - if (completionHandler) { completionHandler((int)err.code, err.localizedDescription); } + // for all other errors, log and forward + if (logger) { + logger(err.localizedDescription); + } + completionHandler(err.code, err.localizedDescription); return; } - if (heartbeatRunning) { return; } + if(heartbeatRunning) { + return; + } startHeartbeat( pairingFile, &provider, &heartbeatRunning, ^(int result, const char *message) { - if (completionHandler) { - completionHandler(result, - [NSString stringWithCString:message encoding:NSASCIIStringEncoding]); - } + completionHandler(result, + [NSString stringWithCString:message + encoding:NSASCIIStringEncoding]); }, [self createCLogger:logger] ); } - (void)ensureHeartbeat { - // wait a bit until heartbeat finishes. wait at most 10s + // wait a bit until heartbeat finish. wait at most 10s int deadline = 50; while((!lastHeartbeatDate || [[NSDate now] timeIntervalSinceDate:lastHeartbeatDate] > 15) && deadline) { --deadline; @@ -132,97 +133,66 @@ JITEnableContext* sharedJITContext = nil; - (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback { if (!provider) { - if (logger) { logger(@"Provider not initialized!"); } + if (logger) { + logger(@"Provider not initialized!"); + } NSLog(@"Provider not initialized!"); return NO; } + [self ensureHeartbeat]; - return debug_app(provider, [bundleID UTF8String], [self createCLogger:logger], jsCallback) == 0; + + return debug_app(provider, + [bundleID UTF8String], + [self createCLogger:logger], jsCallback) == 0; } - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback { if (!provider) { - if (logger) { logger(@"Provider not initialized!"); } + if (logger) { + logger(@"Provider not initialized!"); + } NSLog(@"Provider not initialized!"); return NO; } + [self ensureHeartbeat]; - return debug_app_pid(provider, pid, [self createCLogger:logger], jsCallback) == 0; + + return debug_app_pid(provider, + pid, + [self createCLogger:logger], jsCallback) == 0; } -// Build a simple map bundleID -> appName from the richer structure. - (NSDictionary*)getAppListWithError:(NSError**)error { if (!provider) { NSLog(@"Provider not initialized!"); - if (error) { *error = [self errorWithStr:@"Provider not initialized!" code:-1]; } + *error = [self errorWithStr:@"Provider not initialized!" code:-1]; return nil; } NSString* errorStr = nil; - NSDictionary*>* full = - list_installed_apps_with_icons(provider, &errorStr); + NSDictionary* apps = list_installed_apps(provider, &errorStr); if (errorStr) { - if (error) { *error = [self errorWithStr:errorStr code:-17]; } + *error = [self errorWithStr:errorStr code:-17]; return nil; } - if (!full) { return @{}; } - - NSMutableDictionary* simple = [NSMutableDictionary dictionaryWithCapacity:full.count]; - [full enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) { - NSString* name = obj[@"name"]; - if (![name isKindOfClass:[NSString class]] || name.length == 0) { name = @"Unknown"; } - simple[key] = name; - }]; - return simple.copy; + return apps; } -// Per-app icon fetcher using SpringBoardServices, mirroring applist.m behavior. - (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { if (!provider) { NSLog(@"Provider not initialized!"); - if (error) { *error = [self errorWithStr:@"Provider not initialized!" code:-1]; } - return nil; - } - - SpringBoardServicesClientHandle *sbClient = NULL; - if (springboard_services_connect(provider, &sbClient)) { - if (error) { *error = [self errorWithStr:@"Failed to connect to SpringBoard Services" code:-17]; } - return nil; - } - - void *pngData = NULL; - size_t dataLen = 0; - UIImage *icon = nil; - int rc = springboard_services_get_icon(sbClient, [bundleId UTF8String], &pngData, &dataLen); - if (rc == 0 && pngData && dataLen > 0) { - NSData *data = [NSData dataWithBytes:pngData length:dataLen]; - free(pngData); - icon = [UIImage imageWithData:data]; - } else { - if (pngData) { free(pngData); } - if (error) { *error = [self errorWithStr:@"Failed to fetch icon" code:rc ?: -17]; } - } - - springboard_services_free(sbClient); - return icon; -} - -// NEW: Expose the full app metadata (name, version, build, icon) to Swift callers. -- (NSDictionary*>*)getDetailedAppListWithError:(NSError**)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - if (error) { *error = [self errorWithStr:@"Provider not initialized!" code:-1]; } + *error = [self errorWithStr:@"Provider not initialized!" code:-1]; return nil; } NSString* errorStr = nil; - NSDictionary*>* full = - list_installed_apps_with_icons(provider, &errorStr); + UIImage* icon = getAppIcon(provider, bundleId, &errorStr); if (errorStr) { - if (error) { *error = [self errorWithStr:errorStr code:-17]; } + *error = [self errorWithStr:errorStr code:-17]; return nil; } - return full ?: @{}; + return icon; } - (void)dealloc { diff --git a/StikJIT/idevice/applist.h b/StikJIT/idevice/applist.h index c9c0e0c..1696ddb 100644 --- a/StikJIT/idevice/applist.h +++ b/StikJIT/idevice/applist.h @@ -10,18 +10,7 @@ @import Foundation; @import UIKit; -#ifdef __cplusplus -extern "C" { -#endif - -// Returns a dictionary keyed by bundle ID. -// Each value is a dictionary with keys: "name" (NSString), "version" (NSString), -// "build" (NSString), and "icon" (UIImage or NSNull). -NSDictionary*>* -list_installed_apps_with_icons(IdeviceProviderHandle* provider, NSString** error); - -#ifdef __cplusplus -} // extern "C" -#endif +NSDictionary* list_installed_apps(IdeviceProviderHandle* provider, NSString** error); +UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSString** error); #endif /* APPLIST_H */ diff --git a/StikJIT/idevice/applist.m b/StikJIT/idevice/applist.m index 246496f..95478ff 100644 --- a/StikJIT/idevice/applist.m +++ b/StikJIT/idevice/applist.m @@ -11,48 +11,39 @@ #include #import "applist.h" -NSDictionary*>* -list_installed_apps_with_icons(IdeviceProviderHandle* provider, NSString** error) { - InstallationProxyClientHandle *ipClient = NULL; - if (installation_proxy_connect_tcp(provider, &ipClient)) { +NSDictionary* list_installed_apps(IdeviceProviderHandle* provider, NSString** error) { + InstallationProxyClientHandle *client = NULL; + if (installation_proxy_connect_tcp(provider, &client)) { *error = @"Failed to connect to installation proxy"; return nil; } void *apps = NULL; size_t count = 0; - if (installation_proxy_get_apps(ipClient, "User", NULL, 0, &apps, &count)) { - installation_proxy_client_free(ipClient); + if (installation_proxy_get_apps(client, "User", NULL, 0, &apps, &count)) { + installation_proxy_client_free(client); *error = @"Failed to get apps"; return nil; } - NSMutableDictionary*> *result = - [NSMutableDictionary dictionaryWithCapacity:count]; - - // Connect SpringBoardServices once for all icon requests - SpringBoardServicesClientHandle *sbClient = NULL; - if (springboard_services_connect(provider, &sbClient)) { - installation_proxy_client_free(ipClient); - *error = @"Failed to connect to SpringBoard Services"; - return nil; - } + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:count]; for (size_t i = 0; i < count; i++) { plist_t app = ((plist_t *)apps)[i]; - - // Only include apps with get-task-allow entitlement plist_t ent = plist_dict_get_item(app, "Entitlements"); if (!ent) continue; + plist_t tnode = plist_dict_get_item(ent, "get-task-allow"); if (!tnode) continue; + uint8_t isAllowed = 0; plist_get_bool_val(tnode, &isAllowed); if (!isAllowed) continue; - // Bundle ID - char *bidC = NULL; plist_t bidNode = plist_dict_get_item(app, "CFBundleIdentifier"); + if (!bidNode) continue; + + char *bidC = NULL; plist_get_string_val(bidNode, &bidC); if (!bidC || bidC[0] == '\0') { free(bidC); @@ -61,56 +52,43 @@ list_installed_apps_with_icons(IdeviceProviderHandle* provider, NSString** error NSString *bundleID = [NSString stringWithUTF8String:bidC]; free(bidC); - // App Name NSString *appName = @"Unknown"; - char *nameC = NULL; plist_t nameNode = plist_dict_get_item(app, "CFBundleName"); - plist_get_string_val(nameNode, &nameC); - if (nameC && nameC[0] != '\0') { - appName = [NSString stringWithUTF8String:nameC]; - } - free(nameC); - - // Version - NSString *version = @""; - char *versionC = NULL; - plist_t versionNode = plist_dict_get_item(app, "CFBundleShortVersionString"); - plist_get_string_val(versionNode, &versionC); - if (versionC && versionC[0] != '\0') { - version = [NSString stringWithUTF8String:versionC]; - } - free(versionC); - - // Build - NSString *build = @""; - char *buildC = NULL; - plist_t buildNode = plist_dict_get_item(app, "CFBundleVersion"); - plist_get_string_val(buildNode, &buildC); - if (buildC && buildC[0] != '\0') { - build = [NSString stringWithUTF8String:buildC]; - } - free(buildC); - - // Icon - void *pngData = NULL; - size_t dataLen = 0; - UIImage *icon = nil; - if (!springboard_services_get_icon(sbClient, [bundleID UTF8String], &pngData, &dataLen)) { - NSData *data = [NSData dataWithBytes:pngData length:dataLen]; - free(pngData); - icon = [UIImage imageWithData:data]; + if (nameNode) { + char *nameC = NULL; + plist_get_string_val(nameNode, &nameC); + if (nameC && nameC[0] != '\0') { + appName = [NSString stringWithUTF8String:nameC]; + } + free(nameC); } - // Store info - result[bundleID] = @{ - @"name": appName, - @"version": version, - @"build": build, - @"icon": icon ?: [NSNull null] - }; + result[bundleID] = appName; } - springboard_services_free(sbClient); - installation_proxy_client_free(ipClient); + installation_proxy_client_free(client); return result; } + +UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSString** error) { + SpringBoardServicesClientHandle *client = NULL; + if (springboard_services_connect(provider, &client)) { + *error = @"Failed to connect to SpringBoard Services"; + return nil; + } + + void *pngData = NULL; + size_t dataLen = 0; + if (springboard_services_get_icon(client, [bundleID UTF8String], &pngData, &dataLen)) { + springboard_services_free(client); + *error = @"Failed to get app icon"; + return nil; + } + + NSData *data = [NSData dataWithBytes:pngData length:dataLen]; + free(pngData); + UIImage *icon = [UIImage imageWithData:data]; + + springboard_services_free(client); + return icon; +}