* feat: integrate Sparkle 2 for auto-update Adds the standard pieces for Sparkle-based silent updates: - Package.swift: add Sparkle 2 SPM dep, wired into OpenLessApp. - Sources/OpenLessApp/Updater/UpdaterController.swift: thin wrapper around SPUStandardUpdaterController. Owned by AppDelegate so the updater starts on app launch. - AppDelegate: instantiate UpdaterController before installing the main menu; pass it to ApplicationMenu so the menu item can target it. - ApplicationMenu: add "检查更新…" as the first item in the OpenLess menu when an UpdaterController target is present. - scripts/build-app.sh: copy Sparkle.framework (with Updater.app + XPC services + Autoupdate) from the SPM artifacts dir into Contents/Frameworks/, inject SUFeedURL / SUPublicEDKey / SUEnableAutomaticChecks / SUEnableInstallerLauncherService / SUScheduledCheckInterval into Info.plist, and ad-hoc sign the embedded helpers explicitly (deep sign alone is unreliable for Sparkle's nested XPC bundles). - scripts/release.sh: one-shot release pipeline. ./scripts/release.sh <version> [<build>] [<notes>] Bumps version in build-app.sh, builds, zips, signs the zip with the Keychain-stored EdDSA private key (auto-downloads sign_update from the Sparkle 2.9.1 release on first run), prepends a new <item> to appcast.xml, commits + tags + pushes main, and creates the GitHub release with the zip uploaded. - appcast.xml: empty initial feed file at repo root, served via the raw.githubusercontent.com URL referenced by SUFeedURL. Items are appended automatically by release.sh. Public EdDSA key embedded in the build: iT00+eUw/55obn1suEnWqI7za2pc8mHIFIdRbOWXW1Q= Private key lives only in the local Keychain on the release machine (account: macOS login user; service: https://sparkle-project.org). First-time install still requires the user to remove the quarantine xattr (no Apple Developer ID); subsequent updates flow through Sparkle's installer helper and bypass quarantine. * fix(sparkle): add @executable_path/../Frameworks rpath to OpenLessApp Without this, the SPM-built executable links Sparkle's symbols but ships with no rpath that points at the embedded framework, so dyld fails at launch with: Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle …and the user sees the macOS Gatekeeper-styled "请与开发者联系,以确保 OpenLess 可以配合此 macOS 版本使用" error. Inject the rpath via linkerSettings.unsafeFlags so the binary itself knows where to look. --------- Co-authored-by: baiqing <lbx12309@icloud.com>
78 lines
3.0 KiB
Swift
78 lines
3.0 KiB
Swift
import AppKit
|
|
import OpenLessHotkey
|
|
import OpenLessPersistence
|
|
import OpenLessRecorder
|
|
|
|
@MainActor
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
private var coordinator: DictationCoordinator?
|
|
private var menuBar: MenuBarController?
|
|
private var onboarding: OnboardingWindowController?
|
|
private var updaterController: UpdaterController?
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
// 不依赖 UserPreferences flag:每次启动直接查实际权限。
|
|
// ad-hoc 签名下每次 rebuild 二进制 hash 都变,TCC 会自动失效,
|
|
// 必须在那种情况下重新进引导,否则用户看不到 menu bar 也就罢了,
|
|
// 还会无声地拿不到快捷键和麦克风。
|
|
if !AccessibilityPermission.isGranted() || !MicrophonePermission.isGranted() {
|
|
showOnboarding()
|
|
return
|
|
}
|
|
let updater = UpdaterController()
|
|
self.updaterController = updater
|
|
ApplicationMenu.install(updaterTarget: updater)
|
|
let coordinator = DictationCoordinator()
|
|
let menuBar = MenuBarController(coordinator: coordinator)
|
|
coordinator.menuBar = menuBar
|
|
coordinator.bootstrap()
|
|
self.coordinator = coordinator
|
|
self.menuBar = menuBar
|
|
coordinator.openHome()
|
|
runLaunchActions(coordinator: coordinator)
|
|
}
|
|
|
|
private func showOnboarding() {
|
|
let controller = OnboardingWindowController { [weak self] in
|
|
// 留 flag=true 仅作"用户走过引导"的标记;启动门是实际权限状态。
|
|
UserPreferences.shared.hasCompletedOnboarding = true
|
|
// 必须重启:CGEventTap 只在进程获得辅助功能权限后下一次创建时才生效。
|
|
self?.showRestartAlertAndQuit()
|
|
}
|
|
self.onboarding = controller
|
|
controller.show()
|
|
}
|
|
|
|
private func showRestartAlertAndQuit() {
|
|
let alert = NSAlert()
|
|
alert.messageText = "权限已就绪"
|
|
alert.informativeText = "OpenLess 需要重启以让快捷键生效。点击「退出」后,请重新打开 OpenLess。"
|
|
alert.addButton(withTitle: "退出")
|
|
alert.runModal()
|
|
NSApp.terminate(nil)
|
|
}
|
|
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
false
|
|
}
|
|
|
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
|
coordinator?.openHome()
|
|
return true
|
|
}
|
|
|
|
private func runLaunchActions(coordinator: DictationCoordinator) {
|
|
let arguments = Set(CommandLine.arguments.dropFirst())
|
|
if arguments.contains("--open-settings") {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
|
coordinator.openSettings()
|
|
}
|
|
}
|
|
if arguments.contains("--start-recording") {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
|
|
coordinator.toggleDictationFromMenu()
|
|
}
|
|
}
|
|
}
|
|
}
|