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>
116 lines
No EOL
4 KiB
Swift
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
|
|
}
|
|
} |