* 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>
63 lines
2.5 KiB
Swift
63 lines
2.5 KiB
Swift
import AppKit
|
|
|
|
@MainActor
|
|
enum ApplicationMenu {
|
|
static func install(updaterTarget: UpdaterController? = nil) {
|
|
let mainMenu = NSMenu()
|
|
|
|
let appItem = NSMenuItem()
|
|
let appMenu = NSMenu(title: "OpenLess")
|
|
|
|
if let updaterTarget {
|
|
let checkUpdates = NSMenuItem(
|
|
title: "检查更新…",
|
|
action: #selector(UpdaterController.checkForUpdates(_:)),
|
|
keyEquivalent: ""
|
|
)
|
|
checkUpdates.target = updaterTarget
|
|
appMenu.addItem(checkUpdates)
|
|
appMenu.addItem(.separator())
|
|
}
|
|
|
|
appMenu.addItem(NSMenuItem(title: "Hide OpenLess", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h"))
|
|
appMenu.addItem(NSMenuItem(title: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h"))
|
|
appMenu.items.last?.keyEquivalentModifierMask = [.command, .option]
|
|
appMenu.addItem(NSMenuItem(title: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: ""))
|
|
appMenu.addItem(.separator())
|
|
appMenu.addItem(NSMenuItem(title: "Quit OpenLess", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
|
|
appItem.submenu = appMenu
|
|
mainMenu.addItem(appItem)
|
|
|
|
let editItem = NSMenuItem()
|
|
editItem.submenu = editMenu()
|
|
mainMenu.addItem(editItem)
|
|
|
|
NSApp.mainMenu = mainMenu
|
|
}
|
|
|
|
private static func editMenu() -> NSMenu {
|
|
let menu = NSMenu(title: "Edit")
|
|
|
|
menu.addItem(menuItem("Undo", action: Selector(("undo:")), key: "z"))
|
|
let redo = menuItem("Redo", action: Selector(("redo:")), key: "Z")
|
|
redo.keyEquivalentModifierMask = [.command, .shift]
|
|
menu.addItem(redo)
|
|
menu.addItem(.separator())
|
|
|
|
menu.addItem(menuItem("Cut", action: #selector(NSText.cut(_:)), key: "x"))
|
|
menu.addItem(menuItem("Copy", action: #selector(NSText.copy(_:)), key: "c"))
|
|
menu.addItem(menuItem("Paste", action: #selector(NSText.paste(_:)), key: "v"))
|
|
menu.addItem(menuItem("Delete", action: #selector(NSText.delete(_:)), key: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(menuItem("Select All", action: #selector(NSText.selectAll(_:)), key: "a"))
|
|
|
|
return menu
|
|
}
|
|
|
|
private static func menuItem(_ title: String, action: Selector, key: String) -> NSMenuItem {
|
|
let item = NSMenuItem(title: title, action: action, keyEquivalent: key)
|
|
item.target = nil
|
|
return item
|
|
}
|
|
}
|