Files
ggbond268--mactools/Plugins/Appearance/Sources/AppearancePlugin.swift
T
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

145 lines
4.7 KiB
Swift

import AppKit
import Foundation
import OSLog
import SwiftUI
import MacToolsPluginKit
public final class AppearancePluginFactory: NSObject, MacToolsPluginBundleFactory {
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
AppearancePluginProvider(context: context)
}
}
@MainActor
private struct AppearancePluginProvider: PluginProvider {
let context: PluginRuntimeContext
func makePlugins() -> [any MacToolsPlugin] {
[AppearancePlugin(localization: PluginLocalization(bundle: context.resourceBundle))]
}
}
@MainActor
final class AppearancePlugin: MacToolsPlugin, PluginPrimaryPanel {
let metadata: PluginMetadata
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .keepPresented
)
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "cc.ggbond.mactools", category: "AppearancePlugin")
private let localization: PluginLocalization
private var isDarkMode: Bool = false
private nonisolated(unsafe) var themeObserver: NSObjectProtocol?
init(localization: PluginLocalization = PluginLocalization(bundle: .main)) {
self.localization = localization
self.metadata = PluginMetadata(
id: "appearance",
title: localization.string("metadata.title", defaultValue: "深色模式"),
iconName: "circle.lefthalf.filled",
iconTint: Color(nsColor: .systemIndigo),
order: 30,
defaultDescription: localization.string(
"metadata.description",
defaultValue: "切换系统亮色与深色外观"
)
)
isDarkMode = Self.readSystemDarkMode()
observeSystemAppearanceChanges()
}
deinit {
if let observer = themeObserver {
DistributedNotificationCenter.default().removeObserver(observer)
}
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: isDarkMode
? localization.string("panel.subtitle.enabled", defaultValue: "已开启")
: localization.string("panel.subtitle.disabled", defaultValue: "已关闭"),
isOn: isDarkMode,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var permissionRequirements: [PluginPermissionRequirement] { [] }
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
func handlePermissionAction(id: String) {}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {}
func refresh() {
let current = Self.readSystemDarkMode()
if current != isDarkMode {
isDarkMode = current
onStateChange?()
}
}
func handleAction(_ action: PluginPanelAction) {
guard case let .setSwitch(enable) = action else { return }
setDarkMode(enable)
}
// MARK: - Private
private static func readSystemDarkMode() -> Bool {
let style = UserDefaults(suiteName: ".GlobalPreferences")?.string(forKey: "AppleInterfaceStyle")
return style == "Dark"
}
private func setDarkMode(_ enable: Bool) {
let script = """
tell application "System Events"
tell appearance preferences
set dark mode to \(enable ? "true" : "false")
end tell
end tell
"""
let appleScript = NSAppleScript(source: script)
var error: NSDictionary?
appleScript?.executeAndReturnError(&error)
if let error {
logger.error("Failed to set dark mode: \(error)")
} else {
isDarkMode = enable
onStateChange?()
}
}
private func observeSystemAppearanceChanges() {
themeObserver = DistributedNotificationCenter.default().addObserver(
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
object: nil,
queue: .main
) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
let current = Self.readSystemDarkMode()
if current != self.isDarkMode {
self.isDarkMode = current
self.onStateChange?()
}
}
}
}
}