import PromptGarageCore import SwiftUI struct StatisticsView: View { @Bindable var model: AppModel var body: some View { ScrollView { VStack(alignment: .leading, spacing: 22) { VStack(alignment: .leading, spacing: 7) { Text("Statistik") .font(.largeTitle.weight(.semibold)) Text("Ein lokaler Blick auf deine Sammlung — ohne Sync- oder Cloud-Annahmen.") .foregroundStyle(.secondary) } LazyVGrid(columns: [GridItem(.adaptive(minimum: 175), spacing: 12)], spacing: 12) { MetricCard(title: "Gesamt", value: model.statistics.total, systemImage: "tray.full") MetricCard(title: "Favoriten", value: model.statistics.starred, systemImage: "star.fill") MetricCard(title: "Verwendungen", value: model.statistics.totalUses, systemImage: "arrow.up.forward.circle") } if model.statistics.total == 0 && !model.isLoadingStatistics { ContentUnavailableView { Label("Noch keine Statistik", systemImage: "chart.bar.xaxis") } description: { Text("Sobald Prompts vorhanden sind, erscheinen hier Quellen und Bewertungen.") } .frame(maxWidth: .infinity, minHeight: 260) } else { VStack(alignment: .leading, spacing: 13) { Text("Quellenfamilien") .font(.headline) ForEach(model.statistics.sourceFamilies.prefix(10)) { source in CountBar( name: source.name.capitalized, count: source.count, maximum: model.statistics.sourceFamilies.first?.count ?? 1, tint: AppUI.accent ) } } .statisticSection() VStack(alignment: .leading, spacing: 13) { Text("Bewertungen") .font(.headline) ForEach(Array((0...5).reversed()), id: \.self) { rating in CountBar( name: rating == 0 ? "Unbewertet" : String(repeating: "★", count: rating), count: model.statistics.ratingDistribution[rating, default: 0], maximum: model.statistics.ratingDistribution.values.max() ?? 1, tint: rating == 0 ? .secondary : AppUI.accent ) } } .statisticSection() } } .padding(24) .frame(maxWidth: 760) .frame(maxWidth: .infinity) } .navigationTitle("Statistik") .overlay { if model.isLoadingStatistics { LoadingOverlay(label: "Statistik berechnen …") } } .task { await model.refreshStatistics() } } } struct StatisticsDetailView: View { @Bindable var model: AppModel var body: some View { ScrollView { VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 6) { Text("Top Tags") .font(.title2.weight(.semibold)) Text("Bestehende Tags, rein lokal gezählt.") .font(.callout) .foregroundStyle(.secondary) } if model.statistics.tags.isEmpty { ContentUnavailableView { Label("Keine Tags", systemImage: "tag") } description: { Text("Tagge Prompts, um Themen in deiner Sammlung sichtbar zu machen.") } .frame(minHeight: 240) } else { ForEach(model.statistics.tags.prefix(20)) { tag in CountBar( name: tag.name, count: tag.count, maximum: model.statistics.tags.first?.count ?? 1, tint: .blue ) } } Divider() VStack(alignment: .leading, spacing: 12) { Text("Modelle") .font(.headline) ForEach(model.statistics.models.prefix(10)) { modelName in HStack { Text(modelName.name) .lineLimit(1) Spacer() Text(modelName.count.formatted()) .font(.callout.monospacedDigit()) .foregroundStyle(.secondary) } } } } .padding(24) .frame(maxWidth: 660, alignment: .leading) .frame(maxWidth: .infinity) } .navigationTitle("Sammlung") } } private struct CountBar: View { let name: String let count: Int let maximum: Int let tint: Color private var fraction: Double { guard maximum > 0 else { return 0 } return min(1, Double(count) / Double(maximum)) } var body: some View { VStack(spacing: 6) { HStack { Text(name) .lineLimit(1) Spacer() Text(count.formatted()) .font(.callout.monospacedDigit()) .foregroundStyle(.secondary) } GeometryReader { proxy in ZStack(alignment: .leading) { Capsule().fill(.quaternary) Capsule() .fill(tint) .frame(width: max(3, proxy.size.width * fraction)) } } .frame(height: 6) } } } private extension View { func statisticSection() -> some View { padding(18) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: AppUI.cardCornerRadius)) .overlay { RoundedRectangle(cornerRadius: AppUI.cardCornerRadius) .stroke(Color(nsColor: .separatorColor).opacity(0.6)) } } }