prole/prole-app/Sources/SplashTipWindowController.swift
chrisfu 8a6e8738db ProleStatus: app-window default, light splash restored, status bar controls; endpoint config via properties; build + docs
- Default to Application Window mode with full app menu; status bar overlay remains available
- Restore clickable startup Splash Tip (5s) with animated GIF and live counter; use light theme (Aqua)
- Status bar item: monospaced "P" icon; left click shows overlay (status mode) or main window (app mode)
- Overlay: add Maximize button (□) to toggle back to main window; keep click‑through elsewhere
- Global hotkey Cmd+Opt+Shift+P toggles modes; Prole menu item mirrors the same toggle and updates title dynamically
- Application menu (Prole): Show Status Bar / Show Main Window (contextual), Refresh Now, Quit
- Application Window layout: non‑scrolling, vertical 1‑line rows (svc, k3s aggregate, local) with top‑right timestamp; bottom‑left controls (⟳ Refresh, _ Minimize)
- Parameterize endpoints via `prole.properties` (bundled + user override). Replace legacy raspberry with retropie defaults
- ServiceChecker + UI read endpoints from Config loader; tooltips/labels reflect configured hosts/ports
- Build script: generate `Info.plist` with `LSUIElement=false`; bundle resources (`prole-type.gif`, `prole.properties`); ad‑hoc codesign. Universal build supported
- Documentation: rewrite README with technical build/run/config details and operational posture

Files:
- proleStatus/Sources/: AppDelegate.swift, OverlayWindow.swift, StatusView.swift, StatusItemController.swift,
  SplashTipWindowController.swift, MainWindowController.swift, AppStatusView.swift, ServiceChecker.swift, Config.swift
- proleStatus/build.sh
- proleStatus/prole.properties
- proleStatus/README.md

Notes:
- Build verified via `./build.sh build` (arm64). App starts in App Window mode with light theme; menu + hotkey + overlay toggle operate as intended
2025-12-01 21:08:30 -08:00

146 lines
6.1 KiB
Swift

import AppKit
final class SplashTipWindowController: NSWindowController {
private let imageView = NSImageView()
private let tipLabel = NSTextField(labelWithString: "Tip: Press Cmd+Opt+Shift+P to change modes")
private let counterLabel = NSTextField(labelWithString: "0.0")
private var timer: DispatchSourceTimer?
private var startTime: DispatchTime?
private let displayDuration: TimeInterval = 5.0
init() {
let style: NSWindow.StyleMask = [.borderless]
let initialRect = NSRect(x: 0, y: 0, width: 520, height: 360)
let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false)
super.init(window: window)
window.isReleasedWhenClosed = false
// Light theme background
window.backgroundColor = NSColor(calibratedWhite: 1.0, alpha: 0.96)
window.isOpaque = false
window.hasShadow = true
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces]
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.appearance = NSAppearance(named: .aqua)
let content = NSView()
content.wantsLayer = true
content.translatesAutoresizingMaskIntoConstraints = false
window.contentView = content
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.imageScaling = .scaleProportionallyUpOrDown
imageView.animates = true
tipLabel.translatesAutoresizingMaskIntoConstraints = false
tipLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
tipLabel.textColor = .labelColor
tipLabel.alignment = .center
tipLabel.lineBreakMode = .byWordWrapping
tipLabel.maximumNumberOfLines = 2
counterLabel.translatesAutoresizingMaskIntoConstraints = false
counterLabel.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .regular)
counterLabel.textColor = NSColor(white: 0.1, alpha: 0.85)
counterLabel.alignment = .left
content.addSubview(imageView)
content.addSubview(tipLabel)
content.addSubview(counterLabel)
NSLayoutConstraint.activate([
content.widthAnchor.constraint(equalToConstant: initialRect.width),
content.heightAnchor.constraint(equalToConstant: initialRect.height),
imageView.topAnchor.constraint(equalTo: content.topAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalToConstant: 260),
tipLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 12),
tipLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
tipLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
counterLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 6),
counterLabel.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -4)
])
// Load background/splash image from config (bundled in Resources) if present
let bgName = Config.shared.uiBackground
let candidates: [(String, String?)] = [
(bgName, nil),
("prole-type", "gif")
]
var loaded = false
for (res, ext) in candidates {
if let url = Bundle.main.url(forResource: res, withExtension: ext),
let img = NSImage(contentsOf: url) {
imageView.image = img
loaded = true
break
}
}
if !loaded {
// Fallback: show placeholder text if the resource is missing
let placeholder = NSTextField(labelWithString: "Splash background not found")
placeholder.textColor = .secondaryLabelColor
placeholder.alignment = .center
placeholder.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(placeholder)
NSLayoutConstraint.activate([
placeholder.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
placeholder.centerYAnchor.constraint(equalTo: imageView.centerYAnchor)
])
}
// Click anywhere to dismiss early
let clickRecognizer = NSClickGestureRecognizer(target: self, action: #selector(dismissSplash))
content.addGestureRecognizer(clickRecognizer)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func show() {
guard let screen = NSScreen.main, let window = window else { return }
let frame = window.frame
let x = screen.visibleFrame.midX - frame.width / 2
let y = screen.visibleFrame.midY - frame.height / 2
window.setFrame(NSRect(x: x, y: y, width: frame.width, height: frame.height), display: true)
window.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: false)
// Start counter timer
startTime = .now()
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.schedule(deadline: .now(), repeating: .milliseconds(100))
timer.setEventHandler { [weak self] in
self?.tick()
}
timer.resume()
self.timer = timer
// Auto dismiss after duration
DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration) { [weak self] in
self?.dismissSplash()
}
}
private func tick() {
guard let start = startTime else { return }
let elapsed = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds
let seconds = Double(elapsed) / 1_000_000_000.0
let whole = Int(seconds)
let tenths = Int((seconds - Double(whole)) * 10.0)
counterLabel.stringValue = String(format: "%d.%d", whole, tenths)
}
@objc private func dismissSplash() {
timer?.cancel()
timer = nil
window?.orderOut(nil)
close()
}
}