Add export for logs

This commit is contained in:
世界
2025-11-26 22:25:51 +08:00
parent a71c1784bb
commit b7a810b168
3 changed files with 187 additions and 0 deletions
+127
View File
@@ -1,5 +1,13 @@
import Library
import SwiftUI
#if !os(tvOS)
import UniformTypeIdentifiers
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
#endif
public struct LogView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@@ -41,6 +49,14 @@ private struct LogViewContent: View {
}
}
}
.alertBinding($viewModel.alert)
.background(
LogExportView(
showFileExporter: $viewModel.showFileExporter,
logFileURL: $viewModel.logFileURL,
alert: $viewModel.alert
)
)
#endif
}
@@ -180,6 +196,24 @@ private struct LogViewContent: View {
} label: {
Label("Log Level", systemImage: "slider.horizontal.3")
}
#if !os(tvOS)
Menu {
Button(action: viewModel.copyToClipboard) {
Label("To Clipboard", systemImage: "doc.on.clipboard")
}
Button(action: {
viewModel.prepareLogFile()
viewModel.showFileExporter = true
}) {
Label("To File", systemImage: "arrow.down.doc")
}
Button(action: viewModel.prepareLogFile) {
Label("Share", systemImage: "square.and.arrow.up")
}
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
#endif
Divider()
Button(role: .destructive, action: viewModel.clearLogs) {
Label(NSLocalizedString("Clear Logs", comment: "Clear all logs"), systemImage: "trash")
@@ -204,4 +238,97 @@ private struct LogViewContent: View {
}
}
}
private struct LogExportView: View {
@Binding var showFileExporter: Bool
@Binding var logFileURL: URL?
@Binding var alert: Alert?
@State private var showShareSheet = false
var body: some View {
Color.clear
.fileExporter(
isPresented: $showFileExporter,
document: logFileURL.map { LogTextDocument(url: $0) },
contentType: .plainText,
defaultFilename: "logs.txt"
) { result in
if case let .failure(error) = result {
alert = Alert(error)
}
}
.sheet(isPresented: $showShareSheet) {
if let url = logFileURL {
#if os(iOS)
ShareViewController(activityItems: [url])
#elseif os(macOS)
ShareView(items: [url], alert: $alert)
#endif
}
}
.onChange(of: logFileURL) { newValue in
if newValue != nil, !showFileExporter {
showShareSheet = true
}
}
.onChange(of: showShareSheet) { newValue in
if !newValue {
logFileURL = nil
}
}
}
}
private struct LogTextDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
private let url: URL
init(url: URL) {
self.url = url
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try data.write(to: tempURL)
url = tempURL
}
func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
try FileWrapper(url: url)
}
}
#if os(iOS)
private struct ShareViewController: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context _: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}
func updateUIViewController(_: UIActivityViewController, context _: Context) {}
}
#elseif os(macOS)
private struct ShareView: NSViewRepresentable {
let items: [Any]
@Binding var alert: Alert?
func makeNSView(context _: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context _: Context) {
let picker = NSSharingServicePicker(items: items)
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
#endif
#endif
@@ -1,6 +1,12 @@
import Combine
import Foundation
import Library
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
@MainActor
public class LogViewModel: ObservableObject {
@@ -9,6 +15,9 @@ public class LogViewModel: ObservableObject {
@Published public var searchText = ""
@Published public var isSearching = false
@Published public var filteredLogs: [LogEntry] = []
@Published public var alert: Alert?
@Published public var showFileExporter = false
@Published public var logFileURL: URL?
private let commandClient: CommandClient
@@ -53,4 +62,35 @@ public class LogViewModel: ObservableObject {
commandClient.logList.removeAll()
isPaused = false
}
#if !os(tvOS)
public func getLogsText() -> String {
filteredLogs.map(\.message).joined(separator: "\n")
}
public func copyToClipboard() {
let text = getLogsText()
#if os(iOS)
UIPasteboard.general.string = text
#elseif os(macOS)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(text, forType: .string)
#endif
}
public func prepareLogFile() {
do {
let text = getLogsText()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd-HH:mm:ss"
let dateString = dateFormatter.string(from: Date())
let tempDirectory = FileManager.default.temporaryDirectory
let fileURL = tempDirectory.appendingPathComponent("logs-\(dateString).txt")
try text.write(to: fileURL, atomically: true, encoding: .utf8)
logFileURL = fileURL
} catch {
alert = Alert(error)
}
}
#endif
}
+20
View File
@@ -1590,6 +1590,26 @@
}
}
},
"To Clipboard" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "到剪贴板"
}
}
}
},
"To File" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "到文件"
}
}
}
},
"To import configurations from your iPhone or iPad, make sure sing-box is the **same version** on both devices and **VPN is disabled**." : {
"localizations" : {
"zh-Hans" : {