PromtGarageMac / Sources /PromptGarageApp /Views /StatisticsView.swift
smlflg's picture
Initial public upload from Projekte/PromtGarageMac
4977db2 verified
Raw History Blame Contribute Delete
6.73 kB
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))
}
}
}