@@ -39,7 +39,7 @@ struct InstalledAppsListView: View {
3939 @AppStorage ( " pinnedSystemAppNames " ) private var pinnedSystemAppNames : [ String : String ] = [ : ]
4040
4141 @Environment ( \. dismiss) private var dismiss
42- var onSelectApp : ( String ) -> Void
42+ var onSelectApp : ( String , String ) -> Void
4343 var showDoneButton : Bool = true
4444 var onImportPairingFile : ( ( ) -> Void ) ? = nil
4545
@@ -372,12 +372,13 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
372372 bundleID: bundleID, appName: appName,
373373 isLaunching: launchingBundles. contains ( bundleID) ,
374374 performanceMode: performanceMode
375- ) { startLaunching ( bundleID: bundleID) }
375+ ) { startLaunching ( bundleID: bundleID, appName : appName ) }
376376 . overlay ( alignment: . topTrailing) {
377377 if isPinned {
378378 Image ( systemName: " star.fill " )
379379 . font ( . system( size: 12 , weight: . semibold) )
380380 . foregroundStyle ( . yellow) . padding ( 6 )
381+ . accessibilityHidden ( true )
381382 }
382383 }
383384 . contextMenu {
@@ -448,20 +449,24 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
448449 if touched { WidgetCenter . shared. reloadAllTimelines ( ) }
449450 }
450451
451- private func startLaunching( bundleID: String ) {
452+ private func startLaunching( bundleID: String , appName : String ) {
452453 guard !launchingBundles. contains ( bundleID) else { return }
453454 launchingBundles. insert ( bundleID)
454455 Haptics . selection ( )
456+ AccessibilityAnnouncer . announce ( String ( format: " Launching %@ " . localized, appName) )
455457
456458 viewModel. launchWithoutDebug ( bundleID: bundleID) { success in
457459 launchingBundles. remove ( bundleID)
458460
459- let message = success ? " Launch request sent " . localized : " Launch failed " . localized
461+ let message = success
462+ ? String ( format: " Launch request sent for %@ " . localized, appName)
463+ : String ( format: " Launch failed for %@ " . localized, appName)
460464 let feedback = LaunchFeedback ( message: message, success: success)
461465
462466 if success {
463467 Haptics . light ( )
464468 }
469+ AccessibilityAnnouncer . announce ( message)
465470
466471 withAnimation {
467472 launchFeedback = feedback
@@ -521,7 +526,7 @@ struct AppButton: View {
521526 @AppStorage ( " loadAppIconsOnJIT " ) private var loadAppIconsOnJIT = true
522527 @AppStorage ( " enableAdvancedOptions " ) private var enableAdvancedOptions = false
523528
524- var onSelectApp : ( String ) -> Void
529+ var onSelectApp : ( String , String ) -> Void
525530 let sharedDefaults : UserDefaults
526531 let performanceMode : Bool
527532
@@ -534,7 +539,7 @@ struct AppButton: View {
534539 appName: String ,
535540 recentApps: Binding < [ String ] > ,
536541 favoriteApps: Binding < [ String ] > ,
537- onSelectApp: @escaping ( String ) -> Void ,
542+ onSelectApp: @escaping ( String , String ) -> Void ,
538543 sharedDefaults: UserDefaults ,
539544 performanceMode: Bool
540545 ) {
@@ -577,6 +582,7 @@ struct AppButton: View {
577582 }
578583 }
579584 . padding ( . vertical, loadAppIconsOnJIT ? 4 : 8 )
585+ . contentShape ( Rectangle ( ) )
580586 }
581587 . buttonStyle ( . plain)
582588 . contextMenu {
@@ -588,8 +594,7 @@ struct AppButton: View {
588594 . disabled ( !favoriteApps. contains ( bundleID) && favoriteApps. count >= 4 )
589595 }
590596 Button {
591- UIPasteboard . general. string = bundleID
592- Haptics . light ( )
597+ copyBundleID ( )
593598 } label: {
594599 Label ( " Copy Bundle ID " , systemImage: " doc.on.doc " )
595600 }
@@ -615,8 +620,7 @@ struct AppButton: View {
615620 . tint ( . yellow)
616621
617622 Button {
618- UIPasteboard . general. string = bundleID
619- Haptics . light ( )
623+ copyBundleID ( )
620624 } label: {
621625 Label ( " Copy ID " , systemImage: " doc.on.doc " )
622626 }
@@ -638,8 +642,34 @@ struct AppButton: View {
638642 }
639643 }
640644 . accessibilityElement ( children: . ignore)
641- . accessibilityLabel ( " \( appName) " )
642- . accessibilityHint ( " Double-tap to select. Swipe for actions, long-press for options. " )
645+ . accessibilityLabel ( String ( format: " Enable JIT for %@ " . localized, appName) )
646+ . accessibilityValue ( accessibilityValue)
647+ . accessibilityHint ( " Double-tap to open the app and enable JIT. Use the actions rotor for favorites or bundle ID. " . localized)
648+ . accessibilityAddTraits ( . isButton)
649+ . accessibilityRemoveTraits ( . isStaticText)
650+ . accessibilityAction ( named: Text ( favoriteAccessibilityActionLabel) ) {
651+ toggleFavorite ( )
652+ }
653+ . accessibilityAction ( named: Text ( " Copy Bundle ID " . localized) ) {
654+ copyBundleID ( )
655+ }
656+ }
657+
658+ private var accessibilityValue : String {
659+ var parts = [ String ( format: " Bundle ID %@ " . localized, bundleID) ]
660+ if favoriteApps. contains ( bundleID) {
661+ parts. append ( " Favorite " . localized)
662+ }
663+ if let assignedScriptName {
664+ parts. append ( String ( format: " Assigned script %@ " . localized, assignedScriptName) )
665+ }
666+ return parts. joined ( separator: " , " )
667+ }
668+
669+ private var favoriteAccessibilityActionLabel : String {
670+ favoriteApps. contains ( bundleID)
671+ ? " Remove from Favorites " . localized
672+ : " Add to Favorites " . localized
643673 }
644674
645675 // MARK: Icon
@@ -678,18 +708,29 @@ struct AppButton: View {
678708 recentApps = Array ( recentApps. prefix ( 3 ) )
679709 }
680710 persistIfChanged ( )
681- onSelectApp ( bundleID)
711+ onSelectApp ( bundleID, appName )
682712 }
683713
684714 private func toggleFavorite( ) {
685715 Haptics . light ( )
686- if favoriteApps. contains ( bundleID) {
716+ let wasFavorite = favoriteApps. contains ( bundleID)
717+ if wasFavorite {
687718 favoriteApps. removeAll { $0 == bundleID }
688719 } else if favoriteApps. count < 4 {
689720 favoriteApps. insert ( bundleID, at: 0 )
690721 recentApps. removeAll { $0 == bundleID }
722+ } else {
723+ AccessibilityAnnouncer . announce ( " Favorites are full " . localized)
724+ return
691725 }
692726 persistIfChanged ( )
727+ AccessibilityAnnouncer . announce ( wasFavorite ? " Removed from Favorites " . localized : " Added to Favorites " . localized)
728+ }
729+
730+ private func copyBundleID( ) {
731+ UIPasteboard . general. string = bundleID
732+ Haptics . light ( )
733+ AccessibilityAnnouncer . announce ( " Bundle ID copied " . localized)
693734 }
694735
695736 private func assignScript( _ url: URL ? ) {
@@ -792,6 +833,7 @@ struct LaunchAppRow: View {
792833 }
793834 }
794835 . padding ( . vertical, loadAppIconsOnJIT ? 4 : 8 )
836+ . contentShape ( Rectangle ( ) )
795837 }
796838 . buttonStyle ( . plain)
797839 . disabled ( isLaunching)
@@ -806,8 +848,22 @@ struct LaunchAppRow: View {
806848 }
807849 }
808850 . accessibilityElement ( children: . ignore)
809- . accessibilityLabel ( " \( appName) " )
810- . accessibilityHint ( " Double-tap to launch via CoreDevice. " )
851+ . accessibilityLabel ( String ( format: " Launch %@ " . localized, appName) )
852+ . accessibilityValue ( accessibilityValue)
853+ . accessibilityHint ( isLaunching
854+ ? " Launch request in progress. " . localized
855+ : " Double-tap to launch this app without enabling JIT. " . localized)
856+ . accessibilityAddTraits ( . isButton)
857+ . accessibilityRemoveTraits ( . isStaticText)
858+ . accessibilityAction ( named: Text ( " Launch App " . localized) ) {
859+ guard !isLaunching else { return }
860+ launchAction ( )
861+ }
862+ }
863+
864+ private var accessibilityValue : String {
865+ let state = isLaunching ? " Launching " . localized : " Ready " . localized
866+ return " \( state) , \( String ( format: " Bundle ID %@ " . localized, bundleID) ) "
811867 }
812868
813869 private var iconView : some View {
@@ -1112,7 +1168,7 @@ extension Dictionary: @retroactive RawRepresentable where Key: Codable, Value: C
11121168
11131169struct InstalledAppsListView_Previews : PreviewProvider {
11141170 static var previews : some View {
1115- InstalledAppsListView { _ in }
1171+ InstalledAppsListView { _, _ in }
11161172 . environment ( \. colorScheme, . dark)
11171173 }
11181174}
0 commit comments