mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Add more accessibility features
This commit is contained in:
parent
0169ec97fe
commit
3b1e779aa6
3 changed files with 159 additions and 26 deletions
|
|
@ -8,6 +8,14 @@ import Foundation
|
|||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
enum AccessibilityAnnouncer {
|
||||
static func announce(_ message: String) {
|
||||
DispatchQueue.main.async {
|
||||
UIAccessibility.post(notification: .announcement, argument: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PairingFileStore {
|
||||
static let fileName = "rp_pairing_file.plist"
|
||||
private static let legacyFileName = "pairingFile.plist"
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ struct HomeView: View {
|
|||
@State private var viewDidAppeared = false
|
||||
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var debugFeedback: DebugFeedback?
|
||||
|
||||
@State var scriptViewShow = false
|
||||
@State private var isShowingConsole = false
|
||||
|
|
@ -89,12 +90,26 @@ struct HomeView: View {
|
|||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
private struct DebugFeedback: Identifiable {
|
||||
let id = UUID()
|
||||
let message: String
|
||||
let isError: Bool
|
||||
let isWorking: Bool
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
InstalledAppsListView(onSelectApp: { selectedBundle in
|
||||
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
|
||||
bundleID = selectedBundle
|
||||
HapticFeedbackHelper.trigger()
|
||||
startJITInBackground(bundleID: selectedBundle)
|
||||
startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
|
||||
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
|
||||
.overlay(alignment: .bottom) {
|
||||
if let debugFeedback {
|
||||
debugFeedbackView(debugFeedback)
|
||||
.padding(.bottom, 40)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startTunnelInBackground()
|
||||
MountingProgress.shared.checkforMounted()
|
||||
|
|
@ -227,6 +242,28 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func debugFeedbackView(_ feedback: DebugFeedback) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
if feedback.isWorking {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
Text(feedback.message)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(Capsule().fill(.ultraThinMaterial))
|
||||
.foregroundStyle(feedback.isError ? .red : .primary)
|
||||
.shadow(radius: 4)
|
||||
.padding(.horizontal, 20)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(feedback.message)
|
||||
}
|
||||
|
||||
private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback {
|
||||
return { pid, debugProxyHandle, remoteServerHandle, semaphore in
|
||||
let model = RunJSViewModel(pid: Int(pid),
|
||||
|
|
@ -250,9 +287,15 @@ 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, displayName: String? = nil) {
|
||||
isProcessing = true
|
||||
let targetName = displayName ?? bundleID ?? pid.map { String(format: "process %d".localized, $0) } ?? "app".localized
|
||||
let startingMessage = String(format: "Starting JIT for %@".localized, targetName)
|
||||
LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))")
|
||||
withAnimation {
|
||||
debugFeedback = DebugFeedback(message: startingMessage, isError: false, isWorking: true)
|
||||
}
|
||||
AccessibilityAnnouncer.announce(startingMessage)
|
||||
|
||||
if triggeredByURLScheme {
|
||||
pubTunnelConnected = false
|
||||
|
|
@ -266,9 +309,31 @@ struct HomeView: View {
|
|||
if triggeredByURLScheme {
|
||||
sleep(1)
|
||||
}
|
||||
let finishProcessing = {
|
||||
|
||||
let finishProcessing: (Bool, String?) -> Void = { success, detail in
|
||||
DispatchQueue.main.async {
|
||||
isProcessing = false
|
||||
let message = success
|
||||
? String(format: "JIT request completed for %@".localized, targetName)
|
||||
: String(format: "JIT failed for %@".localized, targetName)
|
||||
let feedback = DebugFeedback(message: message, isError: !success, isWorking: false)
|
||||
withAnimation {
|
||||
debugFeedback = feedback
|
||||
}
|
||||
AccessibilityAnnouncer.announce(message)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
if debugFeedback?.id == feedback.id {
|
||||
withAnimation {
|
||||
debugFeedback = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
let failureMessage = detail ?? "StikDebug could not launch or attach to the selected app. Check that the VPN is enabled, the pairing file is current, and the app is still installed.".localized
|
||||
showAlert(title: "Failed to Enable JIT".localized, message: failureMessage, showOk: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,16 +351,20 @@ struct HomeView: View {
|
|||
callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script")
|
||||
}
|
||||
|
||||
let logger: LogFunc = { message in if let message { LogManager.shared.addInfoLog(message) } }
|
||||
var lastDebugMessage: String?
|
||||
let logger: LogFunc = { message in
|
||||
if let message {
|
||||
lastDebugMessage = message
|
||||
LogManager.shared.addInfoLog(message)
|
||||
}
|
||||
}
|
||||
var success: Bool
|
||||
if let pid {
|
||||
success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: logger, jsCallback: callback)
|
||||
} else if let bundleID {
|
||||
success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: logger, jsCallback: callback)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
showAlert(title: "Failed to Debug App".localized, message: "Either bundle ID or PID should be specified.".localized, showOk: true)
|
||||
}
|
||||
lastDebugMessage = "Either bundle ID or PID should be specified.".localized
|
||||
success = false
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +377,7 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
finishProcessing()
|
||||
finishProcessing(success, success ? nil : lastDebugMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ struct InstalledAppsListView: View {
|
|||
@AppStorage("pinnedSystemAppNames") private var pinnedSystemAppNames: [String: String] = [:]
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var onSelectApp: (String) -> Void
|
||||
var onSelectApp: (String, String) -> Void
|
||||
var showDoneButton: Bool = true
|
||||
var onImportPairingFile: (() -> Void)? = nil
|
||||
|
||||
|
|
@ -372,12 +372,13 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
bundleID: bundleID, appName: appName,
|
||||
isLaunching: launchingBundles.contains(bundleID),
|
||||
performanceMode: performanceMode
|
||||
) { startLaunching(bundleID: bundleID) }
|
||||
) { startLaunching(bundleID: bundleID, appName: appName) }
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if isPinned {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(.yellow).padding(6)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
|
|
@ -448,20 +449,24 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
if touched { WidgetCenter.shared.reloadAllTimelines() }
|
||||
}
|
||||
|
||||
private func startLaunching(bundleID: String) {
|
||||
private func startLaunching(bundleID: String, appName: String) {
|
||||
guard !launchingBundles.contains(bundleID) else { return }
|
||||
launchingBundles.insert(bundleID)
|
||||
Haptics.selection()
|
||||
AccessibilityAnnouncer.announce(String(format: "Launching %@".localized, appName))
|
||||
|
||||
viewModel.launchWithoutDebug(bundleID: bundleID) { success in
|
||||
launchingBundles.remove(bundleID)
|
||||
|
||||
let message = success ? "Launch request sent".localized : "Launch failed".localized
|
||||
let message = success
|
||||
? String(format: "Launch request sent for %@".localized, appName)
|
||||
: String(format: "Launch failed for %@".localized, appName)
|
||||
let feedback = LaunchFeedback(message: message, success: success)
|
||||
|
||||
if success {
|
||||
Haptics.light()
|
||||
}
|
||||
AccessibilityAnnouncer.announce(message)
|
||||
|
||||
withAnimation {
|
||||
launchFeedback = feedback
|
||||
|
|
@ -521,7 +526,7 @@ struct AppButton: View {
|
|||
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
|
||||
@AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false
|
||||
|
||||
var onSelectApp: (String) -> Void
|
||||
var onSelectApp: (String, String) -> Void
|
||||
let sharedDefaults: UserDefaults
|
||||
let performanceMode: Bool
|
||||
|
||||
|
|
@ -534,7 +539,7 @@ struct AppButton: View {
|
|||
appName: String,
|
||||
recentApps: Binding<[String]>,
|
||||
favoriteApps: Binding<[String]>,
|
||||
onSelectApp: @escaping (String) -> Void,
|
||||
onSelectApp: @escaping (String, String) -> Void,
|
||||
sharedDefaults: UserDefaults,
|
||||
performanceMode: Bool
|
||||
) {
|
||||
|
|
@ -577,6 +582,7 @@ struct AppButton: View {
|
|||
}
|
||||
}
|
||||
.padding(.vertical, loadAppIconsOnJIT ? 4 : 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
|
|
@ -588,8 +594,7 @@ struct AppButton: View {
|
|||
.disabled(!favoriteApps.contains(bundleID) && favoriteApps.count >= 4)
|
||||
}
|
||||
Button {
|
||||
UIPasteboard.general.string = bundleID
|
||||
Haptics.light()
|
||||
copyBundleID()
|
||||
} label: {
|
||||
Label("Copy Bundle ID", systemImage: "doc.on.doc")
|
||||
}
|
||||
|
|
@ -615,8 +620,7 @@ struct AppButton: View {
|
|||
.tint(.yellow)
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = bundleID
|
||||
Haptics.light()
|
||||
copyBundleID()
|
||||
} label: {
|
||||
Label("Copy ID", systemImage: "doc.on.doc")
|
||||
}
|
||||
|
|
@ -638,8 +642,34 @@ struct AppButton: View {
|
|||
}
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("\(appName)")
|
||||
.accessibilityHint("Double-tap to select. Swipe for actions, long-press for options.")
|
||||
.accessibilityLabel(String(format: "Enable JIT for %@".localized, appName))
|
||||
.accessibilityValue(accessibilityValue)
|
||||
.accessibilityHint("Double-tap to open the app and enable JIT. Use the actions rotor for favorites or bundle ID.".localized)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityRemoveTraits(.isStaticText)
|
||||
.accessibilityAction(named: Text(favoriteAccessibilityActionLabel)) {
|
||||
toggleFavorite()
|
||||
}
|
||||
.accessibilityAction(named: Text("Copy Bundle ID".localized)) {
|
||||
copyBundleID()
|
||||
}
|
||||
}
|
||||
|
||||
private var accessibilityValue: String {
|
||||
var parts = [String(format: "Bundle ID %@".localized, bundleID)]
|
||||
if favoriteApps.contains(bundleID) {
|
||||
parts.append("Favorite".localized)
|
||||
}
|
||||
if let assignedScriptName {
|
||||
parts.append(String(format: "Assigned script %@".localized, assignedScriptName))
|
||||
}
|
||||
return parts.joined(separator: ", ")
|
||||
}
|
||||
|
||||
private var favoriteAccessibilityActionLabel: String {
|
||||
favoriteApps.contains(bundleID)
|
||||
? "Remove from Favorites".localized
|
||||
: "Add to Favorites".localized
|
||||
}
|
||||
|
||||
// MARK: Icon
|
||||
|
|
@ -678,18 +708,29 @@ struct AppButton: View {
|
|||
recentApps = Array(recentApps.prefix(3))
|
||||
}
|
||||
persistIfChanged()
|
||||
onSelectApp(bundleID)
|
||||
onSelectApp(bundleID, appName)
|
||||
}
|
||||
|
||||
private func toggleFavorite() {
|
||||
Haptics.light()
|
||||
if favoriteApps.contains(bundleID) {
|
||||
let wasFavorite = favoriteApps.contains(bundleID)
|
||||
if wasFavorite {
|
||||
favoriteApps.removeAll { $0 == bundleID }
|
||||
} else if favoriteApps.count < 4 {
|
||||
favoriteApps.insert(bundleID, at: 0)
|
||||
recentApps.removeAll { $0 == bundleID }
|
||||
} else {
|
||||
AccessibilityAnnouncer.announce("Favorites are full".localized)
|
||||
return
|
||||
}
|
||||
persistIfChanged()
|
||||
AccessibilityAnnouncer.announce(wasFavorite ? "Removed from Favorites".localized : "Added to Favorites".localized)
|
||||
}
|
||||
|
||||
private func copyBundleID() {
|
||||
UIPasteboard.general.string = bundleID
|
||||
Haptics.light()
|
||||
AccessibilityAnnouncer.announce("Bundle ID copied".localized)
|
||||
}
|
||||
|
||||
private func assignScript(_ url: URL?) {
|
||||
|
|
@ -792,6 +833,7 @@ struct LaunchAppRow: View {
|
|||
}
|
||||
}
|
||||
.padding(.vertical, loadAppIconsOnJIT ? 4 : 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isLaunching)
|
||||
|
|
@ -806,8 +848,22 @@ struct LaunchAppRow: View {
|
|||
}
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("\(appName)")
|
||||
.accessibilityHint("Double-tap to launch via CoreDevice.")
|
||||
.accessibilityLabel(String(format: "Launch %@".localized, appName))
|
||||
.accessibilityValue(accessibilityValue)
|
||||
.accessibilityHint(isLaunching
|
||||
? "Launch request in progress.".localized
|
||||
: "Double-tap to launch this app without enabling JIT.".localized)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityRemoveTraits(.isStaticText)
|
||||
.accessibilityAction(named: Text("Launch App".localized)) {
|
||||
guard !isLaunching else { return }
|
||||
launchAction()
|
||||
}
|
||||
}
|
||||
|
||||
private var accessibilityValue: String {
|
||||
let state = isLaunching ? "Launching".localized : "Ready".localized
|
||||
return "\(state), \(String(format: "Bundle ID %@".localized, bundleID))"
|
||||
}
|
||||
|
||||
private var iconView: some View {
|
||||
|
|
@ -1112,7 +1168,7 @@ extension Dictionary: @retroactive RawRepresentable where Key: Codable, Value: C
|
|||
|
||||
struct InstalledAppsListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InstalledAppsListView { _ in }
|
||||
InstalledAppsListView { _, _ in }
|
||||
.environment(\.colorScheme, .dark)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue