mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Move Scripts to Tools tab, add bookmarks to Location Simulator, and replace Home screen with Apps tab
This commit is contained in:
parent
875e3cb04a
commit
d51f8e9b6c
5 changed files with 193 additions and 246 deletions
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
struct JITEnableConfiguration {
|
||||
var bundleID: String? = nil
|
||||
|
|
@ -17,21 +18,12 @@ struct JITEnableConfiguration {
|
|||
|
||||
struct HomeView: View {
|
||||
|
||||
@AppStorage("username") private var username = "User"
|
||||
@AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
@AppStorage("bundleID") private var bundleID: String = ""
|
||||
@State private var isProcessing = false
|
||||
@State private var isShowingInstalledApps = false
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var pairingFileExists: Bool = false
|
||||
@State private var showPairingFileMessage = false
|
||||
@State private var pairingFileIsValid = false
|
||||
@State private var isImportingFile = false
|
||||
@State private var importProgress: Float = 0.0
|
||||
|
||||
@State private var viewDidAppeared = false
|
||||
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
|
||||
@State var scriptViewShow = false
|
||||
@State private var isShowingConsole = false
|
||||
|
|
@ -39,142 +31,22 @@ struct HomeView: View {
|
|||
@State var jsModel: RunJSViewModel?
|
||||
@ObservedObject private var mounting = MountingProgress.shared
|
||||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 25) {
|
||||
Spacer()
|
||||
VStack(spacing: 5) {
|
||||
Text("Welcome to StikDebug, \(username)!")
|
||||
.font(.system(.largeTitle, design: .rounded))
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
|
||||
Text(pairingFileExists ? "Tap \"Enable JIT\" to get started" : "Pick pairing file to get started")
|
||||
.font(.system(.subheadline, design: .rounded))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
// Main action button - changes based on whether we have a pairing file
|
||||
Button(action: {
|
||||
|
||||
|
||||
if pairingFileExists {
|
||||
// Refresh heartbeat
|
||||
pubHeartBeat = false
|
||||
startHeartbeatInBackground()
|
||||
|
||||
// Got a pairing file, check mount status
|
||||
let mountStatus = checkMountStatus()
|
||||
if mountStatus == .notMounted {
|
||||
showAlert(title: "Device Not Mounted", message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.", showOk: true) { cool in
|
||||
// No Need
|
||||
}
|
||||
return
|
||||
} else if mountStatus == .unreachable {
|
||||
// Don't show a separate error here — the heartbeat
|
||||
// will fail and show its own connectivity error.
|
||||
return
|
||||
}
|
||||
|
||||
isShowingInstalledApps = true
|
||||
|
||||
} else {
|
||||
// No pairing file yet, let's get one
|
||||
isShowingPairingFilePicker = true
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: pairingFileExists ? "bolt.fill" : "doc.badge.plus")
|
||||
.font(.system(size: 20))
|
||||
Text(pairingFileExists ? "Enable JIT" : "Select Pairing File")
|
||||
.font(.system(.title3, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(16)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
Button(action: {
|
||||
isShowingConsole = true
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "terminal")
|
||||
.font(.system(size: 20))
|
||||
Text("Open Console")
|
||||
.font(.system(.title3, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.foregroundColor(.primary)
|
||||
.cornerRadius(16)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// Status message area - keeps layout consistent
|
||||
ZStack {
|
||||
// Progress bar for importing file
|
||||
if isImportingFile {
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Text("Processing pairing file...")
|
||||
.font(.system(.caption, design: .rounded))
|
||||
Spacer()
|
||||
Text("\(Int(importProgress * 100))%")
|
||||
.font(.system(.caption, design: .rounded))
|
||||
}
|
||||
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.black.opacity(0.2))
|
||||
.frame(height: 8)
|
||||
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.green)
|
||||
.frame(width: geometry.size.width * CGFloat(importProgress), height: 8)
|
||||
.animation(.linear(duration: 0.3), value: importProgress)
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
|
||||
// Success message
|
||||
if showPairingFileMessage && pairingFileIsValid {
|
||||
Text("✓ Pairing file successfully imported")
|
||||
.font(.system(.callout, design: .rounded))
|
||||
.foregroundColor(.green)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 12)
|
||||
.background(Color.green.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
// Invisible text to reserve space - no layout jumps
|
||||
Text(" ").opacity(0)
|
||||
}
|
||||
.frame(height: isImportingFile ? 60 : 30) // Adjust height based on what's showing
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
InstalledAppsListView(onSelectApp: { selectedBundle in
|
||||
bundleID = selectedBundle
|
||||
HapticFeedbackHelper.trigger()
|
||||
startJITInBackground(bundleID: selectedBundle)
|
||||
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
|
||||
.onAppear {
|
||||
checkPairingFileExists()
|
||||
startHeartbeatInBackground()
|
||||
MountingProgress.shared.checkforMounted()
|
||||
viewDidAppeared = true
|
||||
if let config = pendingJITEnableConfiguration {
|
||||
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
|
||||
pendingJITEnableConfiguration = nil
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady)) { notification in
|
||||
guard let model = notification.userInfo?["model"] as? RunJSViewModel else { return }
|
||||
|
|
@ -185,85 +57,10 @@ struct HomeView: View {
|
|||
scriptViewShow = true
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
checkPairingFileExists()
|
||||
if mounting.mountingThread == nil && !mounting.coolisMounted {
|
||||
MountingProgress.shared.checkforMounted()
|
||||
}
|
||||
}
|
||||
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", 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!")
|
||||
|
||||
// Show progress bar and initialize progress
|
||||
DispatchQueue.main.async {
|
||||
isImportingFile = true
|
||||
importProgress = 0.0
|
||||
pairingFileExists = true
|
||||
}
|
||||
|
||||
startHeartbeatInBackground()
|
||||
|
||||
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||
DispatchQueue.main.async {
|
||||
if importProgress < 1.0 {
|
||||
importProgress += 0.25
|
||||
} else {
|
||||
timer.invalidate()
|
||||
isImportingFile = false
|
||||
pairingFileIsValid = true
|
||||
|
||||
// Show success message
|
||||
withAnimation {
|
||||
showPairingFileMessage = true
|
||||
}
|
||||
|
||||
// Hide message after delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
withAnimation {
|
||||
showPairingFileMessage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure timer keeps running
|
||||
RunLoop.current.add(progressTimer, forMode: .common)
|
||||
|
||||
} catch {
|
||||
print("Error copying file: \(error)")
|
||||
}
|
||||
} else {
|
||||
print("Source file does not exist.")
|
||||
}
|
||||
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
case .failure(let error):
|
||||
print("Failed to import file: \(error)")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isShowingInstalledApps) {
|
||||
InstalledAppsListView { selectedBundle in
|
||||
bundleID = selectedBundle
|
||||
isShowingInstalledApps = false
|
||||
HapticFeedbackHelper.trigger()
|
||||
startJITInBackground(bundleID: selectedBundle)
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
guard let host = url.host() else { return }
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
|
|
@ -324,11 +121,26 @@ struct HomeView: View {
|
|||
break
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
viewDidAppeared = true
|
||||
if let config = pendingJITEnableConfiguration {
|
||||
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
|
||||
pendingJITEnableConfiguration = nil
|
||||
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!, .propertyList]) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
let fileManager = FileManager.default
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
if fileManager.fileExists(atPath: url.path) {
|
||||
do {
|
||||
let dest = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
|
||||
if fileManager.fileExists(atPath: dest.path) {
|
||||
try fileManager.removeItem(at: dest)
|
||||
}
|
||||
try fileManager.copyItem(at: url, to: dest)
|
||||
startHeartbeatInBackground()
|
||||
} catch {
|
||||
print("Error copying pairing file: \(error)")
|
||||
}
|
||||
}
|
||||
if accessing { url.stopAccessingSecurityScopedResource() }
|
||||
case .failure(let error):
|
||||
print("Failed to import pairing file: \(error)")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isShowingConsole) {
|
||||
|
|
@ -360,7 +172,6 @@ struct HomeView: View {
|
|||
}
|
||||
|
||||
|
||||
|
||||
private func autoScript(for bundleID: String) -> (data: Data, name: String)? {
|
||||
guard ProcessInfo.processInfo.hasTXM else { return nil }
|
||||
guard #available(iOS 26, *) else { return nil }
|
||||
|
|
@ -442,10 +253,6 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func checkPairingFileExists() {
|
||||
pairingFileExists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path)
|
||||
}
|
||||
|
||||
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))")
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ struct InstalledAppsListView: View {
|
|||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var onSelectApp: (String) -> Void
|
||||
var showDoneButton: Bool = true
|
||||
var onImportPairingFile: (() -> Void)? = nil
|
||||
|
||||
|
||||
private var currentSearchBinding: Binding<String> {
|
||||
|
|
@ -164,7 +166,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
tabContent(for: selectedTab)
|
||||
.transition(.opacity)
|
||||
.transaction { t in t.disablesAnimations = true }
|
||||
.navigationTitle("Installed Apps".localized)
|
||||
.navigationTitle(selectedTab == .debuggable ? "Enable JIT".localized : "Launch Apps".localized)
|
||||
.searchable(
|
||||
text: currentSearchBinding,
|
||||
placement: .navigationBarDrawer(displayMode: .always),
|
||||
|
|
@ -182,10 +184,19 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
.pickerStyle(.segmented)
|
||||
.frame(width: 220)
|
||||
}
|
||||
if let onImportPairingFile {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: onImportPairingFile) {
|
||||
Image(systemName: "doc.badge.plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
if showDoneButton {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") { dismiss() }.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
}
|
||||
}
|
||||
|
|
@ -305,7 +316,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
}
|
||||
Section("All Applications".localized) {
|
||||
Section("Apps with get-task-allow".localized) {
|
||||
ForEach(filteredDebuggableApps, id: \.key) { bundleID, appName in
|
||||
AppButton(
|
||||
bundleID: bundleID, appName: appName,
|
||||
|
|
@ -342,7 +353,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
.listRowBackground(Color.clear)
|
||||
}
|
||||
} else {
|
||||
Section("Other Apps".localized) {
|
||||
Section("All Apps".localized) {
|
||||
ForEach(filteredLaunchApps, id: \.key) { bundleID, appName in
|
||||
let isPinned = pinnedSystemApps.contains(bundleID)
|
||||
LaunchAppRow(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ struct MainTabView: View {
|
|||
|
||||
private var configurableTabs: [TabDescriptor] {
|
||||
var tabs: [TabDescriptor] = [
|
||||
TabDescriptor(id: "home", title: "Home", systemImage: "house") { AnyView(HomeView()) },
|
||||
TabDescriptor(id: "home", title: "Apps", systemImage: "square.grid.2x2") { AnyView(HomeView()) },
|
||||
TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) },
|
||||
TabDescriptor(id: "tools", title: "Tools", systemImage: "wrench.and.screwdriver") { AnyView(ToolsView()) },
|
||||
TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) },
|
||||
|
|
@ -62,10 +62,10 @@ struct MainTabView: View {
|
|||
}
|
||||
|
||||
private var displayTabs: [TabDescriptor] {
|
||||
var tabs = ["home", "scripts", "tools"].compactMap { id in
|
||||
var tabs = ["home", "tools"].compactMap { id in
|
||||
configurableTabs.first(where: { $0.id == id })
|
||||
}
|
||||
tabs.insert(settingsTab, at: min(3, tabs.count))
|
||||
tabs.insert(settingsTab, at: min(2, tabs.count))
|
||||
return tabs
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@ extension CLLocationCoordinate2D: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Bookmark Model
|
||||
|
||||
struct LocationBookmark: Identifiable, Codable {
|
||||
var id: UUID = UUID()
|
||||
var name: String
|
||||
var latitude: Double
|
||||
var longitude: Double
|
||||
|
||||
var coordinate: CLLocationCoordinate2D {
|
||||
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Search Completer
|
||||
|
||||
@MainActor
|
||||
|
|
@ -61,6 +74,12 @@ struct LocationSimulationView: View {
|
|||
@State private var searchText = ""
|
||||
@StateObject private var searchCompleter = LocationSearchCompleter()
|
||||
|
||||
// Bookmarks
|
||||
@State private var bookmarks: [LocationBookmark] = []
|
||||
@State private var showBookmarks = false
|
||||
@State private var showSaveBookmark = false
|
||||
@State private var newBookmarkName = ""
|
||||
|
||||
private var pairingFilePath: String {
|
||||
URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path()
|
||||
}
|
||||
|
|
@ -166,6 +185,14 @@ struct LocationSimulationView: View {
|
|||
Button("Simulate Location", action: simulate)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!pairingExists || isBusy)
|
||||
|
||||
Button {
|
||||
showSaveBookmark = true
|
||||
} label: {
|
||||
Image(systemName: "bookmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.blue)
|
||||
}
|
||||
} else {
|
||||
Text("Tap map to drop pin")
|
||||
|
|
@ -179,6 +206,13 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button {
|
||||
showBookmarks = true
|
||||
} label: {
|
||||
Image(systemName: "bookmark.fill")
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
TextField("Search location...", text: $searchText)
|
||||
.padding(.leading, 6)
|
||||
|
|
@ -193,6 +227,25 @@ struct LocationSimulationView: View {
|
|||
} message: {
|
||||
Text(alertMessage)
|
||||
}
|
||||
.alert("Save Bookmark", isPresented: $showSaveBookmark) {
|
||||
TextField("Name", text: $newBookmarkName)
|
||||
Button("Save") { addBookmark() }
|
||||
Button("Cancel", role: .cancel) { newBookmarkName = "" }
|
||||
} message: {
|
||||
Text("Enter a name for this location.")
|
||||
}
|
||||
.sheet(isPresented: $showBookmarks) {
|
||||
BookmarksView(bookmarks: $bookmarks) { bookmark in
|
||||
coordinate = bookmark.coordinate
|
||||
showBookmarks = false
|
||||
} onDelete: { offsets in
|
||||
bookmarks.remove(atOffsets: offsets)
|
||||
saveBookmarks()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadBookmarks()
|
||||
}
|
||||
.onDisappear {
|
||||
stopResendLoop()
|
||||
if backgroundTaskID != .invalid {
|
||||
|
|
@ -202,6 +255,35 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Bookmarks
|
||||
|
||||
private func loadBookmarks() {
|
||||
guard let data = UserDefaults.standard.data(forKey: "locationBookmarks"),
|
||||
let decoded = try? JSONDecoder().decode([LocationBookmark].self, from: data) else { return }
|
||||
bookmarks = decoded
|
||||
}
|
||||
|
||||
private func saveBookmarks() {
|
||||
if let data = try? JSONEncoder().encode(bookmarks) {
|
||||
UserDefaults.standard.set(data, forKey: "locationBookmarks")
|
||||
}
|
||||
}
|
||||
|
||||
private func addBookmark() {
|
||||
guard let coord = coordinate else { return }
|
||||
let name = newBookmarkName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let bookmark = LocationBookmark(
|
||||
name: name.isEmpty ? String(format: "%.4f, %.4f", coord.latitude, coord.longitude) : name,
|
||||
latitude: coord.latitude,
|
||||
longitude: coord.longitude
|
||||
)
|
||||
bookmarks.append(bookmark)
|
||||
saveBookmarks()
|
||||
newBookmarkName = ""
|
||||
}
|
||||
|
||||
// MARK: - Location
|
||||
|
||||
private func selectSearchResult(_ result: MKLocalSearchCompletion) {
|
||||
searchText = ""
|
||||
searchCompleter.results = []
|
||||
|
|
@ -289,3 +371,49 @@ struct LocationSimulationView: View {
|
|||
resendTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Bookmarks Sheet
|
||||
|
||||
struct BookmarksView: View {
|
||||
@Binding var bookmarks: [LocationBookmark]
|
||||
let onSelect: (LocationBookmark) -> Void
|
||||
let onDelete: (IndexSet) -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if bookmarks.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Bookmarks",
|
||||
systemImage: "bookmark.slash",
|
||||
description: Text("Drop a pin on the map and tap the bookmark icon to save a location.")
|
||||
)
|
||||
} else {
|
||||
List {
|
||||
ForEach(bookmarks) { bookmark in
|
||||
Button {
|
||||
onSelect(bookmark)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(bookmark.name)
|
||||
.foregroundStyle(.primary)
|
||||
Text(String(format: "%.6f, %.6f", bookmark.latitude, bookmark.longitude))
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete(perform: onDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Bookmarks")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if !bookmarks.isEmpty {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ struct ToolsView: View {
|
|||
|
||||
private var tools: [ToolItem] {
|
||||
[
|
||||
ToolItem(id: "scripts", title: "Scripts", detail: "Manage and run JS scripts", systemImage: "scroll", destination: AnyView(ScriptListView())),
|
||||
ToolItem(id: "console", title: "Console", detail: "Live device logs", systemImage: "terminal", destination: AnyView(ConsoleLogsView())),
|
||||
ToolItem(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", systemImage: "iphone.and.arrow.forward", destination: AnyView(DeviceInfoView())),
|
||||
ToolItem(id: "profiles", title: "App Expiry", detail: "Check app expiration dates", systemImage: "calendar.badge.clock", destination: AnyView(ProfileView())),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue