1→import SwiftUI 2→import SwiftData 3→ 4→/// Flag to track if app is using in-memory storage (data won't persist) 5→var isUsingInMemoryStorage = false 6→ 7→@main 8→struct YouTubeTranscriptsApp: App { 9→ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 10→ 11→ var sharedModelContainer: ModelContainer = { 12→ let schema = Schema([SavedVideo.self, Course.self, CourseVideo.self]) 13→ 14→ // Create app data directory if it doesn't exist 15→ let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 16→ let appDataURL = appSupportURL.appendingPathComponent("YouTubeTranscripts", isDirectory: true) 17→ 18→ do { 19→ try FileManager.default.createDirectory(at: appDataURL, withIntermediateDirectories: true) 20→ } catch { 21→ Task { 22→ await AppLogger.shared.error("Could not create app data directory: \(error)", category: .app) 23→ } 24→ } 25→ 26→ let storeURL = appDataURL.appendingPathComponent("YouTubeTranscripts.store") 27→ 28→ let modelConfiguration = ModelConfiguration( 29→ schema: schema, 30→ url: storeURL, 31→ allowsSave: true 32→ ) 33→ 34→ do { 35→ return try ModelContainer(for: schema, configurations: [modelConfiguration]) 36→ } catch { 37→ // Fallback to in-memory if persistent storage fails 38→ Task { 39→ await AppLogger.shared.critical("Persistent store failed, using in-memory: \(error)", category: .app) 40→ } 41→ isUsingInMemoryStorage = true 42→ let memoryConfig = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) 43→ do { 44→ return try ModelContainer(for: schema, configurations: [memoryConfig]) 45→ } catch { 46→ fatalError("Cannot create model container: \(error.localizedDescription)") 47→ } 48→ } 49→ }() 50→ 51→ var body: some Scene { 52→ WindowGroup { 53→ ContentView() 54→ } 55→ .modelContainer(sharedModelContainer) 56→ .commands { 57→ CommandGroup(replacing: .appSettings) { 58→ Button("Settings...") { 59→ NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) 60→ } 61→ .keyboardShortcut(",", modifiers: [.command]) 62→ } 63→ 64→ // File Menu 65→ CommandGroup(after: .newItem) { 66→ Button("New Transcript") { 67→ sendCommandCompat(.newTranscript) 68→ } 69→ .keyboardShortcut("n", modifiers: [.command]) 70→ 71→ Divider() 72→ 73→ Button("Export Transcript...") { 74→ sendCommandCompat(.exportTranscript) 75→ } 76→ .keyboardShortcut("e", modifiers: [.command]) 77→ 78→ Button("Export as Markdown...") { 79→ sendCommandCompat(.exportMarkdown) 80→ } 81→ .keyboardShortcut("e", modifiers: [.command, .shift]) 82→ 83→ Button("Export as PDF...") { 84→ sendCommandCompat(.exportPDF) 85→ } 86→ .keyboardShortcut("p", modifiers: [.command, .shift]) 87→ 88→ Button("Open in Browser") { 89→ sendCommandCompat(.openInBrowser) 90→ } 91→ .keyboardShortcut("b", modifiers: [.command]) 92→ 93→ Divider() 94→ 95→ Button("Delete Video") { 96→ sendCommandCompat(.deleteVideo) 97→ } 98→ .keyboardShortcut(.delete, modifiers: [.command]) 99→ } 100→ 101→ // Edit Menu 102→ CommandGroup(after: .pasteboard) { 103→ Divider() 104→ 105→ Button("Find in Transcript") { 106→ sendCommandCompat(.findInTranscript) 107→ } 108→ .keyboardShortcut("f", modifiers: [.command]) 109→ 110→ Button("Copy Transcript") { 111→ sendCommandCompat(.copyTranscript) 112→ } 113→ .keyboardShortcut("c", modifiers: [.command, .shift]) 114→ 115→ Divider() 116→ 117→ Button("Highlight Selection") { 118→ sendCommandCompat(.highlightYellow) 119→ } 120→ .keyboardShortcut("h", modifiers: [.command]) 121→ 122→ Button("Underline Selection") { 123→ sendCommandCompat(.underlineSelection) 124→ } 125→ .keyboardShortcut("u", modifiers: [.command]) 126→ 127→ Button("Clear All Annotations") { 128→ sendCommandCompat(.clearAnnotations) 129→ } 130→ .keyboardShortcut("h", modifiers: [.command, .shift]) 131→ } 132→ 133→ // View Menu 134→ CommandGroup(after: .toolbar) { 135→ Button("Increase Font Size") { 136→ sendCommandCompat(.increaseFontSize) 137→ } 138→ .keyboardShortcut("+", modifiers: [.command]) 139→ 140→ Button("Decrease Font Size") { 141→ sendCommandCompat(.decreaseFontSize) 142→ } 143→ .keyboardShortcut("-", modifiers: [.command]) 144→ 145→ Button("Reset Font Size") { 146→ sendCommandCompat(.resetFontSize) 147→ } 148→ .keyboardShortcut("0", modifiers: [.command]) 149→ 150→ Divider() 151→ 152→ Button("Toggle Notes Panel") { 153→ sendCommandCompat(.toggleNotesPanel) 154→ } 155→ .keyboardShortcut("n", modifiers: [.command, .shift]) 156→ 157→ Button("Generate AI Summary") { 158→ sendCommandCompat(.generateAISummary) 159→ } 160→ .keyboardShortcut("s", modifiers: [.command, .shift]) 161→ } 162→ 163→ // Playback Menu (TTS) 164→ CommandMenu("Playback") { 165→ Button("Play/Pause") { 166→ sendCommandCompat(.ttsPlayPause) 167→ } 168→ .keyboardShortcut(" ", modifiers: []) 169→ 170→ Button("Stop") { 171→ sendCommandCompat(.ttsStop) 172→ } 173→ .keyboardShortcut(.escape, modifiers: []) 174→ 175→ Divider() 176→ 177→ Button("Skip Forward") { 178→ sendCommandCompat(.ttsSkipForward) 179→ } 180→ .keyboardShortcut(.rightArrow, modifiers: [.command]) 181→ 182→ Button("Skip Backward") { 183→ sendCommandCompat(.ttsSkipBackward) 184→ } 185→ .keyboardShortcut(.leftArrow, modifiers: [.command]) 186→ 187→ Divider() 188→ 189→ Button("Speed Up") { 190→ sendCommandCompat(.ttsSpeedUp) 191→ } 192→ .keyboardShortcut("]", modifiers: [.command]) 193→ 194→ Button("Speed Down") { 195→ sendCommandCompat(.ttsSpeedDown) 196→ } 197→ .keyboardShortcut("[", modifiers: [.command]) 198→ } 199→ 200→ // Developer Menu 201→ CommandMenu("Developer") { 202→ Button("Import Palantir Transcript") { 203→ sendCommandCompat(.importWhisperTranscript) 204→ } 205→ .keyboardShortcut("i", modifiers: [.command, .option, .shift]) 206→ } 207→ 208→ // Help Menu 209→ CommandGroup(replacing: .help) { 210→ Button("Open Diagnostics") { 211→ sendCommandCompat(.openDiagnostics) 212→ } 213→ .keyboardShortcut("d", modifiers: [.command, .option]) 214→ 215→ Divider() 216→ 217→ Button("Export Logs...") { 218→ sendCommandCompat(.exportLogs) 219→ } 220→ 221→ Button("Export Analytics...") { 222→ sendCommandCompat(.exportAnalytics) 223→ } 224→ } 225→ } 226→ 227→ Settings { 228→ SettingsView() 229→ } 230→ 231→ // Diagnostics Window 232→ Window("Diagnostics", id: "diagnostics") { 233→ DiagnosticsView() 234→ } 235→ .defaultSize(width: 900, height: 700) 236→ } 237→} 238→ 239→// MARK: - App Delegate 240→ 241→class AppDelegate: NSObject, NSApplicationDelegate { 242→ func applicationDidFinishLaunching(_ notification: Notification) { 243→ // Initialize logging (session automatically started in AppLogger.init) 244→ Task { 245→ await AppLogger.shared.info("Application launched", category: .app) 246→ } 247→ 248→ // Setup crash handler 249→ setupCrashHandler() 250→ } 251→ 252→ func applicationWillTerminate(_ notification: Notification) { 253→ Task { 254→ await AppLogger.shared.info("Application terminating", category: .app) 255→ await AppLogger.shared.endSession() 256→ } 257→ } 258→ 259→ func applicationDidBecomeActive(_ notification: Notification) { 260→ trackEvent(.appBecameActive) 261→ } 262→ 263→ func applicationDidResignActive(_ notification: Notification) { 264→ trackEvent(.appResignedActive) 265→ } 266→ 267→ private func setupCrashHandler() { 268→ NSSetUncaughtExceptionHandler { exception in 269→ let message = "Uncaught exception: \(exception.name.rawValue) - \(exception.reason ?? "Unknown")" 270→ // Write to stderr (async-signal-safe) 271→ FileHandle.standardError.write(Data("[CRASH] \(message)\n".utf8)) 272→ 273→ // Try to save crash info (best effort, may not complete) 274→ Task { 275→ await AppLogger.shared.endSession(crashReason: message) 276→ } 277→ } 278→ 279→ // Note: Signal handlers removed - they cannot safely call non-async-signal-safe functions. 280→ // macOS crash reporter handles SIGABRT/SIGSEGV automatically. 281→ } 282→} 283→ 284→// MARK: - Notifications 285→ 286→extension Notification.Name { 287→ // File operations 288→ static let newTranscript = Notification.Name("newTranscript") 289→ static let exportTranscript = Notification.Name("exportTranscript") 290→ static let exportMarkdown = Notification.Name("exportMarkdown") 291→ static let exportPDF = Notification.Name("exportPDF") 292→ static let openInBrowser = Notification.Name("openInBrowser") 293→ static let deleteVideo = Notification.Name("deleteVideo") 294→ static let findInTranscript = Notification.Name("findInTranscript") 295→ static let copyTranscript = Notification.Name("copyTranscript") 296→ static let increaseFontSize = Notification.Name("increaseFontSize") 297→ static let decreaseFontSize = Notification.Name("decreaseFontSize") 298→ static let resetFontSize = Notification.Name("resetFontSize") 299→ // Annotation shortcuts 300→ static let highlightYellow = Notification.Name("highlightYellow") 301→ static let underlineSelection = Notification.Name("underlineSelection") 302→ static let clearAnnotations = Notification.Name("clearAnnotations") 303→ // Notes panel shortcuts 304→ static let toggleNotesPanel = Notification.Name("toggleNotesPanel") 305→ static let generateAISummary = Notification.Name("generateAISummary") 306→ // TTS/Playback shortcuts 307→ static let ttsPlayPause = Notification.Name("ttsPlayPause") 308→ static let ttsStop = Notification.Name("ttsStop") 309→ static let ttsSkipForward = Notification.Name("ttsSkipForward") 310→ static let ttsSkipBackward = Notification.Name("ttsSkipBackward") 311→ static let ttsSpeedUp = Notification.Name("ttsSpeedUp") 312→ static let ttsSpeedDown = Notification.Name("ttsSpeedDown") 313→ // Diagnostics 314→ static let openDiagnostics = Notification.Name("openDiagnostics") 315→ static let exportLogs = Notification.Name("exportLogs") 316→ static let exportAnalytics = Notification.Name("exportAnalytics") 317→ // Developer 318→ static let importWhisperTranscript = Notification.Name("importWhisperTranscript") 319→ // Courses 320→ static let importCourse = Notification.Name("importCourse") 321→ static let startCourseProcessing = Notification.Name("startCourseProcessing") 322→ static let pauseCourseProcessing = Notification.Name("pauseCourseProcessing") 323→} 324→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.