import PromptGarageCore import SwiftUI struct LibraryView: View { @Bindable var model: AppModel private var title: String { switch model.destination { case .all: "Alle Prompts" case .favorites: "Favoriten" case .mostUsed: "Meistgenutzt" case .byDay: "Nach Tagen" case .byTopic: "Nach Topics" default: "Bibliothek" } } var body: some View { VStack(spacing: 0) { if model.destination == .byDay || model.destination == .byTopic { groupingBar Divider() } filterBar Divider() if model.prompts.isEmpty && !model.isLoadingLibrary { LibraryEmptyState( hasActiveQuery: !model.searchText.isEmpty || model.selectedTag != nil || model.selectedSourceFamily != nil || model.destination == .byDay || model.destination == .byTopic, canCreate: model.canMutatePrompts, clearFilters: model.clearLibraryFilters, createPrompt: model.presentCreatePrompt ) } else { List(selection: $model.selectedPromptID) { ForEach(model.prompts) { prompt in PromptRow(prompt: prompt) .tag(prompt.id) .contextMenu { Button("Nur kopieren") { Task { await model.copy(prompt, countAsUse: false) } } Button(prompt.isStarred ? "Aus Favoriten entfernen" : "Zu Favoriten") { Task { await model.toggleStar(prompt) } } .disabled(!model.canMutatePrompts) Divider() Button("Bearbeiten …") { model.presentEditPrompt(prompt) } .disabled(!model.canMutatePrompts) } } if model.hasMoreLibraryResults { HStack { Text( "\(model.prompts.count.formatted()) von " + "\(model.totalResults.formatted()) geladen" ) .font(.caption) .foregroundStyle(.secondary) Spacer(minLength: 12) Button { Task { await model.loadMoreLibraryResults() } } label: { if model.isLoadingMoreLibrary { HStack(spacing: 8) { ProgressView().controlSize(.small) Text("Weitere Prompts laden …") } } else { Label( "Weitere \(model.nextLibraryPageCount.formatted()) laden", systemImage: "arrow.down.circle" ) } } .buttonStyle(.bordered) .disabled(!model.canLoadMoreLibraryResults) } .padding(.vertical, 10) .listRowSeparator(.hidden) } else if model.totalResults > 0 { Text("Alle \(model.totalResults.formatted()) Prompts geladen") .font(.caption) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 8) .listRowSeparator(.hidden) } } .listStyle(.inset) .overlay { if model.isLoadingLibrary { ProgressView("Prompts laden …") .padding() .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10)) } } } } .navigationTitle(title) .searchable(text: $model.searchText, placement: .toolbar, prompt: "Prompts durchsuchen") .toolbar { ToolbarItemGroup { Button { model.destination = .importCenter } label: { Label("Importieren", systemImage: "square.and.arrow.down") } Button { model.presentCreatePrompt() } label: { Label("Neuer Prompt", systemImage: "plus") } .disabled(!model.canMutatePrompts) } } .task(id: model.libraryQueryKey) { try? await Task.sleep(for: .milliseconds(180)) guard !Task.isCancelled else { return } await model.refreshLibrary() } } private var filterBar: some View { HStack(spacing: 10) { Text("\(model.totalResults.formatted())") .font(.headline.monospacedDigit()) Text(model.totalResults == 1 ? "Prompt" : "Prompts") .font(.subheadline) .foregroundStyle(.secondary) Spacer(minLength: 12) if model.destination != .byTopic { Menu { Button("Alle Tags") { model.selectedTag = nil } Divider() ForEach(model.availableTags) { tag in Button { model.selectedTag = tag.name } label: { HStack { Text(tag.name) Spacer() Text(tag.count.formatted()) } } } } label: { Label(model.selectedTag ?? "Tag", systemImage: "tag") } .menuStyle(.borderlessButton) .fixedSize() } Menu { Button("Alle Quellen") { model.selectedSourceFamily = nil } Divider() ForEach(model.availableSourceFamilies) { source in Button { model.selectedSourceFamily = source.name } label: { HStack { Text(source.name.capitalized) Spacer() Text(source.count.formatted()) } } } } label: { Label( model.selectedSourceFamily?.capitalized ?? "Quelle", systemImage: "shippingbox" ) } .menuStyle(.borderlessButton) .fixedSize() Picker("Sortierung", selection: $model.sort) { Text("Zuletzt geändert").tag(PromptQuery.Sort.updatedDescending) Text("Titel A–Z").tag(PromptQuery.Sort.titleAscending) Text("Meistgenutzt").tag(PromptQuery.Sort.mostUsed) } .labelsHidden() .frame(width: 155) if model.selectedTag != nil || model.selectedSourceFamily != nil || !model.searchText.isEmpty { Button("Filter löschen", action: model.clearLibraryFilters) .buttonStyle(.borderless) .font(.caption) } } .padding(.horizontal, 14) .frame(minHeight: 49) .background(.bar) } @ViewBuilder private var groupingBar: some View { if model.destination == .byDay { VStack(alignment: .leading, spacing: 7) { HStack { Label("Prompt-Datum", systemImage: "calendar") .font(.subheadline.weight(.semibold)) Spacer() Picker("Tag", selection: $model.selectedDay) { ForEach(model.dayGroups) { group in Text("\(formattedDay(group.name)) · \(group.count.formatted())") .tag(Optional(group.name)) } } .labelsHidden() .frame(maxWidth: 220) } if model.legacyUndatedCount > 0 { Label( "\(model.legacyUndatedCount.formatted()) Legacy-Prompts: Originaldatum unbekannt. Sie werden nicht umdatiert.", systemImage: "clock.badge.questionmark" ) .font(.caption) .foregroundStyle(.secondary) } } .padding(.horizontal, 14) .padding(.vertical, 10) .background(AppUI.accent.opacity(0.045)) } else { HStack(spacing: 10) { Label("Bestehendes Tag", systemImage: "tag") .font(.subheadline.weight(.semibold)) Picker("Topic", selection: $model.selectedTopic) { ForEach(model.topicGroups) { group in Text("\(group.displayName) · \(group.count.formatted())") .tag(Optional(group)) } } .labelsHidden() .frame(maxWidth: 220) Spacer() Text("Read-only · kein AI-Clustering") .font(.caption) .foregroundStyle(.secondary) } .padding(.horizontal, 14) .frame(minHeight: 49) .background(AppUI.accent.opacity(0.045)) } } private func formattedDay(_ value: String) -> String { let parser = DateFormatter() parser.calendar = Calendar(identifier: .gregorian) parser.locale = Locale(identifier: "en_US_POSIX") parser.dateFormat = "yyyy-MM-dd" guard let date = parser.date(from: value) else { return value } return date.formatted(date: .abbreviated, time: .omitted) } } private struct PromptRow: View { let prompt: PromptRecord var body: some View { VStack(alignment: .leading, spacing: 7) { HStack(alignment: .firstTextBaseline, spacing: 7) { if prompt.isStarred { Image(systemName: "star.fill") .font(.caption) .foregroundStyle(AppUI.accent) } Text(prompt.title) .font(.body.weight(.semibold)) .lineLimit(1) Spacer(minLength: 8) if prompt.useCount > 0 { Text("\(prompt.useCount)×") .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } } Text(prompt.content.replacingOccurrences(of: "\n", with: " ")) .font(.callout) .foregroundStyle(.secondary) .lineLimit(2) HStack(spacing: 6) { StatusChip(title: prompt.sourceFamily.capitalized, systemImage: "shippingbox") ForEach(prompt.tagList.prefix(2), id: \.self) { tag in StatusChip(title: tag, systemImage: "tag") } if prompt.tagList.count > 2 { Text("+\(prompt.tagList.count - 2)") .font(.caption2) .foregroundStyle(.tertiary) } } } .padding(.vertical, 5) .accessibilityElement(children: .combine) } } private struct LibraryEmptyState: View { let hasActiveQuery: Bool let canCreate: Bool let clearFilters: () -> Void let createPrompt: () -> Void var body: some View { ContentUnavailableView { Label( hasActiveQuery ? "Keine Treffer" : "Die Garage ist leer", systemImage: hasActiveQuery ? "magnifyingglass" : "archivebox" ) } description: { Text( hasActiveQuery ? "Passe Suchbegriff oder Filter an. Deine lokale Sammlung wurde nicht verändert." : "Erstelle deinen ersten Prompt oder importiere eine bestehende Sammlung." ) } actions: { if hasActiveQuery { Button("Filter löschen", action: clearFilters) } else if canCreate { Button("Prompt erstellen", action: createPrompt) .buttonStyle(.borderedProminent) } } } }