ui: adopt Sunset palette and warm glass styling
This commit is contained in:
@@ -184,11 +184,11 @@ Terebi reports facts, not watched status. A consumer such as Tubular decides com
|
|||||||
|
|
||||||
## Playback UI
|
## Playback UI
|
||||||
|
|
||||||
The layout follows [IINA's video-first window](https://iina.io/img/screenshot.png) and the supplied screenshots: a 32-point transparent titlebar with native traffic lights and file icon, plus a dark frosted 440 × 68-point floating controller. AppKit materials blur the video beneath the controller; muted icons, an orange volume track, a narrow seek thumb, and zero-padded time labels follow the reference. Shared vertical padding and an optical offset for symbol/slider insets balance the controller's visible top and bottom margins in both play and pause states. The controller starts centered with a four-percent bottom inset and keeps its relative position when the window changes size.
|
The layout follows [IINA's video-first window](https://iina.io/img/screenshot.png) and the supplied screenshots: a 32-point transparent titlebar with native traffic lights and file icon, plus a frosted 440 × 68-point floating controller. Conjure-inspired **Sunset** styling uses translucent dark-brown glass, warm cream primary text, sand secondary text, and a fixed orange accent for the volume fill, active pin, and drop target. Native slider thumbs and buffered ranges share the palette; warning symbols retain their semantic yellow. Shared vertical padding and an optical offset for symbol/slider insets balance the controller's visible top and bottom margins in both play and pause states. The controller starts centered with a four-percent bottom inset and keeps its relative position when the window changes size.
|
||||||
|
|
||||||
The empty player uses the same HUD-glass material, 12-point rounded corners, and muted icon/text palette in a centered welcome card. File and YouTube opening actions share a consistently sized row; the card fits the 400 × 240-point minimum window, and the full window still accepts media-file and YouTube-link drops.
|
The empty player uses the same tinted HUD-glass material, faint warm outlines, 12-point rounded corners, and Sunset palette in a centered welcome card. File and YouTube opening actions share a consistently sized row with a subtle orange fill; the card fits the 400 × 240-point minimum window, and the full window still accepts media-file and YouTube-link drops.
|
||||||
|
|
||||||
With no media open, the full window uses native behind-window HUD glass to frost the desktop and windows beneath it. The idle Metal surface stays attached but hidden; opening media restores a solid black background and letterboxing, and closing it restores the 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 stays over the existing background. Compact titles sit in a controls-style frosted box with 8-point top/left insets, matching the controller’s bottom inset. Long titles truncate in the middle and fit the 160-point minimum width. 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`. Normal-mode titles and their file icons have a small rounded HUD-glass backdrop for legibility; the pin stays over the existing background. Compact titles sit in a controls-style frosted box with 8-point top/left insets, matching the controller’s bottom inset. Long titles truncate in the middle and fit the 160-point minimum width. Native traffic lights and window dragging are preserved.
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ Initial window setup and returning from the file/YouTube dialogs leave keyboard
|
|||||||
Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. Consecutive same-direction repeats share one counted queue entry without skipping frames or reordering seeks/direction changes. The last presented image remains visible during decoder preroll and queued steps; the progress thumb follows each authoritative stepped position. Opening/closing media still clears the old image. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame.
|
Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. Consecutive same-direction repeats share one counted queue entry without skipping frames or reordering seeks/direction changes. The last presented image remains visible during decoder preroll and queued steps; the progress thumb follows each authoritative stepped position. Opening/closing media still clears the old image. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame.
|
||||||
|
|
||||||
- Existing shortcuts remain: **⌘O** opens files, **←/→** seeks five seconds, **↑/↓** adjusts volume by five percent, **⌃⌘F** toggles fullscreen, and **Esc** leaves fullscreen. Double-clicking normal-mode video also toggles fullscreen.
|
- Existing shortcuts remain: **⌘O** opens files, **←/→** seeks five seconds, **↑/↓** adjusts volume by five percent, **⌃⌘F** toggles fullscreen, and **Esc** leaves fullscreen. Double-clicking normal-mode video also toggles fullscreen.
|
||||||
- The timeline slider seeks on release and holds the requested position until queued seeking finishes. Both slider tracks span the native thumb-center travel range; the orange fill ends at the current volume thumb center. The volume thumb is opaque light gray.
|
- The timeline slider seeks on release and holds the requested position until queued seeking finishes. Both slider tracks span the native thumb-center travel range; the orange fill ends at the current volume thumb center. The enabled volume thumb is opaque cream; secondary sand marks the seek thumb and buffered range, with dimmed disabled thumbs.
|
||||||
- **Single-click normal-mode video:** hide controls. Mouse movement reveals them; video controls fade after 2.5 seconds, including while paused. While the pointer is inside, hovering the normal titlebar or either controller and an inactive normal-mode window keep them visible; audio-only controls remain visible. Leaving the window starts the title/control fade immediately in either mode, including paused, loading, and audio-only playback; late hover/focus updates do not reshow them. Reentry reveals them. Active dragging/seeking, errors, empty-window opening actions, and VoiceOver remain protected; a drag released or seek acknowledged outside hides the chrome. Tab navigation can explicitly reveal controls without moving the pointer. Compact hover and window exit never hide the desktop cursor.
|
- **Single-click normal-mode video:** hide controls. Mouse movement reveals them; video controls fade after 2.5 seconds, including while paused. While the pointer is inside, hovering the normal titlebar or either controller and an inactive normal-mode window keep them visible; audio-only controls remain visible. Leaving the window starts the title/control fade immediately in either mode, including paused, loading, and audio-only playback; late hover/focus updates do not reshow them. Reentry reveals them. Active dragging/seeking, errors, empty-window opening actions, and VoiceOver remain protected; a drag released or seek acknowledged outside hides the chrome. Tab navigation can explicitly reveal controls without moving the pointer. Compact hover and window exit never hide the desktop cursor.
|
||||||
- Click the right-hand time label to switch between total and remaining time. The titlebar pin keeps the window on top.
|
- Click the right-hand time label to switch between total and remaining time. The titlebar pin keeps the window on top.
|
||||||
|
|
||||||
|
|||||||
+98
-26
@@ -1018,9 +1018,62 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private enum SunsetPalette {
|
||||||
|
static let background = NSColor(srgbRed: 0.12, green: 0.09, blue: 0.08, alpha: 0.35)
|
||||||
|
static let text = NSColor(srgbRed: 1.00, green: 0.89, blue: 0.71, alpha: 1)
|
||||||
|
static let secondaryText = NSColor(srgbRed: 0.85, green: 0.78, blue: 0.65, alpha: 1)
|
||||||
|
static let accent = NSColor(srgbRed: 0.95, green: 0.58, blue: 0.23, alpha: 1)
|
||||||
|
static let ember = NSColor(srgbRed: 0.76, green: 0.27, blue: 0.18, alpha: 1)
|
||||||
|
static let outline = secondaryText.withAlphaComponent(0.22)
|
||||||
|
static let actionFill = accent.withAlphaComponent(0.08)
|
||||||
|
static let track = secondaryText.withAlphaComponent(0.18)
|
||||||
|
static let bufferedTrack = secondaryText.withAlphaComponent(0.4)
|
||||||
|
static let disabledThumb = secondaryText.withAlphaComponent(0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class PlayerBackdrop: NSView {
|
||||||
|
var ambientGlows = false
|
||||||
|
private static let amber = NSGradient(colors: [
|
||||||
|
SunsetPalette.accent.withAlphaComponent(0.18),
|
||||||
|
SunsetPalette.accent.withAlphaComponent(0.065),
|
||||||
|
SunsetPalette.accent.withAlphaComponent(0),
|
||||||
|
])!
|
||||||
|
private static let ember = NSGradient(colors: [
|
||||||
|
SunsetPalette.ember.withAlphaComponent(0.26),
|
||||||
|
SunsetPalette.ember.withAlphaComponent(0.10),
|
||||||
|
SunsetPalette.ember.withAlphaComponent(0),
|
||||||
|
])!
|
||||||
|
|
||||||
|
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||||
|
|
||||||
|
override func draw(_ dirtyRect: NSRect) {
|
||||||
|
SunsetPalette.background.setFill()
|
||||||
|
dirtyRect.fill()
|
||||||
|
guard ambientGlows else { return }
|
||||||
|
let upperGlow = NSPoint(x: bounds.width * 0.58, y: bounds.height * 0.94)
|
||||||
|
Self.ember.draw(fromCenter: upperGlow, radius: 0, toCenter: upperGlow,
|
||||||
|
radius: bounds.width * 0.40, options: [])
|
||||||
|
let lowerGlow = NSPoint(x: bounds.width * 0.88, y: bounds.height * 0.10)
|
||||||
|
Self.amber.draw(fromCenter: lowerGlow, radius: 0, toCenter: lowerGlow,
|
||||||
|
radius: bounds.width * 0.55, options: [])
|
||||||
|
let leftGlow = NSPoint(x: bounds.width * 0.08, y: bounds.height * 0.65)
|
||||||
|
NSGraphicsContext.saveGraphicsState()
|
||||||
|
NSGraphicsContext.current?.cgContext.setAlpha(0.45)
|
||||||
|
Self.amber.draw(fromCenter: leftGlow, radius: 0, toCenter: leftGlow,
|
||||||
|
radius: bounds.width * 0.32, options: [])
|
||||||
|
NSGraphicsContext.restoreGraphicsState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private struct PlayerGlass: NSViewRepresentable {
|
private struct PlayerGlass: NSViewRepresentable {
|
||||||
var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
|
var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
|
||||||
|
var cornerRadius: CGFloat = 12
|
||||||
|
var ambientGlows = false
|
||||||
|
|
||||||
|
func makeCoordinator() -> PlayerBackdrop { PlayerBackdrop(frame: .zero) }
|
||||||
|
|
||||||
func makeNSView(context: Context) -> NSVisualEffectView {
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||||
let view = NSVisualEffectView()
|
let view = NSVisualEffectView()
|
||||||
@@ -1028,11 +1081,26 @@ private struct PlayerGlass: NSViewRepresentable {
|
|||||||
view.state = .active
|
view.state = .active
|
||||||
view.appearance = NSAppearance(named: .vibrantDark)
|
view.appearance = NSAppearance(named: .vibrantDark)
|
||||||
view.material = .hudWindow
|
view.material = .hudWindow
|
||||||
|
view.wantsLayer = true
|
||||||
|
view.layer?.cornerRadius = cornerRadius
|
||||||
|
view.layer?.masksToBounds = true
|
||||||
|
view.layer?.borderWidth = cornerRadius > 0 ? 0.5 : 0
|
||||||
|
view.layer?.borderColor = SunsetPalette.outline.cgColor
|
||||||
|
let backdrop = context.coordinator
|
||||||
|
backdrop.frame = view.bounds
|
||||||
|
backdrop.autoresizingMask = [.width, .height]
|
||||||
|
backdrop.ambientGlows = ambientGlows
|
||||||
|
backdrop.setAccessibilityElement(false)
|
||||||
|
view.addSubview(backdrop)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ view: NSVisualEffectView, context: Context) {
|
func updateNSView(_ view: NSVisualEffectView, context: Context) {
|
||||||
view.blendingMode = blendingMode
|
view.blendingMode = blendingMode
|
||||||
|
if context.coordinator.ambientGlows != ambientGlows {
|
||||||
|
context.coordinator.ambientGlows = ambientGlows
|
||||||
|
context.coordinator.needsDisplay = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,12 +1170,12 @@ private struct PlaybackSlider: NSViewRepresentable {
|
|||||||
override func drawBar(inside rect: NSRect, flipped: Bool) {
|
override func drawBar(inside rect: NSRect, flipped: Bool) {
|
||||||
let bar = NSRect(x: rect.minX + knobThickness / 2, y: rect.midY - 1.5,
|
let bar = NSRect(x: rect.minX + knobThickness / 2, y: rect.midY - 1.5,
|
||||||
width: max(0, rect.width - knobThickness), height: 3)
|
width: max(0, rect.width - knobThickness), height: 3)
|
||||||
NSColor.white.withAlphaComponent(0.12).setFill()
|
SunsetPalette.track.setFill()
|
||||||
NSBezierPath(roundedRect: bar, xRadius: 1.5, yRadius: 1.5).fill()
|
NSBezierPath(roundedRect: bar, xRadius: 1.5, yRadius: 1.5).fill()
|
||||||
if volume {
|
if volume {
|
||||||
let fraction = (doubleValue - minValue) / (maxValue - minValue)
|
let fraction = (doubleValue - minValue) / (maxValue - minValue)
|
||||||
let fill = NSRect(x: bar.minX, y: bar.minY, width: bar.width * fraction, height: bar.height)
|
let fill = NSRect(x: bar.minX, y: bar.minY, width: bar.width * fraction, height: bar.height)
|
||||||
NSColor.systemOrange.withAlphaComponent(0.85).setFill()
|
SunsetPalette.accent.setFill()
|
||||||
NSBezierPath(roundedRect: fill, xRadius: 1.5, yRadius: 1.5).fill()
|
NSBezierPath(roundedRect: fill, xRadius: 1.5, yRadius: 1.5).fill()
|
||||||
} else if let bufferedRange, maxValue > minValue {
|
} else if let bufferedRange, maxValue > minValue {
|
||||||
let lower = min(maxValue, max(minValue, bufferedRange.lowerBound))
|
let lower = min(maxValue, max(minValue, bufferedRange.lowerBound))
|
||||||
@@ -1115,7 +1183,7 @@ private struct PlaybackSlider: NSViewRepresentable {
|
|||||||
let scale = bar.width / (maxValue - minValue)
|
let scale = bar.width / (maxValue - minValue)
|
||||||
let fill = NSRect(x: bar.minX + (lower - minValue) * scale, y: bar.minY,
|
let fill = NSRect(x: bar.minX + (lower - minValue) * scale, y: bar.minY,
|
||||||
width: (upper - lower) * scale, height: bar.height)
|
width: (upper - lower) * scale, height: bar.height)
|
||||||
NSColor.white.withAlphaComponent(0.4).setFill()
|
SunsetPalette.bufferedTrack.setFill()
|
||||||
NSBezierPath(roundedRect: fill, xRadius: 1.5, yRadius: 1.5).fill()
|
NSBezierPath(roundedRect: fill, xRadius: 1.5, yRadius: 1.5).fill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1123,8 +1191,8 @@ private struct PlaybackSlider: NSViewRepresentable {
|
|||||||
let width: CGFloat = volume ? 16 : 3
|
let width: CGFloat = volume ? 16 : 3
|
||||||
let height: CGFloat = volume ? 12 : 15
|
let height: CGFloat = volume ? 12 : 15
|
||||||
let knob = NSRect(x: rect.midX - width / 2, y: rect.midY - height / 2, width: width, height: height)
|
let knob = NSRect(x: rect.midX - width / 2, y: rect.midY - height / 2, width: width, height: height)
|
||||||
let color = volume ? NSColor(white: isEnabled ? 0.85 : 0.45, alpha: 1)
|
let color = isEnabled ? (volume ? SunsetPalette.text : SunsetPalette.secondaryText)
|
||||||
: NSColor.white.withAlphaComponent(isEnabled ? 0.65 : 0.25)
|
: SunsetPalette.disabledThumb
|
||||||
color.setFill()
|
color.setFill()
|
||||||
NSBezierPath(roundedRect: knob, xRadius: volume ? 6 : 1, yRadius: volume ? 6 : 1).fill()
|
NSBezierPath(roundedRect: knob, xRadius: volume ? 6 : 1, yRadius: volume ? 6 : 1).fill()
|
||||||
}
|
}
|
||||||
@@ -1219,7 +1287,7 @@ private struct PlayerContent: View {
|
|||||||
return (model.isCompact ? "" : "Buffer: ") + String(format: "−%.0fs +%.0fs", behind, ahead)
|
return (model.isCompact ? "" : "Buffer: ") + String(format: "−%.0fs +%.0fs", behind, ahead)
|
||||||
} ?? (model.isCompact ? "Buffer —" : "Buffer: —"))
|
} ?? (model.isCompact ? "Buffer —" : "Buffer: —"))
|
||||||
.font(.system(size: 10).monospacedDigit())
|
.font(.system(size: 10).monospacedDigit())
|
||||||
.foregroundStyle(.white.opacity(0.75))
|
.foregroundStyle(Color(SunsetPalette.secondaryText))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(height: 12)
|
.frame(height: 12)
|
||||||
.help(bufferedStatus ?? "Reusable buffer unavailable.")
|
.help(bufferedStatus ?? "Reusable buffer unavailable.")
|
||||||
@@ -1260,17 +1328,17 @@ private struct PlayerContent: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(PlayerGlass().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",
|
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)
|
||||||
.foregroundStyle(.white.opacity(model.isOnTop ? 1 : 0.6))
|
.foregroundStyle(Color(model.isOnTop ? SunsetPalette.accent : SunsetPalette.secondaryText))
|
||||||
.disabled(model.isFullScreen || model.compactWindow.isTransitioning)
|
.disabled(model.isFullScreen || model.compactWindow.isTransitioning)
|
||||||
}
|
}
|
||||||
.padding(.leading, model.isFullScreen ? 16 : 86)
|
.padding(.leading, model.isFullScreen ? 16 : 86)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
.frame(height: 32)
|
.frame(height: 32)
|
||||||
.foregroundStyle(.white.opacity(0.6))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
.onHover(perform: model.setTitleHovered)
|
.onHover(perform: model.setTitleHovered)
|
||||||
.opacity(model.controlsVisible ? 1 : 0)
|
.opacity(model.controlsVisible ? 1 : 0)
|
||||||
.allowsHitTesting(model.controlsVisible)
|
.allowsHitTesting(model.controlsVisible)
|
||||||
@@ -1319,8 +1387,8 @@ private struct PlayerContent: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, showsBufferReadout ? 6 : 8)
|
.padding(.vertical, showsBufferReadout ? 6 : 8)
|
||||||
.foregroundStyle(.white.opacity(0.85))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 10)))
|
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)))
|
||||||
.onHover(perform: model.setControlsHovered)
|
.onHover(perform: model.setControlsHovered)
|
||||||
.opacity(model.controlsVisible ? 1 : 0)
|
.opacity(model.controlsVisible ? 1 : 0)
|
||||||
.allowsHitTesting(model.controlsVisible)
|
.allowsHitTesting(model.controlsVisible)
|
||||||
@@ -1395,13 +1463,14 @@ private struct PlayerContent: View {
|
|||||||
time(showsRemainingTime ? model.duration - model.seekPosition : model.duration))
|
time(showsRemainingTime ? model.duration - model.seekPosition : model.duration))
|
||||||
}
|
}
|
||||||
.font(.system(size: 11).monospacedDigit())
|
.font(.system(size: 11).monospacedDigit())
|
||||||
|
.foregroundStyle(Color(SunsetPalette.secondaryText))
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
if showsBufferReadout { bufferReadout }
|
if showsBufferReadout { bufferReadout }
|
||||||
}
|
}
|
||||||
.padding(.vertical, 11)
|
.padding(.vertical, 11)
|
||||||
// Optical centering accounts for symbol insets and the native seek thumb.
|
// Optical centering accounts for symbol insets and the native seek thumb.
|
||||||
.offset(y: -1.5)
|
.offset(y: -1.5)
|
||||||
.foregroundStyle(.white.opacity(0.6))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
.onHover(perform: model.setControlsHovered)
|
.onHover(perform: model.setControlsHovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1418,7 +1487,8 @@ private struct PlayerContent: View {
|
|||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
if model.fileName == nil {
|
if model.fileName == nil {
|
||||||
PlayerGlass(blendingMode: .behindWindow)
|
PlayerGlass(blendingMode: .behindWindow, cornerRadius: 0,
|
||||||
|
ambientGlows: !model.isBusy && model.errorText == nil)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
} else {
|
} else {
|
||||||
@@ -1445,13 +1515,13 @@ private struct PlayerContent: View {
|
|||||||
}
|
}
|
||||||
.padding(24)
|
.padding(24)
|
||||||
.frame(maxWidth: 420)
|
.frame(maxWidth: 420)
|
||||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
|
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 12)).allowsHitTesting(false))
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else if model.isBusy {
|
} else if model.isBusy {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
ProgressView().controlSize(.large)
|
ProgressView().controlSize(.large)
|
||||||
Text(model.resolvingYouTube ? "Resolving YouTube…" : "Opening…")
|
Text(model.resolvingYouTube ? "Resolving YouTube…" : "Opening…")
|
||||||
.font(.callout).foregroundStyle(.secondary)
|
.font(.callout).foregroundStyle(Color(SunsetPalette.secondaryText))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else if model.fileName == nil {
|
} else if model.fileName == nil {
|
||||||
@@ -1460,14 +1530,14 @@ private struct PlayerContent: View {
|
|||||||
Image(systemName: "play.rectangle.fill")
|
Image(systemName: "play.rectangle.fill")
|
||||||
.font(.system(size: 28))
|
.font(.system(size: 28))
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
.foregroundStyle(.white.opacity(0.6))
|
.foregroundStyle(Color(SunsetPalette.accent))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("terebi").font(.system(size: 20, weight: .semibold))
|
Text("terebi").font(.system(size: 20, weight: .semibold))
|
||||||
.foregroundStyle(.white.opacity(0.85))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
Text("Open media to get started")
|
Text("Open media to get started")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.white.opacity(0.6))
|
.foregroundStyle(Color(SunsetPalette.secondaryText))
|
||||||
}
|
}
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
@@ -1486,14 +1556,14 @@ private struct PlayerContent: View {
|
|||||||
.accessibilityLabel("Open YouTube URL…")
|
.accessibilityLabel("Open YouTube URL…")
|
||||||
}
|
}
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.system(size: 13, weight: .medium))
|
||||||
.foregroundStyle(.white.opacity(0.85))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
.imageScale(.large)
|
.imageScale(.large)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(!model.canOpen)
|
.disabled(!model.canOpen)
|
||||||
.background(.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 8))
|
.background(Color(SunsetPalette.actionFill), in: RoundedRectangle(cornerRadius: 8))
|
||||||
Text("Drop a media file or YouTube link here")
|
Text("Drop a media file or YouTube link here")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundStyle(.white.opacity(0.6))
|
.foregroundStyle(Color(SunsetPalette.secondaryText))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
@@ -1506,14 +1576,14 @@ private struct PlayerContent: View {
|
|||||||
Image(systemName: "waveform").font(.system(size: 64, weight: .ultraLight))
|
Image(systemName: "waveform").font(.system(size: 64, weight: .ultraLight))
|
||||||
Text(model.fileName ?? "").font(.headline).lineLimit(1)
|
Text(model.fileName ?? "").font(.headline).lineLimit(1)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(Color(SunsetPalette.secondaryText))
|
||||||
.padding(40)
|
.padding(40)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
if model.state.buffering && !model.isBusy && model.errorText == nil {
|
if model.state.buffering && !model.isBusy && model.errorText == nil {
|
||||||
ProgressView("Buffering…")
|
ProgressView("Buffering…")
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 10)))
|
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)))
|
||||||
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
|
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
@@ -1525,10 +1595,10 @@ private struct PlayerContent: View {
|
|||||||
.truncationMode(.middle)
|
.truncationMode(.middle)
|
||||||
.help(model.fileName ?? "terebi")
|
.help(model.fileName ?? "terebi")
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
|
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)).allowsHitTesting(false))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.foregroundStyle(.white.opacity(0.75))
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
.opacity(model.controlsVisible ? 1 : 0)
|
.opacity(model.controlsVisible ? 1 : 0)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
.accessibilityHidden(!model.controlsVisible)
|
.accessibilityHidden(!model.controlsVisible)
|
||||||
@@ -1581,7 +1651,7 @@ private struct PlayerContent: View {
|
|||||||
}
|
}
|
||||||
if dropTargeted {
|
if dropTargeted {
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: 10)
|
||||||
.strokeBorder(.white.opacity(0.75), lineWidth: 2)
|
.strokeBorder(Color(SunsetPalette.accent), lineWidth: 2)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
@@ -1607,6 +1677,8 @@ private struct PlayerContent: View {
|
|||||||
minHeight: model.isCompact || model.compactWindow.isTransitioning ? model.compactContentMinimumHeight : 240)
|
minHeight: model.isCompact || model.compactWindow.isTransitioning ? model.compactContentMinimumHeight : 240)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
.foregroundStyle(Color(SunsetPalette.text))
|
||||||
|
.tint(Color(SunsetPalette.accent))
|
||||||
.background(WindowChrome(model: model))
|
.background(WindowChrome(model: model))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user