tv-anarchy/Sources/TVAnarchyCore/Display/DisplayService.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

116 lines
No EOL
4 KiB
Swift

import Foundation
#if canImport(AppKit)
import AppKit
#endif
/// Enumerate displays and route local players (VLC / QuickTime) to a screen.
public enum DisplayService {
public static func list() -> [DisplayInfo] {
#if canImport(AppKit)
return NSScreen.screens.enumerated().map { i, screen in
let frame = screen.frame
let num = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
let displayId = num?.uint32Value ?? UInt32(i)
let name = screen.localizedName
let isPrimary = screen == NSScreen.main
let isBuiltIn = name.localizedCaseInsensitiveContains("built-in")
return DisplayInfo(
displayId: displayId,
name: name,
width: Int(frame.width.rounded()),
height: Int(frame.height.rounded()),
isPrimary: isPrimary,
isBuiltIn: isBuiltIn
)
}
#else
return []
#endif
}
/// Persist VLC's fullscreen-output device (`macosx-vdev` pref).
public static func setVlcOutputDevice(_ displayId: UInt32) {
_ = ProcessRunner.run(
"/usr/bin/defaults",
["write", "org.videolan.vlc", "macosx-vdev", "-int", String(displayId)]
)
}
/// Route VLC to `display` video on the target screen plus matching HDMI audio
/// (not the laptop's default output when the TV is selected).
@discardableResult
public static func routeVlc(to display: DisplayInfo) async -> Bool {
setVlcOutputDevice(display.displayId)
_ = await AudioOutputService.routeVlc(for: display)
#if canImport(AppKit)
guard let screen = screen(for: display) else { return true }
let (l, t, r, b) = appleScriptBounds(for: screen)
let script = """
tell application "VLC"
activate
try
set bounds of window 1 to {\(l), \(t), \(r), \(b)}
end try
delay 0.4
set fullscreen mode to true
end tell
"""
return await runAppleScript(script)
#else
return true
#endif
}
/// Move QuickTime's front window onto `display` and present fullscreen.
@discardableResult
public static func routeQuickTime(to display: DisplayInfo) async -> Bool {
#if canImport(AppKit)
guard let screen = screen(for: display) else { return false }
let (l, t, r, b) = appleScriptBounds(for: screen)
let script = """
tell application "QuickTime Player"
activate
if (count windows) > 0 then
set bounds of window 1 to {\(l), \(t), \(r), \(b)}
try
tell document 1 to present
end try
end if
end tell
"""
return await runAppleScript(script)
#else
return false
#endif
}
#if canImport(AppKit)
public static func screen(for display: DisplayInfo) -> NSScreen? {
NSScreen.screens.first { s in
let num = s.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
return num?.uint32Value == display.displayId
}
}
/// AppleScript window bounds `{left, top, right, bottom}` from an NSScreen frame.
public static func appleScriptBounds(for screen: NSScreen) -> (Int, Int, Int, Int) {
let f = screen.frame
let mainH = NSScreen.screens.map(\.frame.maxY).max() ?? f.maxY
return (
Int(f.minX.rounded()),
Int((mainH - f.maxY).rounded()),
Int(f.maxX.rounded()),
Int((mainH - f.minY).rounded())
)
}
#endif
private static func runAppleScript(_ script: String) async -> Bool {
let quoted = "'" + script.replacingOccurrences(of: "'", with: "'\\''") + "'"
let r: ProcessResult = await Task.detached(priority: .utility) {
ProcessRunner.runShell("osascript -e \(quoted)", timeout: 12)
}.value
return r.ok
}
}