ui: match titlebar chrome and prefer upward caption menus
This commit is contained in:
@@ -251,9 +251,9 @@ The inline URL field, including its placeholder, uses the same 12-point system f
|
|||||||
|
|
||||||
With no media open, the full window uses native behind-window HUD glass with a 35%-opaque brown tint. Static, cached amber/rust radial gradients add ambient color only to the empty screen; they disappear during loading or errors, and are never drawn over video or playback chrome. Glass title/control/error surfaces use the same brown tint with half-point sand outlines. Decorative drawing does not intercept input. The idle Metal surface stays attached but hidden; opening media restores a solid black background and letterboxing without tinting the video, and closing it restores the ambient frosted background. macOS controls the material’s appearance, including Reduce Transparency.
|
With no media open, the full window uses native behind-window HUD glass with a 35%-opaque brown tint. Static, cached amber/rust radial gradients add ambient color only to the empty screen; they disappear during loading or errors, and are never drawn over video or playback chrome. Glass title/control/error surfaces use the same brown tint with half-point sand outlines. Decorative drawing does not intercept input. The idle Metal surface stays attached but hidden; opening media restores a solid black background and letterboxing without tinting the video, and closing it restores the ambient frosted background. macOS controls the material’s appearance, including Reduce Transparency.
|
||||||
|
|
||||||
The captions selector sits immediately right of the compact-mode button in both playback-control layouts and is disabled when no external subtitle tracks are available. Subtitle text uses the floating controller's rounded HUD-glass background rather than an opaque black box. Subtitle-load errors remain visible independently of auto-hidden controls.
|
The captions selector sits immediately right of the compact-mode button in both playback-control layouts and is disabled when no external subtitle tracks are available. Its explicitly sized native symbol has a 24-point footprint comparable to the compact-mode icon. The native pull-down menu prefers opening above the button; AppKit can reposition it to fit the screen. Subtitle text uses the floating controller's rounded HUD-glass background rather than an opaque black box. Subtitle-load errors remain visible independently of auto-hidden controls.
|
||||||
|
|
||||||
Normal and compact titlebars have no full-width glass strip or separator: content extends beneath AppKit’s transparent titlebar using `fullSizeContentView`, `titlebarAppearsTransparent`, and `titlebarSeparatorStyle = .none`. Titles retain their rounded HUD-glass backdrop. The pin and settings buttons share a separate container with the same tint, outline and rounded treatment; settings sits immediately right of the pin, and both retain 24-point click targets. Compact titles and the shared button container occupy the top row with 8-point insets; title and progress remain adjacent. Native traffic lights and window dragging are preserved.
|
Normal and compact titlebars have no full-width glass strip or separator: content extends beneath AppKit’s transparent titlebar using `fullSizeContentView`, `titlebarAppearsTransparent`, and `titlebarSeparatorStyle = .none`. Titles retain their rounded HUD-glass backdrop. The pin and settings buttons share a separate container matching the title's height, horizontal padding, tint, outline and corner radius: 24 points high in normal mode and 28 in compact mode. Settings sits immediately right of the pin, and both retain 24-point click targets. Each title/button pair shares the same vertical inset; compact titles and buttons occupy the top row with 8-point outer insets, while title and progress remain adjacent. Native traffic lights and window dragging are preserved.
|
||||||
|
|
||||||
Compact titles default to `<title> · <position> / <duration> <unit>`, using acknowledged playback position rather than slider previews. Total duration selects the format: below one minute, `12 / 45 s`; from one minute to below one hour, `05:12 / 42:08 m`; from one hour onward, `01:23 / 02:10 h`. Values truncate to the displayed precision. Set `[ui].compact_title_progress = "percentage"` to append only a whole percentage number (`<title> · 59`, without a percent sign), or `"none"` for title-only display. Percentages truncate and stay within 0–100. Loading, failed, live, unknown-duration and closed media have no progress suffix. Normal-mode titles remain unchanged.
|
Compact titles default to `<title> · <position> / <duration> <unit>`, using acknowledged playback position rather than slider previews. Total duration selects the format: below one minute, `12 / 45 s`; from one minute to below one hour, `05:12 / 42:08 m`; from one hour onward, `01:23 / 02:10 h`. Values truncate to the displayed precision. Set `[ui].compact_title_progress = "percentage"` to append only a whole percentage number (`<title> · 59`, without a percent sign), or `"none"` for title-only display. Percentages truncate and stay within 0–100. Loading, failed, live, unknown-duration and closed media have no progress suffix. Normal-mode titles remain unchanged.
|
||||||
|
|
||||||
|
|||||||
+78
-26
@@ -1305,6 +1305,73 @@ private struct VideoSurface: NSViewRepresentable {
|
|||||||
func updateNSView(_ nsView: NSView, context: Context) {}
|
func updateNSView(_ nsView: NSView, context: Context) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private struct CaptionSelector: NSViewRepresentable {
|
||||||
|
let model: PlayerModel
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> Selector { Selector(model: model) }
|
||||||
|
func updateNSView(_ view: Selector, context: Context) { view.updateTracks() }
|
||||||
|
|
||||||
|
final class Selector: NSPopUpButton {
|
||||||
|
private let model: PlayerModel
|
||||||
|
private var displayedTracks: [ResolvedSubtitle] = []
|
||||||
|
private var displayedSelection: Int?
|
||||||
|
private let captionItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
||||||
|
|
||||||
|
init(model: PlayerModel) {
|
||||||
|
self.model = model
|
||||||
|
super.init(frame: NSRect(x: 0, y: 0, width: 24, height: 24), pullsDown: true)
|
||||||
|
usesItemFromMenu = false
|
||||||
|
preferredEdge = .maxY
|
||||||
|
isBordered = false
|
||||||
|
imagePosition = .imageOnly
|
||||||
|
imageScaling = .scaleProportionallyUpOrDown
|
||||||
|
(cell as? NSPopUpButtonCell)?.arrowPosition = .noArrow
|
||||||
|
let symbol = NSImage(systemSymbolName: "captions.bubble", accessibilityDescription: "Subtitles")?
|
||||||
|
.withSymbolConfiguration(.init(pointSize: 22, weight: .regular))
|
||||||
|
symbol?.size = NSSize(width: 24, height: 24)
|
||||||
|
captionItem.image = symbol
|
||||||
|
contentTintColor = SunsetPalette.text
|
||||||
|
toolTip = "Subtitles"
|
||||||
|
setAccessibilityLabel("Subtitles")
|
||||||
|
updateTracks()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { nil }
|
||||||
|
|
||||||
|
func updateTracks() {
|
||||||
|
guard menu == nil || displayedSelection != model.selectedSubtitle ||
|
||||||
|
displayedTracks.count != model.subtitleTracks.count ||
|
||||||
|
zip(displayedTracks, model.subtitleTracks).contains(where: {
|
||||||
|
$0.label != $1.label || $0.language != $1.language
|
||||||
|
}) else { return }
|
||||||
|
displayedTracks = model.subtitleTracks
|
||||||
|
displayedSelection = model.selectedSubtitle
|
||||||
|
let tracks = NSMenu()
|
||||||
|
tracks.autoenablesItems = false
|
||||||
|
func add(_ label: String, index: Int?) {
|
||||||
|
let item = NSMenuItem(title: label, action: #selector(selectTrack(_:)), keyEquivalent: "")
|
||||||
|
item.target = self
|
||||||
|
item.tag = index ?? -1
|
||||||
|
item.state = model.selectedSubtitle == index ? .on : .off
|
||||||
|
tracks.addItem(item)
|
||||||
|
}
|
||||||
|
add("Off", index: nil)
|
||||||
|
for (index, track) in model.subtitleTracks.enumerated() {
|
||||||
|
add(track.label + " (" + track.language + ")", index: index)
|
||||||
|
}
|
||||||
|
menu = tracks
|
||||||
|
(cell as? NSPopUpButtonCell)?.menuItem = captionItem
|
||||||
|
isEnabled = !model.subtitleTracks.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func selectTrack(_ item: NSMenuItem) {
|
||||||
|
model.selectSubtitle(item.tag < 0 ? nil : item.tag)
|
||||||
|
model.revealControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private struct CompactDragSurface: NSViewRepresentable {
|
private struct CompactDragSurface: NSViewRepresentable {
|
||||||
let model: PlayerModel
|
let model: PlayerModel
|
||||||
@@ -1452,10 +1519,10 @@ private struct PlayerContent: View {
|
|||||||
.help(model.fileName ?? "terebi")
|
.help(model.fileName ?? "terebi")
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.frame(height: 24)
|
||||||
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
|
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
|
||||||
Spacer(minLength: 12)
|
Spacer(minLength: 12)
|
||||||
titleControls
|
titleControls(compact: false)
|
||||||
}
|
}
|
||||||
.padding(.leading, model.isFullScreen ? 16 : 86)
|
.padding(.leading, model.isFullScreen ? 16 : 86)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
@@ -1467,7 +1534,7 @@ private struct PlayerContent: View {
|
|||||||
.accessibilityHidden(!model.controlsVisible)
|
.accessibilityHidden(!model.controlsVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var titleControls: some View {
|
private func titleControls(compact: Bool) -> some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
iconButton(model.isOnTop ? "pin.fill" : "pin",
|
iconButton(model.isOnTop ? "pin.fill" : "pin",
|
||||||
model.isOnTop ? "Stop Keeping Window on Top" : "Keep Window on Top", action: model.toggleOnTop)
|
model.isOnTop ? "Stop Keeping Window on Top" : "Keep Window on Top", action: model.toggleOnTop)
|
||||||
@@ -1477,30 +1544,14 @@ private struct PlayerContent: View {
|
|||||||
actionsMenu
|
actionsMenu
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.frame(height: compact ? 28 : 24)
|
||||||
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
|
.background(PlayerGlass(cornerRadius: compact ? 10 : 8)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: compact ? 10 : 8)).allowsHitTesting(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var captionsMenu: some View {
|
private var captionsMenu: some View {
|
||||||
Menu {
|
CaptionSelector(model: model)
|
||||||
Button("Off") { model.selectSubtitle(nil) }
|
.frame(width: 24, height: 24)
|
||||||
ForEach(model.subtitleTracks.indices, id: \.self) { index in
|
|
||||||
Button {
|
|
||||||
model.selectSubtitle(index)
|
|
||||||
} label: {
|
|
||||||
Label(model.subtitleTracks[index].label + " (" + model.subtitleTracks[index].language + ")",
|
|
||||||
systemImage: model.selectedSubtitle == index ? "checkmark" : "text.bubble")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "captions.bubble").font(.system(size: 17)).frame(width: 24, height: 24)
|
|
||||||
}
|
|
||||||
.menuStyle(.borderlessButton)
|
|
||||||
.menuIndicator(.hidden)
|
|
||||||
.fixedSize()
|
|
||||||
.disabled(model.subtitleTracks.isEmpty)
|
|
||||||
.accessibilityLabel("Subtitles")
|
|
||||||
.help("Subtitles")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var actionsMenu: some View {
|
private var actionsMenu: some View {
|
||||||
@@ -1817,11 +1868,12 @@ private struct PlayerContent: View {
|
|||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
.help((model.fileName ?? "terebi") + (progress.map { " · \($0)" } ?? ""))
|
.help((model.fileName ?? "terebi") + (progress.map { " · \($0)" } ?? ""))
|
||||||
.padding(8)
|
.padding(.horizontal, 8)
|
||||||
|
.frame(height: 28)
|
||||||
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
|
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
titleControls
|
titleControls(compact: true)
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.foregroundStyle(Color(SunsetPalette.text))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
|
|||||||
Reference in New Issue
Block a user