mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
* Change default UPLOAD_IPA value to 'false' * Part 1 of the great debloat * Remove Mini Tools, adopt native alerts in StikJITApp, and update HomeView UI * Refactor views to use native SwiftUI alerts instead of CustomErrorView * Update ProfileView to use minimal List UI style with native alerts * Fix up homeview * Squash: Redesign HomeView, remove Feature Flags, enable all tabs, refactor alerts, and updates - Redesign HomeView to a simpler style with 'Enable JIT' action. - Remove 'Help & Info' and 'Quick Launch' from Home. - Fix DDI false positive by starting heartbeat on HomeView appear. - Restore robust DDI checks and update app name to 'StikDebug'. - Remove FeatureFlags and Beta tags; enable 'profiles', 'processes', 'location' tabs by default. - Remove 'Devices' tab; add 'Target Device IP' field in Settings. - Hardcode 'Settings' tab to 4th position. - Refactor 'SettingsView' tab customization to be reorder-only. - Remove 'Appearance' theme section from Settings. - Refactor custom error cards to use native SwiftUI/UIKit alerts across the app (HomeView, ConsoleLogsView, ProfileView, etc). - Restore JS callback support and script execution logic in HomeView. - Add necessary Color extensions. - Update project version to 3.0.0. * Clean up: Remove In-App Purchases, update checker, and unused IPA installer code * delete more bloat * fix build issue * Remove widget to save on App IDs * Attempt heartbeat restart when Enable JIT button is pressed * Get rid of PIP * Update Tab Naming * remove more bloat * final changes for today * Remove unused packages * UX Improvements * revert bundle id * Update README.md * remove old idevice * add updated idevice * Fix ddi false positive errors and add templates for GitHub stuff * Remove more unused stuff. * Fix links and update README content * Update compatibility table in README.md * Improve performance of console logs * Remove old code * Change default UPLOAD_IPA value to 'true' * remove the welcome sheet * import support for .mobiledevicepair * Add back LC JIT stuff * Fix heartbeat issues with LC JIT * update the build yml for nightly stuff * Update README.md * Update README.md * Update GitHub Actions workflow for IPA build * Update GitHub Actions workflow for nightly builds * Add URL scheme to kill tasks stikjit://kill-process?pid=1234 * fix build issues * Added App Intents * Unify the error handling style across the app
430 lines
18 KiB
Swift
430 lines
18 KiB
Swift
// SettingsView.swift
|
|
// StikJIT
|
|
//
|
|
// Created by Stephen on 3/27/25.
|
|
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
import UIKit
|
|
|
|
struct SettingsView: View {
|
|
@AppStorage("username") private var username = "User"
|
|
@AppStorage("selectedAppIcon") private var selectedAppIcon: String = "AppIcon"
|
|
@AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false
|
|
@AppStorage("enableAdvancedBetaOptions") private var enableAdvancedBetaOptions = false
|
|
@AppStorage("enableTesting") private var enableTesting = false
|
|
@AppStorage(UserDefaults.Keys.txmOverride) private var overrideTXMDetection = false
|
|
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
|
|
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
|
|
@AppStorage("customTargetIP") private var customTargetIP = ""
|
|
@AppStorage(TabConfiguration.storageKey) private var enabledTabIdentifiers = TabConfiguration.defaultRawValue
|
|
@AppStorage("primaryTabSelection") private var tabSelection = TabConfiguration.defaultIDs.first ?? "home"
|
|
|
|
@State private var isShowingPairingFilePicker = false
|
|
@State private var showPairingFileMessage = false
|
|
@State private var isImportingFile = false
|
|
@State private var importProgress: Float = 0.0
|
|
@State private var pairingStatusMessage: String? = nil
|
|
@State private var showDDIConfirmation = false
|
|
@State private var isRedownloadingDDI = false
|
|
@State private var ddiDownloadProgress: Double = 0.0
|
|
@State private var ddiStatusMessage: String = ""
|
|
@State private var ddiResultMessage: (text: String, isError: Bool)?
|
|
|
|
|
|
private var appVersion: String {
|
|
let marketingVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
|
|
return marketingVersion
|
|
}
|
|
|
|
struct TabOption: Identifiable {
|
|
let id: String
|
|
let title: String
|
|
let detail: String
|
|
let icon: String
|
|
let isBeta: Bool
|
|
}
|
|
|
|
private var tabOptions: [TabOption] {
|
|
var options: [TabOption] = [
|
|
TabOption(id: "home", title: "Home", detail: "Dashboard overview", icon: "house", isBeta: false),
|
|
TabOption(id: "scripts", title: "Scripts", detail: "Manage automation scripts", icon: "scroll", isBeta: false),
|
|
TabOption(id: "tools", title: "Tools", detail: "Access additional tools", icon: "wrench.and.screwdriver", isBeta: false)
|
|
]
|
|
options.append(TabOption(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", icon: "iphone.and.arrow.forward", isBeta: false))
|
|
options.append(TabOption(id: "profiles", title: "App Expiry", detail: "Check app expiration date, install/remove profiles", icon: "calendar.badge.clock", isBeta: false))
|
|
options.append(TabOption(id: "processes", title: "Processes", detail: "Inspect running apps", icon: "rectangle.stack.person.crop", isBeta: false))
|
|
options.append(TabOption(id: "location", title: "Location Sim", detail: "Sideload only", icon: "location", isBeta: false))
|
|
return options
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
// 1) App Header
|
|
Section {
|
|
HStack {
|
|
Spacer()
|
|
VStack(spacing: 12) {
|
|
Image("StikDebug")
|
|
.resizable().aspectRatio(contentMode: .fit)
|
|
.frame(width: 80, height: 80)
|
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
Text("StikDebug").font(.title2.weight(.semibold))
|
|
}
|
|
Spacer()
|
|
}
|
|
.listRowBackground(Color.clear)
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
// 2) Profile
|
|
Section("Profile") {
|
|
HStack {
|
|
Text("Username")
|
|
Spacer()
|
|
TextField("User", text: $username)
|
|
.multilineTextAlignment(.trailing)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
// 3) Pairing File
|
|
Section("Pairing File") {
|
|
Button { isShowingPairingFilePicker = true } label: {
|
|
Label("Import Pairing File", systemImage: "doc.badge.plus")
|
|
}
|
|
if showPairingFileMessage && !isImportingFile {
|
|
Label("Imported successfully", systemImage: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
}
|
|
}
|
|
|
|
// 5) Background Keep-Alive
|
|
Section {
|
|
Toggle(isOn: $keepAliveAudio) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Silent Audio")
|
|
Text("Plays inaudible audio so iOS keeps the app running.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.onChange(of: keepAliveAudio) { _, enabled in
|
|
if enabled { BackgroundAudioManager.shared.start() }
|
|
else { BackgroundAudioManager.shared.stop() }
|
|
}
|
|
|
|
Toggle(isOn: $keepAliveLocation) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Background Location")
|
|
Text("Uses low-accuracy location to stay alive even when another app plays audio.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.onChange(of: keepAliveLocation) { _, enabled in
|
|
if enabled { BackgroundLocationManager.shared.start() }
|
|
else { BackgroundLocationManager.shared.stop() }
|
|
}
|
|
} header: {
|
|
Text("Background Keep-Alive")
|
|
} footer: {
|
|
Text("For Background Location to work reliably, go to **Settings → Privacy & Security → Location Services → StikDebug** and select **Always**.")
|
|
}
|
|
|
|
// 6) Behavior
|
|
Section("Behavior") {
|
|
Toggle(isOn: $overrideTXMDetection) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Always Run Scripts")
|
|
Text("Treats device as TXM-capable to bypass hardware checks.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 7) Advanced
|
|
Section("Advanced") {
|
|
HStack {
|
|
Text("Target Device IP")
|
|
Spacer()
|
|
TextField("10.7.0.1", text: $customTargetIP)
|
|
.multilineTextAlignment(.trailing)
|
|
.keyboardType(.decimalPad)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .keyboard) {
|
|
Spacer()
|
|
Button("Done") {
|
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Button { openAppFolder() } label: {
|
|
Label("App Folder", systemImage: "folder")
|
|
}.foregroundStyle(.primary)
|
|
Button { showDDIConfirmation = true } label: {
|
|
Label("Redownload DDI", systemImage: "arrow.down.circle")
|
|
}.foregroundStyle(.primary).disabled(isRedownloadingDDI)
|
|
if isRedownloadingDDI {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
ProgressView(value: ddiDownloadProgress, total: 1.0)
|
|
Text(ddiStatusMessage).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
} else if let result = ddiResultMessage {
|
|
Text(result.text).font(.caption).foregroundStyle(result.isError ? .red : .green)
|
|
}
|
|
}
|
|
|
|
// 7) Help
|
|
Section("Help") {
|
|
Link(destination: URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md")!) {
|
|
Label("Pairing File Guide", systemImage: "questionmark.circle")
|
|
}
|
|
Link(destination: URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")!) {
|
|
Label("Download LocalDevVPN", systemImage: "arrow.down.circle")
|
|
}
|
|
Link(destination: URL(string: "https://discord.gg/qahjXNTDwS")!) {
|
|
Label("Discord Support", systemImage: "bubble.left.and.bubble.right")
|
|
}
|
|
}
|
|
|
|
// 8) Version footer
|
|
Section {
|
|
Text(versionFooter)
|
|
.font(.footnote).foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.listRowBackground(Color.clear)
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
}
|
|
.fileImporter(
|
|
isPresented: $isShowingPairingFilePicker,
|
|
allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!, .propertyList],
|
|
allowsMultipleSelection: false
|
|
) { result in
|
|
switch result {
|
|
case .success(let urls):
|
|
guard let url = urls.first else { return }
|
|
|
|
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"))
|
|
DispatchQueue.main.async {
|
|
isImportingFile = true
|
|
importProgress = 0.0
|
|
pairingStatusMessage = nil
|
|
showPairingFileMessage = false
|
|
}
|
|
|
|
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
|
DispatchQueue.main.async {
|
|
if importProgress < 1.0 {
|
|
importProgress += 0.05
|
|
} else {
|
|
timer.invalidate()
|
|
isImportingFile = false
|
|
}
|
|
}
|
|
}
|
|
|
|
RunLoop.current.add(progressTimer, forMode: .common)
|
|
DispatchQueue.main.async {
|
|
startHeartbeatInBackground()
|
|
}
|
|
|
|
} catch { }
|
|
}
|
|
|
|
if accessing {
|
|
url.stopAccessingSecurityScopedResource()
|
|
}
|
|
case .failure:
|
|
break
|
|
}
|
|
}
|
|
.confirmationDialog("Redownload DDI Files?", isPresented: $showDDIConfirmation, titleVisibility: .visible) {
|
|
Button("Redownload", role: .destructive) {
|
|
redownloadDDIPressed()
|
|
}
|
|
Button("Cancel", role: .cancel) { }
|
|
} message: {
|
|
Text("Existing DDI files will be removed before downloading fresh copies.")
|
|
}
|
|
.overlay { if isImportingFile { importBusyOverlay } }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var importBusyOverlay: some View {
|
|
Color.black.opacity(0.35).ignoresSafeArea()
|
|
VStack(spacing: 12) {
|
|
ProgressView("Processing pairing file…")
|
|
VStack(spacing: 8) {
|
|
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(importProgress), height: 8)
|
|
.animation(.linear(duration: 0.3), value: importProgress)
|
|
}
|
|
}
|
|
.frame(height: 8)
|
|
Text("\(Int(importProgress * 100))%")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.top, 6)
|
|
}
|
|
.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)
|
|
}
|
|
|
|
private var versionFooter: String {
|
|
let processInfo = ProcessInfo.processInfo
|
|
let txmLabel: String
|
|
if processInfo.isTXMOverridden {
|
|
txmLabel = "TXM (Override)"
|
|
} else {
|
|
txmLabel = processInfo.hasTXM ? "TXM" : "Non TXM"
|
|
}
|
|
return "Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)"
|
|
}
|
|
|
|
// MARK: - Business Logic
|
|
|
|
private func openAppFolder() {
|
|
guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
|
let path = documentsURL.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
|
|
if let url = URL(string: path) {
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
}
|
|
|
|
private func redownloadDDIPressed() {
|
|
guard !isRedownloadingDDI else { return }
|
|
Task {
|
|
await MainActor.run {
|
|
isRedownloadingDDI = true
|
|
ddiDownloadProgress = 0
|
|
ddiStatusMessage = "Preparing download…"
|
|
ddiResultMessage = nil
|
|
}
|
|
do {
|
|
try await redownloadDDI { progress, status in
|
|
Task { @MainActor in
|
|
self.ddiDownloadProgress = progress
|
|
self.ddiStatusMessage = status
|
|
}
|
|
}
|
|
await MainActor.run {
|
|
isRedownloadingDDI = false
|
|
ddiResultMessage = ("DDI files refreshed successfully.", false)
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
isRedownloadingDDI = false
|
|
ddiResultMessage = ("Failed to redownload DDI files: \(error.localizedDescription)", true)
|
|
}
|
|
}
|
|
}
|
|
scheduleDDIStatusDismiss()
|
|
}
|
|
|
|
private func scheduleDDIStatusDismiss() {
|
|
Task {
|
|
try? await Task.sleep(nanoseconds: 4_000_000_000)
|
|
await MainActor.run {
|
|
if !isRedownloadingDDI {
|
|
ddiResultMessage = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Tab Customization
|
|
|
|
struct TabCustomizationView: View {
|
|
let tabOptions: [SettingsView.TabOption]
|
|
@Binding var enabledTabIdentifiers: String
|
|
@Binding var tabSelection: String
|
|
|
|
private var selectedIDs: [String] {
|
|
TabConfiguration.sanitize(raw: enabledTabIdentifiers)
|
|
}
|
|
|
|
private var pinnedOptions: [SettingsView.TabOption] {
|
|
selectedIDs.compactMap { id in tabOptions.first(where: { $0.id == id }) }
|
|
}
|
|
|
|
private var availableOptions: [SettingsView.TabOption] {
|
|
tabOptions.filter { !selectedIDs.contains($0.id) }
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
ForEach(pinnedOptions) { option in
|
|
HStack {
|
|
Label(option.title, systemImage: option.icon)
|
|
}
|
|
}
|
|
.onMove { indices, newOffset in
|
|
var ids = selectedIDs
|
|
ids.move(fromOffsets: indices, toOffset: newOffset)
|
|
enabledTabIdentifiers = TabConfiguration.serialize(ids)
|
|
}
|
|
} header: {
|
|
Text("Pinned")
|
|
} footer: {
|
|
Text("Settings is fixed as the 4th tab.")
|
|
}
|
|
|
|
if !availableOptions.isEmpty {
|
|
Section("Available") {
|
|
ForEach(availableOptions) { option in
|
|
Button {
|
|
var ids = selectedIDs
|
|
guard ids.count < TabConfiguration.maxSelectableTabs else { return }
|
|
ids.append(option.id)
|
|
enabledTabIdentifiers = TabConfiguration.serialize(ids)
|
|
} label: {
|
|
HStack {
|
|
Label(option.title, systemImage: option.icon)
|
|
}
|
|
}
|
|
.foregroundStyle(.primary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Tab Bar")
|
|
.toolbar {
|
|
EditButton()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ConsoleLogsView_Preview: PreviewProvider {
|
|
static var previews: some View {
|
|
ConsoleLogsView()
|
|
}
|
|
}
|