import ManaCore import SwiftData import SwiftUI /// Detail-View für ein Public-Deck. Subscribe-Button löst Auto-Fork /// serverseitig aus und navigiert anschließend zur eigenen Deck-Detail. struct PublicDeckView: View { let slug: String @Environment(AuthClient.self) private var auth @Environment(\.modelContext) private var context @State private var detail: PublicDeckDetail? @State private var isLoading = false @State private var isSubscribing = false @State private var errorMessage: String? @State private var subscribed: SubscribeResponse? var body: some View { ZStack { CardsTheme.background.ignoresSafeArea() content } .navigationTitle(detail?.deck.title ?? "Deck") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .task(id: slug) { await load() } } @ViewBuilder private var content: some View { if isLoading, detail == nil { ProgressView() .tint(CardsTheme.primary) } else if let detail { ScrollView { VStack(alignment: .leading, spacing: 16) { header(detail: detail) Divider().background(CardsTheme.border) metadata(detail: detail) Divider().background(CardsTheme.border) subscribeSection(detail: detail) if let errorMessage { Text(errorMessage) .font(.caption) .foregroundStyle(CardsTheme.error) .padding(.horizontal, 16) } } .padding(.vertical, 16) } } else if let errorMessage { ContentUnavailableView( "Deck nicht gefunden", systemImage: "questionmark.folder", description: Text(errorMessage) ) .foregroundStyle(CardsTheme.foreground) } } private func header(detail: PublicDeckDetail) -> some View { VStack(alignment: .leading, spacing: 8) { HStack { Text(detail.deck.title) .font(.title.bold()) .foregroundStyle(CardsTheme.foreground) if detail.deck.isFeatured { Image(systemName: "star.fill") .foregroundStyle(CardsTheme.warning) } } if let description = detail.deck.description, !description.isEmpty { Text(description) .foregroundStyle(CardsTheme.mutedForeground) } } .padding(.horizontal, 16) } private func metadata(detail: PublicDeckDetail) -> some View { VStack(alignment: .leading, spacing: 8) { if let owner = detail.owner { HStack(spacing: 6) { Image(systemName: "person.crop.circle") .foregroundStyle(CardsTheme.mutedForeground) Text(owner.displayName) .foregroundStyle(CardsTheme.foreground) if owner.verifiedMana { Image(systemName: "checkmark.seal.fill") .font(.caption) .foregroundStyle(CardsTheme.primary) } } .font(.subheadline) } HStack(spacing: 16) { if let version = detail.latestVersion { Label("v\(version.semver)", systemImage: "tag") Label("\(version.cardCount) Karten", systemImage: "rectangle.stack") } Label(detail.deck.license, systemImage: "doc.text") if let language = detail.deck.language { Label(language.uppercased(), systemImage: "globe") } } .font(.caption) .foregroundStyle(CardsTheme.mutedForeground) if let changelog = detail.latestVersion?.changelog, !changelog.isEmpty { Text("Changelog") .font(.caption.weight(.semibold)) .foregroundStyle(CardsTheme.mutedForeground) .padding(.top, 8) Text(changelog) .font(.caption) .foregroundStyle(CardsTheme.foreground) } } .padding(.horizontal, 16) } @ViewBuilder private func subscribeSection(detail: PublicDeckDetail) -> some View { VStack(spacing: 12) { if let subscribed { Label("Abonniert — dein Fork ist in deiner Bibliothek", systemImage: "checkmark.circle.fill") .foregroundStyle(CardsTheme.success) .padding() .frame(maxWidth: .infinity) .background(CardsTheme.success.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) NavigationLink(value: subscribed.privateDeckId) { Label("Zum eigenen Deck", systemImage: "arrow.right.circle") .frame(maxWidth: .infinity) .padding(.vertical, 10) .background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10)) .foregroundStyle(CardsTheme.primaryForeground) } .buttonStyle(.plain) } else if detail.deck.isTakedown { Label("Dieses Deck wurde entfernt", systemImage: "exclamationmark.triangle") .foregroundStyle(CardsTheme.error) } else if detail.deck.latestVersionId == nil { Label("Noch keine Version veröffentlicht", systemImage: "clock") .foregroundStyle(CardsTheme.mutedForeground) } else { Button { Task { await subscribe(detail: detail) } } label: { HStack { if isSubscribing { ProgressView() .controlSize(.small) .tint(CardsTheme.primaryForeground) } Text(detail.deck.priceCredits > 0 ? "Abonnieren (\(detail.deck.priceCredits) Credits)" : "Abonnieren") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10)) .foregroundStyle(CardsTheme.primaryForeground) } .buttonStyle(.plain) .disabled(isSubscribing) } } .padding(.horizontal, 16) } private func load() async { isLoading = true defer { isLoading = false } let api = CardsAPI(auth: auth) do { detail = try await api.publicDeck(slug: slug) } catch { errorMessage = (error as? LocalizedError)?.errorDescription ?? String(describing: error) } } private func subscribe(detail: PublicDeckDetail) async { isSubscribing = true errorMessage = nil defer { isSubscribing = false } let api = CardsAPI(auth: auth) do { let response = try await api.subscribe(slug: slug) subscribed = response // Cache neu laden, damit der Fork in der Liste auftaucht let store = DeckListStore(auth: auth, context: context) await store.refresh() } catch { errorMessage = (error as? LocalizedError)?.errorDescription ?? String(describing: error) } } }