Fix logs
This commit is contained in:
@@ -26,28 +26,71 @@ struct LogTextView: View {
|
||||
class LogCoordinator {
|
||||
var lastLogsCount: Int = 0
|
||||
var lastLog: LogEntry?
|
||||
var lastSearchText: String = ""
|
||||
var cachedAttributedString: NSAttributedString?
|
||||
|
||||
func shouldUpdate(logs: [LogEntry]) -> Bool {
|
||||
func shouldUpdate(logs: [LogEntry], searchText: String) -> UpdateStrategy {
|
||||
let currentCount = logs.count
|
||||
if currentCount == lastLogsCount, currentCount > 0 {
|
||||
let searchChanged = searchText != lastSearchText
|
||||
|
||||
// Check if nothing changed
|
||||
if currentCount == lastLogsCount, currentCount > 0, !searchChanged {
|
||||
if let lastLog = logs.last, let previousLastLog = self.lastLog {
|
||||
if lastLog.id == previousLastLog.id {
|
||||
return false
|
||||
return .noUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine update strategy
|
||||
let strategy: UpdateStrategy
|
||||
if currentCount == 0 || searchChanged || lastLogsCount > currentCount {
|
||||
// Full rebuild needed
|
||||
strategy = .fullRebuild
|
||||
cachedAttributedString = nil
|
||||
} else if currentCount > lastLogsCount {
|
||||
// Incremental update possible
|
||||
strategy = .incremental(from: lastLogsCount)
|
||||
} else {
|
||||
// Same count but different last log (shouldn't happen normally)
|
||||
strategy = .fullRebuild
|
||||
cachedAttributedString = nil
|
||||
}
|
||||
|
||||
lastLogsCount = currentCount
|
||||
lastLog = logs.last
|
||||
return true
|
||||
lastSearchText = searchText
|
||||
return strategy
|
||||
}
|
||||
|
||||
enum UpdateStrategy {
|
||||
case noUpdate
|
||||
case fullRebuild
|
||||
case incremental(from: Int)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
private func buildAttributedString(logs: [LogEntry], monoFont: PlatformFont, defaultColor: PlatformColor, searchText: String) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
private func buildAttributedString(logs: [LogEntry], monoFont: PlatformFont, defaultColor: PlatformColor, searchText: String, baseAttributedString: NSAttributedString? = nil, startIndex: Int = 0) -> NSAttributedString {
|
||||
let result: NSMutableAttributedString
|
||||
let highlightColor: PlatformColor = .systemYellow
|
||||
|
||||
for (index, log) in logs.enumerated() {
|
||||
if let base = baseAttributedString {
|
||||
result = NSMutableAttributedString(attributedString: base)
|
||||
// Add newline separator if appending to existing content
|
||||
if result.length > 0 {
|
||||
result.append(NSAttributedString(string: "\n", attributes: [
|
||||
.foregroundColor: defaultColor,
|
||||
.font: monoFont,
|
||||
]))
|
||||
}
|
||||
} else {
|
||||
result = NSMutableAttributedString()
|
||||
}
|
||||
|
||||
let logsToProcess = logs[startIndex...]
|
||||
|
||||
for (offset, log) in logsToProcess.enumerated() {
|
||||
let attributedString = ANSIColors.parseAnsiString(log.message)
|
||||
let nsAttributedString = NSMutableAttributedString(string: String(attributedString.characters))
|
||||
|
||||
@@ -72,7 +115,8 @@ class LogCoordinator {
|
||||
|
||||
result.append(nsAttributedString)
|
||||
|
||||
if index < logs.count - 1 {
|
||||
let isLastLog = (startIndex + offset) == (logs.count - 1)
|
||||
if !isLastLog {
|
||||
result.append(NSAttributedString(string: "\n", attributes: [
|
||||
.foregroundColor: defaultColor,
|
||||
.font: monoFont,
|
||||
@@ -152,8 +196,20 @@ class LogCoordinator {
|
||||
}
|
||||
|
||||
func updateUIView(_ textView: UITextView, context: Context) {
|
||||
guard context.coordinator.shouldUpdate(logs: logs) else { return }
|
||||
textView.attributedText = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText)
|
||||
let updateStrategy = context.coordinator.shouldUpdate(logs: logs, searchText: searchText)
|
||||
|
||||
switch updateStrategy {
|
||||
case .noUpdate:
|
||||
return
|
||||
case .fullRebuild:
|
||||
let attributedString = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText)
|
||||
context.coordinator.cachedAttributedString = attributedString
|
||||
textView.attributedText = attributedString
|
||||
case let .incremental(from: startIndex):
|
||||
let attributedString = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText, baseAttributedString: context.coordinator.cachedAttributedString, startIndex: startIndex)
|
||||
context.coordinator.cachedAttributedString = attributedString
|
||||
textView.attributedText = attributedString
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> LogCoordinator {
|
||||
@@ -200,13 +256,22 @@ class LogCoordinator {
|
||||
guard let textView = scrollView.documentView as? NSTextView else { return }
|
||||
|
||||
let lastCount = context.coordinator.lastLogsCount
|
||||
guard context.coordinator.shouldUpdate(logs: logs) else { return }
|
||||
let updateStrategy = context.coordinator.shouldUpdate(logs: logs, searchText: searchText)
|
||||
|
||||
switch updateStrategy {
|
||||
case .noUpdate:
|
||||
return
|
||||
case .fullRebuild:
|
||||
let attributedText = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText)
|
||||
let shouldScroll = shouldAutoScroll && logs.count != lastCount
|
||||
|
||||
context.coordinator.cachedAttributedString = attributedText
|
||||
textView.textStorage?.setAttributedString(attributedText)
|
||||
case let .incremental(from: startIndex):
|
||||
let attributedText = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText, baseAttributedString: context.coordinator.cachedAttributedString, startIndex: startIndex)
|
||||
context.coordinator.cachedAttributedString = attributedText
|
||||
textView.textStorage?.setAttributedString(attributedText)
|
||||
}
|
||||
|
||||
let shouldScroll = shouldAutoScroll && logs.count != lastCount
|
||||
if shouldScroll {
|
||||
DispatchQueue.main.async {
|
||||
textView.scrollToEndOfDocument(nil)
|
||||
|
||||
@@ -54,7 +54,8 @@ private struct LogViewContent: View {
|
||||
LogExportView(
|
||||
showFileExporter: $viewModel.showFileExporter,
|
||||
logFileURL: $viewModel.logFileURL,
|
||||
alert: $viewModel.alert
|
||||
alert: $viewModel.alert,
|
||||
cleanup: viewModel.cleanupLogFile
|
||||
)
|
||||
)
|
||||
#endif
|
||||
@@ -214,7 +215,6 @@ private struct LogViewContent: View {
|
||||
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")
|
||||
}
|
||||
@@ -244,6 +244,7 @@ private struct LogViewContent: View {
|
||||
@Binding var logFileURL: URL?
|
||||
@Binding var alert: Alert?
|
||||
@State private var showShareSheet = false
|
||||
let cleanup: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Color.clear
|
||||
@@ -253,6 +254,8 @@ private struct LogViewContent: View {
|
||||
contentType: .plainText,
|
||||
defaultFilename: "logs.txt"
|
||||
) { result in
|
||||
cleanup()
|
||||
logFileURL = nil
|
||||
if case let .failure(error) = result {
|
||||
alert = Alert(error)
|
||||
}
|
||||
@@ -273,6 +276,7 @@ private struct LogViewContent: View {
|
||||
}
|
||||
.onChange(of: showShareSheet) { newValue in
|
||||
if !newValue {
|
||||
cleanup()
|
||||
logFileURL = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
#if canImport(UIKit)
|
||||
@@ -20,6 +21,9 @@ public class LogViewModel: ObservableObject {
|
||||
@Published public var logFileURL: URL?
|
||||
|
||||
private let commandClient: CommandClient
|
||||
private var lastProcessedLogCount = 0
|
||||
private var lastEffectiveLevel: Int?
|
||||
private var lastSearchText = ""
|
||||
|
||||
public var isEmpty: Bool { commandClient.logList.isEmpty }
|
||||
public var isConnected: Bool { commandClient.isConnected }
|
||||
@@ -36,16 +40,41 @@ public class LogViewModel: ObservableObject {
|
||||
$selectedLogLevel,
|
||||
debouncedSearchText
|
||||
)
|
||||
.map { logList, defaultLogLevel, selectedLogLevel, searchText in
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] logList, defaultLogLevel, selectedLogLevel, searchText in
|
||||
guard let self = self else { return }
|
||||
let effectiveLevel = selectedLogLevel ?? defaultLogLevel
|
||||
return logList.filter { log in
|
||||
|
||||
// Check if we can do incremental filtering
|
||||
let canIncrement = self.lastProcessedLogCount > 0 &&
|
||||
logList.count > self.lastProcessedLogCount &&
|
||||
effectiveLevel == self.lastEffectiveLevel &&
|
||||
searchText == self.lastSearchText
|
||||
|
||||
if canIncrement {
|
||||
// Incremental filtering: only filter new logs
|
||||
let newLogs = logList[self.lastProcessedLogCount...]
|
||||
let newFilteredLogs = newLogs.filter { log in
|
||||
log.level <= effectiveLevel &&
|
||||
(searchText.isEmpty || log.message.contains(searchText))
|
||||
}
|
||||
self.filteredLogs.append(contentsOf: newFilteredLogs)
|
||||
} else {
|
||||
// Full refiltering needed
|
||||
self.filteredLogs = logList.filter { log in
|
||||
log.level <= effectiveLevel &&
|
||||
(searchText.isEmpty || log.message.contains(searchText))
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: &$filteredLogs)
|
||||
|
||||
self.lastProcessedLogCount = logList.count
|
||||
self.lastEffectiveLevel = effectiveLevel
|
||||
self.lastSearchText = searchText
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public func togglePause() {
|
||||
isPaused.toggle()
|
||||
@@ -59,11 +88,23 @@ public class LogViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
public func clearLogs() {
|
||||
commandClient.logList.removeAll()
|
||||
isPaused = false
|
||||
lastProcessedLogCount = 0
|
||||
lastEffectiveLevel = nil
|
||||
lastSearchText = ""
|
||||
Task.detached {
|
||||
let client = LibboxNewStandaloneCommandClient()
|
||||
try? client?.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd-HH:mm:ss"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
public func getLogsText() -> String {
|
||||
filteredLogs.map(\.message).joined(separator: "\n")
|
||||
}
|
||||
@@ -78,12 +119,16 @@ public class LogViewModel: ObservableObject {
|
||||
#endif
|
||||
}
|
||||
|
||||
public func cleanupLogFile() {
|
||||
guard let url = logFileURL else { return }
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
|
||||
public func prepareLogFile() {
|
||||
cleanupLogFile()
|
||||
do {
|
||||
let text = getLogsText()
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd-HH:mm:ss"
|
||||
let dateString = dateFormatter.string(from: Date())
|
||||
let dateString = Self.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)
|
||||
|
||||
@@ -222,8 +222,9 @@ public class CommandClient: ObservableObject {
|
||||
let logEntry = messageList.next()!
|
||||
commandClient.logList.append(LogEntry(level: Int(logEntry.level), message: logEntry.message))
|
||||
}
|
||||
if commandClient.logList.count >= commandClient.logMaxLines {
|
||||
commandClient.logList.removeSubrange(0 ... commandClient.logList.count - commandClient.logMaxLines)
|
||||
if commandClient.logList.count > commandClient.logMaxLines {
|
||||
let removeCount = commandClient.logList.count - commandClient.logMaxLines
|
||||
commandClient.logList.removeFirst(removeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user