mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 12:51:22 +08:00
feat: open-source 小浣熊阅读 for iOS
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct DiagnosticLogView: View {
|
||||
@State private var entries: [DiagnosticLogEntry] = []
|
||||
@State private var query = ""
|
||||
@State private var level = "all"
|
||||
@State private var confirmsClear = false
|
||||
@State private var exportsLog = false
|
||||
@State private var exportText = ""
|
||||
@State private var errorMessage: String?
|
||||
|
||||
private var filtered: [DiagnosticLogEntry] {
|
||||
entries.filter {
|
||||
(level == "all" || $0.level.rawValue == level)
|
||||
&& (query.isEmpty || "\($0.category) \($0.message)".localizedCaseInsensitiveContains(query))
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
Picker("级别", selection: $level) {
|
||||
Text("全部").tag("all")
|
||||
Text("信息").tag("info")
|
||||
Text("警告").tag("warning")
|
||||
Text("错误").tag("error")
|
||||
}.pickerStyle(.segmented)
|
||||
Text("\(entries.count) 条日志 · 下拉刷新")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
} footer: {
|
||||
Text("在“其他设置”开启记录后,会保存网络请求、JS 输出和运行错误。崩溃诊断由 iOS 延迟提供,可搜索“系统诊断”查看。")
|
||||
}
|
||||
if filtered.isEmpty {
|
||||
ReadingUnavailableView(query.isEmpty ? "暂无日志" : "没有匹配的日志", systemImage: "doc.text.magnifyingglass")
|
||||
} else {
|
||||
ForEach(filtered) { entry in
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(entry.category).font(.caption.weight(.semibold))
|
||||
Text(levelTitle(entry.level)).font(.caption)
|
||||
.foregroundStyle(entry.level == .error ? .red : .secondary)
|
||||
Spacer()
|
||||
Text(entry.date, format: .dateTime.month().day().hour().minute().second())
|
||||
.font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
Text(entry.message).font(.system(.footnote, design: .monospaced)).textSelection(.enabled)
|
||||
}.padding(.vertical, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("日志与诊断")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.searchable(text: $query, prompt: "搜索内容或分类")
|
||||
.task { reload() }
|
||||
.refreshable { reload() }
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Menu("日志操作", systemImage: "ellipsis") {
|
||||
Button("刷新", systemImage: "arrow.clockwise") { reload() }
|
||||
Button("导出全部日志", systemImage: "square.and.arrow.up") {
|
||||
exportText = DiagnosticLog.shared.export()
|
||||
exportsLog = true
|
||||
}.disabled(entries.isEmpty)
|
||||
Button("清空日志", systemImage: "trash", role: .destructive) { confirmsClear = true }
|
||||
.disabled(entries.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog("清空全部日志?", isPresented: $confirmsClear, titleVisibility: .visible) {
|
||||
Button("清空日志", role: .destructive) {
|
||||
do { try DiagnosticLog.shared.clear(); reload() }
|
||||
catch { errorMessage = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
.fileExporter(isPresented: $exportsLog, document: DiagnosticLogDocument(text: exportText), contentType: .plainText, defaultFilename: "阅读诊断日志") { result in
|
||||
if case .failure(let error) = result { errorMessage = error.localizedDescription }
|
||||
}
|
||||
.alert("日志操作失败", isPresented: Binding(get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } })) {
|
||||
Button("好") { errorMessage = nil }
|
||||
} message: { Text(errorMessage ?? "") }
|
||||
}
|
||||
|
||||
private func reload() { entries = DiagnosticLog.shared.snapshot() }
|
||||
private func levelTitle(_ level: DiagnosticLogEntry.Level) -> String {
|
||||
switch level { case .info: "信息"; case .warning: "警告"; case .error: "错误" }
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiagnosticLogDocument: FileDocument {
|
||||
static var readableContentTypes: [UTType] { [.plainText] }
|
||||
var text: String
|
||||
init(text: String) { self.text = text }
|
||||
init(configuration: ReadConfiguration) throws {
|
||||
text = String(decoding: configuration.file.regularFileContents ?? Data(), as: UTF8.self)
|
||||
}
|
||||
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { .init(regularFileWithContents: Data(text.utf8)) }
|
||||
}
|
||||
Reference in New Issue
Block a user