ui: integrate captions and unify playback chrome backgrounds
This commit is contained in:
@@ -251,13 +251,15 @@ 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.
|
||||||
|
|
||||||
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`. Normal-mode titles and their file icons have a small rounded HUD-glass backdrop for legibility; the pin uses a small-scale symbol over the existing background while retaining its 24-point click target. Compact titles sit in a content-sized, left-aligned frosted box with 8-point top/left insets, matching the controller’s bottom inset. Title and progress remain adjacent with no expanding gap. When they cannot fit, the title truncates at the end with an ellipsis while progress stays visible; the box fits the 160-point minimum width. Native traffic lights and window dragging are preserved.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
Opening video fits the window to its display aspect ratio, including non-square pixels, and constrains subsequent window resizing. Fullscreen retains the normal letterboxed renderer; audio-only playback and close release the aspect constraint. Extreme ratios that cannot fit both the screen and usable controls keep a freely resizable letterboxed window.
|
Opening video fits the window to its display aspect ratio, including non-square pixels, and constrains subsequent window resizing. Fullscreen retains the normal letterboxed renderer; audio-only playback and close release the aspect constraint. Extreme ratios that cannot fit both the screen and usable controls keep a freely resizable letterboxed window.
|
||||||
|
|
||||||
**Compact mode** uses the same window, Metal view, and playback session—not a second window or macOS system PiP. Use the PiP-shaped button in the normal playback controls, **⌃⌘P**, or **Playback Actions → Enter Compact Mode**. The compact-mode button replaces the fullscreen button; fullscreen remains available through its menu, keyboard shortcuts, and double-clicking normal-mode video. Compact mode starts at the current screen's top-right corner with a 16-point inset and the last manually chosen compact width (320 points before any resize). The width persists across mode changes and player restarts; height follows the current video's display aspect where usable screen bounds and the 160 × 90-point minimum allow, with extreme ratios retaining letterboxing. Normal-mode resize, transition animations, and automatic aspect/screen fitting do not overwrite the saved preference. Screen fitting may temporarily reduce the displayed size without reducing the saved width. Compact mode floats above ordinary windows, joins all Spaces, and is configured to join eligible other-app fullscreen spaces and Stage Manager sets. Small hover controls provide play/pause, restore, and Playback Actions; keyboard playback controls remain available.
|
**Compact mode** uses the same window, Metal view, and playback session—not a second window or macOS system PiP. Use the PiP-shaped button in the normal playback controls, **⌃⌘P**, or **Playback Actions → Enter Compact Mode**. The compact-mode button replaces the fullscreen button; fullscreen remains available through its menu, keyboard shortcuts, and double-clicking normal-mode video. Compact mode starts at the current screen's top-right corner with a 16-point inset and the last manually chosen compact width (320 points before any resize). The width persists across mode changes and player restarts; height follows the current video's display aspect where usable screen bounds and the 160 × 90-point minimum allow, with extreme ratios retaining letterboxing. Normal-mode resize, transition animations, and automatic aspect/screen fitting do not overwrite the saved preference. Screen fitting may temporarily reduce the displayed size without reducing the saved width. Compact mode floats above ordinary windows, joins all Spaces, and is configured to join eligible other-app fullscreen spaces and Stage Manager sets. Small hover controls provide play/pause and restore, with the captions selector immediately right of restore; settings lives beside the titlebar pin. Keyboard playback controls remain available.
|
||||||
|
|
||||||
Compact-mode shortcuts and menu actions remain available during YouTube URL resolution, media opening, and network buffering. While metadata is unavailable, the window uses a provisional video ratio; decoded metadata then updates its geometry without leaving compact mode.
|
Compact-mode shortcuts and menu actions remain available during YouTube URL resolution, media opening, and network buffering. While metadata is unavailable, the window uses a provisional video ratio; decoded metadata then updates its geometry without leaving compact mode.
|
||||||
|
|
||||||
|
|||||||
+63
-39
@@ -1455,11 +1455,7 @@ private struct PlayerContent: View {
|
|||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.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)
|
||||||
iconButton(model.isOnTop ? "pin.fill" : "pin",
|
titleControls
|
||||||
model.isOnTop ? "Stop Keeping Window on Top" : "Keep Window on Top", action: model.toggleOnTop)
|
|
||||||
.imageScale(.small)
|
|
||||||
.foregroundStyle(Color(model.isOnTop ? SunsetPalette.accent : SunsetPalette.secondaryText))
|
|
||||||
.disabled(model.isFullScreen || model.compactWindow.isTransitioning)
|
|
||||||
}
|
}
|
||||||
.padding(.leading, model.isFullScreen ? 16 : 86)
|
.padding(.leading, model.isFullScreen ? 16 : 86)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
@@ -1471,6 +1467,42 @@ private struct PlayerContent: View {
|
|||||||
.accessibilityHidden(!model.controlsVisible)
|
.accessibilityHidden(!model.controlsVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var titleControls: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
iconButton(model.isOnTop ? "pin.fill" : "pin",
|
||||||
|
model.isOnTop ? "Stop Keeping Window on Top" : "Keep Window on Top", action: model.toggleOnTop)
|
||||||
|
.imageScale(.small)
|
||||||
|
.foregroundStyle(Color(model.isOnTop ? SunsetPalette.accent : SunsetPalette.secondaryText))
|
||||||
|
.disabled(model.isFullScreen || model.compactWindow.isTransitioning)
|
||||||
|
actionsMenu
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var captionsMenu: some View {
|
||||||
|
Menu {
|
||||||
|
Button("Off") { model.selectSubtitle(nil) }
|
||||||
|
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 {
|
||||||
Menu {
|
Menu {
|
||||||
PlayerActionButton(model: model, action: .openFile, title: "Open File…")
|
PlayerActionButton(model: model, action: .openFile, title: "Open File…")
|
||||||
@@ -1507,7 +1539,7 @@ private struct PlayerContent: View {
|
|||||||
.disabled(!model.canTogglePlayback)
|
.disabled(!model.canTogglePlayback)
|
||||||
iconButton("pip.exit", "Exit Compact Mode", action: model.toggleCompact)
|
iconButton("pip.exit", "Exit Compact Mode", action: model.toggleCompact)
|
||||||
.disabled(!model.canToggleCompact)
|
.disabled(!model.canToggleCompact)
|
||||||
actionsMenu
|
captionsMenu
|
||||||
}
|
}
|
||||||
if showsBufferReadout { bufferReadout }
|
if showsBufferReadout { bufferReadout }
|
||||||
}
|
}
|
||||||
@@ -1557,7 +1589,7 @@ private struct PlayerContent: View {
|
|||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
iconButton("pip.enter", "Enter Compact Mode", action: model.toggleCompact)
|
iconButton("pip.enter", "Enter Compact Mode", action: model.toggleCompact)
|
||||||
.disabled(!model.canToggleCompact)
|
.disabled(!model.canToggleCompact)
|
||||||
actionsMenu
|
captionsMenu
|
||||||
}
|
}
|
||||||
.frame(width: 106, alignment: .trailing)
|
.frame(width: 106, alignment: .trailing)
|
||||||
}
|
}
|
||||||
@@ -1638,33 +1670,20 @@ private struct PlayerContent: View {
|
|||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(.black.opacity(0.75), in: RoundedRectangle(cornerRadius: 4))
|
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 12)).allowsHitTesting(false))
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.bottom, model.controlsVisible ? 110 : 24)
|
.padding(.bottom, model.controlsVisible ? 110 : 24)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
if !model.subtitleTracks.isEmpty && (model.controlsVisible || model.subtitleError != nil) {
|
if let error = model.subtitleError {
|
||||||
VStack(alignment: .trailing) {
|
Text(error)
|
||||||
Menu {
|
.font(.caption)
|
||||||
Button("Off") { model.selectSubtitle(nil) }
|
.foregroundStyle(.yellow)
|
||||||
ForEach(model.subtitleTracks.indices, id: \.self) { index in
|
.padding(8)
|
||||||
Button {
|
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 12)).allowsHitTesting(false))
|
||||||
model.selectSubtitle(index)
|
.padding(.top, 44).padding(.trailing, 16)
|
||||||
} label: {
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
Label(model.subtitleTracks[index].label + " (" + model.subtitleTracks[index].language + ")",
|
|
||||||
systemImage: model.selectedSubtitle == index ? "checkmark" : "text.bubble")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "captions.bubble").accessibilityLabel("Subtitles")
|
|
||||||
}
|
|
||||||
if let error = model.subtitleError {
|
|
||||||
Text(error).font(.caption).foregroundStyle(.yellow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 44).padding(.trailing, 16)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
|
||||||
}
|
}
|
||||||
if let error = model.errorText {
|
if let error = model.errorText {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
@@ -1789,22 +1808,27 @@ private struct PlayerContent: View {
|
|||||||
|
|
||||||
let progress = model.isCompact || model.compactWindow.isTransitioning ? model.compactProgress : nil
|
let progress = model.isCompact || model.compactWindow.isTransitioning ? model.compactProgress : nil
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ViewThatFits(in: .horizontal) {
|
HStack(spacing: 8) {
|
||||||
compactTitleLabel(progress: progress, constrained: false)
|
ViewThatFits(in: .horizontal) {
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
compactTitleLabel(progress: progress, constrained: false)
|
||||||
compactTitleLabel(progress: progress, constrained: true)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
compactTitleLabel(progress: progress, constrained: true)
|
||||||
|
}
|
||||||
.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(8)
|
||||||
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
|
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(8)
|
|
||||||
.foregroundStyle(Color(SunsetPalette.text))
|
|
||||||
.opacity(model.controlsVisible ? 1 : 0)
|
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
.accessibilityHidden(!model.controlsVisible)
|
Spacer(minLength: 0)
|
||||||
|
titleControls
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
|
.onHover(perform: model.setTitleHovered)
|
||||||
|
.opacity(model.controlsVisible ? 1 : 0)
|
||||||
|
.allowsHitTesting(model.controlsVisible)
|
||||||
|
.accessibilityHidden(!model.controlsVisible)
|
||||||
compactControls
|
compactControls
|
||||||
.position(x: geometry.size.width / 2, y: geometry.size.height - compactControlHeight / 2 - 8)
|
.position(x: geometry.size.width / 2, y: geometry.size.height - compactControlHeight / 2 - 8)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user