import SwiftUI import UniformTypeIdentifiers /// Project summary returned by the `inspect` command. struct TimelineSummary { let name: String let duration: Double let frameRate: Double let width: Int let height: Int let clips: Int let cuts: Int let connected: Int let markers: Int init(json: [String: Any]) { name = json["name"] as? String ?? "Untitled" duration = json["duration_seconds"] as? Double ?? 0 frameRate = json["frame_rate"] as? Double ?? 0 width = json["width"] as? Int ?? 0 height = json["height"] as? Int ?? 0 clips = json["clips"] as? Int ?? 0 cuts = json["cuts"] as? Int ?? 0 connected = json["connected"] as? Int ?? 0 markers = json["markers"] as? Int ?? 0 } var durationText: String { let t = Int(duration) return String(format: "%02d:%02d:%02d", t / 3600, (t % 3600) / 60, t % 60) } } struct ProjectInfo { let path: String let name: String let version: String let timelines: [TimelineSummary] init(json: [String: Any]) { path = json["path"] as? String ?? "" name = json["name"] as? String ?? "Untitled" version = json["fcpxml_version"] as? String ?? "?" timelines = (json["timelines"] as? [[String: Any]] ?? []).map(TimelineSummary.init) } } /// Guia "Projeto" — primeira tela do app. Permite importar um arquivo FCPXML /// clicando para selecionar ou arrastando e soltando do Final Cut Pro, e mostra /// um resumo do projeto carregado. struct ProjectView: View { @State private var project: ProjectInfo? @State private var isInspecting = false @State private var errorMessage: String? @State private var isTargeted = false @State private var showTranscription = false var body: some View { Form { Section { dropZone HStack { Button("Selecionar Arquivo…") { pickProjectFile() } Spacer() if let project { Button { NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: project.path)]) } label: { Image(systemName: "folder") } .help("Mostrar no Finder") } } if showTranscription, let project { TranscriptionView(projectPath: project.path, embedded: true) } } header: { Text("Projeto do Final Cut Pro") } footer: { Text("Selecione ou arraste o arquivo XML (.fcpxml) ou o bundle (.fcpxmld) exportado pelo Final Cut Pro.") .font(.caption) .foregroundStyle(.secondary) } if let errorMessage { Section { Label(errorMessage, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.red) } } if isInspecting { Section { ProgressView().controlSize(.small) .frame(maxWidth: .infinity, alignment: .center) } } if let project { summarySection(project) } } .formStyle(.grouped) } /// Presents an NSOpenPanel configured to select a single file (not a /// folder). The panel filters to the project extensions that matter. private func pickProjectFile() { let panel = NSOpenPanel() panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false panel.prompt = "Selecionar" panel.message = "Selecione o arquivo de projeto (.fcpxml) exportado pelo Final Cut Pro." if panel.runModal() == .OK, let url = panel.url { if isAccepted(url) { inspect(url.path) } else { errorMessage = "Selecione um arquivo .fcpxml ou .xml do Final Cut Pro." } } } // MARK: - Drop zone private var dropZone: some View { VStack(spacing: 10) { Image(systemName: isTargeted ? "arrow.down.doc.fill" : "doc.badge.arrow.down") .font(.system(size: 40)) .foregroundStyle(isTargeted ? Color.accentColor : .secondary) Text(isTargeted ? "Solte para importar" : "Arraste o XML do Final Cut Pro aqui") .font(.callout.weight(.medium)) Text("Ou clique em “Selecionar Arquivo…” abaixo.") .font(.caption).foregroundStyle(.secondary) } .frame(maxWidth: .infinity, minHeight: 120) .padding() .background( RoundedRectangle(cornerRadius: 10) .fill(Color.accentColor.opacity(isTargeted ? 0.12 : 0.05)) ) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder( Color.accentColor.opacity(isTargeted ? 1 : 0.4), style: StrokeStyle(lineWidth: 2, dash: [6]) ) ) .onDrop( of: [.fileURL], isTargeted: $isTargeted ) { providers in handleDrop(providers) } } private func handleDrop(_ providers: [NSItemProvider]) -> Bool { guard let provider = providers.first else { return false } provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, _ in var url: URL? if let data = item as? Data { url = URL(dataRepresentation: data, relativeTo: nil) } else if let u = item as? URL { url = u } if let url, isAccepted(url) { DispatchQueue.main.async { inspect(url.path) } } else { DispatchQueue.main.async { errorMessage = "Este arquivo não parece ser um projeto do Final Cut Pro." } } } return true } private func isAccepted(_ url: URL) -> Bool { let ext = url.pathExtension.lowercased() return ext == "fcpxml" || ext == "fcpxmld" || ext == "xml" } // MARK: - Summary @ViewBuilder private func summarySection(_ project: ProjectInfo) -> some View { Section { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 10) { Image(systemName: "film.fill") .font(.title2) .foregroundStyle(.tint) VStack(alignment: .leading, spacing: 2) { Text(project.name).font(.headline) Text("FCPXML v\(project.version)") .font(.caption).foregroundStyle(.secondary) } } if project.timelines.isEmpty { Text("Nenhuma timeline encontrada.") .font(.callout).foregroundStyle(.secondary) } else { ForEach(project.timelines, id: \.name) { tl in timelineRow(tl) } } } } header: { Text("Resumo do Projeto") } } private func timelineRow(_ tl: TimelineSummary) -> some View { HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(tl.name).font(.body.weight(.medium)) Text("\(tl.width) × \(tl.height) · \(formattedFPS(tl.frameRate)) fps") .font(.caption).foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) VStack(alignment: .trailing, spacing: 4) { Text(tl.durationText).font(.body.weight(.medium)).monospacedDigit() Text("\(tl.clips) clipes · \(tl.cuts) cortes") .font(.caption).foregroundStyle(.secondary) } } .padding(.vertical, 2) } private func formattedFPS(_ fps: Double) -> String { if fps == fps.rounded() { return String(format: "%.0f", fps) } return String(format: "%.2f", fps) } // MARK: - Actions private func inspect(_ path: String) { isInspecting = true errorMessage = nil PythonBridge.call(command: "inspect", arguments: ["path": path]) { result, err in DispatchQueue.main.async { isInspecting = false if let result, result["ok"] as? Bool == true { project = ProjectInfo(json: result) showTranscription = true } else { errorMessage = result?["error"] as? String ?? err ?? "Falha ao ler o projeto." } } } } }