chore: adiciona .gitignore e commit.command

This commit is contained in:
João Henrique
2026-08-18 08:25:29 -04:00
parent 68958fde00
commit 8fca456ceb
215 changed files with 65752 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
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."
}
}
}
}
}