243 lines
6.1 KiB
Swift
243 lines
6.1 KiB
Swift
import Combine
|
|
import Foundation
|
|
import FlutterMacOS
|
|
|
|
enum DockDisplayMode {
|
|
case regular
|
|
case menuBarOnly
|
|
}
|
|
|
|
enum MenuBarConnectionState: String {
|
|
case connected
|
|
case connecting
|
|
case disconnected
|
|
case error
|
|
|
|
init(rawValue: String, fallbackLabel: String) {
|
|
switch rawValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
|
case "connected":
|
|
self = .connected
|
|
case "connecting":
|
|
self = .connecting
|
|
case "error":
|
|
self = .error
|
|
case "disconnected":
|
|
self = .disconnected
|
|
default:
|
|
self = fallbackLabel.lowercased().contains("connect")
|
|
? .connecting
|
|
: .disconnected
|
|
}
|
|
}
|
|
|
|
var symbolName: String {
|
|
switch self {
|
|
case .connected:
|
|
return "checkmark.circle.fill"
|
|
case .connecting:
|
|
return "arrow.triangle.2.circlepath.circle"
|
|
case .disconnected:
|
|
return "xmark.circle"
|
|
case .error:
|
|
return "exclamationmark.triangle.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AppStatusSnapshot {
|
|
let connectionState: MenuBarConnectionState
|
|
let connectionLabel: String
|
|
let runningTasks: Int
|
|
let pausedTasks: Int
|
|
let timedOutTasks: Int
|
|
let queuedTasks: Int
|
|
let scheduledTasks: Int
|
|
let failedTasks: Int
|
|
let totalTasks: Int
|
|
let badgeCount: Int
|
|
|
|
init(
|
|
connectionState: MenuBarConnectionState,
|
|
connectionLabel: String,
|
|
runningTasks: Int,
|
|
pausedTasks: Int,
|
|
timedOutTasks: Int,
|
|
queuedTasks: Int,
|
|
scheduledTasks: Int,
|
|
failedTasks: Int,
|
|
totalTasks: Int,
|
|
badgeCount: Int
|
|
) {
|
|
self.connectionState = connectionState
|
|
self.connectionLabel = connectionLabel
|
|
self.runningTasks = runningTasks
|
|
self.pausedTasks = pausedTasks
|
|
self.timedOutTasks = timedOutTasks
|
|
self.queuedTasks = queuedTasks
|
|
self.scheduledTasks = scheduledTasks
|
|
self.failedTasks = failedTasks
|
|
self.totalTasks = totalTasks
|
|
self.badgeCount = badgeCount
|
|
}
|
|
|
|
static let unavailable = AppStatusSnapshot(
|
|
connectionState: .disconnected,
|
|
connectionLabel: "Disconnected",
|
|
runningTasks: 0,
|
|
pausedTasks: 0,
|
|
timedOutTasks: 0,
|
|
queuedTasks: 0,
|
|
scheduledTasks: 0,
|
|
failedTasks: 0,
|
|
totalTasks: 0,
|
|
badgeCount: 0
|
|
)
|
|
|
|
init(payload: [String: Any]) {
|
|
let connectionLabel = (payload["connectionLabel"] as? String)?.trimmingCharacters(
|
|
in: .whitespacesAndNewlines
|
|
)
|
|
let rawStatus = (payload["connectionStatus"] as? String) ?? ""
|
|
self.connectionLabel = connectionLabel?.isEmpty == false ? connectionLabel! : "Disconnected"
|
|
connectionState = MenuBarConnectionState(
|
|
rawValue: rawStatus,
|
|
fallbackLabel: self.connectionLabel
|
|
)
|
|
runningTasks = Self.intValue(payload["runningTasks"])
|
|
pausedTasks = Self.intValue(payload["pausedTasks"])
|
|
timedOutTasks = Self.intValue(payload["timedOutTasks"])
|
|
queuedTasks = Self.intValue(payload["queuedTasks"])
|
|
scheduledTasks = Self.intValue(payload["scheduledTasks"])
|
|
failedTasks = Self.intValue(payload["failedTasks"])
|
|
totalTasks = Self.intValue(payload["totalTasks"])
|
|
let badgeCount = Self.intValue(payload["badgeCount"])
|
|
self.badgeCount = badgeCount > 0 ? badgeCount : runningTasks + queuedTasks
|
|
}
|
|
|
|
var menuTaskSummary: String {
|
|
"任务: 运行 \(runningTasks) | 暂停 \(pausedTasks) | 超时 \(timedOutTasks)"
|
|
}
|
|
|
|
var menuStatusSummary: String {
|
|
"当前状态: \(connectionLabel)"
|
|
}
|
|
|
|
var buttonBadgeText: String {
|
|
badgeCount > 0 ? " \(badgeCount)" : ""
|
|
}
|
|
|
|
private static func intValue(_ value: Any?) -> Int {
|
|
if let number = value as? NSNumber {
|
|
return number.intValue
|
|
}
|
|
if let value = value as? Int {
|
|
return value
|
|
}
|
|
if let value = value as? Double {
|
|
return Int(value)
|
|
}
|
|
if let value = value as? String, let parsed = Int(value) {
|
|
return parsed
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
|
|
protocol AppStatusSnapshotProviding: AnyObject {
|
|
func bind(channel: FlutterMethodChannel?)
|
|
func fetchSnapshot(completion: @escaping (AppStatusSnapshot) -> Void)
|
|
func prepareForExit(completion: @escaping () -> Void)
|
|
}
|
|
|
|
final class FlutterAppStatusProvider: AppStatusSnapshotProviding {
|
|
private weak var channel: FlutterMethodChannel?
|
|
|
|
func bind(channel: FlutterMethodChannel?) {
|
|
self.channel = channel
|
|
}
|
|
|
|
func fetchSnapshot(completion: @escaping (AppStatusSnapshot) -> Void) {
|
|
guard let channel else {
|
|
completion(.unavailable)
|
|
return
|
|
}
|
|
channel.invokeMethod("desktopStatusSnapshot", arguments: nil) { result in
|
|
guard let payload = result as? [String: Any] else {
|
|
completion(.unavailable)
|
|
return
|
|
}
|
|
completion(AppStatusSnapshot(payload: payload))
|
|
}
|
|
}
|
|
|
|
func prepareForExit(completion: @escaping () -> Void) {
|
|
guard let channel else {
|
|
completion()
|
|
return
|
|
}
|
|
channel.invokeMethod("prepareForExit", arguments: nil) { _ in
|
|
completion()
|
|
}
|
|
}
|
|
}
|
|
|
|
final class AppStatusViewModel: ObservableObject {
|
|
@Published var snapshot: AppStatusSnapshot = .unavailable
|
|
|
|
private let provider: AppStatusSnapshotProviding
|
|
private let refreshInterval: TimeInterval
|
|
private var refreshCancellable: AnyCancellable?
|
|
|
|
init(
|
|
provider: AppStatusSnapshotProviding,
|
|
refreshInterval: TimeInterval = 5
|
|
) {
|
|
self.provider = provider
|
|
self.refreshInterval = refreshInterval
|
|
}
|
|
|
|
func bind(channel: FlutterMethodChannel?) {
|
|
provider.bind(channel: channel)
|
|
refreshNow()
|
|
}
|
|
|
|
func startRefreshing() {
|
|
guard refreshCancellable == nil else {
|
|
return
|
|
}
|
|
refreshNow()
|
|
refreshCancellable = Timer.publish(
|
|
every: refreshInterval,
|
|
on: .main,
|
|
in: .common
|
|
)
|
|
.autoconnect()
|
|
.sink { [weak self] _ in
|
|
self?.refreshNow()
|
|
}
|
|
}
|
|
|
|
func stopRefreshing() {
|
|
refreshCancellable?.cancel()
|
|
refreshCancellable = nil
|
|
}
|
|
|
|
func refreshNow() {
|
|
provider.fetchSnapshot { [weak self] snapshot in
|
|
DispatchQueue.main.async {
|
|
self?.snapshot = snapshot
|
|
}
|
|
}
|
|
}
|
|
|
|
func prepareForExit(completion: @escaping () -> Void) {
|
|
provider.prepareForExit(completion: completion)
|
|
}
|
|
|
|
func applyRemoteSnapshot(_ snapshot: AppStatusSnapshot) {
|
|
DispatchQueue.main.async {
|
|
self.snapshot = snapshot
|
|
}
|
|
}
|
|
}
|