import SwiftUI struct FirstLaunchView: View { @Bindable var model: AppModel private var isBusy: Bool { if case .validating = model.launchState { return true } return false } var body: some View { ZStack { LinearGradient( colors: [ AppUI.accent.opacity(0.10), Color.clear, Color(nsColor: .windowBackgroundColor), ], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea() VStack(spacing: 28) { VStack(spacing: 14) { Image(systemName: "archivebox.fill") .font(.system(size: 44, weight: .medium)) .foregroundStyle(AppUI.accent) .frame(width: 84, height: 84) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 22)) .overlay { RoundedRectangle(cornerRadius: 22) .stroke(AppUI.accent.opacity(0.22)) } .shadow(color: AppUI.accent.opacity(0.16), radius: 24, y: 10) Text("Deine Garage auf diesem Mac") .font(.largeTitle.weight(.semibold)) Text(model.requiresManualDatabaseRecovery ? "Die vorhandene App-Datenbank braucht eine manuelle Wiederherstellung. Sie wird niemals automatisch ersetzt." : "Übernimm einmalig eine bestehende PromptGarage-Datenbank oder starte mit einer leeren, lokalen Sammlung.") .font(.body) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 560) } VStack(alignment: .leading, spacing: 18) { Label( model.requiresManualDatabaseRecovery ? "Manuelle Wiederherstellung erforderlich" : "Vorhandene Sammlung übernehmen", systemImage: model.requiresManualDatabaseRecovery ? "externaldrive.badge.exclamationmark" : "cylinder.split.1x2" ) .font(.headline) HStack(spacing: 12) { Image(systemName: model.requiresManualDatabaseRecovery ? "lock.fill" : (model.selectedBootstrapURL == nil ? "doc.badge.plus" : "cylinder.fill")) .font(.title3) .foregroundStyle( model.requiresManualDatabaseRecovery || model.selectedBootstrapURL == nil ? .secondary : AppUI.accent ) VStack(alignment: .leading, spacing: 3) { Text(model.requiresManualDatabaseRecovery ? "Vorhandene App-Datenbank gesperrt" : (model.selectedBootstrapURL?.lastPathComponent ?? "Noch keine prompts.db ausgewählt")) .font(.callout.weight(.medium)) .lineLimit(1) Text(model.requiresManualDatabaseRecovery ? model.applicationDatabasePath : (model.selectedBootstrapURL?.deletingLastPathComponent().path(percentEncoded: false) ?? "Das Original bleibt unverändert.")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } Spacer() Button("Auswählen …") { model.chooseBootstrapDatabase() } .disabled(isBusy || !model.canChooseBootstrapDatabase) } .padding(14) .background( Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 11) ) .overlay { RoundedRectangle(cornerRadius: 11) .stroke(Color(nsColor: .separatorColor).opacity(0.6)) } LabeledContent("Ziel") { Text(model.applicationDatabasePath) .font(.caption.monospaced()) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) } if model.requiresManualDatabaseRecovery { VStack(alignment: .leading, spacing: 8) { Label( model.bootstrapFailureMessage ?? "Die vorhandene App-Datenbank kann nicht geöffnet werden.", systemImage: "xmark.octagon.fill" ) .foregroundStyle(.red) Text("Automatischer Ersatz ist gesperrt. Beende PromptGarage und verschiebe oder benenne die vorhandene Datei im Finder oder Terminal manuell um:") .foregroundStyle(.secondary) Text(model.applicationDatabasePath) .font(.caption.monospaced()) .textSelection(.enabled) Text("Starte PromptGarage anschließend erneut. Erst wenn am Ziel keine Datenbank mehr liegt, kannst du eine Quelle übernehmen oder leer starten.") .foregroundStyle(.secondary) } .font(.callout) .textSelection(.enabled) } else if case let .invalid(message) = model.launchState { VStack(alignment: .leading, spacing: 5) { Label(message, systemImage: "xmark.octagon.fill") .foregroundStyle(.red) Text("Die gewählte Quelle wurde nicht kopiert. Am App-Ziel liegt weiterhin keine Datenbank; du kannst eine andere Quelle wählen oder leer starten.") .foregroundStyle(.secondary) } .font(.callout) .textSelection(.enabled) } else if case let .failed(message) = model.launchState { VStack(alignment: .leading, spacing: 5) { Label(message, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.red) .textSelection(.enabled) if !model.isBootstrapAvailable { Text("Datenbank auswählen, übernehmen und Leerstart sind deaktiviert, bis der App-Speicher wieder verfügbar ist.") .foregroundStyle(.secondary) } } .font(.callout) } HStack { if model.requiresManualDatabaseRecovery { StatusChip( title: "Auswahl, Übernahme und Leerstart gesperrt", systemImage: "lock.fill", tint: .secondary ) } else { Button("Leer starten") { Task { await model.startWithEmptyDatabase() } } .disabled(isBusy || !model.canCreateEmptyDatabase) } Spacer() Button { Task { await model.importSelectedBootstrapDatabase() } } label: { if isBusy { HStack(spacing: 8) { ProgressView().controlSize(.small) Text("Prüfen und übernehmen …") } } else { Text("Datenbank prüfen und übernehmen") } } .buttonStyle(.borderedProminent) .disabled( !model.canInstallSelectedBootstrapDatabase || isBusy ) } } .padding(24) .frame(maxWidth: 680) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18)) .overlay { RoundedRectangle(cornerRadius: 18) .stroke(Color(nsColor: .separatorColor).opacity(0.55)) } LocalStatusLabel(databaseName: nil) } .padding(40) } } }