2.2.5 (Sorry - A little messy)

This commit is contained in:
Stephen B 2025-09-16 16:47:13 -04:00
parent c5bf809472
commit 0424339135
19 changed files with 1658 additions and 1302 deletions

View file

@ -725,7 +725,7 @@
"$(PROJECT_DIR)/StikJIT/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 2.2.3;
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.3;
MARKETING_VERSION = 2.2.5;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;

View file

@ -25,7 +25,7 @@ struct ScriptEditorView: View {
text: $scriptContent,
position: $position,
messages: $messages,
language: .swift() // ← JavaScript syntax highlighting
language: .swift()
)
.font(.system(.footnote, design: .monospaced))
.frame(maxWidth: .infinity, maxHeight: .infinity)
@ -36,7 +36,6 @@ struct ScriptEditorView: View {
Divider()
// Equal-width, centered, rounded-rectangle buttons
HStack(spacing: 12) {
WideGlassyButton(title: "Cancel", systemImage: "xmark") {
dismiss()
@ -81,12 +80,12 @@ private struct WideGlassyButton: View {
.lineLimit(1)
.minimumScaleFactor(0.85)
}
.frame(maxWidth: .infinity, alignment: .center) // center label+icon
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 10)
.padding(.horizontal, 14)
}
.frame(height: 44)
.frame(maxWidth: .infinity) // equal widths within HStack
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(.ultraThinMaterial)

View file

@ -24,46 +24,36 @@ struct ScriptListView: View {
@State private var justCopied = false
@State private var searchText = ""
// delete confirmation
@State private var showDeleteConfirmation = false
@State private var pendingDelete: URL? = nil
var onSelectScript: ((URL?) -> Void)? = nil
private var isPickerMode: Bool { onSelectScript != nil }
private var filteredScripts: [URL] {
guard !searchText.isEmpty else { return scripts }
return scripts.filter { $0.lastPathComponent.localizedCaseInsensitiveContains(searchText) }
}
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
private var currentTheme: AppTheme { AppTheme(rawValue: appThemeRaw) ?? .system }
var body: some View {
NavigationStack {
ZStack {
// Background gradient
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ThemedBackground(style: currentTheme.backgroundStyle)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
// Search + actions card
headerCard
if filteredScripts.isEmpty {
emptyCard
} else {
ForEach(filteredScripts, id: \.self) { script in
NavigationLink {
ScriptEditorView(scriptURL: script)
} label: {
scriptCard(script)
}
.buttonStyle(.plain)
scriptRow(script)
}
}
}
@ -109,14 +99,16 @@ struct ScriptListView: View {
.animation(.easeInOut(duration: 0.25), value: justCopied)
}
}
.navigationTitle("Scripts")
.navigationTitle(isPickerMode ? "Choose Script" : "Scripts")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button { showNewFileAlert = true } label: {
Label("New", systemImage: "doc.badge.plus")
}
Button { showImporter = true } label: {
Label("Import", systemImage: "tray.and.arrow.down")
if !isPickerMode {
Button { showNewFileAlert = true } label: {
Label("New", systemImage: "doc.badge.plus")
}
Button { showImporter = true } label: {
Label("Import", systemImage: "tray.and.arrow.down")
}
}
}
}
@ -156,13 +148,21 @@ struct ScriptListView: View {
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1)
)
// Equal-width rounded-rectangle buttons with centered content
HStack(spacing: 12) {
WideGlassyButton(title: "New", systemImage: "doc.badge.plus") {
showNewFileAlert = true
}
WideGlassyButton(title: "Import", systemImage: "tray.and.arrow.down") {
showImporter = true
if isPickerMode {
WideGlassyButton(title: "None", systemImage: "nosign") {
onSelectScript?(nil)
}
WideGlassyButton(title: "Import", systemImage: "tray.and.arrow.down") {
showImporter = true
}
} else {
WideGlassyButton(title: "New", systemImage: "doc.badge.plus") {
showNewFileAlert = true
}
WideGlassyButton(title: "Import", systemImage: "tray.and.arrow.down") {
showImporter = true
}
}
}
}
@ -170,7 +170,26 @@ struct ScriptListView: View {
.background(glassyBackground)
}
private func scriptCard(_ script: URL) -> some View {
@ViewBuilder
private func scriptRow(_ script: URL) -> some View {
if isPickerMode {
Button {
onSelectScript?(script)
} label: {
scriptCard(script, showDefaultStar: true, showDelete: false)
}
.buttonStyle(.plain)
} else {
NavigationLink {
ScriptEditorView(scriptURL: script)
} label: {
scriptCard(script, showDefaultStar: true, showDelete: true)
}
.buttonStyle(.plain)
}
}
private func scriptCard(_ script: URL, showDefaultStar: Bool, showDelete: Bool) -> some View {
let isDefault = defaultScriptName == script.lastPathComponent
return HStack(spacing: 12) {
@ -184,19 +203,21 @@ struct ScriptListView: View {
Spacer()
if isDefault {
if showDefaultStar, isDefault {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
Button(role: .destructive) {
pendingDelete = script
showDeleteConfirmation = true
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
if showDelete {
Button(role: .destructive) {
pendingDelete = script
showDeleteConfirmation = true
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
}
.buttonStyle(.borderless)
}
.buttonStyle(.borderless)
}
.padding(20)
.background(glassyBackground)
@ -207,18 +228,20 @@ struct ScriptListView: View {
Button { copyPath(script) } label: {
Label("Copy Path", systemImage: "folder")
}
Button { saveDefaultScript(script) } label: {
Label("Set Default", systemImage: "star")
if !isPickerMode {
Button { saveDefaultScript(script) } label: {
Label("Set Default", systemImage: "star")
}
}
// (Delete removed from context menu)
}
}
private var emptyCard: some View {
VStack(spacing: 6) {
Label("No scripts found", systemImage: "doc.text.magnifyingglass")
Label(isPickerMode ? "No scripts available" : "No scripts found",
systemImage: "doc.text.magnifyingglass")
.font(.subheadline.weight(.semibold))
Text("Tap New or Import to get started.")
Text(isPickerMode ? "Import a file or choose None." : "Tap New or Import to get started.")
.font(.footnote)
.foregroundColor(.secondary)
}
@ -243,7 +266,7 @@ struct ScriptListView: View {
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("scripts")
var isDir: ObjCBool = false
var exists = FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDir)
let exists = FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDir)
do {
if exists && !isDir.boolValue {
try FileManager.default.removeItem(at: dir)
@ -311,7 +334,7 @@ struct ScriptListView: View {
DispatchQueue.global(qos: .userInitiated).async {
defer { DispatchQueue.main.async { self.isBusy = false } }
do {
let dest = scriptsDirectory().appendingPathComponent(fileURL.lastPathComponent)
let dest = self.scriptsDirectory().appendingPathComponent(fileURL.lastPathComponent)
if FileManager.default.fileExists(atPath: dest.path) {
try FileManager.default.removeItem(at: dest)
}
@ -375,11 +398,11 @@ private struct WideGlassyButton: View {
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity, alignment: .center) // center contents horizontally
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, 14)
}
.frame(height: 44)
.frame(maxWidth: .infinity) // make both buttons equal width
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(.ultraThinMaterial)

View file

@ -24,19 +24,16 @@ private func registerAdvancedOptionsDefault() {
struct WelcomeSheetView: View {
var onDismiss: (() -> Void)?
@Environment(\.colorScheme) private var colorScheme
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
private var accent: Color {
customAccentColorHex.isEmpty ? .accentColor : (Color(hex: customAccentColorHex) ?? .accentColor)
}
var body: some View {
ZStack {
// Subtle gradient background to match app style
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
// Background now comes from global BackgroundContainer
Color.clear.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
@ -57,7 +54,7 @@ struct WelcomeSheetView: View {
// App description
VStack(alignment: .leading, spacing: 6) {
Label("On‑device debugger", systemImage: "bolt.shield.fill")
.foregroundColor(.white)
.foregroundColor(accent)
.font(.headline)
Text("StikDebug is an on‑device debugger designed specifically for self‑developed apps. It helps streamline testing and troubleshooting without sending any data to external servers.")
.font(.callout)
@ -68,7 +65,7 @@ struct WelcomeSheetView: View {
// VPN explanation
VStack(alignment: .leading, spacing: 6) {
Label("Why VPN permission?", systemImage: "lock.shield.fill")
.foregroundColor(.white)
.foregroundColor(accent)
.font(.headline)
Text("The next step will prompt you to allow VPN permissions. This is necessary for the app to function properly. The VPN configuration allows your device to securely connect to itself — nothing more. No data is collected or sent externally; everything stays on your device.")
.font(.callout)
@ -80,12 +77,16 @@ struct WelcomeSheetView: View {
Button(action: { onDismiss?() }) {
Text("Continue")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.foregroundColor(.black)
.foregroundColor(accent.contrastText())
.frame(height: 44)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color.white)
.fill(accent)
)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.primary.opacity(0.15), lineWidth: 1)
)
}
.padding(.top, 8)
@ -97,11 +98,11 @@ struct WelcomeSheetView: View {
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
.strokeBorder(Color.primary.opacity(0.15), lineWidth: 1)
)
)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
.shadow(color: .black.opacity(colorScheme == .dark ? 0.15 : 0.08), radius: 12, x: 0, y: 4)
// Footer version info for consistency
HStack {
@ -117,7 +118,7 @@ struct WelcomeSheetView: View {
.padding(.vertical, 30)
}
}
.preferredColorScheme(.dark)
// Inherit preferredColorScheme from BackgroundContainer (no local override)
}
}
@ -322,7 +323,7 @@ class TunnelManager: ObservableObject {
// MARK: - AccentColor Environment Key (leave available but unused)
struct AccentColorKey: EnvironmentKey {
static let defaultValue: Color = .white
static let defaultValue: Color = .accentColor
}
extension EnvironmentValues {
@ -406,7 +407,7 @@ class DNSChecker: ObservableObject {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
group.notify(queue: .main) {
if self.controlIP == nil {
self.dnsError = "No internet connection."
print("Control host lookup failed, so no internet connection.")
@ -430,11 +431,7 @@ class DNSChecker: ObservableObject {
private func checkIfConnectedToWifi(completion: @escaping (Bool) -> Void) {
let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
completion(true)
} else {
completion(false)
}
completion(path.status == .satisfied)
monitor.cancel()
}
let queue = DispatchQueue.global(qos: .background)
@ -456,9 +453,7 @@ class DNSChecker: ObservableObject {
var res: UnsafeMutablePointer<addrinfo>?
let err = getaddrinfo(host, nil, &hints, &res)
if err != 0 {
DispatchQueue.main.async {
completion(nil)
}
DispatchQueue.main.async { completion(nil) }
return
}
@ -477,9 +472,7 @@ class DNSChecker: ObservableObject {
ptr = ptr?.pointee.ai_next
}
freeaddrinfo(res)
DispatchQueue.main.async {
completion(ipAddress)
}
DispatchQueue.main.async { completion(ipAddress) }
}
}
}
@ -492,6 +485,8 @@ var pubHeartBeat = false
@main
struct HeartbeatApp: App {
@AppStorage("hasLaunchedBefore") var hasLaunchedBefore: Bool = false
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@State private var showWelcomeSheet: Bool = false
@State private var isLoading2 = true
@State private var isPairing = false
@ -529,14 +524,15 @@ struct HeartbeatApp: App {
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
method_exchangeImplementations(origMethod, fixMethod)
// Force global dark mode for the entire app
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
window.overrideUserInterfaceStyle = .dark
}
// Ensure UIKit controls also use white tint globally
UIView.appearance().tintColor = .white
// Initialize UIKit tint from stored accent at launch
HeartbeatApp.updateUIKitTintFromStoredAccent()
}
// Make this static so we can call it without capturing self in init
private static func updateUIKitTintFromStoredAccent() {
let hex = UserDefaults.standard.string(forKey: "customAccentColor") ?? ""
let color = hex.isEmpty ? UIColor.tintColor : UIColor(Color(hex: hex) ?? .accentColor)
UIView.appearance().tintColor = color
}
func newVerCheck() {
@ -557,188 +553,195 @@ struct HeartbeatApp: App {
}
}
private var globalAccent: Color {
let hex = customAccentColorHex
return hex.isEmpty ? .accentColor : (Color(hex: hex) ?? .accentColor)
}
var body: some Scene {
WindowGroup {
Group {
if isLoading2 {
LoadingView(showAlert: $show_alert, alertTitle: $alert_title, alertMessage: $alert_string)
.onAppear {
dnsChecker.checkDNS()
timeoutTimer?.invalidate()
timeoutTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: false) { _ in
if isLoading2 {
showTimeoutError = true
BackgroundContainer {
Group {
if isLoading2 {
LoadingView(showAlert: $show_alert, alertTitle: $alert_title, alertMessage: $alert_string)
.onAppear {
dnsChecker.checkDNS()
timeoutTimer?.invalidate()
timeoutTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: false) { _ in
if isLoading2 {
showTimeoutError = true
}
}
}
checkVPNConnection() { result, vpn_error in
if result {
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if pubHeartBeat {
isLoading2 = false
timer.invalidate()
} else {
if let error {
if error == -9 { // InvalidHostID is -9
isPairing = true
} else {
startHeartbeatInBackground()
checkVPNConnection() { result, vpn_error in
if result {
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if pubHeartBeat {
isLoading2 = false
timer.invalidate()
} else {
if let error {
if error == -9 { // InvalidHostID is -9
isPairing = true
} else {
startHeartbeatInBackground()
}
self.error = nil
}
self.error = nil
}
}
}
startHeartbeatInBackground()
} else {
isLoading2 = false
}
} else if let vpn_error {
showAlert(title: "Error", message: "EM Proxy failed to connect: \(vpn_error)", showOk: true) { _ in
exit(0)
}
}
}
}
.fileImporter(
isPresented: $isPairing,
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 {
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
}
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!")
startHeartbeatInBackground()
} catch {
print("Error copying file: \(error)")
}
} else {
print("Source file does not exist.")
}
if accessing {
url.stopAccessingSecurityScopedResource()
}
case .failure(_):
print("Failed")
}
}
.overlay(
ZStack {
if showTimeoutError {
CustomErrorView(
title: "Connection Error",
message: "Check your connection and ensure your pairing file is valid and try again.",
onDismiss: {
showTimeoutError = false
},
showButton: true,
primaryButtonText: "Continue Anyway",
secondaryButtonText: "View Logs",
onPrimaryButtonTap: {
showContinueWarning = true
},
onSecondaryButtonTap: {
showLogs = true
},
showSecondaryButton: true
)
}
if showContinueWarning {
CustomErrorView(
title: "Proceeding Without Connection",
message: "StikDebug will not function as expected if you choose to continue.",
onDismiss: {
showContinueWarning = false
},
showButton: true,
primaryButtonText: "I Understand",
onPrimaryButtonTap: {
showContinueWarning = false
startHeartbeatInBackground()
} else {
isLoading2 = false
}
)
}
}
)
.sheet(isPresented: $showLogs, onDismiss: {
isLoading2 = false
}) {
ConsoleLogsView()
.preferredColorScheme(.dark)
}
} else {
MainTabView()
.onAppear {
// Force dark mode on main window as well
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
window.overrideUserInterfaceStyle = .dark
}
let fileManager = FileManager.default
for (index, urlString) in urls.enumerated() {
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index])
if !fileManager.fileExists(atPath: destinationURL.path) {
downloadFile(from: urlString, to: destinationURL) { result in
if (result != "") {
alert_title = "An Error has Occurred"
alert_string = "[Download DDI Error]: " + result
show_alert = true
} else if let vpn_error {
showAlert(title: "Error", message: "EM Proxy failed to connect: \(vpn_error)", showOk: true) { _ in
exit(0)
}
}
}
}
}
.overlay(
ZStack {
if show_alert {
CustomErrorView(
title: alert_title,
message: alert_string,
onDismiss: {
show_alert = false
},
showButton: true,
primaryButtonText: "OK"
)
.fileImporter(
isPresented: $isPairing,
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 {
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
}
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!")
startHeartbeatInBackground()
} catch {
print("Error copying file: \(error)")
}
} else {
print("Source file does not exist.")
}
if accessing {
url.stopAccessingSecurityScopedResource()
}
case .failure(_):
print("Failed")
}
}
)
.preferredColorScheme(.dark)
.overlay(
ZStack {
if showTimeoutError {
CustomErrorView(
title: "Connection Error",
message: "Check your connection and ensure your pairing file is valid and try again.",
onDismiss: {
showTimeoutError = false
},
showButton: true,
primaryButtonText: "Continue Anyway",
secondaryButtonText: "View Logs",
onPrimaryButtonTap: {
showContinueWarning = true
},
onSecondaryButtonTap: {
showLogs = true
},
showSecondaryButton: true
)
}
if showContinueWarning {
CustomErrorView(
title: "Proceeding Without Connection",
message: "StikDebug will not function as expected if you choose to continue.",
onDismiss: {
showContinueWarning = false
},
showButton: true,
primaryButtonText: "I Understand",
onPrimaryButtonTap: {
showContinueWarning = false
isLoading2 = false
}
)
}
}
)
.sheet(isPresented: $showLogs, onDismiss: {
isLoading2 = false
}) {
ConsoleLogsView()
}
} else {
MainTabView()
.onAppear {
let fileManager = FileManager.default
for (index, urlString) in urls.enumerated() {
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index])
if !fileManager.fileExists(atPath: destinationURL.path) {
downloadFile(from: urlString, to: destinationURL) { result in
if (result != "") {
alert_title = "An Error has Occurred"
alert_string = "[Download DDI Error]: " + result
show_alert = true
}
}
}
}
}
.overlay(
ZStack {
if show_alert {
CustomErrorView(
title: alert_title,
message: alert_string,
onDismiss: {
show_alert = false
},
showButton: true,
primaryButtonText: "OK"
)
}
}
)
}
}
// Apply global tint to all SwiftUI views in this window
.tint(globalAccent)
.onAppear {
// On first launch, present the welcome sheet.
// Otherwise, start the VPN automatically.
if !hasLaunchedBefore {
showWelcomeSheet = true
} else {
TunnelManager.shared.startVPN()
}
// Update UIKit tint now and subscribe to changes without capturing self
HeartbeatApp.updateUIKitTintFromStoredAccent()
NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { _ in
HeartbeatApp.updateUIKitTintFromStoredAccent()
}
}
.sheet(isPresented: $showWelcomeSheet) {
WelcomeSheetView {
// When the user taps "Continue", mark the app as launched and start the VPN.
hasLaunchedBefore = true
showWelcomeSheet = false
TunnelManager.shared.startVPN()
}
}
}
// Apply global white tint to all SwiftUI views in the window
.tint(Color.white)
.onAppear {
// On first launch, present the welcome sheet.
// Otherwise, start the VPN automatically.
if !hasLaunchedBefore {
showWelcomeSheet = true
} else {
TunnelManager.shared.startVPN()
}
}
.sheet(isPresented: $showWelcomeSheet) {
WelcomeSheetView {
// When the user taps "Continue", mark the app as launched and start the VPN.
hasLaunchedBefore = true
showWelcomeSheet = false
TunnelManager.shared.startVPN()
}
.preferredColorScheme(.dark)
}
.preferredColorScheme(.dark)
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
@ -804,7 +807,7 @@ actor FunctionGuard<T> {
func execute(_ work: @escaping @Sendable () -> T) async -> T {
if let task = runningTask {
return await task.value // Return existing task's result if running
return await task.value
}
let task = Task.detached { work() }
runningTask = task
@ -903,7 +906,6 @@ func startHeartbeatInBackground() {
} else {
print("Error: \(message ?? "") (Code: \(result))")
DispatchQueue.main.async {
// Special handling for InvalidHostID error (code -9)
if result == -9 {
do {
try FileManager.default.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
@ -951,32 +953,45 @@ struct LoadingView: View {
@State private var animate = false
@Environment(\.colorScheme) private var colorScheme
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
private var accentColor: Color {
if customAccentColorHex.isEmpty {
return .accentColor
} else {
return Color(hex: customAccentColorHex) ?? .accentColor
}
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
// Background now comes from global BackgroundContainer
Color.clear.ignoresSafeArea()
VStack {
ZStack {
Circle()
.stroke(lineWidth: 8)
.foregroundColor(Color.white.opacity(0.15))
.foregroundColor(accentColor.opacity(0.18))
.frame(width: 80, height: 80)
Circle()
.trim(from: 0, to: 0.7)
.stroke(AngularGradient(
gradient: Gradient(colors: [
Color.white.opacity(0.9),
Color.white.opacity(0.4)
]),
center: .center
), style: StrokeStyle(lineWidth: 6, lineCap: .round))
.stroke(
AngularGradient(
gradient: Gradient(colors: [
accentColor.opacity(0.95),
accentColor.opacity(0.45)
]),
center: .center
),
style: StrokeStyle(lineWidth: 6, lineCap: .round)
)
.rotationEffect(.degrees(animate ? 360 : 0))
.frame(width: 80, height: 80)
.animation(Animation.linear(duration: 1.2).repeatForever(autoreverses: false), value: animate)
}
.shadow(color: Color.white.opacity(0.25), radius: 10, x: 0, y: 0)
.shadow(color: accentColor.opacity(0.25), radius: 10, x: 0, y: 0)
.onAppear {
animate = true
let os = ProcessInfo.processInfo.operatingSystemVersion
@ -989,13 +1004,13 @@ struct LoadingView: View {
Text("Loading...")
.font(.system(size: 20, weight: .medium, design: .rounded))
.foregroundColor(.white.opacity(0.85))
.foregroundColor(.primary)
.padding(.top, 20)
.opacity(animate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: animate)
}
}
.preferredColorScheme(.dark)
// Inherit system color scheme
}
}
@ -1094,3 +1109,4 @@ func downloadFile(from urlString: String, to destinationURL: URL, completion: @e
task.resume()
completion("")
}

View file

@ -0,0 +1,283 @@
//
// AppTheme.swift
// StikJIT
//
// Created by Assistant on 9/12/25.
//
import SwiftUI
// MARK: - AppTheme model
enum AppTheme: String, CaseIterable, Identifiable {
case system // follows system appearance; static gradient
case darkStatic // dark + static gradient
case neonAnimated // animated gradient
case blobs // morphing blobs
case particles // subtle particle field
var id: String { rawValue }
var displayName: String {
switch self {
case .system: return "System"
case .darkStatic: return "Dark"
case .neonAnimated: return "Neon"
case .blobs: return "Blobs"
case .particles: return "Particles"
}
}
// If you want the theme to force a color scheme, set it here.
// Your app currently forces dark globally; we keep nil so nothing fights it.
var preferredColorScheme: ColorScheme? {
switch self {
case .system: return nil
case .darkStatic: return .dark
case .neonAnimated: return .dark
case .blobs: return .dark
case .particles: return .dark
}
}
var backgroundStyle: BackgroundStyle {
switch self {
case .system: return .staticGradient
case .darkStatic: return .staticGradient
case .neonAnimated: return .animatedGradient
case .blobs: return .blobs
case .particles: return .particles
}
}
}
// MARK: - Background styles and factory
enum BackgroundStyle {
case staticGradient
case animatedGradient
case blobs
case particles
}
// Slightly brighter, clearly-not-black static background that still respects dark mode
private func staticBackgroundGradient() -> LinearGradient {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
struct ThemedBackground: View {
let style: BackgroundStyle
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
Group {
switch style {
case .staticGradient:
staticBackgroundGradient()
.ignoresSafeArea()
case .animatedGradient:
if reduceMotion {
staticBackgroundGradient()
.ignoresSafeArea()
} else {
AnimatedGradientBackground()
}
case .blobs:
if reduceMotion {
staticBackgroundGradient()
.ignoresSafeArea()
} else {
BlobBackground()
}
case .particles:
if reduceMotion {
staticBackgroundGradient()
.ignoresSafeArea()
} else {
ParticleFieldBackground()
}
}
}
}
}
// MARK: - Background container to use at app root
struct BackgroundContainer<Content: View>: View {
@AppStorage("appTheme") private var rawTheme: String = AppTheme.system.rawValue
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
private var theme: AppTheme {
AppTheme(rawValue: rawTheme) ?? .system
}
var body: some View {
ZStack {
ThemedBackground(style: theme.backgroundStyle)
.ignoresSafeArea()
content
}
.preferredColorScheme(theme.preferredColorScheme) // nil means no override
}
}
// MARK: - Animated backgrounds
private struct AnimatedGradientBackground: View {
@State private var t: Double = 0
var body: some View {
TimelineView(.animation) { timeline in
let now = timeline.date.timeIntervalSinceReferenceDate
let speed = 0.15
let phase = now * speed
let c1 = Color(hue: (sin(phase) * 0.5 + 0.5), saturation: 0.55, brightness: 0.45)
let c2 = Color(hue: (sin(phase + .pi * 0.66) * 0.5 + 0.5), saturation: 0.75, brightness: 0.35)
let c3 = Color(hue: (sin(phase + .pi * 1.33) * 0.5 + 0.5), saturation: 0.65, brightness: 0.30)
let angle = Angle(degrees: (sin(phase * 0.7) * 45) + 45)
AngularGradient(colors: [c1, c2, c3, c1], center: .center, angle: angle)
.saturation(1.0)
.brightness(0.0)
.opacity(0.9)
.ignoresSafeArea()
.overlay(
LinearGradient(colors: [.black.opacity(0.25), .clear],
startPoint: .top,
endPoint: .bottom)
.ignoresSafeArea()
)
}
}
}
private struct BlobBackground: View {
@State private var t: CGFloat = 0
var body: some View {
Canvas { ctx, size in
let baseColors: [Color] = [
.blue.opacity(0.35),
.purple.opacity(0.35),
.pink.opacity(0.35),
.cyan.opacity(0.35)
]
let blobs = 6
let radius = min(size.width, size.height) * 0.45
for i in 0..<blobs {
let p = CGFloat(i) / CGFloat(blobs)
let angle = t * (0.5 + 0.1 * CGFloat(i)) + p * .pi * 2
let r = radius * (0.4 + 0.2 * sin(t + CGFloat(i)))
let x = size.width * 0.5 + cos(angle) * r
let y = size.height * 0.5 + sin(angle * 0.9) * r * 0.7
var path = Path()
path.addEllipse(in: CGRect(x: x - 140, y: y - 140, width: 280, height: 280))
ctx.addFilter(.blur(radius: 40))
ctx.fill(path, with: .color(baseColors[i % baseColors.count]))
}
}
.background(staticBackgroundGradient())
.ignoresSafeArea()
.onAppear {
withAnimation(.linear(duration: 12).repeatForever(autoreverses: false)) {
t = .pi * 2
}
}
}
}
private struct ParticleFieldBackground: View {
struct Particle: Identifiable {
let id = UUID()
var position: CGPoint
var velocity: CGVector
var size: CGFloat
var opacity: Double
}
@State private var particles: [Particle] = []
private let count = 120
var body: some View {
GeometryReader { geo in
TimelineView(.animation) { _ in
ZStack {
staticBackgroundGradient()
.ignoresSafeArea()
Canvas { ctx, size in
for p in particles {
var circle = Path(ellipseIn: CGRect(x: p.position.x,
y: p.position.y,
width: p.size,
height: p.size))
ctx.addFilter(.blur(radius: 1.2))
ctx.opacity = p.opacity
ctx.fill(circle, with: .color(.white.opacity(0.85)))
}
}
}
.onAppear {
if particles.isEmpty {
particles = (0..<count).map { _ in
Particle(
position: CGPoint(x: .random(in: 0...geo.size.width),
y: .random(in: 0...geo.size.height)),
velocity: CGVector(dx: .random(in: -0.3...0.3),
dy: .random(in: -0.3...0.3)),
size: .random(in: 1.5...3.5),
opacity: .random(in: 0.15...0.45)
)
}
}
}
.onChange(of: geo.size) { _, newSize in
particles = particles.map { p in
var np = p
np.position.x = min(max(0, np.position.x), newSize.width)
np.position.y = min(max(0, np.position.y), newSize.height)
return np
}
}
.task {
while true {
try? await Task.sleep(nanoseconds: 16_000_000)
var next = particles
for i in next.indices {
var p = next[i]
p.position.x += p.velocity.dx
p.position.y += p.velocity.dy
if p.position.x < 0 { p.position.x = geo.size.width }
if p.position.x > geo.size.width { p.position.x = 0 }
if p.position.y < 0 { p.position.y = geo.size.height }
if p.position.y > geo.size.height { p.position.y = 0 }
next[i] = p
}
particles = next
}
}
}
}
.ignoresSafeArea()
}
}

View file

@ -13,12 +13,6 @@ extension FileManager {
}
}
public extension ProcessInfo {
var hasTXM: Bool {
{ if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 } else { return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 }) ?? false } }()
}
}
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)

View file

@ -14,534 +14,283 @@ struct ConsoleLogsView: View {
@Environment(\.accentColor) private var environmentAccentColor
@StateObject private var logManager = LogManager.shared
@State private var autoScroll = true
@State private var paused = false
@State private var scrollView: ScrollViewProxy? = nil
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
// Alert handling
@State private var showingCustomAlert = false
@State private var alertMessage = ""
@State private var alertTitle = ""
@State private var isError = false
// Timer to check for log updates
@State private var logCheckTimer: Timer? = nil
// Track if the view is active (visible)
@State private var isViewActive = false
@State private var lastProcessedLineCount = 0
@State private var isLoadingLogs = false
@State private var isAtBottom = true
// Search
@State private var searchText = ""
// Filter by level
enum LevelFilter: String, CaseIterable, Identifiable {
case all = "ALL"
case info = "INFO"
case warning = "WARNING"
case error = "ERROR"
case debug = "DEBUG"
var id: String { rawValue }
}
@State private var level: LevelFilter = .all
// Gutter toggle (line numbers)
@State private var showGutter = false
// Share / export helpers
@State private var showShareSheet = false
@State private var shareItems: [Any] = []
// Copy toast
@State private var justCopied = false
// Blinking cursor
@State private var showCursor = true
@State private var cursorTimer: Timer? = nil
// Fixed terminal viewport height (taller)
private let terminalHeight: CGFloat = 520
private var accentColor: Color {
if customAccentColorHex.isEmpty {
return .white
return .blue
} else {
return Color(hex: customAccentColorHex) ?? .white
}
}
private var filteredLogs: [LogManager.LogEntry] {
let base = logManager.logs.filter { entry in
switch level {
case .all: return true
case .info: return entry.type == .info
case .warning: return entry.type == .warning
case .error: return entry.type == .error
case .debug: return entry.type == .debug
}
}
guard !searchText.isEmpty else { return base }
return base.filter {
$0.message.localizedCaseInsensitiveContains(searchText)
|| $0.type.rawValue.localizedCaseInsensitiveContains(searchText)
return Color(hex: customAccentColorHex) ?? .blue
}
}
var body: some View {
NavigationStack {
NavigationView {
ZStack {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
Color(colorScheme == .dark ? .black : .white)
.edgesIgnoringSafeArea(.all)
ScrollView {
VStack(spacing: 20) {
consoleCard
footerInfo
}
.padding(.horizontal, 20)
.padding(.vertical, 30)
}
.sheet(isPresented: $showShareSheet) {
ActivityViewController(items: shareItems)
}
if isLoadingLogs {
Color.black.opacity(0.2).ignoresSafeArea()
ProgressView("Loading logs…")
.padding(16)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
)
)
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
}
if showingCustomAlert {
CustomErrorView(
title: alertTitle,
message: alertMessage,
onDismiss: { showingCustomAlert = false },
showButton: true,
primaryButtonText: "OK",
messageType: isError ? .error : .success
)
}
if justCopied {
VStack {
Spacer()
Text("Copied")
.font(.footnote.weight(.semibold))
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(.ultraThinMaterial, in: Capsule())
.overlay(Capsule().strokeBorder(Color.white.opacity(0.15), lineWidth: 1))
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 3)
.transition(.move(edge: .bottom).combined(with: .opacity))
.padding(.bottom, 30)
}
.animation(.easeInOut(duration: 0.25), value: justCopied)
}
}
.navigationTitle("Console")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
dismiss()
} label: {
Label("Exit", systemImage: "chevron.left")
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
Task { await loadIdeviceLogsAsync() }
} label: {
Image(systemName: "arrow.clockwise")
}
Button {
exportLogs()
} label: {
Image(systemName: "square.and.arrow.up")
}
Menu {
Button {
copyLogsToPasteboard()
} label: {
Label("Copy All", systemImage: "doc.on.doc")
VStack(spacing: 0) {
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 4) {
Text("=== DEVICE INFORMATION ===")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(colorScheme == .dark ? .white : .black)
.padding(.vertical, 4)
Text("iOS Version: \(UIDevice.current.systemVersion)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(colorScheme == .dark ? .white : .black)
Text("Device: \(UIDevice.current.name)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(colorScheme == .dark ? .white : .black)
Text("Model: \(UIDevice.current.model)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(colorScheme == .dark ? .white : .black)
Text("=== LOG ENTRIES ===")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(colorScheme == .dark ? .white : .black)
.padding(.vertical, 4)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 4)
ForEach(logManager.logs) { logEntry in
Text(AttributedString(createLogAttributedString(logEntry)))
.font(.system(size: 11, design: .monospaced))
.textSelection(.enabled)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 1)
.padding(.horizontal, 4)
.id(logEntry.id)
}
}
.background(
GeometryReader { geometry in
Color.clear.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scroll")).minY
)
}
)
}
Button {
shareAll()
} label: {
Label("Share…", systemImage: "square.and.arrow.up.on.square")
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
isAtBottom = offset > -20
}
Divider()
Button {
showGutter.toggle()
} label: {
Label(showGutter ? "Hide Line Numbers" : "Show Line Numbers", systemImage: "number")
.onChange(of: logManager.logs.count) { _ in
if isAtBottom {
withAnimation {
if let lastLog = logManager.logs.last {
proxy.scrollTo(lastLog.id, anchor: .bottom)
}
}
}
}
Divider()
Button(role: .destructive) {
logManager.clearLogs()
} label: {
Label("Clear Logs", systemImage: "trash")
.onAppear {
scrollView = proxy
isViewActive = true
Task {
await loadIdeviceLogsAsync()
}
startLogCheckTimer()
}
.onDisappear {
isViewActive = false
stopLogCheckTimer()
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
.onAppear {
isViewActive = true
Task { await loadIdeviceLogsAsync() }
startLogCheckTimer()
startCursorBlink()
}
.onDisappear {
isViewActive = false
stopLogCheckTimer()
stopCursorBlink()
}
}
}
// MARK: - Console UI
private var consoleCard: some View {
VStack(alignment: .leading, spacing: 0) {
controlStrip
searchField
consoleCanvas
promptBar
}
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
)
)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
}
// Top strip: icon-only controls to avoid smushing
private var controlStrip: some View {
HStack(spacing: 10) {
// Chips in a horizontal scroller to prevent wrapping
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
levelFilterChip(.all)
levelFilterChip(.info)
levelFilterChip(.warning)
levelFilterChip(.error)
levelFilterChip(.debug)
}
.padding(.vertical, 2)
}
.frame(maxWidth: .infinity)
// Right-side icon-only controls
HStack(spacing: 12) {
// Pause/Resume
Button {
paused.toggle()
} label: {
Image(systemName: paused ? "play.fill" : "pause.fill")
.font(.system(size: 14, weight: .bold))
.foregroundColor(paused ? .green : .orange)
.padding(8)
.background(
Circle()
.fill((paused ? Color.green : Color.orange).opacity(0.18))
)
.overlay(
Circle()
.stroke((paused ? Color.green : Color.orange).opacity(0.45), lineWidth: 0.8)
)
}
.accessibilityLabel(paused ? "Resume" : "Pause")
.accessibilityHint("Toggle pausing of live output")
.buttonStyle(.plain)
// Auto-scroll toggle (icon-only)
Button {
autoScroll.toggle()
} label: {
Image(systemName: autoScroll ? "arrow.down.circle.fill" : "arrow.down.circle")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(autoScroll ? accentColor : .secondary)
}
.accessibilityLabel("Auto Scroll")
.accessibilityValue(autoScroll ? "On" : "Off")
.buttonStyle(.plain)
// Clear
Button {
logManager.clearLogs()
} label: {
Image(systemName: "trash")
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.secondary)
}
.accessibilityLabel("Clear Logs")
.buttonStyle(.plain)
}
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color(UIColor.tertiarySystemBackground))
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
)
)
.padding(.horizontal, 12)
.padding(.top, 12)
}
private var searchField: some View {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass").foregroundColor(.secondary)
TextField("Filter text…", text: $searchText)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.font(.caption.monospaced())
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(UIColor.tertiarySystemBackground))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
)
)
.padding(.horizontal, 12)
.padding(.top, 10)
.padding(.bottom, 8)
}
// Fixed-size, scrollable terminal viewport with auto-scroll
private var consoleCanvas: some View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
consolePreface
ForEach(Array(filteredLogs.indices), id: \.self) { idx in
let entry = filteredLogs[idx]
ConsoleLineRow(
index: idx,
entry: entry,
showGutter: showGutter,
attributed: makeAttributed(entry)
)
.id(entry.id)
}
CursorLine(showGutter: showGutter, showCursor: showCursor)
.opacity(paused ? 0.5 : 1.0)
}
.background(
GeometryReader { geometry in
Color.clear.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("consoleScroll")).minY
)
Spacer(minLength: 0)
HStack(spacing: 12) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.imageScale(.small)
.foregroundColor(.red)
Text("\(logManager.errorCount) Errors")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer(minLength: 8)
Button {
var logsContent = "=== DEVICE INFORMATION ===\n"
logsContent += "Version: \(UIDevice.current.systemVersion)\n"
logsContent += "Name: \(UIDevice.current.name)\n"
logsContent += "Model: \(UIDevice.current.model)\n"
logsContent += "StikJIT Version: App Version: 1.0\n\n"
logsContent += "=== LOG ENTRIES ===\n"
logsContent += logManager.logs.map {
"[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)"
}.joined(separator: "\n")
UIPasteboard.general.string = logsContent
alertTitle = "Logs Copied"
alertMessage = "Logs have been copied to clipboard."
isError = false
showingCustomAlert = true
} label: {
HStack(spacing: 6) {
Image(systemName: "doc.on.doc")
Text("Copy")
}
.foregroundColor(accentColor)
}
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
exportControl
}
)
}
.frame(height: terminalHeight)
.coordinateSpace(name: "consoleScroll")
.background(consoleBackground)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.padding(.horizontal, 12)
.padding(.bottom, 10)
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
isAtBottom = offset > -20
}
.onChange(of: logManager.logs.count) { _ in
guard autoScroll && !paused else { return }
if isAtBottom {
withAnimation {
if let last = filteredLogs.last {
proxy.scrollTo(last.id, anchor: .bottom)
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Console Logs")
.font(.headline)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: { dismiss() }) {
HStack(spacing: 2) {
Text("Exit")
.fontWeight(.regular)
}
.foregroundColor(accentColor)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button(action: {
Task { await loadIdeviceLogsAsync() }
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(accentColor)
}
Button(action: {
logManager.clearLogs()
}) {
Text("Clear")
.foregroundColor(accentColor)
}
}
}
}
}
.onAppear {
scrollView = proxy
}
}
}
private var promptBar: some View {
HStack(spacing: 8) {
Text("\(UIDevice.current.name.replacingOccurrences(of: " ", with: "")):\u{007E}$")
.font(.system(size: 12, weight: .bold, design: .monospaced))
.foregroundColor(accentColor)
Text(paused ? "Paused — output suppressed" : "Read-only console")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(.secondary)
Spacer()
Button {
copyLogsToPasteboard()
} label: {
Label("Copy", systemImage: "doc.on.doc")
.labelStyle(.iconOnly)
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.tertiarySystemBackground))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
)
)
.padding(.horizontal, 12)
.padding(.bottom, 12)
}
private var consoleBackground: some View {
ZStack {
Color.black
LinearGradient(
colors: [
Color.white.opacity(0.03),
.clear
],
startPoint: .top,
endPoint: .bottom
)
}
}
private var consolePreface: some View {
VStack(alignment: .leading, spacing: 0) {
consoleLine("# === DEVICE INFORMATION ===", color: .secondary, bold: true)
consoleLine("# iOS Version: \(UIDevice.current.systemVersion)", color: .secondary)
consoleLine("# Device: \(UIDevice.current.name)", color: .secondary)
consoleLine("# Model: \(UIDevice.current.model)", color: .secondary)
consoleLine("# === LOG ENTRIES ===", color: .secondary, bold: true)
}
.padding(.top, 8)
}
private func levelFilterChip(_ f: LevelFilter) -> some View {
let selected = level == f
return Button {
level = f
} label: {
HStack(spacing: 6) {
if f != .all {
Circle()
.fill(levelColor(f).opacity(0.9))
.frame(width: 8, height: 8)
}
Text(f.rawValue)
.font(.caption.monospaced())
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
Capsule(style: .continuous)
.fill(selected ? accentColor.opacity(0.2) : Color(UIColor.tertiarySystemBackground))
)
.overlay(
Capsule(style: .continuous)
.stroke(selected ? accentColor.opacity(0.6) : Color.white.opacity(0.08), lineWidth: 0.8)
Group {
if showingCustomAlert {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
.overlay(
CustomErrorView(
title: alertTitle,
message: alertMessage,
onDismiss: {
showingCustomAlert = false
},
showButton: true,
primaryButtonText: "OK",
messageType: isError ? .error : .success
)
)
}
}
)
.foregroundColor(selected ? accentColor : .primary)
}
.buttonStyle(.plain)
}
private func levelColor(_ f: LevelFilter) -> Color {
switch f {
case .all: return .gray
case .info: return .green
case .warning: return .orange
case .error: return .red
case .debug: return accentColor
}
}
private func consoleLine(_ text: String, color: Color = .secondary, bold: Bool = false) -> some View {
Text(text)
.font(.system(size: 12, weight: bold ? .semibold : .regular, design: .monospaced))
.foregroundColor(colorScheme == .dark ? color.opacity(0.8) : color.opacity(0.9))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 2)
.padding(.horizontal, 10)
}
private var footerInfo: some View {
HStack {
Spacer()
Text("iOS \(UIDevice.current.systemVersion)")
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
@ViewBuilder
private var exportControl: some View {
let logURL: URL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
if FileManager.default.fileExists(atPath: logURL.path) {
ShareLink(
item: logURL,
preview: SharePreview("idevice_log.txt", image: Image(systemName: "doc.text"))
) {
HStack(spacing: 6) {
Image(systemName: "square.and.arrow.up")
Text("Export")
}
.foregroundColor(accentColor)
}
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
} else {
Button {
alertTitle = "Export Failed"
alertMessage = "No idevice logs found"
isError = true
showingCustomAlert = true
} label: {
HStack(spacing: 6) {
Image(systemName: "square.and.arrow.up")
Text("Export")
}
.foregroundColor(accentColor)
}
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
}
.padding(.top, 6)
}
// MARK: - Attributed log helpers
private func makeAttributed(_ entry: LogManager.LogEntry) -> AttributedString {
let ns = createTerminalAttributedString(entry)
return AttributedString(ns)
}
private func createTerminalAttributedString(_ logEntry: LogManager.LogEntry) -> NSAttributedString {
let full = NSMutableAttributedString()
private func createLogAttributedString(_ logEntry: LogManager.LogEntry) -> NSAttributedString {
let fullString = NSMutableAttributedString()
// Timestamp (dim)
let ts = "[\(formatTime(date: logEntry.timestamp))]"
full.append(NSAttributedString(
string: ts + " ",
attributes: [
.foregroundColor: UIColor(white: 1.0, alpha: 0.45)
]
))
let timestampString = "[\(formatTime(date: logEntry.timestamp))]"
let timestampAttr = NSAttributedString(
string: timestampString,
attributes: [.foregroundColor: colorScheme == .dark ? UIColor.gray : UIColor.darkGray]
)
fullString.append(timestampAttr)
fullString.append(NSAttributedString(string: " "))
// Type (colored)
let typeString = "[\(logEntry.type.rawValue)]"
let typeColor = UIColor(colorForLogType(logEntry.type))
full.append(NSAttributedString(
string: "[\(logEntry.type.rawValue)] ",
let typeAttr = NSAttributedString(
string: typeString,
attributes: [.foregroundColor: typeColor]
))
)
fullString.append(typeAttr)
fullString.append(NSAttributedString(string: " "))
// Message (bright)
full.append(NSAttributedString(
let messageAttr = NSAttributedString(
string: logEntry.message,
attributes: [.foregroundColor: UIColor.white]
))
attributes: [.foregroundColor: colorScheme == .dark ? UIColor.white : UIColor.black]
)
fullString.append(messageAttr)
return full
return fullString
}
private func timeString() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter.string(from: Date())
}
private func formatTime(date: Date) -> String {
@ -563,8 +312,6 @@ struct ConsoleLogsView: View {
}
}
// MARK: - File loading and timer
private func loadIdeviceLogsAsync() async {
guard !isLoadingLogs else { return }
isLoadingLogs = true
@ -583,10 +330,10 @@ struct ConsoleLogsView: View {
let logContent = try String(contentsOfFile: logPath, encoding: .utf8)
let lines = logContent.components(separatedBy: .newlines)
// Only last 500 lines
let maxLines = 500
let startIndex = max(0, lines.count - maxLines)
let recentLines = Array(lines[startIndex..<lines.count])
lastProcessedLineCount = lines.count
await MainActor.run {
@ -594,11 +341,12 @@ struct ConsoleLogsView: View {
for line in recentLines {
if line.isEmpty { continue }
if line.contains("=== DEVICE INFORMATION ===") ||
line.contains("Version:") ||
line.contains("Name:") ||
line.contains("Model:") ||
line.contains("=== LOG ENTRIES ===") {
line.contains("Version:") ||
line.contains("Name:") ||
line.contains("Model:") ||
line.contains("=== LOG ENTRIES ===") {
continue
}
@ -626,8 +374,10 @@ struct ConsoleLogsView: View {
private func startLogCheckTimer() {
logCheckTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in
if isViewActive && !paused {
Task { await checkForNewLogs() }
if isViewActive {
Task {
await checkForNewLogs()
}
}
}
}
@ -654,6 +404,7 @@ struct ConsoleLogsView: View {
await MainActor.run {
for line in newLines {
if line.isEmpty { continue }
if line.contains("ERROR") || line.contains("Error") {
logManager.addErrorLog(line)
} else if line.contains("WARNING") || line.contains("Warning") {
@ -665,7 +416,6 @@ struct ConsoleLogsView: View {
}
}
// Keep only the last 500 lines
let maxLines = 500
if logManager.logs.count > maxLines {
let excessCount = logManager.logs.count - maxLines
@ -686,121 +436,22 @@ struct ConsoleLogsView: View {
logCheckTimer?.invalidate()
logCheckTimer = nil
}
private func startCursorBlink() {
stopCursorBlink()
cursorTimer = Timer.scheduledTimer(withTimeInterval: 0.6, repeats: true) { _ in
withAnimation(.easeInOut(duration: 0.15)) {
showCursor.toggle()
}
}
}
private func stopCursorBlink() {
cursorTimer?.invalidate()
cursorTimer = nil
showCursor = true
}
// MARK: - Actions (export/copy/share)
private func allLogsText() -> String {
var logsContent = "=== DEVICE INFORMATION ===\n"
logsContent += "Version: \(UIDevice.current.systemVersion)\n"
logsContent += "Name: \(UIDevice.current.name)\n"
logsContent += "Model: \(UIDevice.current.model)\n"
logsContent += "StikJIT Version: App Version: 1.0\n\n"
logsContent += "=== LOG ENTRIES ===\n"
logsContent += filteredLogs.map {
"[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)"
}.joined(separator: "\n")
return logsContent
}
private func copyLogsToPasteboard() {
UIPasteboard.general.string = allLogsText()
let gen = UINotificationFeedbackGenerator()
gen.notificationOccurred(.success)
showCopiedToast()
}
private func shareAll() {
shareItems = [allLogsText()]
showShareSheet = true
}
private func exportLogs() {
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
if FileManager.default.fileExists(atPath: logPath.path) {
shareItems = [logPath]
showShareSheet = true
} else {
alertTitle = "Export Failed"
alertMessage = "No idevice logs found"
isError = true
showingCustomAlert = true
}
}
private func showCopiedToast() {
withAnimation { justCopied = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation { justCopied = false }
}
}
}
private struct ConsoleLineRow: View {
let index: Int
let entry: LogManager.LogEntry
let showGutter: Bool
let attributed: AttributedString
var body: some View {
HStack(alignment: .top, spacing: 8) {
if showGutter {
Text(String(format: "%5d", index + 1))
.font(.system(size: 11, weight: .regular, design: .monospaced))
.foregroundColor(.white.opacity(0.35))
.frame(width: 44, alignment: .trailing)
}
Text(attributed)
.font(.system(size: 12, weight: .regular, design: .monospaced))
.textSelection(.enabled)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.vertical, 2)
.padding(.horizontal, 10)
}
}
private struct CursorLine: View {
let showGutter: Bool
let showCursor: Bool
var body: some View {
HStack(spacing: 6) {
if showGutter {
Text(" ")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.clear)
.frame(width: 44, alignment: .trailing)
}
Text(" ")
.font(.system(size: 12, design: .monospaced))
.overlay(
Rectangle()
.fill(Color.white.opacity(showCursor ? 0.9 : 0.0))
.frame(width: 8, height: 14)
.offset(x: 0, y: 1),
alignment: .leading
)
.frame(height: 16)
}
.padding(.vertical, 2)
.padding(.horizontal, 10)
private struct GlassOvalButtonStyle: ButtonStyle {
var height: CGFloat = 36
var strokeOpacity: Double = 0.16
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(.horizontal, 14)
.frame(height: height)
.background(.ultraThinMaterial, in: Capsule())
.overlay(
Capsule()
.stroke(.white.opacity(strokeOpacity), lineWidth: 1)
)
.shadow(color: .black.opacity(0.08), radius: configuration.isPressed ? 4 : 10, x: 0, y: configuration.isPressed ? 1 : 4)
.scaleEffect(configuration.isPressed ? 0.98 : 1)
}
}
@ -816,13 +467,3 @@ struct ScrollOffsetPreferenceKey: PreferenceKey {
value = nextValue()
}
}
// MARK: - UIKit Share Sheet wrapper
private struct ActivityViewController: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}

View file

@ -41,7 +41,6 @@ struct CustomErrorView: View {
var body: some View {
ZStack {
// Background overlay
Color.black.opacity(0.6)
.edgesIgnoringSafeArea(.all)
.opacity(opacity)
@ -51,9 +50,7 @@ struct CustomErrorView: View {
}
}
// Card itself
VStack(spacing: 12) {
// Icon based on messageType
switch messageType {
case .error:
Image(systemName: "exclamationmark.triangle.fill")
@ -72,19 +69,16 @@ struct CustomErrorView: View {
.padding(.top, 8)
}
// Title - slightly smaller
Text(title)
.font(.system(size: 18, weight: .bold, design: .rounded))
.foregroundColor(colorScheme == .dark ? .white : .black)
.multilineTextAlignment(.center)
// Divider
Rectangle()
.frame(height: 1)
.foregroundColor(colorScheme == .dark ? .white.opacity(0.2) : .black.opacity(0.2))
.padding(.horizontal, 12)
// Message
Text(LocalizedStringKey(message))
.font(.system(size: 15, design: .rounded))
.foregroundColor(colorScheme == .dark ? .white.opacity(0.9) : .black.opacity(0.9))
@ -93,12 +87,11 @@ struct CustomErrorView: View {
.padding(.horizontal, 12)
// Dismiss button (only shown when showButton is true)
if showButton {
VStack(spacing: 6) {
Button(action: {
dismissWithAnimation()
onPrimaryButtonTap?() // Call primary action if provided
onPrimaryButtonTap?()
}) {
Text(primaryButtonText)
.font(.system(size: 16, weight: .semibold, design: .rounded))
@ -114,7 +107,7 @@ struct CustomErrorView: View {
if showSecondaryButton {
Button(action: {
dismissWithAnimation()
onSecondaryButtonTap?() // Call secondary action if provided
onSecondaryButtonTap?()
}) {
Text(secondaryButtonText)
.font(.system(size: 16, weight: .medium, design: .rounded))
@ -136,7 +129,6 @@ struct CustomErrorView: View {
.frame(height: 12)
}
}
// Made the card smaller overall
.frame(width: min(UIScreen.main.bounds.width - 80, 300))
.background(
RoundedRectangle(cornerRadius: 16)

View file

@ -145,25 +145,19 @@ struct DeviceInfoView: View {
|| $0.value.localizedCaseInsensitiveContains(searchText)
}
}
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
private var currentTheme: AppTheme { AppTheme(rawValue: appThemeRaw) ?? .system }
var body: some View {
NavigationStack {
ZStack {
// Subtle depth but same color scheme
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ThemedBackground(style: currentTheme.backgroundStyle)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
infoCard
versionInfo
}
.padding(.horizontal, 20)
.padding(.vertical, 30)
@ -272,7 +266,6 @@ struct DeviceInfoView: View {
private var infoCard: some View {
VStack(alignment: .leading, spacing: 14) {
// Glassy search field
TextField("Search device info…", text: $searchText)
.padding(12)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
@ -324,17 +317,6 @@ struct DeviceInfoView: View {
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
}
private var versionInfo: some View {
HStack {
Spacer()
Text("iOS \(UIDevice.current.systemVersion)")
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
}
.padding(.top, 6)
}
// MARK: - Copy / Share helpers
private func allAsText() -> String {

View file

@ -12,14 +12,14 @@ struct AccentColorPicker: View {
@Binding var selectedColor: Color
let colors: [Color] = [
.blue, // Default system blue
.init(hex: "#7FFFD4")!, // Aqua
.init(hex: "#50C878")!, // Green
.red, // Red
.init(hex: "#6A5ACD")!, // Purple
.init(hex: "#DA70D6")!, // Pink
.white, // White
.black // Black
.blue,
.init(hex: "#7FFFD4")!,
.init(hex: "#50C878")!,
.red,
.init(hex: "#6A5ACD")!,
.init(hex: "#DA70D6")!,
.white,
.black
]
var body: some View {
@ -44,7 +44,6 @@ struct AccentColorPicker: View {
}
}
// Direct Color Picker Circle
ColorPicker("", selection: $selectedColor)
.labelsHidden()
.frame(width: 28, height: 28)
@ -73,38 +72,46 @@ struct AccentColorPicker: View {
struct DisplayView: View {
@AppStorage("username") private var username = "User"
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
@AppStorage("appTheme") private var appTheme: String = "system"
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
@State private var selectedAccentColor: Color = .blue
@State private var selectedAccentColor: Color = .white
@Environment(\.colorScheme) private var colorScheme
@State private var justSaved = false
private var accentColor: Color {
if customAccentColorHex.isEmpty {
return .blue
return .white
} else {
return Color(hex: customAccentColorHex) ?? .blue
return Color(hex: customAccentColorHex) ?? .white
}
}
private var selectedTheme: AppTheme {
get { AppTheme(rawValue: appThemeRaw) ?? .system }
set { appThemeRaw = newValue.rawValue }
}
private var hardcodedGradient: some View {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
var body: some View {
NavigationStack {
ZStack {
// Background gradient (glassy style)
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
hardcodedGradient.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
usernameCard
accentCard
jitOptionsCard
}
.padding(.horizontal, 20)
@ -130,9 +137,10 @@ struct DisplayView: View {
.navigationTitle("Display")
.onAppear {
loadCustomAccentColor()
applyTheme(appTheme)
applyTheme(selectedTheme)
}
}
.tint(selectedAccentColor)
}
// MARK: - Cards
@ -180,6 +188,70 @@ struct DisplayView: View {
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
}
private var accentCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Accent")
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.primary)
AccentColorPicker(selectedColor: $selectedAccentColor)
HStack(spacing: 12) {
Button {
if let hex = selectedAccentColor.toHex() {
customAccentColorHex = hex
} else {
customAccentColorHex = ""
}
showSavedToast()
} label: {
HStack {
Image(systemName: "checkmark.circle.fill")
Text("Save")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(selectedAccentColor)
)
.foregroundColor(selectedAccentColor.contrastText())
}
Button {
customAccentColorHex = ""
selectedAccentColor = .white
showSavedToast()
} label: {
HStack {
Image(systemName: "arrow.uturn.backward.circle")
Text("Reset")
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.tertiarySystemBackground))
)
}
}
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
)
)
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
.onChange(of: selectedAccentColor) { _, _ in
}
}
private var jitOptionsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("App List")
@ -215,21 +287,15 @@ struct DisplayView: View {
.fontWeight(.semibold)
.foregroundColor(.primary)
HStack(spacing: 12) {
ThemeOptionButton(title: "System", imageName: "theme-system", isSelected: appTheme == "system", accentColor: accentColor) {
appTheme = "system"
applyTheme(appTheme)
showSavedToast()
}
ThemeOptionButton(title: "Light", imageName: "theme-light", isSelected: appTheme == "light", accentColor: accentColor) {
appTheme = "light"
applyTheme(appTheme)
showSavedToast()
}
ThemeOptionButton(title: "Dark", imageName: "theme-dark", isSelected: appTheme == "dark", accentColor: accentColor) {
appTheme = "dark"
applyTheme(appTheme)
showSavedToast()
// Grid of theme previews
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
ForEach(AppTheme.allCases, id: \.self) { theme in
ThemePreviewCard(theme: theme, selected: selectedTheme == theme) {
// Write directly to AppStorage to avoid mutating through a computed property
appThemeRaw = theme.rawValue
applyTheme(theme)
showSavedToast()
}
}
}
}
@ -249,19 +315,20 @@ struct DisplayView: View {
private func loadCustomAccentColor() {
if customAccentColorHex.isEmpty {
selectedAccentColor = .blue
selectedAccentColor = .white
} else {
selectedAccentColor = Color(hex: customAccentColorHex) ?? .blue
selectedAccentColor = Color(hex: customAccentColorHex) ?? .white
}
}
private func applyTheme(_ theme: String) {
private func applyTheme(_ theme: AppTheme) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
switch theme {
case "dark": window.overrideUserInterfaceStyle = .dark
case "light": window.overrideUserInterfaceStyle = .light
default: window.overrideUserInterfaceStyle = .unspecified
case .darkStatic, .neonAnimated, .blobs, .particles:
window.overrideUserInterfaceStyle = .dark
case .system:
window.overrideUserInterfaceStyle = .unspecified
}
}
}
@ -274,35 +341,52 @@ struct DisplayView: View {
}
}
// MARK: - Theme Option Button (Glassy style)
struct ThemeOptionButton: View {
let title: String
let imageName: String
let isSelected: Bool
let accentColor: Color
// MARK: - Theme Preview Card
private struct ThemePreviewCard: View {
let theme: AppTheme
let selected: Bool
let action: () -> Void
private var hardcodedGradient: some View {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
var body: some View {
Button(action: action) {
VStack(spacing: 8) {
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 120)
.cornerRadius(12)
ZStack {
hardcodedGradient
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(isSelected ? accentColor : .clear, lineWidth: 2)
.fill(.ultraThinMaterial)
.padding(8)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
Text(title)
.font(.caption)
.foregroundColor(isSelected ? accentColor : .primary)
VStack(spacing: 6) {
Text(theme.displayName)
.font(.footnote.weight(.semibold))
.foregroundColor(.primary)
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(.ultraThinMaterial, in: Capsule())
}
.padding(8)
}
.padding(8)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
.frame(height: 120)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(selected ? Color.accentColor : Color.white.opacity(0.12), lineWidth: selected ? 2 : 1)
)
.shadow(color: .black.opacity(0.08), radius: 6, x: 0, y: 2)
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
}
}

View file

@ -52,6 +52,11 @@ struct HomeView: View {
@StateObject private var tunnel = TunnelManager.shared
@State private var heartbeatOK = false
@AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
private var currentTheme: AppTheme { AppTheme(rawValue: appThemeRaw) ?? .system }
private var accentColor: Color {
if customAccentColorHex.isEmpty { return .white }
@ -60,22 +65,25 @@ struct HomeView: View {
private var ddiMounted: Bool { isMounted() }
private var canConnectByApp: Bool { pairingFileExists && ddiMounted }
private var isOnOrAfteriOS26: Bool {
let v = ProcessInfo.processInfo.operatingSystemVersion
return v.majorVersion >= 26
}
var body: some View {
NavigationStack {
ZStack {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ThemedBackground(style: currentTheme.backgroundStyle)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
if isOnOrAfteriOS26 || showiOS26Disclaimer {
disclaimerCard
.transition(.opacity.combined(with: .move(edge: .top)))
}
topStatusAndActionsCard
toolsCard
tipsCard
@ -131,7 +139,7 @@ struct HomeView: View {
if fileManager.fileExists(atPath: url.path) {
do {
let dest = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
if fileManager.fileExists(atPath: dest.path) {
if FileManager.default.fileExists(atPath: dest.path) {
try fileManager.removeItem(at: dest)
}
try fileManager.copyItem(at: url, to: dest)
@ -174,7 +182,47 @@ struct HomeView: View {
bundleID = selectedBundle
isShowingInstalledApps = false
HapticFeedbackHelper.trigger()
startJITInBackground(bundleID: selectedBundle)
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(
@ -344,8 +392,8 @@ struct HomeView: View {
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.white, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(.black)
.background(accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(accentColor.contrastText())
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.2), lineWidth: 0.5)
@ -356,7 +404,18 @@ struct HomeView: View {
VStack(alignment: .leading, spacing: 12) {
Text("Tools").font(.headline).foregroundColor(.secondary)
Button(action: { showingConsoleLogsView = true }) {
whiteCardButtonLabel(icon: "terminal", title: "Open Console")
HStack {
Image(systemName: "terminal").font(.system(size: 20))
Text("Open Console").font(.system(.title3, design: .rounded)).fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(accentColor.contrastText())
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.2), lineWidth: 0.5)
)
}
.sheet(isPresented: $showingConsoleLogsView) { ConsoleLogsView() }
}
@ -382,6 +441,32 @@ struct HomeView: View {
tipRow(systemImage: "externaldrive.badge.exclamationmark", title: "Developer Disk Image not mounted", message: "Go to Settings → Developer Disk Image and ensure it’s mounted.")
}
tipRow(systemImage: "lock.shield", title: "Local only", message: "StikDebug runs entirely on-device. No data leaves your device.")
Button {
if let url = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md") {
UIApplication.shared.open(url)
}
} label: {
HStack(alignment: .top, spacing: 12) {
Image(systemName: "questionmark.circle")
.foregroundColor(accentColor)
.font(.system(size: 18))
VStack(alignment: .leading, spacing: 2) {
Text("Pairing File Guide")
.font(.subheadline).fontWeight(.semibold)
Text("Learn how to create and import your pairing file.")
.font(.footnote)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.secondary)
.padding(.top, 2)
}
.padding(.vertical, 4)
}
.buttonStyle(.plain)
}
.padding(20)
.background(
@ -467,7 +552,7 @@ struct HomeView: View {
}
}
private func startJITInBackground(bundleID: String? = nil, pid : Int? = nil, scriptData: Data? = nil, scriptName : String? = nil, triggeredByURLScheme: Bool = false) {
private func startJITInBackground(bundleID: String? = nil, pid : Int? = nil, scriptData: Data? = nil, scriptName: String? = nil, triggeredByURLScheme: Bool = false) {
isProcessing = true
LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))")
@ -487,14 +572,16 @@ struct HomeView: View {
}
}
} else {
scriptData = nil
// keep passed-in auto script if provided; otherwise nil
}
var callback: DebugAppCallback? = nil
if 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
}
let logger: LogFunc = { message in if let message { LogManager.shared.addInfoLog(message) } }
@ -534,6 +621,56 @@ struct HomeView: View {
if pad < 4 { base64 += String(repeating: "=", count: pad) }
return base64
}
// MARK: - iOS 26+ Disclaimer Card (above main card)
private var disclaimerCard: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.yellow)
.imageScale(.large)
VStack(alignment: .leading, spacing: 4) {
Text("Important for iOS 26+")
.font(.subheadline.weight(.semibold))
.foregroundColor(.primary)
Text("Limited compatibility on iOS 26 and later. Some apps may not function as expected yet. We’re actively improving support over time.")
.font(.footnote)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
if !isOnOrAfteriOS26 {
Button {
withAnimation {
showiOS26Disclaimer = false
}
} label: {
Image(systemName: "xmark")
.foregroundColor(.secondary)
.padding(8)
.background(
Circle().fill(Color(UIColor.tertiarySystemBackground))
)
}
.buttonStyle(.plain)
.accessibilityLabel("Dismiss")
}
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
)
)
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
.accessibilityElement(children: .combine)
.accessibilityLabel("Important notice for iOS 26 and later. Limited compatibility; improvements are ongoing.")
}
}
private struct StatusDot: View {
@ -567,25 +704,18 @@ private struct ConnectByPIDSheet: View {
return false
}
private let capsuleHeight: CGFloat = 40
var body: some View {
NavigationView {
ZStack {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
Color.clear.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
VStack(alignment: .leading, spacing: 14) {
Text("Enter a Process ID").font(.headline).foregroundColor(.primary)
// TextField + Clear control
TextField("e.g. 1234", text: $pidText)
.keyboardType(.numberPad)
.textContentType(.oneTimeCode)
@ -604,7 +734,7 @@ private struct ConnectByPIDSheet: View {
// Paste + Clear row
HStack(spacing: 10) {
CapsuleButton(systemName: "doc.on.clipboard", title: "Paste") {
CapsuleButton(systemName: "doc.on.clipboard", title: "Paste", height: capsuleHeight) {
if let n = UIPasteboard.general.string?.trimmingCharacters(in: .whitespacesAndNewlines),
let v = Int(n), v > 0 {
pidText = String(v)
@ -616,7 +746,7 @@ private struct ConnectByPIDSheet: View {
}
}
CapsuleButton(systemName: "xmark", title: "Clear") {
CapsuleButton(systemName: "xmark", title: "Clear", height: capsuleHeight) {
pidText = ""
errorText = nil
}
@ -677,8 +807,8 @@ private struct ConnectByPIDSheet: View {
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background((isValid ? Color.white : Color.white.opacity(0.6)), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(.black)
.background(Color.accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(Color.accentColor.contrastText())
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.2), lineWidth: 0.5)
@ -743,28 +873,36 @@ private struct ConnectByPIDSheet: View {
}
}
@ViewBuilder private func CapsuleButton(systemName: String, title: String, action: @escaping () -> Void) -> some View {
@ViewBuilder private func CapsuleButton(systemName: String, title: String, height: CGFloat = 40, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 6) {
Image(systemName: systemName)
Text(title).font(.subheadline.weight(.semibold))
}
.padding(.vertical, 8)
.frame(height: height) // enforce uniform height
.padding(.horizontal, 12)
.background(Capsule(style: .continuous).fill(Color(UIColor.tertiarySystemBackground)))
}
}
}
// MARK: - InstalledAppsViewModel
class InstalledAppsViewModel: ObservableObject {
@Published var apps: [String: String] = [:]
init() { loadApps() }
func loadApps() {
do { self.apps = try JITEnableContext.shared.getAppList() }
catch { print(error); self.apps = [:] }
.buttonStyle(.plain)
.contentShape(Rectangle())
}
}
#Preview { HomeView() }
// MARK: - TXM detection
public extension ProcessInfo {
var hasTXM: Bool {
{
if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36),
let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) {
return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0
} else {
return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map {
access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0
}) ?? false
}
}()
}
}

View file

@ -41,7 +41,7 @@ struct InstalledAppsListView: View {
var body: some View {
NavigationStack {
ZStack {
BackgroundGradient().ignoresSafeArea()
Color.clear.ignoresSafeArea()
if viewModel.apps.isEmpty {
emptyState
@ -570,3 +570,12 @@ extension Array: @retroactive RawRepresentable where Element: Codable {
InstalledAppsListView { _ in }
.environment(\.colorScheme, .dark)
}
class InstalledAppsViewModel: ObservableObject {
@Published var apps: [String: String] = [:]
init() { loadApps() }
func loadApps() {
do { self.apps = try JITEnableContext.shared.getAppList() }
catch { print(error); self.apps = [:] }
}
}

View file

@ -21,6 +21,9 @@ struct MainTabView: View {
var body: some View {
ZStack {
// Allow global themed background to show
Color.clear.ignoresSafeArea()
// Main tabs
TabView(selection: $selection) {
HomeView()
@ -39,22 +42,16 @@ struct MainTabView: View {
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
.tag(4)
}
.id(customAccentColorHex)
.tint(accentColor)
.preferredColorScheme(.dark)
.onAppear {
checkForUpdate()
}
// Force update overlay
if showForceUpdate {
ZStack {
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
Color.black.opacity(0.001).ignoresSafeArea()
VStack(spacing: 20) {
Text("Update Required")

View file

@ -15,7 +15,10 @@ struct SettingsView: View {
@AppStorage("enableAdvancedBetaOptions") private var enableAdvancedBetaOptions = false
@AppStorage("enableTesting") private var enableTesting = false
@AppStorage("enablePiP") private var enablePiP = false
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
private var currentTheme: AppTheme { AppTheme(rawValue: appThemeRaw) ?? .system }
@State private var isShowingPairingFilePicker = false
@Environment(\.colorScheme) private var colorScheme
@ -27,10 +30,6 @@ struct SettingsView: View {
@State private var is_lc = false
@State private var showColorPickerPopup = false
@StateObject private var mountProg = MountingProgress.shared
@State private var mounted = false
@State private var showingConsoleLogsView = false
@State private var showingDisplayView = false
@ -38,7 +37,11 @@ struct SettingsView: View {
let marketingVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
return marketingVersion
}
private var accentColor: Color {
if customAccentColorHex.isEmpty { return .white }
return Color(hex: customAccentColorHex) ?? .white
}
// Developer profile image URLs
private let developerProfiles: [String: String] = [
"Stephen": "https://github.com/StephenDev0.png",
@ -54,22 +57,14 @@ struct SettingsView: View {
NavigationStack {
ZStack {
// Subtle depth gradient background
LinearGradient(
gradient: Gradient(colors: [
Color(UIColor.systemBackground),
Color(UIColor.secondarySystemBackground)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ThemedBackground(style: currentTheme.backgroundStyle)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
headerCard
appearanceCard
pairingCard
ddiCard
behaviorCard
advancedCard
helpCard
@ -209,7 +204,7 @@ struct SettingsView: View {
// MARK: - Cards
private var headerCard: some View {
materialCard {
glassCard {
VStack(spacing: 16) {
VStack {
Image("StikJIT")
@ -232,7 +227,7 @@ struct SettingsView: View {
}
private var appearanceCard: some View {
materialCard {
glassCard {
VStack(alignment: .leading, spacing: 14) {
Text("Appearance")
.font(.headline)
@ -262,7 +257,7 @@ struct SettingsView: View {
}
private var pairingCard: some View {
materialCard {
glassCard {
VStack(alignment: .leading, spacing: 14) {
Text("Pairing File")
.font(.headline)
@ -279,8 +274,12 @@ struct SettingsView: View {
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.foregroundColor(.black)
.background(Color.white, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(accentColor.contrastText())
.background(accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.2), lineWidth: 0.5)
)
}
if showPairingFileMessage && pairingFileIsValid && !isImportingFile {
@ -298,78 +297,17 @@ struct SettingsView: View {
}
}
private var ddiCard: some View {
materialCard {
VStack(alignment: .leading, spacing: 14) {
Text("Developer Disk Image")
.font(.headline)
.foregroundColor(.primary)
HStack(spacing: 12) {
Image(systemName: mounted || (mountProg.mountProgress == 100) ? "checkmark.circle.fill" : "xmark.circle.fill")
.font(.system(size: 24))
.foregroundColor(mounted || (mountProg.mountProgress == 100) ? .green : .red)
Text(mounted || (mountProg.mountProgress == 100) ? "Successfully Mounted" : "Not Mounted")
.font(.body.weight(.medium))
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.tertiarySystemBackground))
)
if !(mounted || (mountProg.mountProgress == 100)) {
Text("Import pairing file and restart the app to mount DDI")
.font(.caption)
.foregroundColor(.secondary)
}
if mountProg.mountProgress > 0 && mountProg.mountProgress < 100 && !mounted {
VStack(spacing: 8) {
HStack {
Text("Mounting in progress…")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text("\(Int(mountProg.mountProgress))%")
.font(.caption)
.foregroundColor(.secondary)
}
GeometryReader { geometry in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 6)
.fill(Color(UIColor.tertiarySystemFill))
.frame(height: 8)
RoundedRectangle(cornerRadius: 6)
.fill(Color.green)
.frame(width: geometry.size.width * CGFloat(mountProg.mountProgress / 100.0), height: 8)
.animation(.linear(duration: 0.3), value: mountProg.mountProgress)
}
}
.frame(height: 8)
}
}
}
.onAppear {
self.mounted = isMounted()
}
}
}
private var behaviorCard: some View {
materialCard {
glassCard {
VStack(alignment: .leading, spacing: 16) {
Text("Behavior")
.font(.headline)
.foregroundColor(.primary)
Toggle("Run Default Script After Connecting", isOn: $useDefaultScript)
.foregroundColor(.primary)
.tint(accentColor)
Toggle("Picture in Picture", isOn: $enablePiP)
.foregroundColor(.primary)
.tint(accentColor)
}
.onChange(of: enableAdvancedOptions) { _, newValue in
if !newValue {
@ -386,46 +324,9 @@ struct SettingsView: View {
}
}
}
private var aboutCard: some View {
materialCard {
VStack(alignment: .leading, spacing: 16) {
Text("About")
.font(.headline)
.foregroundColor(.primary)
VStack(alignment: .leading, spacing: 12) {
Text("Creators")
.font(.subheadline)
.foregroundColor(.secondary)
HStack(spacing: 12) {
creatorTile(name: "Stephen", role: "App Creator", url: "https://github.com/StephenDev0", imageUrl: developerProfiles["Stephen"] ?? "")
creatorTile(name: "jkcoxson", role: "idevice & em_proxy", url: "https://jkcoxson.com/", imageUrl: developerProfiles["jkcoxson"] ?? "")
}
}
Divider().background(Color.white.opacity(0.12))
VStack(alignment: .leading, spacing: 12) {
Text("Developers")
.font(.subheadline)
.foregroundColor(.secondary)
VStack(spacing: 10) {
CollaboratorRow(name: "Stossy11", url: "https://github.com/Stossy11", imageUrl: developerProfiles["Stossy11"] ?? "")
CollaboratorRow(name: "Neo", url: "https://neoarz.xyz/", imageUrl: developerProfiles["Neo"] ?? "")
CollaboratorRow(name: "Se2crid", url: "https://github.com/Se2crid", imageUrl: developerProfiles["Se2crid"] ?? "")
CollaboratorRow(name: "Huge_Black", url: "https://github.com/HugeBlack", imageUrl: developerProfiles["Huge_Black"] ?? "")
CollaboratorRow(name: "Wynwxst", url: "https://github.com/Wynwxst", imageUrl: developerProfiles["Wynwxst"] ?? "")
}
}
}
}
}
private var advancedCard: some View {
materialCard {
glassCard {
VStack(alignment: .leading, spacing: 14) {
Text("Advanced")
.font(.headline)
@ -469,7 +370,7 @@ struct SettingsView: View {
}
private var helpCard: some View {
materialCard {
glassCard {
VStack(alignment: .leading, spacing: 14) {
Text("Help")
.font(.headline)
@ -520,9 +421,10 @@ struct SettingsView: View {
}
private var versionInfo: some View {
HStack {
let txmLabel = ProcessInfo.processInfo.hasTXM ? "TXM" : "Non TXM"
return HStack {
Spacer()
Text("Version \(appVersion) • iOS \(UIDevice.current.systemVersion)")
Text("Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)")
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
@ -532,7 +434,7 @@ struct SettingsView: View {
// MARK: - Helpers (UI + logic)
private func materialCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
private func glassCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
content()
.padding(20)
.background(
@ -546,29 +448,7 @@ struct SettingsView: View {
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
}
private func creatorTile(name: String, role: String, url: String, imageUrl: String) -> some View {
Button(action: {
if let u = URL(string: url) { UIApplication.shared.open(u) }
}) {
VStack(spacing: 8) {
ProfileImage(url: imageUrl)
.frame(width: 60, height: 60)
Text(name).fontWeight(.semibold)
Text(role)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.vertical, 12)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.tertiarySystemBackground))
)
}
}
private func changeAppIcon(to iconName: String) {
selectedAppIcon = iconName
UIApplication.shared.setAlternateIconName(iconName == "AppIcon" ? nil : iconName) { error in
@ -673,115 +553,6 @@ struct LinkRow: View {
}
}
struct CollaboratorGridItem: View {
var name: String
var url: String
var imageUrl: String
var body: some View {
Button(action: {
if let url = URL(string: url) {
UIApplication.shared.open(url)
}
}) {
VStack(spacing: 8) {
ProfileImage(url: imageUrl)
.frame(width: 50, height: 50)
Text(name)
.foregroundColor(.primary)
.fontWeight(.medium)
.font(.subheadline)
}
.frame(minWidth: 80)
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(Color(UIColor.tertiarySystemBackground))
.cornerRadius(12)
}
.preferredColorScheme(.dark)
}
}
struct ProfileImage: View {
var url: String
@State private var image: UIImage?
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
} else {
Circle()
.fill(Color(UIColor.systemGray4))
.overlay(
Image(systemName: "person.fill")
.font(.system(size: 16))
.foregroundColor(.white)
)
.onAppear {
loadImage()
}
}
}
}
private func loadImage() {
guard let imageUrl = URL(string: url) else { return }
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
if let data = data, let downloadedImage = UIImage(data: data) {
DispatchQueue.main.async {
self.image = downloadedImage
}
}
}.resume()
}
}
struct CollaboratorRow: View {
var name: String
var url: String
var imageUrl: String
var quote: String?
var body: some View {
Button(action: {
if let url = URL(string: url) {
UIApplication.shared.open(url)
}
}) {
HStack(spacing: 12) {
ProfileImage(url: imageUrl)
.frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 2) {
Text(name)
.foregroundColor(.primary)
.fontWeight(.medium)
if let quote = quote {
Text("“\(quote)”")
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
}
Spacer()
Image(systemName: "link")
.font(.system(size: 16))
.foregroundColor(.white)
}
.padding(.vertical, 8)
}
.preferredColorScheme(.dark)
}
}
struct ConsoleLogsView_Preview: PreviewProvider {
static var previews: some View {
ConsoleLogsView()
@ -805,4 +576,3 @@ class FolderViewController: UIViewController {
}
}
}

View file

@ -0,0 +1,32 @@
//
// SharedStubs.swift
// StikJIT
//
// Created by Stephen on 09/12/2025.
//
import SwiftUI
import UniformTypeIdentifiers
import UIKit
// MARK: - Shared glass card wrapper (renamed to avoid conflicts)
@ViewBuilder
func appGlassCard<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {
MaterialCard {
content()
}
}
// MARK: - Open App Folder helper
func openAppFolder() {
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let controller = UIActivityViewController(activityItems: [docs], applicationActivities: nil)
controller.excludedActivityTypes = [.assignToContact, .saveToCameraRoll, .postToFacebook, .postToTwitter]
DispatchQueue.main.async {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController else { return }
root.present(controller, animated: true)
}
}

107
StikJIT/melo.js Normal file
View file

@ -0,0 +1,107 @@
function littleEndianHexStringToNumber(hexStr) {
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}
let num = 0n;
for (let i = 4; i >= 0; i--) {
num = (num << 8n) | BigInt(bytes[i]);
}
return num;
}
function numberToLittleEndianHexString(num) {
const bytes = [];
for (let i = 0; i < 5; i++) {
bytes.push(Number(num & 0xFFn));
num >>= 8n;
}
while (bytes.length < 8) {
bytes.push(0);
}
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
}
function littleEndianHexToU32(hexStr) {
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
}
function extractBrkImmediate(u32) {
return (u32 >> 5) & 0xFFFF;
}
function attach(breakpointcount) {
let pid = get_pid();
log(`pid = ${pid}`);
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);
log(`attach_response = ${attachResponse}`);
let validBreakpoints = 0;
let totalBreakpoints = 0;
while (validBreakpoints < breakpointcount) {
totalBreakpoints++;
log(`Handling breakpoint ${totalBreakpoints} (looking for valid breakpoint ${validBreakpoints + 1}/${breakpointcount})`);
let brkResponse = send_command(`c`);
log(`brkResponse = ${brkResponse}`);
let tidMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
let tid = tidMatch ? tidMatch.groups['tid'] : null;
let pcMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let pc = pcMatch ? pcMatch.groups['reg'] : null;
let x0Match = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x0 = x0Match ? x0Match.groups['reg'] : null;
if (!tid || !pc || !x0) {
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x0=${x0}`);
continue;
}
const pcNum = littleEndianHexStringToNumber(pc);
const x0Num = littleEndianHexStringToNumber(x0);
log(`tid = ${tid}, pc = ${pcNum.toString(16)}, x0 = ${x0Num.toString(16)}`);
let instructionResponse = send_command(`m${pcNum.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
if (brkImmediate !== 0x69) {
log(`Skipping breakpoint: brk immediate was not 0x69 (was 0x${brkImmediate.toString(16)})`);
continue;
}
log(`BRK immediate matches expected value 0x69 - processing valid breakpoint ${validBreakpoints + 1}/${breakpointcount}`);
let requestRXResponse = send_command(`_M${x0Num.toString(16)},rx`);
log(`requestRXResponse = ${requestRXResponse}`);
if (!requestRXResponse || requestRXResponse.length === 0) {
log(`Failed to allocate RX memory`);
continue;
}
let jitPageAddress = BigInt(`0x${requestRXResponse}`);
log(`Allocated JIT page at address: 0x${jitPageAddress.toString(16)}`);
let prepareJITPageResponse = prepare_memory_region(jitPageAddress, x0Num);
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`);
log(`putX0Response = ${putX0Response}`);
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);
validBreakpoints++;
log(`Completed valid breakpoint ${validBreakpoints}/${breakpointcount} - returning address 0x${jitPageAddress.toString(16)}`);
}
let detachResponse = send_command(`D`);
log(`detachResponse = ${detachResponse}`);
}
attach(3); // MeloNX uses 3 breakpoints, adjust as needed.

102
StikJIT/script1.js Normal file
View file

@ -0,0 +1,102 @@
function littleEndianHexStringToNumber(hexStr) {
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}
let num = 0n;
for (let i = 4; i >= 0; i--) {
num = (num << 8n) | BigInt(bytes[i]);
}
return num;
}
function numberToLittleEndianHexString(num) {
const bytes = [];
for (let i = 0; i < 5; i++) {
bytes.push(Number(num & 0xFFn));
num >>= 8n;
}
while (bytes.length < 8) {
bytes.push(0);
}
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
}
function littleEndianHexToU32(hexStr) {
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
}
function extractBrkImmediate(u32) {
return (u32 >> 5) & 0xFFFF;
}
function attach(breakpointcount) {
let pid = get_pid();
log(`pid = ${pid}`);
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);
log(`attach_response = ${attachResponse}`);
let validBreakpoints = 0;
let totalBreakpoints = 0;
while (validBreakpoints < breakpointcount) {
totalBreakpoints++;
log(`Handling breakpoint ${totalBreakpoints} (looking for valid breakpoint ${validBreakpoints + 1}/${breakpointcount})`);
let brkResponse = send_command(`c`);
log(`brkResponse = ${brkResponse}`);
let tidMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
let tid = tidMatch ? tidMatch.groups['tid'] : null;
let pcMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let pc = pcMatch ? pcMatch.groups['reg'] : null;
let x0Match = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x0 = x0Match ? x0Match.groups['reg'] : null;
let x1Match = /01:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x1 = x1Match ? x1Match.groups['reg'] : null;
if (!tid || !pc || !x0) {
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x0=${x0}, x1=${x1}`);
continue;
}
const pcNum = littleEndianHexStringToNumber(pc);
const x0Num = littleEndianHexStringToNumber(x0);
const x1Num = littleEndianHexStringToNumber(x1);
log(`tid = ${tid}, pc = ${pcNum.toString(16)}, x0 = ${x0Num.toString(16)}, x1 = ${x1Num.toString(16)}`);
let instructionResponse = send_command(`m${pcNum.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
if (brkImmediate !== 0x69) {
log(`Skipping breakpoint: brk immediate was not 0x69 (was 0x${brkImmediate.toString(16)})`);
continue;
}
log(`BRK immediate matches expected value 0x69 - processing valid breakpoint ${validBreakpoints + 1}/${breakpointcount}`);
let jitPageAddress = x0Num
log(`Got rx address: 0x${jitPageAddress.toString(16)}`);
let prepareJITPageResponse = prepare_memory_region(jitPageAddress, x1Num);
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`);
log(`putX0Response = ${putX0Response}`);
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);
validBreakpoints++;
log(`Completed valid breakpoint ${validBreakpoints}/${breakpointcount} - returning address 0x${jitPageAddress.toString(16)}`);
}
let detachResponse = send_command(`D`);
log(`detachResponse = ${detachResponse}`);
}
attach(3); // MeloNX uses 3 breakpoints, adjust as needed.

89
StikJIT/script2.js Normal file
View file

@ -0,0 +1,89 @@
function littleEndianHexStringToNumber(hexStr) {
// Convert hex string to byte array
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}
// Assemble number from first 5 bytes (little-endian)
let num = 0n;
for (let i = 4; i >= 0; i--) {
num = (num << 8n) | BigInt(bytes[i]);
}
return num; // BigInt: 0x01016644fc
}
function numberToLittleEndianHexString(num) {
// Extract 5 bytes in little-endian order
const bytes = [];
for (let i = 0; i < 5; i++) {
bytes.push(Number(num & 0xFFn));
num >>= 8n;
}
// Pad with 3 zero bytes to make 8 bytes
while (bytes.length < 8) {
bytes.push(0);
}
// Convert to hex string
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
}
function attach() {
let pid = get_pid();
log(`pid = ${pid}`)
// attach
let attachResponse = send_command(`vAttach;${pid.toString(16)}`)
log(`attach_response = ${attachResponse}`)
// wait for brk
let brkResponse = send_command(`c`)
log(`brkResponse = ${brkResponse}`)
let tid = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse).groups['tid']
let pc = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse).groups['reg']
let x0 = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse).groups['reg']
log(`tid = ${tid}, pc = ${littleEndianHexStringToNumber(pc).toString(16)}, x0 = ${littleEndianHexStringToNumber(x0).toString(16)}`)
pc = numberToLittleEndianHexString(littleEndianHexStringToNumber(pc) + BigInt(4))
let x0Num = littleEndianHexStringToNumber(x0)
// pc+4
let pcPlus4Response = send_command(`P20=${pc};thread:${tid};`)
log(`pcPlus4Response = ${pcPlus4Response}`)
// apply for jit page of requested size
let requestRXResponse = send_command(`_M${x0Num.toString(16)},rx`)
log(`requestRXResponse = ${requestRXResponse}`)
let jitPageAddress = BigInt(`0x${requestRXResponse}`)
// TODO fill the page
let a = 0;
try {
for(let i = 0n; i < x0Num; i += 16384n) {
let curPointer = jitPageAddress + i;
let response = send_command(`M${curPointer.toString(16)},1:69`)
if(a % 1000 == 0) {
log(`progress: ${a}/${Math.ceil(Number(x0Num / BigInt(16384)))}, response = ${response}`)
}
++a;
}
log(`memory write completed. command executed: ${a}`)
}catch(e) {
log(`memory write failed at command ${a}`)
}
// put page address in x0
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`)
log(`putX0Response = ${putX0Response}`)
// detach
let detachResponse = send_command(`D`)
log(`detachResponse = ${detachResponse}`)
}
attach()

98
StikJIT/utmjit.js Normal file
View file

@ -0,0 +1,98 @@
function littleEndianHexStringToNumber(hexStr) {
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}
let num = 0n;
for (let i = 4; i >= 0; i--) {
num = (num << 8n) | BigInt(bytes[i]);
}
return num;
}
function numberToLittleEndianHexString(num) {
const bytes = [];
for (let i = 0; i < 5; i++) {
bytes.push(Number(num & 0xFFn));
num >>= 8n;
}
while (bytes.length < 8) {
bytes.push(0);
}
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
}
function littleEndianHexToU32(hexStr) {
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
}
function extractBrkImmediate(u32) {
return (u32 >> 5) & 0xFFFF;
}
function attach(breakpointcount) {
let pid = get_pid();
log(`pid = ${pid}`);
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);
log(`attach_response = ${attachResponse}`);
let validBreakpoints = 0;
let totalBreakpoints = 0;
while (validBreakpoints < breakpointcount) {
totalBreakpoints++;
log(`Handling breakpoint ${totalBreakpoints} (looking for valid breakpoint ${validBreakpoints + 1}/${breakpointcount})`);
let brkResponse = send_command(`c`);
log(`brkResponse = ${brkResponse}`);
let tidMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
let tid = tidMatch ? tidMatch.groups['tid'] : null;
let pcMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let pc = pcMatch ? pcMatch.groups['reg'] : null;
let x0Match = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x0 = x0Match ? x0Match.groups['reg'] : null;
let x1Match = /01:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x1 = x1Match ? x1Match.groups['reg'] : null;
if (!tid || !pc || !x0 || !x1) {
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x0=${x0}, x1=${x1}`);
continue;
}
const pcNum = littleEndianHexStringToNumber(pc);
const x0Num = littleEndianHexStringToNumber(x0);
const x1Num = littleEndianHexStringToNumber(x1);
log(`tid = ${tid}, pc = ${pcNum.toString(16)}, x0 = ${x0Num.toString(16)}, x1 = ${x1Num.toString(16)}`);
let instructionResponse = send_command(`m${pcNum.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
if (brkImmediate !== 0x69) {
log(`Skipping breakpoint: brk immediate was not 0x69 (was 0x${brkImmediate.toString(16)})`);
continue;
}
log(`BRK immediate matches expected value 0x69 - processing valid breakpoint ${validBreakpoints + 1}/${breakpointcount}`);
log(`Allocated JIT page at address: 0x${x0Num.toString(16)}`);
let prepareJITPageResponse = prepare_memory_region(x0Num, x1Num);
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);
validBreakpoints++;
log(`Completed valid breakpoint ${validBreakpoints}/${breakpointcount}`);
}
let detachResponse = send_command(`D`);
log(`detachResponse = ${detachResponse}`);
}
attach(1);