diff --git a/ApplicationLibrary/Views/Log/ANSIColors.swift b/ApplicationLibrary/Views/Log/ANSIColors.swift index 35406ad..87e2bf6 100644 --- a/ApplicationLibrary/Views/Log/ANSIColors.swift +++ b/ApplicationLibrary/Views/Log/ANSIColors.swift @@ -16,7 +16,7 @@ public enum ANSIColors { private static let logPurple = Color(red: 0.61, green: 0.35, blue: 0.71) private static let logBlueLight = Color(red: 0.36, green: 0.68, blue: 0.89) private static let logWhite = Color(red: 0.93, green: 0.94, blue: 0.95) - + public static func clearCache() { cache.removeAllObjects() } diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index f873299..204f9ac 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -1,5 +1,5 @@ -import SwiftUI import Library +import SwiftUI public struct LogView: View { @Environment(\.selection) private var selection @@ -14,51 +14,29 @@ public struct LogView: View { private struct LogView0: View { @EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var commandClient: CommandClient + @State private var selectedLogLevel: Int? private let logFont = Font.system(.caption2, design: .monospaced) + private var filteredLogList: [LogEntry] { + let effectiveLevel = selectedLogLevel ?? commandClient.defaultLogLevel + return commandClient.logList.filter { $0.level <= effectiveLevel } + } + var body: some View { - if ApplicationLibrary.inPreview { - let logList = [ - "(packet-tunnel) log server started", - "INFO[0000] router: loaded geoip database: 250 codes", - "INFO[0000] router: loaded geosite database: 1400 codes", - "INFO[0000] router: updated default interface en0, index 11", - "inbound/tun[0]: started at utun3", - "sing-box started (1.666s)", - ] - ScrollView { - LazyVStack(alignment: .leading, spacing: 8) { - ForEach(logList.indices, id: \.self) { index in - Text(ANSIColors.parseAnsiString(logList[index])) - .font(logFont) - #if os(tvOS) - .focusable() - #endif - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding() - } - #if os(tvOS) - .focusEffectDisabled() - .focusSection() - #endif - } else if commandClient.logList.isEmpty { - VStack { - if commandClient.isConnected { - Text("Empty logs") - } else { - Text("Service not started").onAppear { - environments.connect() - } - } - } - } else { - ScrollViewReader { reader in + Group { + if ApplicationLibrary.inPreview { + let logList = [ + "(packet-tunnel) log server started", + "INFO[0000] router: loaded geoip database: 250 codes", + "INFO[0000] router: loaded geosite database: 1400 codes", + "INFO[0000] router: updated default interface en0, index 11", + "inbound/tun[0]: started at utun3", + "sing-box started (1.666s)", + ] ScrollView { LazyVStack(alignment: .leading, spacing: 8) { - ForEach(commandClient.logList.indices, id: \.self) { index in - Text(ANSIColors.parseAnsiString(commandClient.logList[index])) + ForEach(logList.indices, id: \.self) { index in + Text(ANSIColors.parseAnsiString(logList[index])) .font(logFont) #if os(tvOS) .focusable() @@ -72,21 +50,67 @@ public struct LogView: View { .focusEffectDisabled() .focusSection() #endif - .onAppear { - if let lastIndex = commandClient.logList.indices.last { - reader.scrollTo(lastIndex, anchor: .bottom) + } else if commandClient.logList.isEmpty { + VStack { + if commandClient.isConnected { + Text("Empty logs") + } else { + Text("Service not started").onAppear { + environments.connect() + } } } - .onChangeCompat(of: commandClient.logList.count) { _ in - guard let lastIndex = commandClient.logList.indices.last else { - return + } else { + ScrollViewReader { reader in + ScrollView { + LazyVStack(alignment: .leading, spacing: 8) { + ForEach(filteredLogList) { logEntry in + Text(ANSIColors.parseAnsiString(logEntry.message)) + .font(logFont) + #if os(tvOS) + .focusable() + #endif + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding() } - withAnimation { - reader.scrollTo(lastIndex, anchor: .bottom) + #if os(tvOS) + .focusEffectDisabled() + .focusSection() + #endif + .onAppear { + if let lastEntry = filteredLogList.last { + reader.scrollTo(lastEntry.id, anchor: .bottom) + } + } + .onChangeCompat(of: filteredLogList.count) { _ in + guard let lastEntry = filteredLogList.last else { + return + } + withAnimation { + reader.scrollTo(lastEntry.id, anchor: .bottom) + } } } } } + #if !os(tvOS) + .toolbar { + ToolbarItem { + Menu { + Picker("Log Level", selection: $selectedLogLevel) { + Text(NSLocalizedString("Default", comment: "Log level filter default option")).tag(Int?.none) + ForEach(LogLevel.allCases) { level in + Text(level.name).tag(Int?.some(level.rawValue)) + } + } + } label: { + Label("Filter", systemImage: "line.3.horizontal.circle") + } + } + } + #endif } } } diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift index 9e184d0..1f14541 100644 --- a/Library/Network/CommandClient.swift +++ b/Library/Network/CommandClient.swift @@ -1,6 +1,39 @@ import Foundation import Libbox +public struct LogEntry: Identifiable { + public let id = UUID() + public let level: Int + public let message: String +} + +public enum LogLevel: Int, CaseIterable, Identifiable { + public var id: Self { + self + } + + case error = 2 + case warn = 3 + case info = 4 + case debug = 5 + case trace = 6 + + public var name: String { + switch self { + case .error: + return "Error" + case .warn: + return "Warn" + case .info: + return "Info" + case .debug: + return "Debug" + case .trace: + return "Trace" + } + } +} + public class CommandClient: ObservableObject { public enum ConnectionType { case status @@ -17,8 +50,9 @@ public class CommandClient: ObservableObject { @Published public var isConnected: Bool @Published public var status: LibboxStatusMessage? @Published public var groups: [LibboxOutboundGroup]? - @Published public var logList: [String] - @Published public var defaultLogLevel: Int32 = 0 + @Published public var logList: [LogEntry] + @Published public var defaultLogLevel = 0 + @Published public var selectedLogLevel: Int? @Published public var clashModeList: [String] @Published public var clashMode: String @@ -164,7 +198,7 @@ public class CommandClient: ObservableObject { func setDefaultLogLevel(_ level: Int32) { DispatchQueue.main.async { [self] in - commandClient.defaultLogLevel = level + commandClient.defaultLogLevel = Int(level) } } @@ -179,17 +213,13 @@ public class CommandClient: ObservableObject { return } DispatchQueue.main.async { [self] in - var newLogList = commandClient.logList while messageList.hasNext() { let logEntry = messageList.next()! - if logEntry.level <= commandClient.defaultLogLevel { - newLogList.append(logEntry.message) - } + commandClient.logList.append(LogEntry(level: Int(logEntry.level), message: logEntry.message)) } - if newLogList.count >= commandClient.logMaxLines { - newLogList.removeSubrange(0 ... newLogList.count - commandClient.logMaxLines) + if commandClient.logList.count >= commandClient.logMaxLines { + commandClient.logList.removeSubrange(0 ... commandClient.logList.count - commandClient.logMaxLines) } - commandClient.logList = newLogList } } diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 52c2e1c..9c62daa 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -375,6 +375,9 @@ } } } + }, + "Default" : { + }, "Delete" : { "localizations" : { @@ -931,6 +934,9 @@ } } } + }, + "Log Level" : { + }, "Logs" : { "localizations" : {