ProdDash Viewer 1.0.0: a Mac app that only shows ProdDash, with a tab per installation
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c043834d1e
commit
8d8c0ee0c3
@@ -0,0 +1,77 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// What the tab bar shows: one tab per connection, in Settings' order.
|
||||
final class TabBarModel: ObservableObject {
|
||||
|
||||
struct Tab: Identifiable, Equatable {
|
||||
let id: UUID
|
||||
var title: String
|
||||
var address: String
|
||||
var unreachable: Bool
|
||||
}
|
||||
|
||||
@Published var tabs: [Tab] = []
|
||||
@Published var selection: UUID?
|
||||
var onSelect: (UUID) -> Void = { _ in }
|
||||
}
|
||||
|
||||
/// Hosts the tabs. A click on a tab works even while another app is in
|
||||
/// front, rather than only bringing the viewer forward.
|
||||
final class TabBarHostingView: NSHostingView<TabBar> {
|
||||
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
||||
}
|
||||
|
||||
/// The slim bar of installation names. It sits in the window's title bar
|
||||
/// area, so it takes the title bar's material in light and dark alike.
|
||||
struct TabBar: View {
|
||||
|
||||
@ObservedObject var model: TabBarModel
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(model.tabs) { tab in
|
||||
TabButton(tab: tab, selected: tab.id == model.selection) { model.onSelect(tab.id) }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TabButton: View {
|
||||
|
||||
let tab: TabBarModel.Tab
|
||||
let selected: Bool
|
||||
let action: () -> Void
|
||||
@State private var hovering = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 5) {
|
||||
if tab.unreachable {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.imageScale(.small)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
Text(tab.title)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.font(.system(size: 12, weight: selected ? .semibold : .regular))
|
||||
.foregroundStyle(selected ? .primary : .secondary)
|
||||
.padding(.horizontal, 10)
|
||||
.frame(maxWidth: .infinity, minHeight: 24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.fill(Color.primary.opacity(selected ? 0.12 : hovering ? 0.06 : 0))
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.focusable(false) // the keyboard belongs to the dashboard; ⌃Tab switches
|
||||
.onHover { hovering = $0 }
|
||||
.help(tab.unreachable ? "\(tab.address) — not answering" : tab.address)
|
||||
.accessibilityAddTraits(selected ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user