mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 19:04:30 +00:00
- 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
146 lines
6.1 KiB
Swift
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()
|
|
}
|
|
}
|