A new Apple-silicon app in viewer/macos (Swift, no dependencies, ~1 MB): one window showing a ProdDash dashboard in WebKit, and nothing else. It runs no server; it points at any ProdDash on the network. - Connections (name + address) in Settings; the add/edit sheet asks /api/modules whether ProdDash answers as you type. No port means 24500. - Two or more connections bring a slim tab bar of their names. In a window it floats over the dashboard and comes out while the pointer is on the title bar; it is not part of the title bar, because anything there makes WebKit push the page down. In full screen it is a titlebar accessory that comes down with the menu bar, and it flashes up after each switch. ⌃Tab / ⌃⇧Tab and ⌘1–9 switch from the keyboard. - Full screen, reopening in full screen if it quit there; window size and the selected tab are kept too. The dashboard's own full-screen button works as well. - Menus: View (Back, Forward, Reload Page, Enter Full Screen), Settings, Edit (copy/paste in the page's text fields), Window, Help (a guide that ships in the app). - Every installation keeps its own live web view. One that can't be reached says why and retries at 2, 4, 8, then every 15 s; a redirect off the installation is reported, not handed to the browser. JS alert, confirm and prompt show as sheets; links to other sites open in the browser. - General settings: open at login (SMAppService), keep the display awake while a dashboard is on screen (default on). build.sh signs with the Developer ID when one is installed and makes a notarised DMG with --notarize, as the launcher's does. It needs Xcode: with only the command line tools the macOS 27 SDK can't expand SwiftUI's @State. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
5.4 KiB
Swift
111 lines
5.4 KiB
Swift
import AppKit
|
|
|
|
/**
|
|
ProdDash Viewer: one window showing a ProdDash dashboard, a tab per
|
|
installation when there's more than one, and Settings to say which.
|
|
|
|
The menus are deliberately short — Back, Forward, Reload Page and Settings,
|
|
plus what every Mac app has: the app menu, Edit (without which Copy and
|
|
Paste do nothing in the dashboard's text fields), Window and Help.
|
|
*/
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
|
static var version: String {
|
|
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
|
|
}
|
|
|
|
private var viewer: ViewerWindowController!
|
|
private lazy var settingsWindow = SettingsWindowController()
|
|
private lazy var helpWindow = HelpWindowController()
|
|
|
|
func applicationWillFinishLaunching(_ notification: Notification) {
|
|
NSWindow.allowsAutomaticWindowTabbing = false
|
|
NSApp.mainMenu = makeMainMenu()
|
|
}
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
viewer = ViewerWindowController()
|
|
viewer.onAddConnection = { [weak self] in self?.settingsWindow.show(addingConnection: true) }
|
|
viewer.showWindow(nil)
|
|
if ViewerSettings.shared.reopenInFullScreen {
|
|
DispatchQueue.main.async { self.viewer.window?.toggleFullScreen(nil) }
|
|
}
|
|
}
|
|
|
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
|
if !flag { viewer.showWindow(nil) }
|
|
return true
|
|
}
|
|
|
|
func applicationWillTerminate(_ notification: Notification) {
|
|
viewer?.rememberFullScreen()
|
|
}
|
|
|
|
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
|
|
|
|
@objc func showSettings(_ sender: Any?) { settingsWindow.show() }
|
|
@objc func showViewerHelp(_ sender: Any?) { helpWindow.show() }
|
|
|
|
// MARK: - Menus
|
|
|
|
private func makeMainMenu() -> NSMenu {
|
|
let name = "ProdDash Viewer"
|
|
let main = NSMenu()
|
|
|
|
let app = submenu(of: main, title: name)
|
|
app.addItem(withTitle: "About \(name)",
|
|
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
|
app.addItem(.separator())
|
|
app.addItem(withTitle: "Settings…", action: #selector(showSettings(_:)), keyEquivalent: ",")
|
|
app.addItem(.separator())
|
|
let services = submenu(of: app, title: "Services")
|
|
NSApp.servicesMenu = services
|
|
app.addItem(.separator())
|
|
app.addItem(withTitle: "Hide \(name)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
|
app.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)),
|
|
keyEquivalent: "h").keyEquivalentModifierMask = [.command, .option]
|
|
app.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
|
|
app.addItem(.separator())
|
|
app.addItem(withTitle: "Quit \(name)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
|
|
let edit = submenu(of: main, title: "Edit")
|
|
edit.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
|
|
edit.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
|
|
edit.addItem(.separator())
|
|
edit.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
|
|
edit.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
edit.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
edit.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
|
|
let view = submenu(of: main, title: "View")
|
|
view.addItem(withTitle: "Back", action: #selector(ViewerWindowController.goBackInPage(_:)), keyEquivalent: "[")
|
|
view.addItem(withTitle: "Forward", action: #selector(ViewerWindowController.goForwardInPage(_:)), keyEquivalent: "]")
|
|
view.addItem(withTitle: "Reload Page", action: #selector(ViewerWindowController.reloadPage(_:)), keyEquivalent: "r")
|
|
view.addItem(.separator())
|
|
// AppKit retitles this "Exit Full Screen" while the window is in full screen.
|
|
view.addItem(withTitle: "Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)),
|
|
keyEquivalent: "f").keyEquivalentModifierMask = [.command, .control]
|
|
|
|
let window = submenu(of: main, title: "Window")
|
|
window.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
|
|
window.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
|
|
window.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
|
|
window.addItem(.separator())
|
|
window.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "")
|
|
NSApp.windowsMenu = window
|
|
|
|
let help = submenu(of: main, title: "Help")
|
|
help.addItem(withTitle: "\(name) Help", action: #selector(showViewerHelp(_:)), keyEquivalent: "?")
|
|
NSApp.helpMenu = help
|
|
|
|
return main
|
|
}
|
|
|
|
@discardableResult
|
|
private func submenu(of parent: NSMenu, title: String) -> NSMenu {
|
|
let menu = NSMenu(title: title)
|
|
parent.addItem(withTitle: title, action: nil, keyEquivalent: "").submenu = menu
|
|
return menu
|
|
}
|
|
}
|