mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
Collapse the four definitions of "one log line" into LogLine (timestamp, level, source, text) with a single renderer and parser. agent.log lines now carry the uniform prefix, absorbing the ssxng: event marker into the source column; legacy lines degrade to raw display without rewriting. Level detection moves into the row model with a classic three-level taxonomy whose unified-log projection stays unchanged. The diagnostics log view merges GUI events and the agent.log tail through DiagnosticLogIntake into one time-ordered list with level and source filters; the tail window logic moves into the intake and blank lines are kept for raw fidelity. The report whitelist projection and unified-log routing are unchanged.
79 lines
2.5 KiB
Swift
79 lines
2.5 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
/// 进程内保留日志来源;轮询只跟随这一个窗口的真实开关状态。
|
|
@MainActor
|
|
final class DiagnosticsWindowState: ObservableObject {
|
|
static let sceneID = "diagnostics"
|
|
|
|
@Published private(set) var isOpen = false
|
|
@Published var filter = DiagnosticLogFilter()
|
|
@Published var exportedDiagnosticsPath: String?
|
|
let errors = ErrorAlertPresenter()
|
|
private weak var window: NSWindow?
|
|
|
|
init() {
|
|
let center = NotificationCenter.default
|
|
center.addObserver(
|
|
self, selector: #selector(windowPresented(_:)),
|
|
name: NSWindow.didBecomeKeyNotification, object: nil)
|
|
center.addObserver(
|
|
self, selector: #selector(windowPresented(_:)),
|
|
name: NSWindow.didChangeOcclusionStateNotification, object: nil)
|
|
center.addObserver(
|
|
self, selector: #selector(windowClosed(_:)),
|
|
name: NSWindow.willCloseNotification, object: nil)
|
|
}
|
|
|
|
deinit { NotificationCenter.default.removeObserver(self) }
|
|
|
|
func attach(to window: NSWindow) {
|
|
self.window = window
|
|
if window.isVisible { isOpen = true }
|
|
}
|
|
|
|
@objc private func windowPresented(_ notification: Notification) {
|
|
guard let candidate = notification.object as? NSWindow,
|
|
candidate === window, candidate.isVisible, !isOpen
|
|
else { return }
|
|
isOpen = true
|
|
}
|
|
|
|
@objc private func windowClosed(_ notification: Notification) {
|
|
guard let candidate = notification.object as? NSWindow, candidate === window else { return }
|
|
isOpen = false
|
|
exportedDiagnosticsPath = nil
|
|
errors.dismiss()
|
|
}
|
|
}
|
|
|
|
/// 只上交宿主窗口;不拥有窗口、不改变 delegate,也不把隐藏误当关闭。
|
|
struct DiagnosticsWindowAnchor: NSViewRepresentable {
|
|
let state: DiagnosticsWindowState
|
|
|
|
func makeNSView(context: Context) -> AnchorView { AnchorView(state: state) }
|
|
func updateNSView(_ nsView: AnchorView, context: Context) {}
|
|
|
|
final class AnchorView: NSView {
|
|
private let state: DiagnosticsWindowState
|
|
|
|
init(state: DiagnosticsWindowState) {
|
|
self.state = state
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("NSView 不走 storyboard 初始化") }
|
|
|
|
override func viewDidMoveToWindow() {
|
|
super.viewDidMoveToWindow()
|
|
guard let window else { return }
|
|
// attach 发生于视图更新中;延迟发布,避免在 SwiftUI 更新中发布状态。
|
|
Task { @MainActor [weak self, weak window] in
|
|
guard let self, let window, self.window === window else { return }
|
|
state.attach(to: window)
|
|
}
|
|
}
|
|
}
|
|
}
|