mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
* Heartbeat rework * remove some debug loggings * stik mini tool poc * Saperate JITEnableContext into extensions and put each feature into their own files. Fix local pairing file is removed after switched to external device by directly loading the remote device's pairing file in place. * custom origin & xmlhttprequest * Fix some memory leak * allow background script access idevice (codes are from the old JSSupport branch) Tests are not carried out but most of the js api should work. Do we need to obfuscate some functions' name? * Fix JITEnableContext singleton initializer race condition * Fix instprox callback --------- Co-authored-by: Stephen B <158498287+StephenDev0@users.noreply.github.com>
253 lines
10 KiB
Swift
253 lines
10 KiB
Swift
//
|
|
// MainTabView.swift
|
|
// StikJIT
|
|
//
|
|
// Created by Stephen on 3/27/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
private struct TabDescriptor: Identifiable {
|
|
let id: String
|
|
let title: String
|
|
let systemImage: String
|
|
let builder: () -> AnyView
|
|
}
|
|
|
|
extension Notification.Name {
|
|
static let switchToTab = Notification.Name("MainTabSwitchNotification")
|
|
}
|
|
|
|
struct MainTabView: View {
|
|
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
|
|
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
|
@AppStorage(TabConfiguration.storageKey) private var enabledTabIdentifiers: String = TabConfiguration.defaultRawValue
|
|
@AppStorage("primaryTabSelection") private var selection: String = TabConfiguration.defaultIDs.first ?? "home"
|
|
@State private var switchObserver: Any?
|
|
@State private var detachedTab: TabDescriptor?
|
|
@State private var didSetInitialHome = false
|
|
|
|
// Update checking
|
|
@State private var showForceUpdate: Bool = false
|
|
@State private var latestVersion: String? = nil
|
|
|
|
@Environment(\.themeExpansionManager) private var themeExpansion
|
|
|
|
private var accentColor: Color {
|
|
themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue
|
|
}
|
|
|
|
private var preferredScheme: ColorScheme? {
|
|
themeExpansion?.preferredColorScheme(for: appThemeRaw)
|
|
}
|
|
|
|
private var isAppStoreBuild: Bool {
|
|
#if APPSTORE
|
|
return true
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
private var configurableTabs: [TabDescriptor] {
|
|
var tabs: [TabDescriptor] = [
|
|
TabDescriptor(id: "home", title: "Home", systemImage: "house") { AnyView(HomeView()) },
|
|
TabDescriptor(id: "console", title: "Console", systemImage: "terminal") { AnyView(ConsoleLogsView()) },
|
|
TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) },
|
|
TabDescriptor(id: "profiles", title: "Profiles", systemImage: "magazine.fill") { AnyView(ProfileView()) },
|
|
TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) },
|
|
TabDescriptor(id: "tools", title: "Mini Tools", systemImage: "shippingbox.fill") { AnyView(MiniToolListView()) },
|
|
]
|
|
if FeatureFlags.showBetaTabs {
|
|
tabs.append(TabDescriptor(id: "processes", title: "Processes", systemImage: "rectangle.stack.person.crop") { AnyView(ProcessInspectorView()) })
|
|
tabs.append(TabDescriptor(id: "devicelibrary", title: "Devices", systemImage: "list.bullet.rectangle") { AnyView(DeviceLibraryView()) })
|
|
if FeatureFlags.isLocationSpoofingEnabled {
|
|
tabs.append(TabDescriptor(id: "location", title: "Location", systemImage: "location") { AnyView(LocationSimulationView()) })
|
|
}
|
|
}
|
|
return tabs
|
|
}
|
|
|
|
private var availableTabs: [TabDescriptor] {
|
|
configurableTabs.filter { descriptor in
|
|
descriptor.id != "location" || (!isAppStoreBuild && FeatureFlags.isLocationSpoofingEnabled && FeatureFlags.showBetaTabs)
|
|
}
|
|
}
|
|
|
|
private let settingsTab = TabDescriptor(id: "settings", title: "Settings", systemImage: "gearshape.fill") {
|
|
AnyView(SettingsView())
|
|
}
|
|
|
|
private var selectedTabDescriptors: [TabDescriptor] {
|
|
let ids = TabConfiguration.sanitize(raw: enabledTabIdentifiers)
|
|
return ids.compactMap { id in
|
|
availableTabs.first(where: { $0.id == id })
|
|
}
|
|
}
|
|
|
|
private func ensureSelectionIsValid() {
|
|
let ids = selectedTabDescriptors.map { $0.id }
|
|
if ids.contains(selection) || selection == settingsTab.id {
|
|
return
|
|
}
|
|
selection = ids.first ?? settingsTab.id
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Allow global themed background to show
|
|
Color.clear.ignoresSafeArea()
|
|
|
|
// Main tabs
|
|
TabView(selection: $selection) {
|
|
ForEach(selectedTabDescriptors) { descriptor in
|
|
descriptor.builder()
|
|
.tabItem { Label(descriptor.title, systemImage: descriptor.systemImage) }
|
|
.tag(descriptor.id)
|
|
}
|
|
|
|
settingsTab.builder()
|
|
.tabItem { Label(settingsTab.title, systemImage: settingsTab.systemImage) }
|
|
.tag(settingsTab.id)
|
|
}
|
|
.id((themeExpansion?.hasThemeExpansion == true) ? customAccentColorHex : "default-accent")
|
|
.tint(accentColor)
|
|
.preferredColorScheme(preferredScheme)
|
|
.onAppear {
|
|
enabledTabIdentifiers = TabConfiguration.serialize(TabConfiguration.sanitize(raw: enabledTabIdentifiers))
|
|
ensureSelectionIsValid()
|
|
if !didSetInitialHome {
|
|
if selectedTabDescriptors.contains(where: { $0.id == "home" }) {
|
|
selection = "home"
|
|
} else if let descriptor = availableTabs.first(where: { $0.id == "home" }) {
|
|
detachedTab = descriptor
|
|
}
|
|
didSetInitialHome = true
|
|
}
|
|
checkForUpdate()
|
|
switchObserver = NotificationCenter.default.addObserver(forName: .switchToTab, object: nil, queue: .main) { note in
|
|
guard let id = note.object as? String else { return }
|
|
if selectedTabDescriptors.contains(where: { $0.id == id }) {
|
|
selection = id
|
|
} else if let descriptor = availableTabs.first(where: { $0.id == id }) {
|
|
detachedTab = descriptor
|
|
}
|
|
}
|
|
}
|
|
.onDisappear {
|
|
if let observer = switchObserver {
|
|
NotificationCenter.default.removeObserver(observer)
|
|
switchObserver = nil
|
|
}
|
|
}
|
|
.onChange(of: enabledTabIdentifiers) { _ in
|
|
ensureSelectionIsValid()
|
|
}
|
|
.sheet(item: $detachedTab) { descriptor in
|
|
NavigationStack {
|
|
descriptor.builder()
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Close") {
|
|
detachedTab = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if showForceUpdate {
|
|
ZStack {
|
|
Color.black.opacity(0.001).ignoresSafeArea()
|
|
|
|
appGlassCard {
|
|
VStack(spacing: 20) {
|
|
Text("Update Required")
|
|
.font(.title.bold())
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text("A new version (\(latestVersion ?? "unknown")) is available. Please update to continue using the app.")
|
|
.multilineTextAlignment(.center)
|
|
.font(.callout)
|
|
.foregroundColor(.secondary)
|
|
.padding(.horizontal)
|
|
|
|
Button(action: {
|
|
let urlString: String
|
|
if isAppStoreBuild {
|
|
urlString = "itms-apps://itunes.apple.com/app/id6744045754"
|
|
} else {
|
|
urlString = "altstore://source?url=https://StikDebug.xyz/apps.json"
|
|
}
|
|
if let url = URL(string: urlString) {
|
|
UIApplication.shared.open(url)
|
|
}
|
|
}) {
|
|
Text("Update Now")
|
|
.font(.headline.weight(.semibold))
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 12)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(Color.accentColor)
|
|
)
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.top, 10)
|
|
}
|
|
}
|
|
.padding(.horizontal, 40)
|
|
}
|
|
.transition(.opacity.combined(with: .scale))
|
|
.animation(.easeInOut, value: showForceUpdate)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Update Checker
|
|
private func checkForUpdate() {
|
|
guard let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return }
|
|
|
|
fetchLatestVersion { latest in
|
|
latestVersion = latest
|
|
if let latest = latest,
|
|
latest.compare(currentVersion, options: .numeric) == .orderedDescending {
|
|
DispatchQueue.main.async {
|
|
showForceUpdate = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func fetchLatestVersion(completion: @escaping (String?) -> Void) {
|
|
guard let url = URL(string: "https://itunes.apple.com/lookup?id=6744045754") else {
|
|
completion(nil)
|
|
return
|
|
}
|
|
|
|
URLSession.shared.dataTask(with: url) { data, _, _ in
|
|
guard let data = data else {
|
|
completion(nil)
|
|
return
|
|
}
|
|
do {
|
|
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let results = json["results"] as? [[String: Any]],
|
|
let appStoreVersion = results.first?["version"] as? String {
|
|
completion(appStoreVersion)
|
|
} else {
|
|
completion(nil)
|
|
}
|
|
} catch {
|
|
completion(nil)
|
|
}
|
|
}.resume()
|
|
}
|
|
}
|
|
|
|
struct MainTabView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
MainTabView()
|
|
.themeExpansionManager(ThemeExpansionManager(previewUnlocked: true))
|
|
}
|
|
}
|