Download Sources/PromptGarageApp/Views/LibraryView.swift from smlflg/PromtGarageMac: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/smlflg/PromtGarageMac/resolve/main/Sources/PromptGarageApp/Views/LibraryView.swift
- Command line
-
hf download hf://smlflg/PromtGarageMac/Sources/PromptGarageApp/Views/LibraryView.swift
-
curl -L -o LibraryView.swift https://huggingface.co/smlflg/PromtGarageMac/resolve/main/Sources/PromptGarageApp/Views/LibraryView.swift
13.4 kB
| import PromptGarageCore | |
| import SwiftUI | |
| struct LibraryView: View { | |
| 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) | |
| } | |
| 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) | |
| } | |
| } | |
| } | |
| } | |