mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
198 lines
6.4 KiB
Swift
198 lines
6.4 KiB
Swift
import Combine
|
|
import Foundation
|
|
import Libbox
|
|
import Library
|
|
import SwiftUI
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#elseif canImport(AppKit)
|
|
import AppKit
|
|
#endif
|
|
|
|
@MainActor
|
|
public class LogDataModel: ObservableObject {
|
|
@Published public var filteredLogs: [LogEntry] = []
|
|
@Published public private(set) var visibleLogs: [LogEntry] = []
|
|
@Published public var showFileExporter = false
|
|
@Published public var logFileURL: URL?
|
|
|
|
private let commandClient: CommandClient
|
|
private weak var viewModel: LogViewModel?
|
|
private var pausedLogSnapshot: [LogEntry]?
|
|
private var lastPaused = false
|
|
private var lastProcessedLogCount = 0
|
|
private var lastEffectiveLevel: Int?
|
|
private var lastSearchText = ""
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
private static let maxVisibleLogs = 1000
|
|
|
|
public var isEmpty: Bool {
|
|
commandClient.logList.isEmpty
|
|
}
|
|
|
|
public var isConnected: Bool {
|
|
commandClient.isConnected
|
|
}
|
|
|
|
private func updateVisibleLogs() {
|
|
if filteredLogs.count <= Self.maxVisibleLogs {
|
|
visibleLogs = filteredLogs
|
|
} else {
|
|
visibleLogs = Array(filteredLogs.suffix(Self.maxVisibleLogs))
|
|
}
|
|
}
|
|
|
|
public init(commandClient: CommandClient, viewModel: LogViewModel) {
|
|
self.commandClient = commandClient
|
|
self.viewModel = viewModel
|
|
|
|
let debouncedSearchText = viewModel.$searchText
|
|
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
|
|
|
Publishers.CombineLatest(
|
|
Publishers.CombineLatest4(
|
|
commandClient.$logList,
|
|
commandClient.$defaultLogLevel,
|
|
viewModel.$selectedLogLevel,
|
|
debouncedSearchText
|
|
),
|
|
viewModel.$isPaused
|
|
)
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] combined, isPaused in
|
|
guard let self else { return }
|
|
let (logList, defaultLogLevel, selectedLogLevel, searchText) = combined
|
|
let effectiveLevel = selectedLogLevel ?? defaultLogLevel
|
|
|
|
if isPaused, !self.lastPaused {
|
|
self.pausedLogSnapshot = logList
|
|
self.lastProcessedLogCount = 0
|
|
} else if !isPaused, self.lastPaused {
|
|
self.pausedLogSnapshot = nil
|
|
self.lastProcessedLogCount = 0
|
|
}
|
|
self.lastPaused = isPaused
|
|
|
|
let sourceList = self.pausedLogSnapshot ?? logList
|
|
|
|
if isPaused, effectiveLevel == self.lastEffectiveLevel, searchText == self.lastSearchText,
|
|
self.lastProcessedLogCount > 0
|
|
{
|
|
return
|
|
}
|
|
|
|
let canIncrement = self.lastProcessedLogCount > 0 &&
|
|
sourceList.count > self.lastProcessedLogCount &&
|
|
effectiveLevel == self.lastEffectiveLevel &&
|
|
searchText == self.lastSearchText
|
|
|
|
if canIncrement {
|
|
let newLogs = sourceList[self.lastProcessedLogCount...]
|
|
let newFilteredLogs = newLogs.filter { log in
|
|
log.level <= effectiveLevel &&
|
|
(searchText.isEmpty || log.message.contains(searchText))
|
|
}
|
|
self.filteredLogs.append(contentsOf: newFilteredLogs)
|
|
} else {
|
|
self.filteredLogs = sourceList.filter { log in
|
|
log.level <= effectiveLevel &&
|
|
(searchText.isEmpty || log.message.contains(searchText))
|
|
}
|
|
}
|
|
|
|
self.updateVisibleLogs()
|
|
self.lastProcessedLogCount = sourceList.count
|
|
self.lastEffectiveLevel = effectiveLevel
|
|
self.lastSearchText = searchText
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
public func clearLogs() {
|
|
viewModel?.isPaused = false
|
|
pausedLogSnapshot = nil
|
|
lastPaused = false
|
|
lastProcessedLogCount = 0
|
|
lastEffectiveLevel = nil
|
|
lastSearchText = ""
|
|
filteredLogs = []
|
|
visibleLogs = []
|
|
commandClient.clearLogs()
|
|
Task.detached {
|
|
try? LibboxNewStandaloneCommandClient()!.clearLogs()
|
|
}
|
|
}
|
|
|
|
public func getLogsText() -> String {
|
|
filteredLogs.map(\.message).joined(separator: "\n")
|
|
}
|
|
|
|
#if !os(tvOS)
|
|
private static let dateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd-HH:mm:ss"
|
|
return formatter
|
|
}()
|
|
|
|
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 cleanupLogFile() {
|
|
guard let url = logFileURL else { return }
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
public func prepareLogFile() {
|
|
cleanupLogFile()
|
|
do {
|
|
let text = getLogsText()
|
|
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)
|
|
logFileURL = fileURL
|
|
} catch {
|
|
viewModel?.alert = AlertState(action: "prepare log file", error: error)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
@MainActor
|
|
public class LogViewModel: BaseViewModel {
|
|
@Published public var selectedLogLevel: Int?
|
|
@Published public var isPaused = false
|
|
@Published public var searchText = ""
|
|
@Published public var isSearching = false
|
|
|
|
public let commandClient: CommandClient
|
|
public private(set) var dataModel: LogDataModel!
|
|
|
|
public init(commandClient: CommandClient, searchText: String = "") {
|
|
self.commandClient = commandClient
|
|
self.searchText = searchText
|
|
self.isSearching = !searchText.isEmpty
|
|
super.init()
|
|
dataModel = LogDataModel(commandClient: commandClient, viewModel: self)
|
|
}
|
|
|
|
public func togglePause() {
|
|
isPaused.toggle()
|
|
}
|
|
|
|
public func toggleSearch() {
|
|
isSearching.toggle()
|
|
if !isSearching {
|
|
searchText = ""
|
|
}
|
|
}
|
|
}
|