import DesignSystem import MusubiCore import SwiftUI import UniformTypeIdentifiers /// Toolbar menu: current scene name and scene actions (control only). struct SceneMenu: View { let coordinator: Coordinator @State private var askingName = false @State private var newName = "" @State private var exporting = false @State private var importing = false @State private var importError: String? var body: some View { Menu { if !coordinator.store.scenes.isEmpty { Section("Szene laden") { ForEach(coordinator.store.scenes) { scene in Button(scene.name) { coordinator.loadScene(scene) } } } } Button("Speichern") { if coordinator.currentSceneName == nil { askName() } else { coordinator.saveScene() } } .keyboardShortcut("s") Button("Speichern als …") { askName() } .keyboardShortcut("s", modifiers: [.command, .shift]) Divider() Button("Exportieren …") { exporting = true } .disabled(coordinator.currentSceneName == nil) Button("Importieren …") { importing = true } } label: { HStack(spacing: Theme.Space.unit / 2) { Text(verbatim: coordinator.currentSceneName ?? String(localized: "Keine Szene")) if coordinator.isModified { Text("· geändert").foregroundStyle(Theme.Color.nezumi) } } } .fixedSize() .help("Szene") .alert("Szene speichern", isPresented: $askingName) { TextField("Name", text: $newName) Button("Speichern") { let name = newName.trimmingCharacters(in: .whitespaces) if !name.isEmpty { coordinator.saveScene(as: name) } } Button("Abbrechen", role: .cancel) {} } message: { Text("Speichert, was Regie und Knoten gerade zeigen und wohin der Ton geht.") } .fileExporter(isPresented: $exporting, document: SceneDocument(scene: coordinator.exportableScene()), contentType: .json, defaultFilename: coordinator.currentSceneName ?? "Szene") { _ in } .fileImporter(isPresented: $importing, allowedContentTypes: [.json]) { result in do { let scene = try coordinator.store.importScene(from: result.get()) coordinator.loadScene(scene) } catch { importError = error.localizedDescription } } .alert("Import fehlgeschlagen", isPresented: Binding(get: { importError != nil }, set: { if !$0 { importError = nil } })) { Button("OK") {} } message: { Text("Die Datei ist keine Musubi-Szene.") } } private func askName() { newName = coordinator.currentSceneName ?? "" askingName = true } } struct SceneDocument: FileDocument { static let readableContentTypes: [UTType] = [.json] var scene: MusubiCore.Scene init(scene: MusubiCore.Scene) { self.scene = scene } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } scene = try JSONDecoder().decode(MusubiCore.Scene.self, from: data) } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] return FileWrapper(regularFileWithContents: try encoder.encode(scene)) } }