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) } }