style: segment playback timeline with rounded gaps
This commit is contained in:
@@ -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/<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.
|
||||
- **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.
|
||||
|
||||
+17
-23
@@ -2005,34 +2005,28 @@ private struct PlaybackSlider: NSViewRepresentable {
|
||||
var volume = false
|
||||
var bufferedRange: ClosedRange<Double>?
|
||||
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()
|
||||
|
||||
@@ -21,3 +21,5 @@
|
||||
- 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.
|
||||
- [ ] 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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user