185 lines
6.3 KiB
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)
|
|
}
|
|
}
|