feat: Long-Press-Kontextmenüs für Decks, Karten + Marktplatz
DeckStackTile: Bearbeiten, Duplizieren, Löschen. CardPreviewRow: Vorder-/Rückseite kopieren, Löschen. BrowseRow (Marktplatz): Link kopieren + teilen. Neuer geteilter PlatformClipboard-Helfer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9392303da
commit
d809658e5f
4 changed files with 105 additions and 0 deletions
20
Sources/Core/PlatformClipboard.swift
Normal file
20
Sources/Core/PlatformClipboard.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import SwiftUI
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
/// Plattform-übergreifendes Kopieren in die Zwischenablage (iOS
|
||||
/// `UIPasteboard`, macOS `NSPasteboard`). Geteilt von den Listen-
|
||||
/// Kontextmenüs (Karten-Vorder-/Rückseite, Marktplatz-Link).
|
||||
enum PlatformClipboard {
|
||||
static func copy(_ text: String) {
|
||||
#if canImport(UIKit)
|
||||
UIPasteboard.general.string = text
|
||||
#elseif canImport(AppKit)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -298,6 +298,28 @@ struct DeckDetailView: View {
|
|||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHint("Tippen zum Bearbeiten")
|
||||
.contextMenu {
|
||||
if let front = card.fields["front"] ?? card.fields["text"], !front.isEmpty {
|
||||
Button {
|
||||
PlatformClipboard.copy(front)
|
||||
} label: {
|
||||
Label("Vorderseite kopieren", systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
if let back = card.fields["back"], !back.isEmpty {
|
||||
Button {
|
||||
PlatformClipboard.copy(back)
|
||||
} label: {
|
||||
Label("Rückseite kopieren", systemImage: "doc.on.doc.fill")
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive) {
|
||||
Task { await deleteCard(card) }
|
||||
} label: {
|
||||
Label("Löschen", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -371,6 +393,18 @@ struct DeckDetailView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func deleteCard(_ card: Card) async {
|
||||
cards.removeAll { $0.id == card.id }
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
try await api.deleteCard(id: card.id)
|
||||
await loadCards()
|
||||
} catch {
|
||||
Log.api.warning("deleteCard failed: \(String(describing: error), privacy: .public)")
|
||||
await loadCards()
|
||||
}
|
||||
}
|
||||
|
||||
private func delete() async {
|
||||
deleteError = nil
|
||||
let api = WordeckAPI(auth: auth)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,28 @@ struct DeckListView: View {
|
|||
decks.filter(\.isFromMarketplace)
|
||||
}
|
||||
|
||||
/// Dupliziert ein Deck serverseitig und lädt die Liste neu.
|
||||
private func duplicateDeck(_ id: String) async {
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
_ = try await api.duplicateDeck(id: id)
|
||||
await store?.refresh()
|
||||
} catch {
|
||||
Log.api.warning("duplicateDeck failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Löscht ein Deck serverseitig und lädt die Liste neu.
|
||||
private func deleteDeck(_ id: String) async {
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
try await api.deleteDeck(id: id)
|
||||
await store?.refresh()
|
||||
} catch {
|
||||
Log.api.warning("deleteDeck failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func deckSection(title: String, icon: String, decks: [CachedDeck]) -> some View {
|
||||
if !decks.isEmpty {
|
||||
|
|
@ -132,6 +154,24 @@ struct DeckListView: View {
|
|||
onEdit: { path.append(DeckRoute.detail(deckId: deck.id)) }
|
||||
)
|
||||
.frame(width: 240)
|
||||
.contextMenu {
|
||||
Button {
|
||||
path.append(DeckRoute.detail(deckId: deck.id))
|
||||
} label: {
|
||||
Label("Bearbeiten", systemImage: "pencil")
|
||||
}
|
||||
Button {
|
||||
Task { await duplicateDeck(deck.id) }
|
||||
} label: {
|
||||
Label("Duplizieren", systemImage: "plus.square.on.square")
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive) {
|
||||
Task { await deleteDeck(deck.id) }
|
||||
} label: {
|
||||
Label("Löschen", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,17 @@ struct BrowseView: View {
|
|||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
|
||||
.contextMenu {
|
||||
let url = "https://wordeck.com/marketplace/decks/\(entry.slug)"
|
||||
Button {
|
||||
PlatformClipboard.copy(url)
|
||||
} label: {
|
||||
Label("Link kopieren", systemImage: "link")
|
||||
}
|
||||
ShareLink(item: URL(string: url) ?? URL(string: "https://wordeck.com")!) {
|
||||
Label("Teilen …", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue