Commit Graph

27 Commits

Author SHA1 Message Date
xRangerDE
227c4bdf82 feat: range slider repaint + number input spinner cleanup
Two leftover form-control oddities from the audit.

Range sliders (used in the clip-cutter modal and any future
slider-group settings) had a stale orange thumb colour (#E5A00D) from
when the app was a different shade of Twitch. Reskinned to the
current purple family: track gets a subtle purple-to-dark gradient
that visually echoes the queue progress bar, thumb is a 16px purple
circle with a 2px white border and a soft shadow, hover scales the
thumb 1.15x and turns the shadow into a purple halo for the "I can
grab this" affordance. Focus-visible adds the same 3px purple ring
the rest of the form controls use, so keyboard tabbing through a
modal lands on a clearly-focused slider. Mirrored ::-moz-range-thumb
+ ::-webkit-slider-thumb so Firefox and Chromium-Electron look
identical.

Number inputs got the OS spinner stack hidden globally
(::-webkit-inner-spin-button / outer + -moz-appearance: textfield).
The default Webkit spinners are a tiny gray arrow pair that always
reads as "unfinished" and clutter the look. Users still get keyboard
arrow keys + wheel scroll. If a custom spinner pattern is needed
later it can come back as a chrome around the input — for now the
inputs read clean as text fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:31:29 +02:00
xRangerDE
12fd2b7217 feat: custom-styled checkboxes + select dropdowns
Audit turned up 20 raw `<input type="checkbox">` in Settings still
rendering with OS-default gray-square look — and most `<select>`
elements showing the OS-default dropdown arrow. With the rest of the
UI now Twitch-themed (purple inputs, modal pops, animated everything),
those felt jarringly out of place.

Checkbox: 16px rounded square, dark base with 1.5px border, hovers to
a purple-tinted border, fills purple + draws a CSS-only white check
on the diagonal when checked, scales down briefly on click, focus
shows the same 3px purple ring as the text inputs. No JS, just
:checked + ::after.

Select: appearance:none everywhere to kill the OS chevron, then an
inline-SVG chevron in background-image at right:8px (gray default,
purple on hover). padding-right boosted to 28px so option text never
overlaps the arrow. The dropdown menu itself still uses the OS list,
but the closed control matches the rest of the input family now.

Cascades globally via input[type="checkbox"] / select selectors — no
markup edits needed. The few selects/checkboxes that previously had
inline accent-color overrides keep working because accent-color
applies to native widgets, which we have now replaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:27:04 +02:00
xRangerDE
f7a54a2007 feat: sidebar streamer counter + VOD duration badge + queue shimmer + chat polish
Round-4 polish.

- Streamer section counter. Tiny line next to the "Streamer" sidebar
  heading: "12" when nobody is live, "12 · 3 live" with the live
  count highlighted red when broadcasters from the watch list are
  on air. Re-rendered on every renderStreamers call so it stays in
  sync with add/remove and the 60s live-status poll.

- VOD duration badge. Twitch-style bottom-right pill on every VOD
  thumbnail showing the recordings duration ("32h37m9s"). 11px,
  white-on-near-black, 2px backdrop-blur, hover deepens the
  background, fades out when the storyboard preview activates so
  the preview frame reads cleanly. Pairs with the existing
  downloaded checkmark badge (top-left) and live-recording badge
  to give each thumbnail a complete at-a-glance status row.

- Queue progress bar shimmer. The fill bar now uses a purple-to-
  light-purple gradient and rides a moving white-translucent
  highlight strip that sweeps L->R every 1.8s. Same translateX-100%
  to 100% trick used everywhere else, but only visible because
  the underlying bar has colour. Makes "currently downloading"
  obvious without needing a separate spinner.

- Chat viewer polish. Replaced the inline per-message styling with
  proper .chat-viewer-* classes: hoverable row background, system
  events (subs/raids/deletions) get a left-purple-border + tinted
  background to set them apart from normal chat lines, the type
  tag (e.g. [sub], [raid]) renders as a real chip with a border,
  timestamps are mono-fonted and faded. Per-user IRC colour from
  Twitch metadata is still respected as an inline override on the
  username.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:23:18 +02:00
xRangerDE
17b715ab24 fix+feat: input focus ring + queue item polish + toast + button class collision
Polish round 3, plus a class-collision fix.

Fix first: the new X-close button class introduced in 4.6.21 was
called .btn-icon, which collided with an EXISTING .btn-icon class
already used by the top-bar Refresh button (and elsewhere). The
second declaration partially overrode the first, leaving Refresh
with the wrong hover state (red instead of purple-tinted). Renamed
the close-button class to .btn-close and updated the two call sites
(btnStreamerBulkRemove + vodFilterClearBtn). Refresh now hovers
correctly with a purple tint + a 180deg SVG icon spin on hover.

Polish bundle:

- Input focus ring globally: every text/search/number/password
  input + textarea + select picks up a 3px rgba purple ring on
  focus, with a smooth 180ms transition on border-color, box-shadow,
  and background. Focus state finally reads as intentional instead
  of the OS default blue glow.

- Queue items: 3px left border that color-codes by status (purple
  while downloading, green when complete, red on error), faint
  purple-tinted background when downloading, soft glow on the
  status dot. The queue list now reads as a status timeline at a
  glance.

- Top-bar Refresh button picks up a 1px border, purple-tint hover
  background, and the SVG arrow spins on hover for the "refresh"
  feel.

- Header search box (Add streamer): consistent border-radius (6px
  vs 4px) and the + button gets a hover shadow + active-press
  micro-bounce.

- App toast: gradient background, accent-color left border (purple
  for info, amber for warn, red for error), animated slide-in from
  the right instead of vertical, backdrop blur for content
  legibility over busy backgrounds, and an extra .error variant
  class. Feels modern instead of like a notice strip from 2010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:18:11 +02:00
xRangerDE
cc23f1e272 feat: skeleton cards + tab fade + modal pop animation + scrollbar polish
Visual-polish round 2.

- VOD skeleton loader: replaces the "Loading..." placeholder with
  six shimmering skeleton cards that share the real cards
  dimensions. The grid no longer collapses+expands as VODs arrive,
  and the shimmer telegraphs that work is happening rather than
  the app sitting silent. CSS @keyframes skel-shimmer drives a
  smooth 1.5s gradient pan.

- Tab switch animation: 180ms ease-out fade-in + 4px lift on
  every .tab-content.active. Switching between VODs / Statistik /
  Archiv / Einstellungen no longer feels like an instant
  paint-swap.

- Modal overhaul: backdrop-filter blur(8px) on the overlay so the
  app behind softly blurs out, animated pop on the modal itself
  (scale 0.96 -> 1 + translateY 8px -> 0 with a clean spring
  curve), proper bordered + glow-cornered card, and the close X
  swapped from a flat 24px text button to a real 30x30 rounded
  pill with hover-red highlight.

- Scrollbar: thin 10px purple-tinted webkit scrollbar across the
  entire app, matching the accent color. Hover deepens to full
  purple. Track is near-transparent. Looks intentional instead of
  the default OS gray.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:14:15 +02:00
xRangerDE
11883889de feat: sidebar live indicators + polished hover + empty-state animation
The killer-feature of this pass is the live indicator: red pulsing
dot next to every streamer in the sidebar that is currently
broadcasting on Twitch. Suddenly the sidebar conveys real-time
state at a glance — you know who to click before clicking.

How it works:
- New live-status batch poller (main.ts) fires every 60s, packs
  every streamer in the user's watch list into a single GQL query
  using aliased user lookups (`u0:user(login:$l0){stream{type}} ...`),
  chunked at 50 logins per request. One roundtrip for the whole
  list — far cheaper than per-streamer polling.
- Updates a liveStatusByLogin Map on the main side, emits an IPC
  `live-status-batch-update` event with only the entries that
  flipped (plus a full snapshot for the renderer to keep in sync).
- Renderer subscribes once at boot via initLiveStatusSubscription,
  keeps a parallel Map, and re-renders the streamer list on
  change. Stamps a .streamer-live-dot before the name. Bold name
  for live streamers so they pop in scannability.
- Restart triggers: app boot, streamer-list change (added/removed
  via save-config) so a freshly added streamer gets their dot in
  seconds without waiting for the next 60s tick.

Polish bundled in the same release:

- VOD card hover gets a more substantial lift: 12px shadow + faint
  purple border-glow on hover. Subtle but enough to feel
  "tactile". Border-color transitions alongside the shadow.

- Empty states get a floating animation and a bigger SVG icon
  with accent-colored tint. "No VODs / select a streamer" now
  feels intentional instead of an oversight.

- Streamer-name span dedicated class (.streamer-name +
  .streamer-name.is-live) so a live streamer's name itself bolds,
  not just gets a dot beside it.

Locale strings: liveNowTooltip ("Currently live on Twitch" / "Aktuell
live auf Twitch") for the dot's tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:11:26 +02:00
xRangerDE
30776c02b9 fix: sticky header opaque + banner visible + missing button styles
Three things from screenshot feedback against 4.6.20:

1) VODs visible through/above the sticky profile header. Root cause
   was a stack: the 0.10/0.04 alpha gradient over var(--bg-card)
   pushed the resulting background just barely under "opaque" in
   some renderers AND .content has padding-top: 25px which let
   VODs scroll through the area above the sticky element when
   top: 0 was used. Fix: drop the gradient (banner-bg + ::before
   pseudo handle the visual interest now), use straight
   var(--bg-card), set top: -25px to negate .contents padding so
   the header pins flush with the visible top edge, bump z-index
   to 100, add isolation:isolate to force a new stacking context
   so VODs cannot escape upward through the header.

2) Banner not visible. Was being suppressed by a 0.78-0.92 alpha
   dimming gradient applied via background-image alongside the
   banner URL — readable for text but visually killed the banner.
   Moved the gradient into a ::before pseudo at z-index 1 with
   gentler 0.55-0.78 alpha, dropped banner-bg blur from 18px to
   10px, took opacity from 0.55 back up to 1.0. Banner now
   actually shows behind the content the way twitch.tv does.

3) Stray un-styled buttons. Scan turned up a handful of action
   buttons rolling their own inline styles (.vodBulkAddBtn /
   MarkBtn / UnmarkBtn / ClearBtn, .vodFilterClearBtn,
   .btnStreamerBulkRemove, .clipDialogConfirmBtn) plus a missing
   .queue-detail-btn rule that was leaving every "View chat",
   "View events", "Open file", "Show in folder" button defaulting
   to the browsers gray fallback. Added three reusable classes
   (.btn-pill default/primary/success/danger, .btn-icon, plus the
   missing .queue-detail-btn) and swapped the inline styles for
   the classes. Visual consistency across queue bulk-bar, archive
   search results, and queue item detail rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:04:22 +02:00
xRangerDE
3c73efbad7 feat: banner background + live preview card + VOD hover storyboard + sticky header
Four interlocking visual upgrades that push the profile area from
"works" to "looks like a real Twitch app". Single release because
all four share data plumbing and need to land coherently.

1) Banner background — getStreamerProfile now also pulls
   bannerImageURL via public GQL, fetches the bytes server-side as a
   data URL (same path as the avatar fix in 4.6.18-4.6.19), and the
   renderer puts it behind the header content with blur(18px) +
   saturate(1.2) + a 0.55 opacity overlay. Result: per-streamer
   colour identity at a glance, like twitch.tv's channel page.

2) Live preview card — when isLive, the public-GQL stream block also
   carries previewImageURL(640x360), viewersCount, title, game{name}.
   A second card slides in below the main profile row showing the
   current frame at 240×135, eye-icon viewer count, big bold title,
   game, and a red "Jetzt aufnehmen" CTA. Click anywhere on the card
   OR on the button triggers triggerLiveRecording — same path as
   the sidebar REC dot, so the recording reaches the queue with
   identical settings.

3) VOD hover storyboard — Twitch ships a seekPreviewsURL per VOD
   pointing at a JSON manifest of sprite-sheet images, each a grid
   of preview thumbnails spanning the recording. New IPC
   get-vod-storyboard fetches the manifest, picks the high-quality
   first sprite, fetches its bytes as a data URL, and returns the
   grid metadata. Renderer (new renderer-vod-hover.ts) hooks
   delegated mouseover on #vodGrid: 220ms debounce, then on
   activation overlays a div positioned over the thumbnail with
   background-image=sprite + a setInterval cycling
   background-position through 4 evenly-spaced cells at 600ms each.
   Per-VOD result cached client-side so repeated hovers don't
   re-fetch. Negative results (private VODs, expired) are also
   cached so we don't re-query a known-empty manifest.

4) Sticky header — position:sticky;top:0;z-index:20 plus a
   backdrop-filter:blur(6px) so the VOD grid scrolling underneath
   reads through the banner subtly. Header stays anchored to the top
   of .content as the user scrolls hundreds of VODs.

GQL refresher: the public schema rejects `broadcasterType` but
accepts `roles{isPartner isAffiliate}`, plus the same query now
includes bannerImageURL and stream{previewImageURL viewersCount
title game{name}}. One single roundtrip pulls everything we need
for the header AND the live card. The old separate-follower-count
roundtrip (fetchOnlyFollowerCount) is now redundant but kept around
for back-compat in case other call sites grow into it.

Also: profile layout switched from one big flex row to a relative
container with two children (.streamer-profile-row for the meta,
.streamer-profile-live-card for the live block). The .live-card
only renders when isLive — offline streamers get the same compact
header they had before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:55:17 +02:00
xRangerDE
9239eebf34 feat: streamer profile header — modern channel-page card above VOD grid
When you pick a streamer in the sidebar, the VODs panel now leads with
a polished channel-style header instead of just the bare page title.
This is the "personal" feel — you are looking at a creator, not a folder.

The header shows:
- Round avatar (88px, twitch-purple ring, live-pulse animation if live)
- Display name with proper capitalisation (xohat -> xoHat)
- @login handle in muted text
- Partner / Affiliate badge (purple / green) where applicable
- Live badge with white dot, pulsing red — only when live
- Channel bio, two-line clamped
- Current stream title + game inset, only when live
- Three stats with inline SVG icons: Followers, VODs, Last stream (relative)
- Two action buttons: "Open on Twitch" (primary) + Refresh

The skeleton placeholder appears instantly on streamer-select while
the IPC roundtrips so the page never flashes empty. Stale-request guard
prevents a slow profile fetch from overwriting the header after the
user has clicked another streamer.

Backend (main.ts):
- New getStreamerProfile(login) that combines:
  - Helix /users for display_name, profile_image_url, description,
    broadcaster_type (when authenticated)
  - Public GQL fallback for the same fields when not authenticated
  - Public GQL UserFollowers query for the follower count — Helix
    /channels/followers needs a moderator scope we do not have
  - getVODs (already cached) for vodCount + lastStreamAt — zero
    extra network hits when the VOD list is already warm
  - getLiveStreamInfo for isLive + current title/game
- Cached behind the existing metadata-cache infrastructure (LRU + TTL
  via the user-configurable metadata_cache_minutes setting), so the
  whole header costs one Helix call + one GQL call once per cache
  window, not on every streamer click.

Frontend:
- New renderer-profile.ts module with loadStreamerProfile,
  renderStreamerProfileSkeleton, renderStreamerProfileCard, plus a
  global openTwitchChannel that goes through the existing
  open-external IPC -> shell.openExternal pipeline.
- Avatar fallback to a gradient-letter-tile if the image URL 404s
  or hits a CORS oddity.
- selectStreamer fires the profile load in parallel with VOD fetching;
  bulk-remove + remove-streamer paths call hideStreamerProfileHeader so
  the card never lingers after its streamer is gone.

CSS adds the .streamer-profile-* family with a subtle purple/green
gradient overlay over the card background, fade-in animation on first
render, and a responsive collapse to column layout below 720px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:38:38 +02:00
xRangerDE
2c40bbf66e feat: live recording health indicator (green/amber dot per item)
In-flight live recordings now show a small coloured dot before the
title indicating whether bytes are still flowing.

The health state is derived from byte-progress liveness: each time
the byte counter advances, we stamp lastBytesAdvancedAt; if more
than 30s pass without an advance we flip the badge to amber to tell
the user the streamlink subprocess has gone quiet (dropped segments,
network blip, or the stream just ended). Until the first segment
arrives we report "unknown" so we don't claim health prematurely on
a streamlink that's still negotiating playlists.

Critical wrinkle: streamlink emits progress events on byte boundaries,
so a hung process emits NO events at all. A pure event-driven badge
would never update from "ok" to "stale" — it'd stay frozen at the
last known good state. To avoid that, downloadLiveStream now runs a
10s health-tick interval that re-emits the most recent progress
event with a fresh health computation. The interval is killed in a
finally block so process termination doesn't leak it.

DownloadProgress + QueueItem in both src/types.ts and the renderer
declaration shadow get the new optional recordingHealth field. The
renderer queue handler copies it onto the item; the queue render
function shows a coloured dot before the title for in-flight live
items only (status === 'downloading' && isLive). Three states:
green pulsing (ok), amber flashing (stale), grey static (unknown).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:04:53 +02:00
xRangerDE
1ab6f01e07 feat: auto-vod-download — per-streamer VOD toggle + background poller
Adds the second half of the live-archive flow. AUTO catches a stream
as it happens; VOD catches the recently published archive. Both
together close the gap a Twitch viewer-side archivist cares about.

Streamer list grows a third per-streamer toggle (blue "VOD") next
to AUTO and REC. When enabled, the main-process auto-VOD poller
periodically scans that streamer's VOD list and queues anything
that is (a) within the rolling age window, (b) not already in
downloaded_vod_ids, and (c) not already in the active queue. The
age window keeps freshly-enabled streamers from suddenly dumping
their entire historical backlog into the queue — when a user flips
VOD on, only VODs published in the last N hours (default 24, capped
at 720) get auto-pulled.

Polling cadence is in minutes, not seconds — VOD-listing scans are
heavier than live-status checks and new VODs only appear after a
stream ends, so minute-level lag is fine. Default 15 min, clamped
[5, 360]. Independent timer from the auto-record poller because
their cadences shouldn't be coupled.

UI:
- Streamer item: blue "VOD" pill next to AUTO/REC, identical interaction.
- Settings card "Auto-VOD download": poll interval + max age fields.
- Discord card: optional "Notify when a VOD gets auto-queued" checkbox.

Wires through save-config so toggling triggers restartAutoVodPoller
without a full app restart, and through shutdownCleanup so the
timer is killed on quit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:59:05 +02:00
xRangerDE
029b2bd407 feat: auto-record polling — set-and-forget live archival
Building on the manual REC button from 4.6.0: each streamer now also
has an AUTO toggle. When enabled, a background poller in the main
process checks the streamers live status every 90s (configurable
30-1800s via config.auto_record_poll_seconds). On an offline -> live
transition, a live recording is queued automatically without the
user having to be at the keyboard.

Server:
- config.auto_record_streamers: string[] holds the watched logins
  (deduped + normalized via normalizeAutoRecordList). Empty list
  stops the poller entirely so users who don't use the feature pay
  zero CPU.
- runAutoRecordPoll iterates the list, hits getLiveStreamInfo
  (existing helper from 4.6.0 — Helix when authed, public GQL
  otherwise), tracks per-streamer last-known live state in
  autoRecordLastLiveState, and only triggers on the offline->live
  edge. If a live item already exists for that streamer (manual
  REC click + auto-poll racing), the auto-trigger backs off.
- restartAutoRecordPoller is wired into save-config so toggling AUTO
  on/off or changing the interval takes effect without a restart;
  state for de-watched streamers is dropped so re-enabling them
  later doesn't suppress an immediate first-poll trigger.
- Wired into app.whenReady (start) and shutdownCleanup (stop).
- Initial poll fires ~1.5s after restart so a streamer that's
  already live when the user enables AUTO gets picked up
  immediately instead of after a full interval.

Renderer:
- AUTO pill next to REC. Off = grey outline, on = green outline +
  green text + faint green background. Click toggles via saveConfig
  with the updated auto_record_streamers array; toast confirms.
- Per-streamer state survives reload (it's in the config file).

DE + EN locale strings for the toggle title + on/off toasts.

Why this matters: VODs vanish from Twitch within 7-60 days. Manual
REC requires the user to be present when the stream starts. AUTO
closes that gap — the app watches in the background and captures
without supervision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:35:19 +02:00
xRangerDE
56261216a9 feat: live stream recording — record streamers as they go live
VODs disappear from Twitch after 7-60 days depending on the channel
partnership tier. Anyone serious about archiving needs to capture
streams while they are still live, not after. The downloader is now
a recorder too.

End-user surface:
- Each streamer in the sidebar has a small red "REC" pill next to
  the remove-x. Click it -> server checks Helix (or public GQL when
  no client_id is configured) for live status. If the channel is
  online a new queue item is added with isLive: true, status:
  pending; the existing queue scheduler picks it up. Toast feedback
  for offline / already-recording / generic-failure cases.
- Live items render with a pulsing red REC badge in the queue title
  row and skip the bulk-select checkbox + the merge-group selector
  (they don't make sense for an open-ended capture).
- Output goes to {download_path}/{streamer}/live/
  {streamer}_LIVE_{YYYY-MM-DD}_{HH-mm-ss}.mp4 — timestamped so back-
  to-back recordings of the same channel never collide.
- Streamlink runs without --hls-start-offset / --hls-duration so it
  records until the stream actually ends or the user hits cancel /
  remove. The existing per-item filename claim, integrity check on
  close, and downloaded_vod_ids tracking apply unchanged (live
  recordings are not added to downloaded_vod_ids since they have
  no Twitch VOD ID).

Server plumbing:
- New getLiveStreamInfo(login) helper. Helix /streams when an app
  token is available (better metadata: title + game), public GQL
  fallback otherwise so users in public-mode still get live status.
- New IPC start-live-recording(streamerName) does the live check,
  refuses with ALREADY_RECORDING if a live item for the same
  channel is already pending or downloading.
- downloadVOD branches into a small downloadLiveStream helper when
  item.isLive — computes the timestamped filename, ensures the
  per-streamer/live folder exists, hands off to downloadVODPart
  with null start/end times.
- sanitizeQueueItem preserves the isLive flag across queue file
  reload so a recording in progress survives an app restart in
  state (though streamlink itself dies on app exit and the user
  has to re-trigger).

DE + EN locale strings for every toast + tooltip + the queue badge.
CSS animation for the pulsing badge so it visually distinguishes
live recordings from regular VOD downloads at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:30:08 +02:00
xRangerDE
b959a930af feat: streamer search/bulk-remove + cutter drag-drop + per-streamer scroll
Three Phase-10 wins.

1. Streamer-list filter + bulk-remove. Above 6 streamers (the magic
   number where the list starts to feel cluttered) a search input
   appears below the section title and a small bulk-remove "x"
   button next to it. Filter is title-substring, case-insensitive.
   Bulk-remove honours the active filter — when the input is empty
   it confirms removing the entire list, when filled it confirms
   removing only the matching subset. Used a confirm() dialog with
   the matching count interpolated into the locale string.

2. Cutter drag-and-drop. Dragging a video file from Explorer onto
   the cutter tab now loads it directly — no separate Browse click.
   Uses Electron's File.path extension on the dropped File object
   (works through contextIsolation:true). selectCutterVideo was
   refactored into loadCutterFromPath + a thin wrapper so the drop
   handler reuses the same loading logic. dragenter/dragleave count
   adds visual outline on #cutterPreview while a Files drag is over
   the tab. Falls back gracefully if the dropped file lacks .path.

3. Per-streamer VOD scroll position. Switching streamers used to
   reset scroll-to-top, painful when cycling between archives.
   vodScrollPositions Record<streamer, scrollY> persisted to
   localStorage, capped to 32 entries to bound storage. Save fires
   on a 250ms scroll-debounce timer + on every selectStreamer
   transition. Restore happens 80ms after renderVODs paints (lets
   the first chunk settle) so scrollTop has somewhere to land.

Plus: bounded the persistence table at 32 entries, locale strings
DE/EN, all wired through applyLanguageToStaticUI for live language
switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:03:47 +02:00
xRangerDE
3f04b42b02 feat: auto-resume queue toggle + already-downloaded VOD indicator
Two real UX wins.

1. Auto-resume queue on startup. New checkbox in Settings -> Download
   ("Queue beim Start automatisch fortsetzen"). When enabled and the
   persisted queue has pending items, processQueue() fires ~5 seconds
   after did-finish-load — long enough for the user to see the queue
   and pause if they did not actually want this. Default off so the
   existing behaviour (explicit Start click) is preserved on upgrade.
   The Settings auto-save fingerprint includes the new flag and
   syncSettingsFormFromConfig restores it. Tooltip explains the
   timing on hover.

2. Already-downloaded indicator on VOD cards. Config gains
   downloaded_vod_ids: string[] (bounded to 4096 latest entries).
   Every successful queue-item download appends its parsed VOD ID
   (or every component ID for merge groups). On the VOD grid each
   card whose vod.id is in the set gets a small green checkmark
   badge in the top-right plus a slightly dimmed thumbnail, with a
   localized "Already downloaded" / "Bereits heruntergeladen"
   tooltip. The lookup builds a Set once per render so it stays
   O(1) per card. The renderer refreshes its local config copy on
   every "newly completed" queue update so the badge appears live
   without waiting for a settings save.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:16:21 +02:00
xRangerDE
44c9173f10 feat: backend i18n for user-visible errors + light-theme color vars
Two related Phase-4 changes.

1. main.ts: tBackend(key, params) helper with DE/EN tables for every
   user-visible error / status string produced server-side. Previously
   every backend message was hardcoded German, so EN-mode users saw
   German errors in the queue (last_error), in download progress
   status, in clip-download responses, and in the preflight panel.
   ~30 keys covered: invalidVodUrl, streamlinkMissing, fileTooSmall,
   integrity*, downloadCancelled / downloadPaused, attemptFailed,
   retryingIn, statusBytesDownloaded, mergeGroupFileMissing,
   notAllPartsDownloaded / notAllClipPartsDownloaded, ffmpegMerge/
   SplitFailed, diskSpaceShortFor, all preflight* messages, etc.
   classifyDownloadError extended to recognize EN equivalents
   (streamlink not found, no video stream, folder) so the retry
   classification still works correctly when the language is EN.
   The hand-rolled translation table in renderer.ts:downloadClip is
   gone — backend strings are already locale-correct.

2. styles.css: --border-soft CSS var added to :root and the
   theme-light override. Inline styles in index.html for the VOD
   filter input / sort select / bulk bar were referencing
   --bg-secondary / --text-primary / --border-color (which don't
   exist) and falling through to dark hex fallbacks (#222 / #fff /
   #444), producing a dark patch in light theme. Now uses
   var(--bg-card) / var(--text) / var(--border-soft) which both
   themes define.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:33:18 +02:00
xRangerDE
386998deaf feat: streamer drag-reorder + bulk-queue checkboxes on VOD cards
Two complete UX features.

1. Streamer list is now drag-and-drop reorderable. The order is
   persisted via the existing config.streamers save path, so it
   survives a restart. The dragstart-then-click race that would
   normally fire selectStreamer when the drag is released is
   suppressed via a 50ms post-dragend window.

2. VOD cards each get a top-left checkbox. Selecting >=1 card opens
   a sticky action bar above the grid with "+ Queue" and "Clear"
   buttons. Bulk-add iterates the selected URLs and calls addToQueue
   for each, with a single per-batch toast summarizing the outcome.
   Selection is cleared on streamer switch (per-streamer mental
   model) but not persisted across reloads (stale selection across
   restarts is more confusing than helpful).

Implementation notes:
- Click-on-checkbox is handled by a single delegated listener on
  vodGrid (initVodGridSelectionDelegation), not per-card inline
  handlers. The card .selected class is toggled in place to avoid
  re-rendering the entire grid on every check.
- Streamer items are rebuilt from createElement so the existing
  `event.stopPropagation(); removeStreamer(...)` inline pattern
  is replaced with a real listener; defends against unusual
  characters in streamer names even though Cycle 4 added the
  4-25-char alphanumeric regex.
- styles.css: position: relative on .vod-card for the absolute-
  positioned checkbox; .selected ring highlight; .dragging
  opacity for streamer drag.
- DE / EN locale strings for the bulk-bar; setText / updateBar
  hook into applyLanguageToStaticUI so the bar count updates on
  language switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:24:29 +02:00
xRangerDE
cf9d7b8334 fix: selector overflow for 10+ items, drag-drop status guard, filename claim set for parallel safety
- Queue selector uses min-width instead of fixed width for double-digit numbers
- Drag-start handler validates item is still pending before allowing drag
- ensureUniqueFilename uses in-memory claim set to prevent TOCTOU race

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:15:25 +01:00
xRangerDE
00d35f1b1c fix(ui): reduce queue action button height for compact layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:10:02 +01:00
xRangerDE
63aafae85d feat: add light theme with toggle in settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:46:26 +01:00
xRangerDE
fbcf3935d0 feat: add drag & drop queue reordering and expandable queue item details
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:42:35 +01:00
xRangerDE
2481230983 feat: add keyboard shortcuts (Del/S) and download statistics bar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:40:39 +01:00
xRangerDE
00cf7781d3 fix(ui): prevent queue buttons from being pushed off-screen by long queue
Queue section now uses flex layout with flex-shrink:0 on action buttons,
so they stay visible regardless of queue list length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:06:10 +01:00
xRangerDE
0e6b219455 feat(merge-split): numbered selection instead of checkboxes, user-defined merge order
Replace checkboxes with numbered selectors (1, 2, 3...) that show the
merge order. Click order determines VOD sequence in the merged result.
Chronological auto-sort removed — user controls the order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 08:55:35 +01:00
xRangerDE
c1a72ebd66 feat(merge-split): add Merge & Split button and queue checkbox/merge-group styles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:55:14 +01:00
xRangerDE
b7cd8fbec2 release: 4.2.3 improve updates and startup UX 2026-03-06 02:34:16 +01:00
xRangerDE
2631924ef5 chore: migrate repository to Codeberg, bump version to 4.2.0, update update logic 2026-03-01 20:23:21 +01:00