Files
nahbar/nahbar/LogExportView.swift

185 lines
6.3 KiB
Swift

import SwiftUI
// MARK: - LogExportView
//
// Zeigt den In-App-Event-Log mit Level-Filter und Export an.
// Erreichbar über Einstellungen Entwickler-Log.
struct LogExportView: View {
@Environment(\.nahbarTheme) var theme
@ObservedObject private var log = AppEventLog.shared
@State private var selectedMinLevel: AppEventLog.Entry.Level = .info
@State private var showingClearConfirm = false
private var filteredEntries: [AppEventLog.Entry] {
log.entries(minLevel: selectedMinLevel).reversed()
}
var body: some View {
ZStack {
theme.backgroundPrimary.ignoresSafeArea()
VStack(spacing: 0) {
filterBar
Divider()
.background(theme.borderSubtle)
if filteredEntries.isEmpty {
emptyState
} else {
entryList
}
}
}
.navigationTitle("Entwickler-Log")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
// Export
ShareLink(
item: LogExportDocument(text: log.exportText()),
preview: SharePreview("nahbar-log.txt", icon: Image(systemName: "doc.text"))
) {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 15))
}
// Löschen
Button {
showingClearConfirm = true
} label: {
Image(systemName: "trash")
.font(.system(size: 15))
.foregroundStyle(.red.opacity(0.8))
}
.confirmationDialog(
"Log löschen?",
isPresented: $showingClearConfirm,
titleVisibility: .visible
) {
Button("Löschen", role: .destructive) { log.clear() }
Button("Abbrechen", role: .cancel) {}
} message: {
Text("Alle \(log.entries.count) Einträge werden entfernt.")
}
}
}
}
// MARK: - Filter Bar
private var filterBar: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(AppEventLog.Entry.Level.allCases, id: \.self) { level in
Button {
selectedMinLevel = level
} label: {
HStack(spacing: 4) {
Text(level.emoji)
.font(.system(size: 12))
Text(level.rawValue)
.font(.system(size: 12, weight: selectedMinLevel == level ? .semibold : .regular))
}
.foregroundStyle(selectedMinLevel == level ? theme.accent : theme.contentTertiary)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
selectedMinLevel == level
? theme.accent.opacity(0.12)
: theme.backgroundSecondary
)
.clipShape(Capsule())
}
}
Spacer()
Text("\(filteredEntries.count) Einträge")
.font(.system(size: 11))
.foregroundStyle(theme.contentTertiary)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
.background(theme.backgroundPrimary)
}
// MARK: - Entry List
private var entryList: some View {
ScrollView {
LazyVStack(spacing: 0, pinnedViews: []) {
ForEach(filteredEntries) { entry in
LogEntryRow(entry: entry)
Divider()
.padding(.leading, 16)
.background(theme.borderSubtle.opacity(0.5))
}
}
.padding(.bottom, 24)
}
}
// MARK: - Empty State
private var emptyState: some View {
VStack(spacing: 12) {
Image(systemName: "doc.text.magnifyingglass")
.font(.system(size: 36))
.foregroundStyle(theme.contentTertiary)
Text("Keine Einträge für diesen Filter")
.font(.system(size: 15))
.foregroundStyle(theme.contentTertiary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: - Log Entry Row
private struct LogEntryRow: View {
@Environment(\.nahbarTheme) var theme
let entry: AppEventLog.Entry
var body: some View {
HStack(alignment: .top, spacing: 10) {
// Level-Indikator
Rectangle()
.fill(entry.level.color)
.frame(width: 3)
.frame(minHeight: 36)
VStack(alignment: .leading, spacing: 3) {
// Timestamp + Category + Level
HStack(spacing: 6) {
Text(entry.formattedTimestamp)
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(theme.contentTertiary)
Text("[\(entry.category)]")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(theme.contentTertiary)
Spacer()
Text(entry.level.emoji)
.font(.system(size: 11))
}
// Nachricht
Text(entry.message)
.font(.system(size: 13, design: .monospaced))
.foregroundStyle(
entry.level == .info || entry.level == .success
? theme.contentPrimary
: entry.level.color
)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 10)
.padding(.trailing, 14)
}
.padding(.leading, 12)
.background(theme.backgroundPrimary)
}
}