style: segment playback timeline with rounded gaps

This commit is contained in:
2026-10-22 11:57:34 +02:00
parent a1c8314391
commit 4bf738d8d2
4 changed files with 23 additions and 27 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ To run without installing, use `make run`. The built `.build/terebi.app` is self
- **Local files:** use **⌘O**, drag files onto the player, or choose Finder's **Open With**. Multiple files form a queue in natural filename order, with automatic advance. Use **View → Show Queue** to play, reorder or remove items; **+** or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it. - **Local files:** use **⌘O**, drag files onto the player, or choose Finder's **Open With**. Multiple files form a queue in natural filename order, with automatic advance. Use **View → Show Queue** to play, reorder or remove items; **+** or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it.
- **Streams:** use **Open Stream…** (**⇧⌘O**) or drop a supported link. YouTube supports individual videos, not playlists or channels. Miruro links must use `https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>` with an explicit positive episode number; arbitrary streaming sites are not supported. - **Streams:** use **Open Stream…** (**⇧⌘O**) or drop a supported link. YouTube supports individual videos, not playlists or channels. Miruro links must use `https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>` with an explicit positive episode number; arbitrary streaming sites are not supported.
- **Miruro metadata:** titles and labeled Opening/Ending bands load independently of playback when available. Bands use measured media duration; missing/invalid metadata leaves playback usable. Timings may differ between cuts. No automatic skipping is performed. - **Miruro metadata:** titles and opening/ending intervals load independently of playback when available. Small rounded gaps mark interval boundaries in the slider, without visible labels; accessibility descriptions retain the interval names and times. Missing/invalid metadata leaves playback usable. Timings may differ between cuts. No automatic skipping is performed.
- **Resume:** videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress. - **Resume:** videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress.
- **Compact mode:** press **P** for a floating player; double-click it to restore. Drag noninteractive areas to move the window. Use the titlebar pin to keep the normal window on top. - **Compact mode:** press **P** for a floating player; double-click it to restore. Drag noninteractive areas to move the window. Use the titlebar pin to keep the normal window on top.
- **Quality:** YouTube defaults to SDR up to 1080p. Use **gear → Quality** for other available resolutions or automatic quality. - **Quality:** YouTube defaults to SDR up to 1080p. Use **gear → Quality** for other available resolutions or automatic quality.
+17 -23
View File
@@ -2005,34 +2005,28 @@ private struct PlaybackSlider: NSViewRepresentable {
var volume = false var volume = false
var bufferedRange: ClosedRange<Double>? var bufferedRange: ClosedRange<Double>?
var intervals: [MiruroInterval] = [] var intervals: [MiruroInterval] = []
private static let labelAttributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 9, weight: .semibold),
.foregroundColor: SunsetPalette.text,
]
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)
if !volume, maxValue > minValue { NSGraphicsContext.saveGraphicsState()
defer { NSGraphicsContext.restoreGraphicsState() }
if !volume, maxValue > minValue, bar.width > 0 {
let scale = bar.width / (maxValue - minValue) let scale = bar.width / (maxValue - minValue)
for interval in intervals { let boundaries = Array(Set([minValue, maxValue] + intervals.flatMap {
let band = NSRect(x: bar.minX + (interval.start - minValue) * scale, [$0.start, $0.end]
y: bar.minY - 2, width: (interval.end - interval.start) * scale, height: 7) }.filter { $0 > minValue && $0 < maxValue })).sorted()
let color = interval.kind == .intro ? SunsetPalette.accent : SunsetPalette.ember let track = NSBezierPath()
color.withAlphaComponent(0.3).setFill() for index in 0..<(boundaries.count - 1) {
NSBezierPath(rect: band).fill() let start = bar.minX + (boundaries[index] - minValue) * scale
SunsetPalette.secondaryText.withAlphaComponent(0.8).setStroke() let end = bar.minX + (boundaries[index + 1] - minValue) * scale
let outline = NSBezierPath(rect: band) let inset = min(1.5, (end - start) / 4)
outline.lineWidth = 0.5 let left = index == 0 ? start : start + inset
outline.stroke() let right = index == boundaries.count - 2 ? end : end - inset
let full = (interval.kind == .intro ? "Opening" : "Ending") as NSString track.append(NSBezierPath(roundedRect:
let label = full.size(withAttributes: Self.labelAttributes).width <= band.width ? full : NSRect(x: left, y: bar.minY, width: right - left, height: bar.height),
(interval.kind == .intro ? "OP" : "ED") as NSString xRadius: 1.5, yRadius: 1.5))
let size = label.size(withAttributes: Self.labelAttributes)
let x = min(max(bar.minX, band.midX - size.width / 2), bar.maxX - size.width)
let above = interval.kind == .intro
let y = above != flipped ? bar.maxY + 3 : bar.minY - size.height - 3
label.draw(at: NSPoint(x: x, y: y), withAttributes: Self.labelAttributes)
} }
track.addClip()
} }
SunsetPalette.track.setFill() SunsetPalette.track.setFill()
NSBezierPath(roundedRect: bar, xRadius: 1.5, yRadius: 1.5).fill() NSBezierPath(roundedRect: bar, xRadius: 1.5, yRadius: 1.5).fill()
+3 -1
View File
@@ -20,4 +20,6 @@
- [x] check whether we're using appropriately new apple ui APIs / SDKs for the project. - [x] check whether we're using appropriately new apple ui APIs / SDKs for the project.
- we should strive to use modern and idiomatic SDKs and APIs including SwiftUI among others - we should strive to use modern and idiomatic SDKs and APIs including SwiftUI among others
- audited SDK 27.0 / Swift 6.4 with the macOS 15 minimum retained; migrated Cast discovery to NWBrowser and production activation to activate(). Real Bonjour identity/name/update/order/deduplication/removal/restart and routable-endpoint checks, loopback Cast transitions, focused startup-error alerts and a cooperating-launcher production smoke for cold start/CLI reuse/document handoff/keyboard playback passed. The existing full CLI snapshot-harness timeout remains; macOS 15 runtime and physical-Cast verification remain unverified. Observation, glass and the existing native bridges were left unchanged. - audited SDK 27.0 / Swift 6.4 with the macOS 15 minimum retained; migrated Cast discovery to NWBrowser and production activation to activate(). Real Bonjour identity/name/update/order/deduplication/removal/restart and routable-endpoint checks, loopback Cast transitions, focused startup-error alerts and a cooperating-launcher production smoke for cold start/CLI reuse/document handoff/keyboard playback passed. The existing full CLI snapshot-harness timeout remains; macOS 15 runtime and physical-Cast verification remain unverified. Observation, glass and the existing native bridges were left unchanged.
- [x] fix the readme. it's currently bloated with irrelevant implementation detail and other such nonsense. it should do the job of a readme only, nothing more. - [x] fix the readme. it's currently bloated with irrelevant implementation detail and other such nonsense. it should do the job of a readme only, nothing more.
- [ ] add a skip button when current playback is inside either an opening or an ending segment.
- skip button should disappear within 3 seconds; if not clicked, the user probably wants to watch the segment.
+2 -2
View File
@@ -31,7 +31,7 @@ The native resolver requests watch-route data and safely follows bounded same-or
Titles come from returned series/selected-episode metadata. Only `op`/`mixed-op` and `ed`/`mixed-ed` become opening/ending bands. Bands retain fractional seconds, require positive player-reported duration, and reject nonfinite, negative, reversed, wrong-episode or out-of-bounds timings; an end equal to duration is valid. Duration changes revalidate raw intervals without trimming, shifting or scaling. Optional provenance/votes/reference-length checks follow the documented frontend evidence; missing fields are not invented. Highest-vote eligible opening and ending win, with response order breaking ties. Overlaps are not merged. Titles come from returned series/selected-episode metadata. Only `op`/`mixed-op` and `ed`/`mixed-ed` become opening/ending bands. Bands retain fractional seconds, require positive player-reported duration, and reject nonfinite, negative, reversed, wrong-episode or out-of-bounds timings; an end equal to duration is valid. Duration changes revalidate raw intervals without trimming, shifting or scaling. Optional provenance/votes/reference-length checks follow the documented frontend evidence; missing fields are not invented. Highest-vote eligible opening and ending win, with response order breaking ties. Overlaps are not merged.
The controls label the bands **Opening** and **Ending**, with accessibility descriptions; neither automatic seeking nor skip buttons are added. Bounds do not prove synchronization across cuts. Metadata tasks are bound to identity and lifecycle generation; replacement clears intervals and rejects stale results, including A → B → A. The controls mark interval boundaries with small rounded gaps in the slider track, without visible labels or boxes. Buffer rendering shares the segmented track; accessibility descriptions retain opening/ending names and times. Neither automatic seeking nor skip buttons are added. Bounds do not prove synchronization across cuts. Metadata tasks are bound to identity and lifecycle generation; replacement clears intervals and rejects stale results, including A → B → A.
### External resolved-source compatibility ### External resolved-source compatibility
@@ -169,7 +169,7 @@ Metadata implementation verification (2026-10-22): protocol/security/bounds chec
## Remaining limits ## Remaining limits
- Confirm title readability and labeled bands in normal/compact controls, buffer/playhead visibility and subtitle alignment in the visible app using Tubular's real launch path. - Confirm title readability and gapped slider segments in normal/compact controls, buffer/playhead visibility and subtitle alignment in the visible app using Tubular's real launch path. Offscreen native rasterization at compact/normal widths confirmed all four interval-boundary gaps remain clear even with full buffer coverage.
- Tubular collection launches, retained-report recovery and watched decisions remain its acceptance responsibility; this implementation changes no collection or progress schema. - Tubular collection launches, retained-report recovery and watched decisions remain its acceptance responsibility; this implementation changes no collection or progress schema.
- Media redirects/cross-origin HLS resources remain unsupported. Native URL launches resolve fresh; externally supplied source documents must be refreshed by their producer. Automatic expired-source refresh is not implemented. - Media redirects/cross-origin HLS resources remain unsupported. Native URL launches resolve fresh; externally supplied source documents must be refreshed by their producer. Automatic expired-source refresh is not implemented.
- Other providers' proxies/HLS rewrites, iframe providers, downloads, thumbnails, automatic skipping, embedded captions and additional external-subtitle formats remain out of scope. - Other providers' proxies/HLS rewrites, iframe providers, downloads, thumbnails, automatic skipping, embedded captions and additional external-subtitle formats remain out of scope.