1→import SwiftUI 2→import SwiftData 3→ 4→// MARK: - Video Filter 5→ 6→enum VideoFilter: String, CaseIterable { 7→ case all = "All" 8→ case favorites = "Favorites" 9→ case recent = "Recent" 10→ 11→ var icon: String { 12→ switch self { 13→ case .all: return "list.bullet" 14→ case .favorites: return "star.fill" 15→ case .recent: return "clock" 16→ } 17→ } 18→} 19→ 20→struct ContentView: View { 21→ @Environment(\.modelContext) private var modelContext 22→ @Environment(\.openWindow) private var openWindow 23→ @Query(sort: \SavedVideo.savedAt, order: .reverse) private var savedVideos: [SavedVideo] 24→ @Query(sort: \Course.createdAt, order: .reverse) private var courses: [Course] 25→ 26→ @State private var selectedVideo: SavedVideo? 27→ @State private var selectedCourse: Course? 28→ @State private var showingInput = false 29→ @State private var showingCourseImport = false 30→ @State private var selectedFilter: VideoFilter = .all 31→ @State private var showingDeleteAlert = false 32→ @State private var videoToDelete: SavedVideo? 33→ @State private var showingInMemoryWarning = false 34→ @State private var selectedTags: Set = [] 35→ 36→ // All unique tags across all videos 37→ private var allTags: [String] { 38→ Array(Set(savedVideos.flatMap { $0.tags })).sorted() 39→ } 40→ 41→ private var filteredVideos: [SavedVideo] { 42→ var videos: [SavedVideo] 43→ switch selectedFilter { 44→ case .all: 45→ videos = savedVideos 46→ case .favorites: 47→ videos = savedVideos.filter { $0.isFavorite } 48→ case .recent: 49→ let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date() 50→ videos = savedVideos.filter { $0.savedAt > oneWeekAgo } 51→ } 52→ 53→ // Apply tag filter 54→ if !selectedTags.isEmpty { 55→ videos = videos.filter { video in 56→ !selectedTags.isDisjoint(with: video.tags) 57→ } 58→ } 59→ 60→ return videos 61→ } 62→ 63→ var body: some View { 64→ NavigationSplitView { 65→ SidebarView( 66→ savedVideos: filteredVideos, 67→ courses: courses, 68→ selectedVideo: $selectedVideo, 69→ selectedCourse: $selectedCourse, 70→ showingInput: $showingInput, 71→ showingCourseImport: $showingCourseImport, 72→ selectedFilter: $selectedFilter, 73→ allTags: allTags, 74→ selectedTags: $selectedTags, 75→ onDelete: confirmDelete, 76→ onToggleFavorite: toggleFavorite 77→ ) 78→ } detail: { 79→ if showingInput { 80→ TranscriptInputView( 81→ onSave: { video in 82→ selectedVideo = video 83→ selectedCourse = nil 84→ showingInput = false 85→ } 86→ ) 87→ } else if let course = selectedCourse { 88→ CourseDetailView(course: course) { savedVideoID in 89→ // Navigate to transcript when video is selected from course 90→ if let video = savedVideos.first(where: { $0.id == savedVideoID }) { 91→ selectedVideo = video 92→ selectedCourse = nil 93→ } 94→ } 95→ } else if let video = selectedVideo { 96→ TranscriptReadingView(video: video) 97→ .id(video.id) // Force view recreation when video changes 98→ } else { 99→ EmptyStateView(showingInput: $showingInput) 100→ } 101→ } 102→ .sheet(isPresented: $showingCourseImport) { 103→ CourseImportView() 104→ } 105→ .frame(minWidth: 800, idealWidth: 1100, minHeight: 550, idealHeight: 700) 106→ .alert("Delete Transcript?", isPresented: $showingDeleteAlert) { 107→ Button("Cancel", role: .cancel) { 108→ videoToDelete = nil 109→ } 110→ Button("Delete", role: .destructive) { 111→ if let video = videoToDelete { 112→ deleteVideo(video) 113→ } 114→ videoToDelete = nil 115→ } 116→ } message: { 117→ if let video = videoToDelete { 118→ Text("Are you sure you want to delete \"\(video.title)\"? This action cannot be undone.") 119→ } 120→ } 121→ .alert("Data Storage Warning", isPresented: $showingInMemoryWarning) { 122→ Button("OK", role: .cancel) { } 123→ } message: { 124→ Text("The app is running in temporary mode. Your transcripts will NOT be saved when you quit the app. Please restart the app or check disk permissions.") 125→ } 126→ .onReceive(NotificationCenter.default.publisher(for: .newTranscript)) { _ in 127→ showingInput = true 128→ } 129→ .onReceive(NotificationCenter.default.publisher(for: .deleteVideo)) { _ in 130→ if let video = selectedVideo { 131→ confirmDelete(video) 132→ } 133→ } 134→ .onReceive(NotificationCenter.default.publisher(for: .openDiagnostics)) { _ in 135→ openWindow(id: "diagnostics") 136→ } 137→ .onReceive(NotificationCenter.default.publisher(for: .importWhisperTranscript)) { _ in 138→ importWhisperTranscript() 139→ } 140→ .onReceive(NotificationCenter.default.publisher(for: .importCourse)) { _ in 141→ checkForCourseImport() 142→ } 143→ .onAppear { 144→ // Check if running in temporary storage mode 145→ if isUsingInMemoryStorage { 146→ showingInMemoryWarning = true 147→ } 148→ checkForCourseImport() 149→ autoResumeProcessing() 150→ } 151→ } 152→ 153→ private func checkForCourseImport() { 154→ Task { 155→ do { 156→ if let course = try TranscriptImporter.importCourseFromFile(modelContext: modelContext) { 157→ await MainActor.run { 158→ selectedCourse = course 159→ selectedVideo = nil 160→ } 161→ await AppLogger.shared.info("Auto-imported course: \(course.name) - Starting transcription", category: .app) 162→ 163→ // Auto-start transcription 164→ try? await Task.sleep(nanoseconds: 1_000_000_000) // Wait 1 second for UI to update 165→ await CourseProcessingService.shared.startProcessing( 166→ courseID: course.id, 167→ modelContext: modelContext 168→ ) { progress in 169→ Task { 170→ await AppLogger.shared.debug("[\(progress.currentVideoIndex + 1)/\(progress.totalVideos)] \(progress.message)", category: .transcript) 171→ } 172→ } 173→ } 174→ } catch { 175→ await AppLogger.shared.error("Failed to auto-import course: \(error.localizedDescription)", category: .app) 176→ } 177→ } 178→ } 179→ 180→ private func autoResumeProcessing() { 181→ Task { 182→ // Find first course with pending videos 183→ if let courseToResume = courses.first(where: { !$0.pendingVideos.isEmpty }) { 184→ await MainActor.run { 185→ selectedCourse = courseToResume 186→ selectedVideo = nil 187→ } 188→ 189→ await AppLogger.shared.info("Auto-resuming course: \(courseToResume.name) with \(courseToResume.pendingVideos.count) pending videos", category: .app) 190→ 191→ // Wait for UI to update 192→ try? await Task.sleep(nanoseconds: 500_000_000) 193→ 194→ // Start processing 195→ await CourseProcessingService.shared.startProcessing( 196→ courseID: courseToResume.id, 197→ modelContext: modelContext 198→ ) { progress in 199→ Task { 200→ await AppLogger.shared.debug("[\(progress.currentVideoIndex + 1)/\(progress.totalVideos)] \(progress.message)", category: .transcript) 201→ } 202→ } 203→ } 204→ } 205→ } 206→ 207→ private func confirmDelete(_ video: SavedVideo) { 208→ videoToDelete = video 209→ showingDeleteAlert = true 210→ } 211→ 212→ private func deleteVideo(_ video: SavedVideo) { 213→ if selectedVideo?.id == video.id { 214→ selectedVideo = nil 215→ } 216→ 217→ // Log deletion 218→ logInfo("Deleting video: \(video.title)", category: .user) 219→ trackEvent(.transcriptDeleted, properties: [ 220→ "video_id": video.videoID, 221→ "title": video.title 222→ ]) 223→ 224→ modelContext.delete(video) 225→ } 226→ 227→ private func toggleFavorite(_ video: SavedVideo) { 228→ video.isFavorite.toggle() 229→ 230→ // Log favorite toggle 231→ trackEvent(.favoriteToggled, properties: [ 232→ "video_id": video.videoID, 233→ "is_favorite": String(video.isFavorite) 234→ ]) 235→ } 236→ 237→ private func importWhisperTranscript() { 238→ Task { 239→ do { 240→ try TranscriptImporter.importPalantirTranscript(modelContext: modelContext) 241→ 242→ await MainActor.run { 243→ // Select the imported video 244→ if let importedVideo = savedVideos.first(where: { $0.videoID == "UjkRz9HkldU" }) { 245→ selectedVideo = importedVideo 246→ showingInput = false 247→ } 248→ } 249→ 250→ await AppLogger.shared.info("Imported Palantir transcript successfully", category: .app) 251→ } catch { 252→ await MainActor.run { 253→ // Show error alert 254→ let alert = NSAlert() 255→ alert.messageText = "Import Failed" 256→ alert.informativeText = error.localizedDescription 257→ alert.alertStyle = .warning 258→ alert.addButton(withTitle: "OK") 259→ alert.runModal() 260→ } 261→ 262→ await AppLogger.shared.error("Failed to import transcript: \(error.localizedDescription)", category: .app) 263→ } 264→ } 265→ } 266→} 267→ 268→// MARK: - Sidebar View 269→ 270→struct SidebarView: View { 271→ let savedVideos: [SavedVideo] 272→ let courses: [Course] 273→ @Binding var selectedVideo: SavedVideo? 274→ @Binding var selectedCourse: Course? 275→ @Binding var showingInput: Bool 276→ @Binding var showingCourseImport: Bool 277→ @Binding var selectedFilter: VideoFilter 278→ let allTags: [String] 279→ @Binding var selectedTags: Set 280→ let onDelete: (SavedVideo) -> Void 281→ let onToggleFavorite: (SavedVideo) -> Void 282→ 283→ var body: some View { 284→ List { 285→ Section { 286→ Button(action: { showingInput = true }) { 287→ Label("New Transcript", systemImage: "plus.circle.fill") 288→ } 289→ .buttonStyle(.plain) 290→ .foregroundStyle(.tint) 291→ } 292→ 293→ // MARK: - Tag Filter Section 294→ if !allTags.isEmpty { 295→ Section("Tags") { 296→ TagFilterBar( 297→ allTags: allTags, 298→ selectedTags: $selectedTags, 299→ onClearAll: { selectedTags.removeAll() } 300→ ) 301→ } 302→ } 303→ 304→ // MARK: - Courses Section 305→ Section("Courses (\(courses.count))") { 306→ if courses.isEmpty { 307→ Text("No courses") 308→ .foregroundStyle(.secondary) 309→ .font(.callout) 310→ } else { 311→ ForEach(courses) { course in 312→ CourseRowView(course: course) 313→ .contentShape(Rectangle()) 314→ .onTapGesture { 315→ selectedCourse = course 316→ selectedVideo = nil 317→ } 318→ .background(selectedCourse?.id == course.id ? Color.accentColor.opacity(0.15) : Color.clear) 319→ .cornerRadius(6) 320→ } 321→ } 322→ 323→ Button(action: { showingCourseImport = true }) { 324→ Label("Import Course...", systemImage: "square.and.arrow.down") 325→ } 326→ .buttonStyle(.plain) 327→ .foregroundStyle(.secondary) 328→ } 329→ 330→ // MARK: - Filter Section 331→ Section { 332→ Picker("Filter", selection: $selectedFilter) { 333→ ForEach(VideoFilter.allCases, id: \.self) { filter in 334→ Label(filter.rawValue, systemImage: filter.icon) 335→ .tag(filter) 336→ } 337→ } 338→ .pickerStyle(.segmented) 339→ .labelsHidden() 340→ } 341→ 342→ // MARK: - Videos Section 343→ Section("\(selectedFilter.rawValue) (\(savedVideos.count))") { 344→ if savedVideos.isEmpty { 345→ Text("No videos") 346→ .foregroundStyle(.secondary) 347→ .font(.callout) 348→ } else { 349→ ForEach(savedVideos) { video in 350→ VideoRowView(video: video, allTags: allTags) 351→ .contentShape(Rectangle()) 352→ .onTapGesture { 353→ selectedVideo = video 354→ selectedCourse = nil 355→ } 356→ .background(selectedVideo?.id == video.id ? Color.accentColor.opacity(0.15) : Color.clear) 357→ .cornerRadius(6) 358→ .swipeActions(edge: .leading, allowsFullSwipe: true) { 359→ Button { 360→ onToggleFavorite(video) 361→ } label: { 362→ Label( 363→ video.isFavorite ? "Unfavorite" : "Favorite", 364→ systemImage: video.isFavorite ? "star.slash" : "star.fill" 365→ ) 366→ } 367→ .tint(.yellow) 368→ } 369→ .swipeActions(edge: .trailing, allowsFullSwipe: false) { 370→ Button(role: .destructive) { 371→ onDelete(video) 372→ } label: { 373→ Label("Delete", systemImage: "trash") 374→ } 375→ } 376→ .contextMenu { 377→ Button { 378→ onToggleFavorite(video) 379→ } label: { 380→ Label( 381→ video.isFavorite ? "Remove from Favorites" : "Add to Favorites", 382→ systemImage: video.isFavorite ? "star.slash" : "star" 383→ ) 384→ } 385→ 386→ // Edit Tags 387→ Button { 388→ // Tag editing handled by popover in VideoRowView 389→ } label: { 390→ Label("Edit Tags", systemImage: "tag") 391→ } 392→ 393→ Divider() 394→ 395→ Button(role: .destructive) { 396→ onDelete(video) 397→ } label: { 398→ Label("Delete", systemImage: "trash") 399→ } 400→ } 401→ } 402→ } 403→ } 404→ } 405→ .listStyle(.sidebar) 406→ .navigationTitle("Transcripts") 407→ .toolbar { 408→ ToolbarItem { 409→ Menu { 410→ Button(action: { showingInput = true }) { 411→ Label("New Transcript", systemImage: "doc.text") 412→ } 413→ Button(action: { showingCourseImport = true }) { 414→ Label("Import Course", systemImage: "book.closed") 415→ } 416→ } label: { 417→ Image(systemName: "plus") 418→ } 419→ } 420→ } 421→ } 422→} 423→ 424→// MARK: - Video Row View 425→ 426→struct VideoRowView: View { 427→ let video: SavedVideo 428→ let allTags: [String] 429→ 430→ @State private var showingTagEditor = false 431→ 432→ init(video: SavedVideo, allTags: [String] = []) { 433→ self.video = video 434→ self.allTags = allTags 435→ } 436→ 437→ var body: some View { 438→ HStack(alignment: .top, spacing: 8) { 439→ VStack(alignment: .leading, spacing: 4) { 440→ HStack(spacing: 6) { 441→ if video.isFavorite { 442→ Image(systemName: "star.fill") 443→ .font(.caption) 444→ .foregroundStyle(.yellow) 445→ } 446→ 447→ Text(video.title) 448→ .font(.headline) 449→ .lineLimit(2) 450→ } 451→ 452→ Text(video.channelName) 453→ .font(.caption) 454→ .foregroundStyle(.secondary) 455→ 456→ // Tags row 457→ if !video.tags.isEmpty { 458→ HStack(spacing: 4) { 459→ ForEach(video.tags.prefix(3), id: \.self) { tag in 460→ Text(tag) 461→ .font(.caption2) 462→ .padding(.horizontal, 6) 463→ .padding(.vertical, 2) 464→ .background(Color.accentColor.opacity(0.15)) 465→ .clipShape(Capsule()) 466→ } 467→ if video.tags.count > 3 { 468→ Text("+\(video.tags.count - 3)") 469→ .font(.caption2) 470→ .foregroundStyle(.secondary) 471→ } 472→ } 473→ } 474→ 475→ HStack { 476→ Image(systemName: "globe") 477→ .font(.caption2) 478→ Text(video.language.uppercased()) 479→ .font(.caption2) 480→ 481→ // Transcript source badge 482→ if let source = video.transcriptSource { 483→ Image(systemName: video.sourceIcon) 484→ .font(.caption2) 485→ .foregroundStyle(sourceColor(for: source)) 486→ } 487→ 488→ // Reading progress indicator 489→ if video.readingProgress > 0 { 490→ Divider() 491→ .frame(height: 10) 492→ HStack(spacing: 2) { 493→ Image(systemName: video.isRead ? "checkmark.circle.fill" : "book") 494→ .font(.caption2) 495→ Text("\(video.readingProgressPercentage)%") 496→ .font(.caption2) 497→ } 498→ .foregroundStyle(video.isRead ? .green : .secondary) 499→ } 500→ 501→ Spacer() 502→ 503→ Text(video.savedAt, style: .date) 504→ .font(.caption2) 505→ .foregroundStyle(.tertiary) 506→ } 507→ .foregroundStyle(.secondary) 508→ } 509→ } 510→ .padding(.vertical, 4) 511→ .popover(isPresented: $showingTagEditor) { 512→ VideoTagEditorPopover( 513→ video: video, 514→ allExistingTags: allTags, 515→ onDismiss: { showingTagEditor = false } 516→ ) 517→ } 518→ } 519→ 520→ // Helper function to get source color 521→ private func sourceColor(for source: String) -> Color { 522→ switch source { 523→ case "whisper": return .purple 524→ case "youtube": return .blue 525→ default: return .gray 526→ } 527→ } 528→} 529→ 530→// MARK: - Empty State View 531→ 532→struct EmptyStateView: View { 533→ @Binding var showingInput: Bool 534→ 535→ var body: some View { 536→ VStack(spacing: 20) { 537→ Image(systemName: "text.quote") 538→ .font(.system(size: 64)) 539→ .foregroundStyle(.tertiary) 540→ 541→ Text("No Transcript Selected") 542→ .font(.title2) 543→ .foregroundStyle(.secondary) 544→ 545→ Text("Select a saved video from the sidebar\nor create a new transcript.") 546→ .font(.body) 547→ .foregroundStyle(.tertiary) 548→ .multilineTextAlignment(.center) 549→ 550→ Button(action: { showingInput = true }) { 551→ Label("New Transcript", systemImage: "plus.circle.fill") 552→ } 553→ .buttonStyle(.borderedProminent) 554→ .controlSize(.large) 555→ } 556→ .frame(maxWidth: .infinity, maxHeight: .infinity) 557→ } 558→} 559→ 560→#Preview { 561→ ContentView() 562→ .modelContainer(for: [SavedVideo.self, Course.self, CourseVideo.self], inMemory: true) 563→} 564→ 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.