mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Update the url scheme to support iOS 26 JIT
This commit is contained in:
parent
f10f56e5e5
commit
0bd73257c2
4 changed files with 454 additions and 69 deletions
|
|
@ -9,47 +9,45 @@ import WidgetKit
|
|||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - Timeline Entry
|
||||
struct AppsEntry: TimelineEntry {
|
||||
// MARK: - Favorites Widget ----------------------------------------------------
|
||||
|
||||
struct FavoritesEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let bundleIDs: [String]
|
||||
}
|
||||
|
||||
// MARK: - Provider
|
||||
struct AppsProvider: TimelineProvider {
|
||||
struct FavoritesProvider: TimelineProvider {
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj")
|
||||
|
||||
func placeholder(in context: Context) -> AppsEntry {
|
||||
AppsEntry(date: .now, bundleIDs: [])
|
||||
func placeholder(in context: Context) -> FavoritesEntry {
|
||||
FavoritesEntry(date: .now, bundleIDs: [])
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (AppsEntry) -> Void) {
|
||||
func getSnapshot(in context: Context, completion: @escaping (FavoritesEntry) -> Void) {
|
||||
completion(makeEntry())
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<AppsEntry>) -> Void) {
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<FavoritesEntry>) -> Void) {
|
||||
let entry = makeEntry()
|
||||
completion(Timeline(entries: [entry], policy: .never))
|
||||
}
|
||||
|
||||
private func makeEntry() -> AppsEntry {
|
||||
let favs = sharedDefaults?.stringArray(forKey: "favoriteApps") ?? []
|
||||
let bundleIDs = Array(favs.prefix(4))
|
||||
return AppsEntry(date: .now, bundleIDs: bundleIDs)
|
||||
private func makeEntry() -> FavoritesEntry {
|
||||
let favorites = sharedDefaults?.stringArray(forKey: "favoriteApps") ?? []
|
||||
return FavoritesEntry(date: .now, bundleIDs: Array(favorites.prefix(4)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget View
|
||||
struct AppsWidgetEntryView: View {
|
||||
let entry: AppsEntry
|
||||
struct FavoritesWidgetEntryView: View {
|
||||
let entry: FavoritesEntry
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<4, id: \.self) { idx in
|
||||
if idx < entry.bundleIDs.count {
|
||||
IconCell(bundleID: entry.bundleIDs[idx])
|
||||
iconCell(bundleID: entry.bundleIDs[idx])
|
||||
} else {
|
||||
PlaceholderCell()
|
||||
placeholderCell()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +56,7 @@ struct AppsWidgetEntryView: View {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func IconCell(bundleID: String) -> some View {
|
||||
private func iconCell(bundleID: String) -> some View {
|
||||
if let img = loadIcon(for: bundleID) {
|
||||
Link(destination: URL(string: "stikjit://enable-jit?bundle-id=\(bundleID)")!) {
|
||||
Image(uiImage: img)
|
||||
|
|
@ -67,12 +65,12 @@ struct AppsWidgetEntryView: View {
|
|||
.cornerRadius(12)
|
||||
}
|
||||
} else {
|
||||
PlaceholderCell()
|
||||
placeholderCell()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func PlaceholderCell() -> some View {
|
||||
private func placeholderCell() -> some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color(UIColor.systemGray5))
|
||||
|
|
@ -82,14 +80,153 @@ struct AppsWidgetEntryView: View {
|
|||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadIcon(for bundleID: String) -> UIImage? {
|
||||
guard let container = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: "group.com.stik.sj")
|
||||
else { return nil }
|
||||
let url = container
|
||||
.appendingPathComponent("icons", isDirectory: true)
|
||||
.appendingPathComponent("\(bundleID).png")
|
||||
return UIImage(contentsOfFile: url.path)
|
||||
struct FavoritesWidget: Widget {
|
||||
let kind: String = "FavoritesWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: FavoritesProvider()) { entry in
|
||||
FavoritesWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("StikDebug Favorites")
|
||||
.description("Quick-launch your top 4 favorite debug targets.")
|
||||
.supportedFamilies([.systemMedium])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - System Apps Widget -------------------------------------------------
|
||||
|
||||
struct SystemAppSnapshot: Identifiable {
|
||||
let bundleID: String
|
||||
let displayName: String
|
||||
var id: String { bundleID }
|
||||
}
|
||||
|
||||
struct SystemAppsEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let items: [SystemAppSnapshot]
|
||||
}
|
||||
|
||||
struct SystemAppsProvider: TimelineProvider {
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj")
|
||||
|
||||
func placeholder(in context: Context) -> SystemAppsEntry {
|
||||
SystemAppsEntry(date: .now, items: [])
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SystemAppsEntry) -> Void) {
|
||||
completion(makeEntry())
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<SystemAppsEntry>) -> Void) {
|
||||
let entry = makeEntry()
|
||||
completion(Timeline(entries: [entry], policy: .never))
|
||||
}
|
||||
|
||||
private func makeEntry() -> SystemAppsEntry {
|
||||
let pinned = sharedDefaults?.stringArray(forKey: "pinnedSystemApps") ?? []
|
||||
let names = sharedDefaults?.dictionary(forKey: "pinnedSystemAppNames") as? [String: String] ?? [:]
|
||||
let snapshots = pinned.prefix(4).map { bundleID -> SystemAppSnapshot in
|
||||
let displayName = names[bundleID] ?? friendlyName(bundleID: bundleID)
|
||||
return SystemAppSnapshot(bundleID: bundleID, displayName: displayName)
|
||||
}
|
||||
return SystemAppsEntry(date: .now, items: snapshots)
|
||||
}
|
||||
|
||||
private func friendlyName(bundleID: String) -> String {
|
||||
let components = bundleID.split(separator: ".")
|
||||
if let last = components.last {
|
||||
let cleaned = last.replacingOccurrences(of: "_", with: " ")
|
||||
let trimmed = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty { return trimmed.capitalized }
|
||||
}
|
||||
return bundleID
|
||||
}
|
||||
}
|
||||
|
||||
struct SystemAppsWidgetEntryView: View {
|
||||
let entry: SystemAppsEntry
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(entry.items) { item in
|
||||
Link(destination: URL(string: "stikjit://enable-jit?bundle-id=\(item.bundleID)")!) {
|
||||
HStack(spacing: 10) {
|
||||
icon(for: item.bundleID)
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.displayName)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
Text(item.bundleID)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(UIColor.secondarySystemBackground).opacity(0.35))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if entry.items.isEmpty {
|
||||
Text("Pin system apps from StikDebug to see them here.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.containerBackground(Color(UIColor.systemBackground), for: .widget)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func icon(for bundleID: String) -> some View {
|
||||
if let image = loadIcon(for: bundleID) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color(UIColor.systemGray5))
|
||||
.overlay(
|
||||
Image(systemName: "app")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
.foregroundColor(.gray)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SystemAppsWidget: Widget {
|
||||
let kind: String = "SystemAppsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: SystemAppsProvider()) { entry in
|
||||
SystemAppsWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Hidden System Apps")
|
||||
.description("Launch your pinned hidden system apps directly from the widget.")
|
||||
.supportedFamilies([.systemMedium])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shared Helpers -----------------------------------------------------
|
||||
|
||||
private func loadIcon(for bundleID: String) -> UIImage? {
|
||||
guard let container = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: "group.com.stik.sj")
|
||||
else { return nil }
|
||||
let url = container
|
||||
.appendingPathComponent("icons", isDirectory: true)
|
||||
.appendingPathComponent("\(bundleID).png")
|
||||
return UIImage(contentsOfFile: url.path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,9 @@ import WidgetKit
|
|||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct AppsWidget: Widget {
|
||||
let kind: String = "AppsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: AppsProvider()) { entry in
|
||||
AppsWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("StikDebug Favorites")
|
||||
.description("Quick-launch your top 4 favorite debug targets.")
|
||||
.supportedFamilies([.systemMedium])
|
||||
struct StikDebugWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
FavoritesWidget()
|
||||
// SystemAppsWidget() // Temporarily disabled
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
|||
import UniformTypeIdentifiers
|
||||
import Pipify
|
||||
import UIKit
|
||||
import WidgetKit
|
||||
|
||||
struct JITEnableConfiguration {
|
||||
var bundleID: String? = nil
|
||||
|
|
@ -59,6 +60,10 @@ struct HomeView: View {
|
|||
@State private var heartbeatOK = false
|
||||
@State private var cachedAppNames: [String: String] = [:]
|
||||
@State private var isLoadingQuickApps = false
|
||||
@AppStorage("pinnedSystemApps") private var pinnedSystemApps: [String] = []
|
||||
@AppStorage("pinnedSystemAppNames") private var pinnedSystemAppNames: [String: String] = [:]
|
||||
@State private var launchingSystemApps: Set<String> = []
|
||||
@State private var systemLaunchMessage: String? = nil
|
||||
|
||||
@AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true
|
||||
|
||||
|
|
@ -96,6 +101,9 @@ struct HomeView: View {
|
|||
|
||||
readinessCard
|
||||
quickConnectCard
|
||||
if !systemPinnedItems.isEmpty {
|
||||
systemAppsCard
|
||||
}
|
||||
toolsCard
|
||||
tipsCard
|
||||
}
|
||||
|
|
@ -124,6 +132,9 @@ struct HomeView: View {
|
|||
if justCopied {
|
||||
toast("Copied")
|
||||
}
|
||||
if let message = systemLaunchMessage {
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
}
|
||||
|
|
@ -218,37 +229,10 @@ 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 == "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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let auto = autoScriptForBundleIfNeeded(bundleID: selectedBundle) {
|
||||
autoScriptData = auto.data
|
||||
autoScriptName = auto.name
|
||||
}
|
||||
|
||||
startJITInBackground(bundleID: selectedBundle,
|
||||
|
|
@ -307,6 +291,15 @@ struct HomeView: View {
|
|||
if let scriptName = components?.queryItems?.first(where: { $0.name == "script-name" })?.value {
|
||||
config.scriptName = scriptName
|
||||
}
|
||||
|
||||
// If no explicit script was provided via URL, try to attach the same
|
||||
// auto-selected script we use from the Installed Apps flow.
|
||||
if config.scriptData == nil, let bundle = config.bundleID,
|
||||
let auto = autoScriptForBundleIfNeeded(bundleID: bundle) {
|
||||
config.scriptData = auto.data
|
||||
config.scriptName = auto.name
|
||||
}
|
||||
|
||||
if viewDidAppeared {
|
||||
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
|
||||
} else {
|
||||
|
|
@ -664,6 +657,32 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var systemAppsCard: some View {
|
||||
homeCard {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("System Apps".localized)
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("Launch hidden system bundles without leaving Home.".localized)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
ForEach(systemPinnedItems) { item in
|
||||
SystemPinnedRow(
|
||||
item: item,
|
||||
accentColor: accentColor,
|
||||
isLaunching: launchingSystemApps.contains(item.bundleID),
|
||||
action: { launchSystemApp(item: item) },
|
||||
onRemove: { removePinnedSystemApp(bundleID: item.bundleID) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var quickConnectItems: [QuickConnectItem] {
|
||||
var seen = Set<String>()
|
||||
var ordered: [QuickConnectItem] = []
|
||||
|
|
@ -675,6 +694,13 @@ struct HomeView: View {
|
|||
return ordered
|
||||
}
|
||||
|
||||
private var systemPinnedItems: [SystemPinnedItem] {
|
||||
pinnedSystemApps.compactMap { bundleID in
|
||||
let displayName = pinnedSystemAppNames[bundleID] ?? friendlyName(for: bundleID)
|
||||
return SystemPinnedItem(bundleID: bundleID, displayName: displayName)
|
||||
}
|
||||
}
|
||||
|
||||
private func friendlyName(for bundleID: String) -> String {
|
||||
if let cached = cachedAppNames[bundleID], !cached.isEmpty { return cached }
|
||||
let components = bundleID.split(separator: ".")
|
||||
|
|
@ -953,6 +979,53 @@ struct HomeView: View {
|
|||
pipRequired = false
|
||||
}
|
||||
}
|
||||
|
||||
private func launchSystemApp(item: SystemPinnedItem) {
|
||||
guard !launchingSystemApps.contains(item.bundleID) else { return }
|
||||
launchingSystemApps.insert(item.bundleID)
|
||||
HapticFeedbackHelper.trigger()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let success = JITEnableContext.shared.launchAppWithoutDebug(item.bundleID, logger: nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
launchingSystemApps.remove(item.bundleID)
|
||||
if success {
|
||||
LogManager.shared.addInfoLog("Launch request sent for \(item.bundleID)")
|
||||
systemLaunchMessage = String(format: "Launch requested: %@".localized, item.displayName)
|
||||
} else {
|
||||
LogManager.shared.addErrorLog("Failed to launch \(item.bundleID)")
|
||||
systemLaunchMessage = String(format: "Failed to launch %@".localized, item.displayName)
|
||||
}
|
||||
scheduleSystemToastDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removePinnedSystemApp(bundleID: String) {
|
||||
Haptics.light()
|
||||
pinnedSystemApps.removeAll { $0 == bundleID }
|
||||
pinnedSystemAppNames.removeValue(forKey: bundleID)
|
||||
persistPinnedSystemApps()
|
||||
}
|
||||
|
||||
private func scheduleSystemToastDismiss() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
if systemLaunchMessage != nil {
|
||||
withAnimation {
|
||||
systemLaunchMessage = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func persistPinnedSystemApps() {
|
||||
if let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj") {
|
||||
sharedDefaults.set(pinnedSystemApps, forKey: "pinnedSystemApps")
|
||||
sharedDefaults.set(pinnedSystemAppNames, forKey: "pinnedSystemAppNames")
|
||||
}
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
private func addRecentPID(_ pid: Int) {
|
||||
var list = recentPIDs.filter { $0 != pid }
|
||||
|
|
@ -1156,6 +1229,71 @@ private struct QuickConnectRow: View {
|
|||
}
|
||||
}
|
||||
|
||||
private struct SystemPinnedRow: View {
|
||||
let item: SystemPinnedItem
|
||||
let accentColor: Color
|
||||
let isLaunching: Bool
|
||||
var action: () -> Void
|
||||
var onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 14) {
|
||||
QuickAppBadge(title: item.displayName, accentColor: accentColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.displayName)
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(item.bundleID)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
if isLaunching {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.tint(accentColor)
|
||||
} else {
|
||||
Image(systemName: "play.fill")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.fill(Color(UIColor.secondarySystemBackground).opacity(0.6))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isLaunching)
|
||||
.contextMenu {
|
||||
Button("Remove from Home".localized, systemImage: "star.slash") {
|
||||
onRemove()
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
onRemove()
|
||||
} label: {
|
||||
Label("Remove".localized, systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct QuickAppBadge: View {
|
||||
let title: String
|
||||
let accentColor: Color
|
||||
|
|
@ -1232,6 +1370,12 @@ private struct QuickConnectItem: Identifiable {
|
|||
var id: String { bundleID }
|
||||
}
|
||||
|
||||
private struct SystemPinnedItem: Identifiable {
|
||||
let bundleID: String
|
||||
let displayName: String
|
||||
var id: String { bundleID }
|
||||
}
|
||||
|
||||
// MARK: - Connect-by-PID Sheet (minus/plus removed)
|
||||
|
||||
private struct ConnectByPIDSheet: View {
|
||||
|
|
@ -1450,3 +1594,39 @@ public extension ProcessInfo {
|
|||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Auto script selection for URL and in-app flows
|
||||
|
||||
private extension HomeView {
|
||||
struct AutoScript {
|
||||
let data: Data
|
||||
let name: String
|
||||
}
|
||||
|
||||
func autoScriptForBundleIfNeeded(bundleID: String) -> AutoScript? {
|
||||
guard #available(iOS 26, *), ProcessInfo.processInfo.hasTXM else { return nil }
|
||||
let appName: String? = {
|
||||
let list = try? JITEnableContext.shared.getAppList()
|
||||
return list?[bundleID]
|
||||
}()
|
||||
|
||||
func load(resource: String, as name: String) -> AutoScript? {
|
||||
guard let url = Bundle.main.url(forResource: resource, withExtension: "js"),
|
||||
let data = try? Data(contentsOf: url) else { return nil }
|
||||
return AutoScript(data: data, name: name)
|
||||
}
|
||||
|
||||
switch appName {
|
||||
case "maciOS":
|
||||
return load(resource: "script1", as: "script1.js")
|
||||
case "Amethyst":
|
||||
return load(resource: "script2", as: "script2.js")
|
||||
case "MeloNX":
|
||||
return load(resource: "melo", as: "melo.js")
|
||||
case "UTM":
|
||||
return load(resource: "utmjit", as: "utmjit.js")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ struct InstalledAppsListView: View {
|
|||
@State private var prefetchedBundleIDs: Set<String> = []
|
||||
@State private var selectedTab: AppListTab = .debuggable
|
||||
@State private var systemSearchText: String = ""
|
||||
@AppStorage("pinnedSystemApps") private var pinnedSystemApps: [String] = []
|
||||
@AppStorage("pinnedSystemAppNames") private var pinnedSystemAppNames: [String: String] = [:]
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var onSelectApp: (String) -> Void
|
||||
|
|
@ -276,6 +278,7 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: selectedTab) { _, _ in prefetchPriorityIcons() }
|
||||
.onChange(of: pinnedSystemApps) { _, _ in prefetchPriorityIcons() }
|
||||
}
|
||||
|
||||
// MARK: Empty State
|
||||
|
|
@ -429,6 +432,7 @@ struct InstalledAppsListView: View {
|
|||
|
||||
appendUnique(favoriteApps)
|
||||
appendUnique(recentApps)
|
||||
appendUnique(pinnedSystemApps)
|
||||
appendUnique(debuggableSortedApps.map { $0.key })
|
||||
appendUnique(sortedSystemApps.map { $0.key })
|
||||
appendUnique(sortedOtherApps.map { $0.key })
|
||||
|
|
@ -462,6 +466,7 @@ struct InstalledAppsListView: View {
|
|||
glassSection(title: "Hidden System Apps".localized) {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(apps, id: \.key) { bundleID, appName in
|
||||
let isPinned = pinnedSystemApps.contains(bundleID)
|
||||
LaunchAppRow(
|
||||
bundleID: bundleID,
|
||||
appName: appName,
|
||||
|
|
@ -471,6 +476,31 @@ struct InstalledAppsListView: View {
|
|||
) {
|
||||
startLaunching(bundleID: bundleID)
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if isPinned {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(.yellow)
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button((isPinned ? "Remove from Home" : "Add to Home").localized, systemImage: isPinned ? "star.slash" : "star") {
|
||||
toggleSystemPin(bundleID: bundleID, appName: appName)
|
||||
}
|
||||
Button("Copy Bundle ID".localized, systemImage: "doc.on.doc") {
|
||||
UIPasteboard.general.string = bundleID
|
||||
Haptics.light()
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button {
|
||||
toggleSystemPin(bundleID: bundleID, appName: appName)
|
||||
} label: {
|
||||
Label((isPinned ? "Unpin" : "Pin").localized, systemImage: "star")
|
||||
}
|
||||
.tint(.yellow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -776,6 +806,8 @@ struct InstalledAppsListView: View {
|
|||
var touched = false
|
||||
let prevR = (sharedDefaults.array(forKey: "recentApps") as? [String]) ?? []
|
||||
let prevF = (sharedDefaults.array(forKey: "favoriteApps") as? [String]) ?? []
|
||||
let prevPinned = (sharedDefaults.array(forKey: "pinnedSystemApps") as? [String]) ?? []
|
||||
let prevPinnedNames = (sharedDefaults.dictionary(forKey: "pinnedSystemAppNames") as? [String: String]) ?? [:]
|
||||
|
||||
if prevR != recentApps {
|
||||
sharedDefaults.set(recentApps, forKey: "recentApps")
|
||||
|
|
@ -785,6 +817,14 @@ struct InstalledAppsListView: View {
|
|||
sharedDefaults.set(favoriteApps, forKey: "favoriteApps")
|
||||
touched = true
|
||||
}
|
||||
if prevPinned != pinnedSystemApps {
|
||||
sharedDefaults.set(pinnedSystemApps, forKey: "pinnedSystemApps")
|
||||
touched = true
|
||||
}
|
||||
if prevPinnedNames != pinnedSystemAppNames {
|
||||
sharedDefaults.set(pinnedSystemAppNames, forKey: "pinnedSystemAppNames")
|
||||
touched = true
|
||||
}
|
||||
if touched { WidgetCenter.shared.reloadAllTimelines() }
|
||||
}
|
||||
|
||||
|
|
@ -816,6 +856,25 @@ struct InstalledAppsListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleSystemPin(bundleID: String, appName: String) {
|
||||
Haptics.light()
|
||||
if let index = pinnedSystemApps.firstIndex(of: bundleID) {
|
||||
pinnedSystemApps.remove(at: index)
|
||||
pinnedSystemAppNames.removeValue(forKey: bundleID)
|
||||
} else {
|
||||
pinnedSystemApps.removeAll { $0 == bundleID }
|
||||
pinnedSystemApps.insert(bundleID, at: 0)
|
||||
pinnedSystemAppNames[bundleID] = appName
|
||||
let maxPins = 8
|
||||
if pinnedSystemApps.count > maxPins {
|
||||
let surplus = Array(pinnedSystemApps.suffix(from: maxPins))
|
||||
for id in surplus { pinnedSystemAppNames.removeValue(forKey: id) }
|
||||
pinnedSystemApps = Array(pinnedSystemApps.prefix(maxPins))
|
||||
}
|
||||
}
|
||||
persistIfChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Button Row
|
||||
|
|
@ -1412,6 +1471,21 @@ extension Array: @retroactive RawRepresentable where Element: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
extension Dictionary: @retroactive RawRepresentable where Key: Codable, Value: Codable {
|
||||
public init?(rawValue: String) {
|
||||
guard let data = rawValue.data(using: .utf8),
|
||||
let result = try? JSONDecoder().decode([Key: Value].self, from: data)
|
||||
else { return nil }
|
||||
self = result
|
||||
}
|
||||
public var rawValue: String {
|
||||
guard let data = try? JSONEncoder().encode(self),
|
||||
let result = String(data: data, encoding: .utf8)
|
||||
else { return "{}" }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue