Store wasOnDemandEnabled flag in providerConfiguration so the widget can restore On Demand state on next start. Clear the flag when On Demand is explicitly disabled in settings or when starting from the main app.
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
|
|
isSearching = !searchText.isEmpty
|
|
super.init()
|
|
dataModel = LogDataModel(commandClient: commandClient, viewModel: self)
|
|
}
|
|
|
|
public func togglePause() {
|
|
isPaused.toggle()
|
|
}
|
|
|
|
public func toggleSearch() {
|
|
isSearching.toggle()
|
|
if !isSearching {
|
|
searchText = ""
|
|
}
|
|
}
|
|
}
|