From 4bf738d8d2adac2f7284450970bbb7e86d6d767f Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 22 Oct 2026 11:57:34 +0200 Subject: [PATCH] style: segment playback timeline with rounded gaps --- README.md | 2 +- Sources/App/TerebiApp.swift | 40 ++++++++++++++------------------- TODO.md | 4 +++- docs/miruro-playback-handoff.md | 4 ++-- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 0b7a908..782bbba 100644 --- a/README.md +++ b/README.md @@ -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. - **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//?ep=` 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. - **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. diff --git a/Sources/App/TerebiApp.swift b/Sources/App/TerebiApp.swift index 3656262..5112425 100644 --- a/Sources/App/TerebiApp.swift +++ b/Sources/App/TerebiApp.swift @@ -2005,34 +2005,28 @@ private struct PlaybackSlider: NSViewRepresentable { var volume = false var bufferedRange: ClosedRange? 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) { let bar = NSRect(x: rect.minX + knobThickness / 2, y: rect.midY - 1.5, 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) - for interval in intervals { - let band = NSRect(x: bar.minX + (interval.start - minValue) * scale, - y: bar.minY - 2, width: (interval.end - interval.start) * scale, height: 7) - let color = interval.kind == .intro ? SunsetPalette.accent : SunsetPalette.ember - color.withAlphaComponent(0.3).setFill() - NSBezierPath(rect: band).fill() - SunsetPalette.secondaryText.withAlphaComponent(0.8).setStroke() - let outline = NSBezierPath(rect: band) - outline.lineWidth = 0.5 - outline.stroke() - let full = (interval.kind == .intro ? "Opening" : "Ending") as NSString - let label = full.size(withAttributes: Self.labelAttributes).width <= band.width ? full : - (interval.kind == .intro ? "OP" : "ED") as NSString - 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) + let boundaries = Array(Set([minValue, maxValue] + intervals.flatMap { + [$0.start, $0.end] + }.filter { $0 > minValue && $0 < maxValue })).sorted() + let track = NSBezierPath() + for index in 0..<(boundaries.count - 1) { + let start = bar.minX + (boundaries[index] - minValue) * scale + let end = bar.minX + (boundaries[index + 1] - minValue) * scale + let inset = min(1.5, (end - start) / 4) + let left = index == 0 ? start : start + inset + let right = index == boundaries.count - 2 ? end : end - inset + track.append(NSBezierPath(roundedRect: + NSRect(x: left, y: bar.minY, width: right - left, height: bar.height), + xRadius: 1.5, yRadius: 1.5)) } + track.addClip() } SunsetPalette.track.setFill() NSBezierPath(roundedRect: bar, xRadius: 1.5, yRadius: 1.5).fill() diff --git a/TODO.md b/TODO.md index 587dac6..87e89e3 100644 --- a/TODO.md +++ b/TODO.md @@ -20,4 +20,6 @@ - [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 - 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. \ No newline at end of file +- [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. \ No newline at end of file diff --git a/docs/miruro-playback-handoff.md b/docs/miruro-playback-handoff.md index 46198de..b74d2ea 100644 --- a/docs/miruro-playback-handoff.md +++ b/docs/miruro-playback-handoff.md @@ -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. -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 @@ -169,7 +169,7 @@ Metadata implementation verification (2026-10-22): protocol/security/bounds chec ## 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. - 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.