vOOice/VoiceInk/Views/PowerModeView.swift
2025-02-22 11:52:41 +05:45

743 lines
29 KiB
Swift

import SwiftUI
// Configuration Mode Enum
enum ConfigurationMode {
case add
case edit(PowerModeConfig)
case editDefault(PowerModeConfig)
var isAdding: Bool {
if case .add = self { return true }
return false
}
var isEditingDefault: Bool {
if case .editDefault = self { return true }
return false
}
var title: String {
switch self {
case .add: return "Add Configuration"
case .editDefault: return "Edit Default Configuration"
case .edit: return "Edit Configuration"
}
}
}
// Configuration Type
enum ConfigurationType {
case application
case website
}
// Main Configuration Sheet
struct ConfigurationSheet: View {
let mode: ConfigurationMode
@Binding var isPresented: Bool
let powerModeManager: PowerModeManager
@EnvironmentObject var enhancementService: AIEnhancementService
// State for configuration
@State private var configurationType: ConfigurationType = .application
@State private var selectedAppURL: URL?
@State private var isAIEnhancementEnabled: Bool
@State private var selectedPromptId: UUID?
@State private var installedApps: [(url: URL, name: String, bundleId: String, icon: NSImage)] = []
@State private var searchText = ""
// Website configuration state
@State private var websiteURL: String = ""
@State private var websiteName: String = ""
private var filteredApps: [(url: URL, name: String, bundleId: String, icon: NSImage)] {
if searchText.isEmpty {
return installedApps
}
return installedApps.filter { app in
app.name.localizedCaseInsensitiveContains(searchText) ||
app.bundleId.localizedCaseInsensitiveContains(searchText)
}
}
init(mode: ConfigurationMode, isPresented: Binding<Bool>, powerModeManager: PowerModeManager) {
self.mode = mode
self._isPresented = isPresented
self.powerModeManager = powerModeManager
switch mode {
case .add:
_isAIEnhancementEnabled = State(initialValue: true)
_selectedPromptId = State(initialValue: nil)
case .edit(let config), .editDefault(let config):
_isAIEnhancementEnabled = State(initialValue: config.isAIEnhancementEnabled)
_selectedPromptId = State(initialValue: config.selectedPrompt.flatMap { UUID(uuidString: $0) })
if case .edit(let config) = mode {
// Initialize website configuration if it exists
if let urlConfig = config.urlConfigs?.first {
_configurationType = State(initialValue: .website)
_websiteURL = State(initialValue: urlConfig.url)
_websiteName = State(initialValue: config.appName)
} else {
_configurationType = State(initialValue: .application)
_selectedAppURL = State(initialValue: NSWorkspace.shared.urlForApplication(withBundleIdentifier: config.bundleIdentifier))
}
}
}
}
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text(mode.title)
.font(.headline)
Spacer()
}
.padding()
Divider()
if mode.isAdding {
// Configuration Type Selector
Picker("Configuration Type", selection: $configurationType) {
Text("Application").tag(ConfigurationType.application)
Text("Website").tag(ConfigurationType.website)
}
.padding()
if configurationType == .application {
// Search bar
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.secondary)
TextField("Search applications...", text: $searchText)
.textFieldStyle(PlainTextFieldStyle())
if !searchText.isEmpty {
Button(action: { searchText = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(8)
.background(Color(.windowBackgroundColor).opacity(0.4))
.cornerRadius(8)
.padding()
// App Grid
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 120), spacing: 16)], spacing: 16) {
ForEach(filteredApps.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }), id: \.bundleId) { app in
AppGridItem(
app: app,
isSelected: app.url == selectedAppURL,
action: { selectedAppURL = app.url }
)
}
}
.padding()
}
} else {
// Website Configuration
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("Website Name")
.font(.headline)
TextField("Enter website name", text: $websiteName)
.textFieldStyle(.roundedBorder)
}
VStack(alignment: .leading, spacing: 8) {
Text("Website URL")
.font(.headline)
TextField("Enter website URL (e.g., google.com)", text: $websiteURL)
.textFieldStyle(.roundedBorder)
}
}
.padding()
}
}
// Configuration Form
if let config = getConfigForForm() {
if let appURL = !mode.isEditingDefault ? NSWorkspace.shared.urlForApplication(withBundleIdentifier: config.bundleIdentifier) : nil {
AppConfigurationFormView(
appName: config.appName,
appIcon: NSWorkspace.shared.icon(forFile: appURL.path),
isDefaultConfig: mode.isEditingDefault,
isAIEnhancementEnabled: $isAIEnhancementEnabled,
selectedPromptId: $selectedPromptId
)
} else {
AppConfigurationFormView(
appName: nil,
appIcon: nil,
isDefaultConfig: mode.isEditingDefault,
isAIEnhancementEnabled: $isAIEnhancementEnabled,
selectedPromptId: $selectedPromptId
)
}
}
Divider()
// Bottom buttons
HStack {
Button("Cancel") {
isPresented = false
}
.keyboardShortcut(.escape, modifiers: [])
Spacer()
Button(mode.isAdding ? "Add" : "Save") {
saveConfiguration()
}
.keyboardShortcut(.return, modifiers: [])
.disabled(mode.isAdding && !canSave)
}
.padding()
}
.frame(width: 600)
.frame(maxHeight: mode.isAdding ? 700 : 600)
.onAppear {
print("🔍 ConfigurationSheet appeared - Mode: \(mode)")
if mode.isAdding {
print("🔍 Loading installed apps...")
loadInstalledApps()
}
}
}
private var canSave: Bool {
if configurationType == .application {
return selectedAppURL != nil
} else {
return !websiteURL.isEmpty && !websiteName.isEmpty
}
}
private func getConfigForForm() -> PowerModeConfig? {
switch mode {
case .add:
if configurationType == .application {
guard let url = selectedAppURL,
let bundle = Bundle(url: url),
let bundleId = bundle.bundleIdentifier else { return nil }
let appName = bundle.infoDictionary?["CFBundleName"] as? String ??
bundle.infoDictionary?["CFBundleDisplayName"] as? String ??
"Unknown App"
return PowerModeConfig(
bundleIdentifier: bundleId,
appName: appName,
isAIEnhancementEnabled: isAIEnhancementEnabled,
selectedPrompt: selectedPromptId?.uuidString
)
} else {
// Create a special PowerModeConfig for websites
let urlConfig = URLConfig(url: websiteURL, promptId: selectedPromptId?.uuidString)
return PowerModeConfig(
bundleIdentifier: "website.\(UUID().uuidString)",
appName: websiteName,
isAIEnhancementEnabled: isAIEnhancementEnabled,
selectedPrompt: selectedPromptId?.uuidString,
urlConfigs: [urlConfig]
)
}
case .edit(let config), .editDefault(let config):
return config
}
}
private func loadInstalledApps() {
// Get both user-installed and system applications
let userAppURLs = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask)
let systemAppURLs = FileManager.default.urls(for: .applicationDirectory, in: .systemDomainMask)
let allAppURLs = userAppURLs + systemAppURLs
let apps = allAppURLs.flatMap { url -> [URL] in
return (try? FileManager.default.contentsOfDirectory(
at: url,
includingPropertiesForKeys: [.isApplicationKey],
options: [.skipsHiddenFiles]
)) ?? []
}
installedApps = apps.compactMap { url in
guard let bundle = Bundle(url: url),
let bundleId = bundle.bundleIdentifier,
let name = (bundle.infoDictionary?["CFBundleName"] as? String) ??
(bundle.infoDictionary?["CFBundleDisplayName"] as? String) else {
return nil
}
let icon = NSWorkspace.shared.icon(forFile: url.path)
return (url: url, name: name, bundleId: bundleId, icon: icon)
}
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
private func saveConfiguration() {
if isAIEnhancementEnabled && selectedPromptId == nil {
selectedPromptId = enhancementService.allPrompts.first?.id
}
switch mode {
case .add:
if let config = getConfigForForm() {
powerModeManager.addConfiguration(config)
}
case .edit(let config), .editDefault(let config):
var updatedConfig = config
updatedConfig.isAIEnhancementEnabled = isAIEnhancementEnabled
updatedConfig.selectedPrompt = selectedPromptId?.uuidString
// Update URL configurations if this is a website config
if configurationType == .website {
let urlConfig = URLConfig(url: cleanURL(websiteURL), promptId: selectedPromptId?.uuidString)
updatedConfig.urlConfigs = [urlConfig]
updatedConfig.appName = websiteName
}
powerModeManager.updateConfiguration(updatedConfig)
}
isPresented = false
}
private func cleanURL(_ url: String) -> String {
var cleanedURL = url.lowercased()
.replacingOccurrences(of: "https://", with: "")
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "www.", with: "")
// Remove trailing slash if present
if cleanedURL.last == "/" {
cleanedURL.removeLast()
}
return cleanedURL
}
}
// Main View
struct PowerModeView: View {
@StateObject private var powerModeManager = PowerModeManager.shared
@EnvironmentObject private var enhancementService: AIEnhancementService
@State private var showingConfigSheet = false {
didSet {
print("🔍 showingConfigSheet changed to: \(showingConfigSheet)")
}
}
@State private var configurationMode: ConfigurationMode? {
didSet {
print("🔍 configurationMode changed to: \(String(describing: configurationMode))")
}
}
var body: some View {
ScrollView {
VStack(spacing: 32) {
// Video CTA Section
VideoCTAView(
url: "https://dub.sh/powermode",
subtitle: "See Power Mode in action"
)
// Default Configuration Section
VStack(alignment: .leading, spacing: 16) {
Text("Default Configuration")
.font(.headline)
ConfiguredAppRow(
config: powerModeManager.defaultConfig,
isEditing: configurationMode?.isEditingDefault ?? false,
action: {
configurationMode = .editDefault(powerModeManager.defaultConfig)
showingConfigSheet = true
}
)
.background(RoundedRectangle(cornerRadius: 8)
.fill(Color(.windowBackgroundColor).opacity(0.4)))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.accentColor.opacity(0.2), lineWidth: 1))
}
.padding(.horizontal)
// Apps Section
VStack(spacing: 16) {
if powerModeManager.configurations.isEmpty {
PowerModeEmptyStateView(
showAddModal: $showingConfigSheet,
configMode: $configurationMode
)
} else {
Text("Power Mode Configurations")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
ConfiguredAppsGrid(powerModeManager: powerModeManager)
Button(action: {
print("🔍 Add button clicked - Setting config mode and showing sheet")
configurationMode = .add
print("🔍 Configuration mode set to: \(String(describing: configurationMode))")
showingConfigSheet = true
print("🔍 showingConfigSheet set to: \(showingConfigSheet)")
}) {
HStack(spacing: 6) {
Image(systemName: "plus")
.font(.system(size: 12, weight: .semibold))
Text("Add New Mode")
.font(.system(size: 13, weight: .medium))
}
}
.buttonStyle(.borderedProminent)
.controlSize(.regular)
.tint(Color(NSColor.controlAccentColor))
.frame(maxWidth: .infinity, alignment: .center)
.help("Add a new mode")
.padding(.top, 12)
}
}
}
.padding(24)
}
.background(Color(NSColor.controlBackgroundColor))
.sheet(isPresented: $showingConfigSheet, onDismiss: {
print("🔍 Sheet dismissed - Clearing configuration mode")
configurationMode = nil
}) {
Group {
if let mode = configurationMode {
ConfigurationSheet(
mode: mode,
isPresented: $showingConfigSheet,
powerModeManager: powerModeManager
)
.environmentObject(enhancementService)
.onAppear {
print("🔍 Creating ConfigurationSheet with mode: \(mode)")
}
}
}
}
}
}
// Supporting Views
struct PowerModeEmptyStateView: View {
@Binding var showAddModal: Bool
@Binding var configMode: ConfigurationMode?
var body: some View {
VStack(spacing: 16) {
Image(systemName: "bolt.circle.fill")
.font(.system(size: 48))
.foregroundColor(.secondary)
Text("No Applications Configured")
.font(.title2)
.fontWeight(.semibold)
Text("Add applications to customize their AI enhancement settings.")
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Button(action: {
print("🔍 Empty state Add Application button clicked")
configMode = .add
print("🔍 Configuration mode set to: \(String(describing: configMode))")
showAddModal = true
print("🔍 Empty state showAddModal set to: \(showAddModal)")
}) {
Label("Add Application", systemImage: "plus.circle.fill")
.font(.headline)
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ConfiguredAppsGrid: View {
@ObservedObject var powerModeManager: PowerModeManager
@EnvironmentObject var enhancementService: AIEnhancementService
@State private var editingConfig: PowerModeConfig?
@State private var showingConfigSheet = false
var body: some View {
ScrollView {
VStack(spacing: 8) {
ForEach(powerModeManager.configurations.sorted(by: { $0.appName.localizedCaseInsensitiveCompare($1.appName) == .orderedAscending })) { config in
ConfiguredAppRow(
config: config,
isEditing: editingConfig?.id == config.id,
action: {
editingConfig = config
showingConfigSheet = true
}
)
.contextMenu {
Button(action: {
editingConfig = config
showingConfigSheet = true
}) {
Label("Edit", systemImage: "pencil")
}
Button(role: .destructive, action: {
powerModeManager.removeConfiguration(for: config.bundleIdentifier)
}) {
Label("Remove", systemImage: "trash")
}
}
}
}
.padding()
}
.sheet(isPresented: $showingConfigSheet, onDismiss: { editingConfig = nil }) {
if let config = editingConfig {
ConfigurationSheet(
mode: .edit(config),
isPresented: $showingConfigSheet,
powerModeManager: powerModeManager
)
.environmentObject(enhancementService)
}
}
}
}
struct ConfiguredAppRow: View {
let config: PowerModeConfig
let isEditing: Bool
let action: () -> Void
@EnvironmentObject var enhancementService: AIEnhancementService
private var selectedPrompt: CustomPrompt? {
guard let promptId = config.selectedPrompt,
let uuid = UUID(uuidString: promptId) else { return nil }
return enhancementService.allPrompts.first { $0.id == uuid }
}
private var isWebsiteConfig: Bool {
return config.urlConfigs != nil && !config.urlConfigs!.isEmpty
}
var body: some View {
Button(action: action) {
HStack(spacing: 12) {
// Icon
if isWebsiteConfig {
Image(systemName: "globe")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32)
.foregroundColor(.accentColor)
} else if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: config.bundleIdentifier) {
Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path))
.resizable()
.frame(width: 32, height: 32)
}
// Info
VStack(alignment: .leading, spacing: 2) {
Text(config.appName)
.font(.headline)
if isWebsiteConfig {
if let urlConfig = config.urlConfigs?.first {
Text(urlConfig.url)
.font(.caption)
.foregroundColor(.secondary)
}
} else {
Text(config.bundleIdentifier)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
HStack(spacing: 8) {
HStack(spacing: 4) {
Image(systemName: config.isAIEnhancementEnabled ? "checkmark.circle.fill" : "circle")
.foregroundColor(config.isAIEnhancementEnabled ? .accentColor : .secondary)
.font(.system(size: 14))
Text("AI Enhancement")
.font(.system(size: 12, weight: .medium))
.foregroundColor(config.isAIEnhancementEnabled ? .accentColor : .secondary)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(RoundedRectangle(cornerRadius: 6)
.fill(config.isAIEnhancementEnabled ? Color.accentColor.opacity(0.1) : Color.secondary.opacity(0.1)))
if config.isAIEnhancementEnabled {
if let prompt = selectedPrompt {
HStack(spacing: 4) {
Image(systemName: prompt.icon.rawValue)
.foregroundColor(.accentColor)
.font(.system(size: 14))
Text(prompt.title)
.font(.system(size: 12))
.foregroundColor(.secondary)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(RoundedRectangle(cornerRadius: 6)
.fill(Color.accentColor.opacity(0.1)))
} else {
Text("No Prompt")
.font(.system(size: 12))
.foregroundColor(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(RoundedRectangle(cornerRadius: 6)
.fill(Color.secondary.opacity(0.1)))
}
}
}
}
.contentShape(Rectangle())
.padding(12)
.background(RoundedRectangle(cornerRadius: 8)
.fill(isEditing ? Color.accentColor.opacity(0.1) : Color(.windowBackgroundColor).opacity(0.4)))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(isEditing ? Color.accentColor : Color.clear, lineWidth: 1))
}
.buttonStyle(.plain)
}
}
struct AppConfigurationFormView: View {
let appName: String?
let appIcon: NSImage?
let isDefaultConfig: Bool
@Binding var isAIEnhancementEnabled: Bool
@Binding var selectedPromptId: UUID?
@EnvironmentObject var enhancementService: AIEnhancementService
var body: some View {
VStack(spacing: 20) {
VStack(spacing: 16) {
if !isDefaultConfig {
if let appIcon = appIcon {
HStack {
Image(nsImage: appIcon)
.resizable()
.frame(width: 32, height: 32)
Text(appName ?? "")
.font(.headline)
Spacer()
}
}
} else {
HStack {
Image(systemName: "gearshape.fill")
.font(.system(size: 24))
.foregroundColor(.accentColor)
Text("Default Settings")
.font(.headline)
Spacer()
}
Text("These settings will be applied to all applications that don't have specific configurations.")
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.bottom, 8)
}
Toggle("AI Enhancement", isOn: $isAIEnhancementEnabled)
}
.padding(.horizontal)
if isAIEnhancementEnabled {
Divider()
VStack(alignment: .leading) {
Text("Select Prompt")
.font(.headline)
.padding(.horizontal)
let columns = [
GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36)
]
LazyVGrid(columns: columns, spacing: 24) {
ForEach(enhancementService.allPrompts) { prompt in
prompt.promptIcon(
isSelected: selectedPromptId == prompt.id,
onTap: { selectedPromptId = prompt.id }
)
}
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
}
}
}
.padding(.vertical)
}
}
struct AppGridItem: View {
let app: (url: URL, name: String, bundleId: String, icon: NSImage)
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 8) {
Image(nsImage: app.icon)
.resizable()
.frame(width: 48, height: 48)
Text(app.name)
.font(.system(size: 12))
.lineLimit(2)
.multilineTextAlignment(.center)
.frame(height: 32)
}
.frame(width: 100)
.padding(8)
.background(RoundedRectangle(cornerRadius: 8)
.fill(isSelected ? Color.accentColor.opacity(0.1) : Color.clear))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 1))
}
.buttonStyle(.plain)
}
}
// New component for feature highlights
struct FeatureHighlight: View {
let icon: String
let title: String
let description: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.blue)
Text(title)
.font(.system(size: 13, weight: .semibold))
}
Text(description)
.font(.system(size: 12))
.foregroundColor(.secondary)
.lineLimit(2)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}