vOOice/VoiceInk/VoiceInk.swift

211 lines
7.9 KiB
Swift

import SwiftUI
import SwiftData
import Sparkle
import AppKit
import OSLog
@main
struct VoiceInkApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let container: ModelContainer
@StateObject private var whisperState: WhisperState
@StateObject private var hotkeyManager: HotkeyManager
@StateObject private var updaterViewModel: UpdaterViewModel
@StateObject private var menuBarManager: MenuBarManager
@StateObject private var aiService = AIService()
@StateObject private var enhancementService: AIEnhancementService
@StateObject private var activeWindowService = ActiveWindowService.shared
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
// Audio cleanup manager for automatic deletion of old audio files
private let audioCleanupManager = AudioCleanupManager.shared
init() {
do {
let schema = Schema([
Transcription.self
])
// Create app-specific Application Support directory URL
let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("com.prakashjoshipax.VoiceInk", isDirectory: true)
// Create the directory if it doesn't exist
try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
// Configure SwiftData to use the conventional location
let storeURL = appSupportURL.appendingPathComponent("default.store")
let modelConfiguration = ModelConfiguration(schema: schema, url: storeURL)
container = try ModelContainer(for: schema, configurations: [modelConfiguration])
// Print SwiftData storage location
if let url = container.mainContext.container.configurations.first?.url {
print("💾 SwiftData storage location: \(url.path)")
}
} catch {
fatalError("Failed to create ModelContainer for Transcription: \(error.localizedDescription)")
}
// Initialize services with proper sharing of instances
let aiService = AIService()
_aiService = StateObject(wrappedValue: aiService)
let updaterViewModel = UpdaterViewModel()
_updaterViewModel = StateObject(wrappedValue: updaterViewModel)
let enhancementService = AIEnhancementService(aiService: aiService, modelContext: container.mainContext)
_enhancementService = StateObject(wrappedValue: enhancementService)
let whisperState = WhisperState(modelContext: container.mainContext, enhancementService: enhancementService)
_whisperState = StateObject(wrappedValue: whisperState)
let hotkeyManager = HotkeyManager(whisperState: whisperState)
_hotkeyManager = StateObject(wrappedValue: hotkeyManager)
let menuBarManager = MenuBarManager(
updaterViewModel: updaterViewModel,
whisperState: whisperState,
container: container,
enhancementService: enhancementService,
aiService: aiService,
hotkeyManager: hotkeyManager
)
_menuBarManager = StateObject(wrappedValue: menuBarManager)
// Configure ActiveWindowService with enhancementService
let activeWindowService = ActiveWindowService.shared
activeWindowService.configure(with: enhancementService)
activeWindowService.configureWhisperState(whisperState)
_activeWindowService = StateObject(wrappedValue: activeWindowService)
}
var body: some Scene {
WindowGroup {
if hasCompletedOnboarding {
ContentView()
.environmentObject(whisperState)
.environmentObject(hotkeyManager)
.environmentObject(updaterViewModel)
.environmentObject(menuBarManager)
.environmentObject(aiService)
.environmentObject(enhancementService)
.modelContainer(container)
.onAppear {
updaterViewModel.silentlyCheckForUpdates()
// Start the automatic audio cleanup process
audioCleanupManager.startAutomaticCleanup(modelContext: container.mainContext)
}
.background(WindowAccessor { window in
WindowManager.shared.configureWindow(window)
})
.onDisappear {
whisperState.unloadModel()
// Stop the automatic audio cleanup process
audioCleanupManager.stopAutomaticCleanup()
}
} else {
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
.environmentObject(hotkeyManager)
.environmentObject(whisperState)
.environmentObject(aiService)
.environmentObject(enhancementService)
.frame(minWidth: 1200, minHeight: 800)
}
}
.commands {
CommandGroup(after: .appInfo) {
CheckForUpdatesView(updaterViewModel: updaterViewModel)
}
}
MenuBarExtra {
MenuBarView()
.environmentObject(whisperState)
.environmentObject(hotkeyManager)
.environmentObject(menuBarManager)
.environmentObject(updaterViewModel)
.environmentObject(aiService)
.environmentObject(enhancementService)
} label: {
let image: NSImage = {
let ratio = $0.size.height / $0.size.width
$0.size.height = 22
$0.size.width = 22 / ratio
return $0
}(NSImage(named: "menuBarIcon")!)
Image(nsImage: image)
}
.menuBarExtraStyle(.menu)
#if DEBUG
WindowGroup("Debug") {
Button("Toggle Menu Bar Only") {
menuBarManager.isMenuBarOnly.toggle()
}
}
#endif
}
}
class UpdaterViewModel: ObservableObject {
private let updaterController: SPUStandardUpdaterController
@Published var canCheckForUpdates = false
init() {
updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
// Enable automatic update checking
updaterController.updater.automaticallyChecksForUpdates = true
updaterController.updater.updateCheckInterval = 24 * 60 * 60
updaterController.updater.publisher(for: \.canCheckForUpdates)
.assign(to: &$canCheckForUpdates)
}
func checkForUpdates() {
// This is for manual checks - will show UI
updaterController.checkForUpdates(nil)
}
func silentlyCheckForUpdates() {
// This checks for updates in the background without showing UI unless an update is found
updaterController.updater.checkForUpdatesInBackground()
}
}
struct CheckForUpdatesView: View {
@ObservedObject var updaterViewModel: UpdaterViewModel
var body: some View {
Button("Check for Updates…", action: updaterViewModel.checkForUpdates)
.disabled(!updaterViewModel.canCheckForUpdates)
}
}
struct WindowAccessor: NSViewRepresentable {
let callback: (NSWindow) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let window = view.window {
callback(window)
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}