beatmatchr/Desktop/YabaiPro/Sources/AppDropdownView.swift
BusyBee3333 7694d965c9 feat: Add structured signal editor with app dropdown and action builder
- Add AppDiscovery provider for running app enumeration
- Implement AppDropdownView with auto-launch functionality
- Create SignalAction models for 40+ yabai commands
- Build ActionBuilderView with nested parameter controls
- Add LiveShellPreview for real-time shell command generation
- Implement ActionValidator for conflict detection
- Add migration parser for existing raw action strings
- Include feature flag for safe rollout
- Maintain full backward compatibility
2025-12-31 01:44:13 -05:00

149 lines
4.7 KiB
Swift

//
// AppDropdownView.swift
// YabaiPro
//
// Created by Jake Shore
// Copyright © 2024 Jake Shore. All rights reserved.
//
import SwiftUI
struct AppDropdownView: View {
@Binding var selectedApp: String
let title: String
let placeholder: String
@State private var discoveredApps: [DiscoveredApp] = []
@State private var isLoading = false
@State private var showCustomInput = false
@State private var customAppName = ""
private let appDiscovery = AppDiscovery.shared
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "app.badge")
.foregroundColor(.blue)
Text(title)
.font(.headline)
.foregroundColor(.primary)
}
if showCustomInput {
VStack(alignment: .leading, spacing: 8) {
HStack {
TextField(placeholder, text: $customAppName)
.textFieldStyle(.roundedBorder)
.onSubmit {
if !customAppName.isEmpty {
selectedApp = customAppName
}
}
Button(action: {
showCustomInput = false
customAppName = ""
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
Text("Enter application name (e.g., Safari, Chrome)")
.font(.caption)
.foregroundColor(.secondary)
}
} else {
Picker("", selection: $selectedApp) {
Text("Any Application").tag("")
Divider()
ForEach(discoveredApps) { app in
HStack {
Text(app.displayName)
if !app.isRunning {
Image(systemName: "power")
.foregroundColor(.orange)
.font(.caption)
}
}
.tag(app.bundleIdentifier ?? app.displayName)
}
Divider()
Text("Custom Application...").tag("__custom__")
}
.pickerStyle(.menu)
.labelsHidden()
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
.onChange(of: selectedApp) { newValue in
handleAppSelection(newValue)
}
}
Text("Select an application or choose 'Custom' to enter manually")
.font(.caption)
.foregroundColor(.secondary)
}
.onAppear {
loadApps()
}
}
private func loadApps() {
isLoading = true
discoveredApps = appDiscovery.getAllDiscoverableApps()
isLoading = false
}
private func handleAppSelection(_ appIdentifier: String) {
if appIdentifier == "__custom__" {
showCustomInput = true
selectedApp = ""
return
}
guard let selectedAppInfo = discoveredApps.first(where: {
($0.bundleIdentifier ?? $0.displayName) == appIdentifier
}) else {
return
}
// If app is not running, offer to launch it
if !selectedAppInfo.isRunning {
Task {
do {
try await appDiscovery.launchApp(selectedAppInfo)
// Refresh the app list after launching
await MainActor.run {
loadApps()
}
} catch {
print("Failed to launch app \(selectedAppInfo.displayName): \(error)")
// Still allow selection even if launch fails
}
}
}
}
}
struct AppDropdownView_Previews: PreviewProvider {
static var previews: some View {
AppDropdownView(
selectedApp: .constant("com.apple.Safari"),
title: "Application",
placeholder: "e.g., Safari, Chrome"
)
.padding()
.frame(width: 400)
}
}