chore: adiciona .gitignore e commit.command
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user