Add export for logs
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
Reference in New Issue
Block a user