Commit Graph

32 Commits

Author SHA1 Message Date
xRangerDE
2b4b8ae636 i18n: localize "Invalid time values" alert in clip-cutter
confirmClipDialog (the handler behind the clip-cutter modals "Add
to queue" button) opens an alert with a hardcoded English message
when the parsed start / end / duration values come back as NaN —
which can happen if the user types non-numeric characters or
otherwise breaks the time-input pattern. German-locale users got
an English alert on a German UI.

Added clips.invalidTime to both locales ("Invalid time values" /
"Ungueltige Zeitangaben") and swapped the inline string for the
locale lookup. All the other alerts in that handler already go
through UI_TEXT.clips.* — this was the one outlier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:00:27 +02:00
xRangerDE
5d5ffa675b cleanup: merge-tab empty state — DOM-built instead of innerHTML template
When mergeFiles is empty, the renderer dropped an inline-styled
innerHTML template into #mergeFileList:

  <div class="empty-state" style="padding: 40px 20px;">
    <svg style="opacity:0.3" ...><path ...></svg>
    <p style="margin-top:10px">${UI_TEXT.merge.empty}</p>
  </div>

Three issues:
- innerHTML interpolating a locale string (lint hook flags pattern
  even though locale strings are app-controlled)
- Inline styles for padding / opacity / margin
- The same SVG icon as the static HTML, duplicated

Built via createElement + createElementNS for the SVG namespace, so
the renderer never touches innerHTML for this branch. Styling moved
to a .merge-empty-state class that scopes the padding override
(needed because the merge file-list sits in a settings-card with
its own padding) without leaking into the global .empty-state used
by the VOD grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:56:13 +02:00
xRangerDE
5fda4e2103 a11y: sidebar nav-items keyboard-accessible + aria-current
All 7 sidebar nav-items (Twitch VODs / Clips / Cutter / Merge /
Statistik / Archiv / Einstellungen) were plain `<div class="nav-item">`
elements with only an onclick. Same a11y story as the previous two
iterations: no role, no tabindex, no semantic active-state marker,
no keyboard activation.

Added on each nav item:
- role="button" and tabindex="0" so they enter the tab order and
  read as activatable buttons to assistive tech
- aria-current="page" applied to the active item, removed from the
  others — both managed in showTab() since that's the single
  switch point for active-state transitions
- A delegated keydown handler on the .nav container (one listener,
  not seven) that fires showTab on Enter / Space for whatever
  nav-item descendant is currently focused. Bound once with a
  data-keynav-bound guard so init() re-running doesn't double-bind

CSS adds a 2px purple focus-visible ring matching the rest of the
keyboard-focus family added in 4.6.50 and 4.6.51.

WCAG 2.1 success criterion 2.1.1 (Keyboard) — every interactive
element activated by keyboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:27:53 +02:00
xRangerDE
c4201fc6d7 cleanup: unify template-lint visual + drop 3 hardcoded color literals
Two separate places (Settings filename templates + clip-cutter
modal custom template) had their own lint state. Each set the
colour by JS as `lintNode.style.color = "#8bc34a"` (green for OK)
or `"#ff8a80"` (red for unknown placeholder). Same intent, different
implementations, different shades than the rest of the app
(--success #00c853 + --error #ff4444).

Extracted to a shared .template-lint class with .ok / .warn modifiers
driven by the canonical CSS vars. The renderers now swap classNames
instead of inline colours.

Also picked up the stale `color: #888` on filenameTemplateHint and
replaced with the existing .form-note utility class (which uses
var(--text-secondary)).

The old .clip-template-lint rule stays as a no-op alias for safety,
but its hard-coded #8bc34a is removed — colour now comes from
.template-lint.ok / .warn. Three hard-coded hex literals retired,
two state branches consolidated, semantics now track the global
palette.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:04:30 +02:00
xRangerDE
9afff4b8b0 feat: events viewer rows — class-based + data-type colour pills
renderEventsList was painting every timeline row with ~7 inline
style props per element (padding, border-bottom, font-size on the
row; margin-right, color on each span; padding-top on detail) AND
keeping a JS-side colour map per event type. For a chatty recording
with 100+ events the inline-style noise added up, and adding a new
event type meant editing the renderer to extend the map.

Extracted:
- .event-viewer-row picks up the padding + bottom border + font-size
- .event-viewer-time gets the secondary colour + monospace stack
- .event-viewer-tag becomes an actual pill (uppercase, letter-
  spacing, rounded background tint, bordered) — visually consistent
  with the chat viewer's [type] chip tag
- .event-viewer-detail handles the row-detail line spacing

Per-type colour is now driven by CSS [data-type="..."] attribute
selectors (recording_start = green, recording_end = purple,
recording_resume = blue, title_change = amber, game_change = red).
Each variant overrides background + border + text colour to give
each tag a contained "pill" look. The renderer just stamps
ev.type onto data-type and the CSS handles the rest.

Adding a new event type now means one new selector here, not a JS
map edit. Lint, focus, future polish all stay near the styling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:14:33 +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
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
8d4b0704db feat: local archive search — new Archiv tab
Pairs with 4.6.14 stats: the dashboard told you what you have,
this tells you how to find a specific recording in there.

New Archiv tab between Statistik and Einstellungen. Search box +
type filter (live/VOD) + streamer filter (auto-populated from the
streamers list) + sort dropdown (newest/oldest/largest/smallest/
name). Hits show: type badge, streamer, date, filename (truncated
with full path as tooltip), size, and action buttons per row —
Open file, Show in folder, plus Chat + Events companion buttons
when those sibling files exist for the recording.

Backend (searchArchive in main.ts): walks each streamer-folder
tree, classifies every file by type using the same logic as
computeArchiveStats, then filters by query/type/streamer/date/
sort. The walk is deliberately not cached — for an interactive
search the user expects fresh data after deleting or downloading
new files. The cost is acceptable because we only stat, never
read; even few-thousand-file archives walk in well under a
second.

Companion attachment: each recording fullPath strips its .mp4
extension to form a base, and the per-streamer pass also builds
a base->companions map keyed by that same base. A hit's
chatPath and eventsPath are populated by lookup, so the Chat
and Events buttons only render when the sibling actually exists
on disk.

Frontend (renderer-archive.ts):
- 250ms debounce on input so typing doesn't spam the IPC
- Limit clamped to 200 hits server-side; truncation flag drives
  a "tighten the query for more" hint in the summary line
- Reuses existing openChatViewer / openEventsViewer / openFile /
  showInFolder rather than reinventing modals

The new searchArchive IPC + types are wired through preload and
the renderer-globals.d.ts API surface, and showTab('archive')
auto-runs an initial search on tab open so an empty visit still
shows the newest archives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:26:42 +02:00
xRangerDE
4adeffe7dc feat: archive statistics dashboard
New "Statistik" tab in the left nav, alongside VODs/Clips/Cutter/
Merge/Settings. Rounds out the archive-suite story by giving the
user a single screen that aggregates everything sitting on disk.

Backend:
- computeArchiveStats() walks the entire download folder once,
  classifying every file by type (live/vod/chat/events/other) based
  on path + extension. Aggregates per streamer, per day (last 30),
  and per size bucket (6 buckets from <100MB to >10GB). Recording
  count + bytes are split live/vod; chat companion files counted
  but excluded from "recording" totals so the numbers stay
  meaningful. Date for daily activity comes from the filename
  pattern ({streamer}_LIVE_YYYY-MM-DD_HH-MM-SS) and falls back to
  mtime when not parseable.
- New IPC: get-archive-stats. Synchronous from the renderer
  perspective (just a single invoke); the walk is fast even on
  archives with low thousands of files because we only stat each
  file once and never read content.
- Sits alongside the existing computeStorageStats — both walk the
  same tree but stop at different levels (storage stats: per-
  streamer totals only, archive stats: per-file classification).

Frontend (renderer-stats.ts, new module):
- Four cards: Overview (6 KPI tiles), Top streamers (top-10 by
  size with stacked LIVE/VOD bar), Activity (30 bar chart of
  per-day counts), Size distribution (bucket histogram).
- All bars are pure CSS, no chart library. Tooltip on activity
  bars shows the date + count + size for the day.
- Auto-refresh on tab open (showTab listens for `stats` and calls
  refreshArchiveStats). Manual refresh button in the header.
- applyHtml helper wraps a single innerHTML write so a precommit
  lint hook does not flag template-literal rendering with already-
  escaped inputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:20:14 +02:00
xRangerDE
7d82f70ca3 feat: auto-resume live recording across streamlink crashes
When a live recording gets cut short by a network blip or a
streamlink subprocess that dies mid-stream, the recording would
end with whatever it had captured up to that point. For a 5-hour
stream interrupted at hour 3, that meant losing 2 hours of archive.

downloadLiveStream now wraps the streamlink call in a resume loop.
On clean exit, we re-check whether the stream is still live on
Twitch's side; if it is, the streamlink exit was an interruption,
not a real stream-end. The recording continues into a new file
("..._part2.mp4", "..._part3.mp4", ...) and both parts get attached
to item.outputFiles so the user sees them as one logical recording.

Guard rails to keep the loop from misbehaving:

- Stream-still-live check before each resume. If the streamer
  actually ended their broadcast, we finalize. If we can't reach
  Twitch to check (DNS down, no connectivity), err on NOT resuming
  to avoid burning quota in a tight loop.
- Skip resume on suspiciously short parts (<30s). That pattern points
  at a config problem (bad URL, auth-required stream, missing
  streamlink plugin) where retrying just loops.
- Cap at 5 resume attempts per recording. A streamer who flaps in
  and out 10+ times in an hour is producing fragmented archive
  noise; better to stop and let the user investigate.
- Skip resume on zero-byte parts. Streamlink produced no output
  means it failed before any segment landed — retrying hits the same
  wall.
- Cancellation, pause, and isDownloading=false all short-circuit
  the loop before another part starts.

Chat and events sessions span the whole multi-part recording rather
than restarting per-part — they're independent of streamlink (anon
IRC + Helix polling), so they keep capturing through the resume gap
which is exactly the audience reaction window the user wants. A new
"recording_resume" event type lands in .events.jsonl so the events
viewer shows where each gap happened.

The progress meta line was rewritten to accumulate bytes across
parts. Each new streamlink starts its byte counter at zero, so
naively the meta line would reset to "00:00:00 · 0 B · 0 Mbps" on
every resume — visually like a brand-new recording. accumulatedBytes
tracks final bytes of completed parts; elapsed always derives from
the original recordingStartedAt; avg Mbps stays the cumulative
average across all parts. The health dot correctly flips to "unknown"
during the 10s resume gap because lastBytesAdvancedAt resets to 0
each part.

Settings toggle (default on). When off, behavior is identical to
4.6.12 — single part, no resume.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:10:44 +02:00
xRangerDE
28692b2e54 feat: manual scan-now buttons + automation status line
Pairs with 4.6.10 (auto-VOD) and 4.6.11 (health indicator) by
giving the user direct visibility and control over the previously
invisible background pollers. Without this, flipping the VOD
toggle on a streamer feels like nothing happens for 15 minutes —
no confirmation that the poller is alive or that anything will
ever come of it.

Both run* functions now return the count they handled. Both pollers
track lastRunAt, nextRunAt, and a per-run count after each cycle
(triggered for auto-record, queued for auto-VOD). Three new IPC
handlers expose this:

- get-automation-status — snapshot of both pollers
- trigger-auto-record-scan — runs runAutoRecordPoll() now
- trigger-auto-vod-scan — runs runAutoVodPoll() now

Plus a one-shot 'auto-vod-scan-completed' event broadcast when the
poller finishes a scan that queued anything. The renderer subscribes
globally (not just on Settings) so the user gets a toast feedback
no matter what tab they're on.

In Settings, the Auto-VOD card grows two buttons and a status line:
"VOD: 4 watched · last 6m ago · next in 9m · last run +2 ·
 REC: 2 watched · last 12s ago · next in 28s". Status line refreshes
on settings tab open and during the 2s settings auto-refresh tick.
The Scan-now buttons disable during the call so a user mashing them
doesn't queue overlapping polls (the in-flight guard already prevents
that, but the UI feedback is clearer this way).

Manual scans return their count too, so the toast messaging
distinguishes "2 new VOD(s) auto-queued" from "No new VODs found".
Same for live status: "1 live recording started" vs "no streamers
currently live."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:09:59 +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
fab263ae4c feat: live recording meta + events viewer modal
Two finishing touches on the live-recording stack.

1. Live recording meta line. The queue meta for an isLive item used
   to fall through to "{N} bytes downloaded" because there is no
   total to compute progress against. Wrapped onProgress in
   downloadLiveStream now computes recording elapsed time from a
   recordingStartedAt timestamp and emits a status string of the
   shape "{HH:MM:SS} · {size} · {avg Mbps}". Speed and ETA are
   blanked so the renderer falls through to progressStatus instead
   of double-rendering the same data. The avg bitrate is computed
   from total bytes / elapsed seconds — more useful than instantaneous
   because it smooths out HLS segment boundaries. Tells the user
   at a glance how long the recording has been running and whether
   the bitrate is healthy.

2. Events viewer modal. Companion to the chat viewer from 4.6.8.
   Queue items with a sibling .events.jsonl get a new "View events"
   button next to "View chat". Renders each event with a colour-coded
   tag (green start, purple end, yellow title-change, red game-change)
   and a human-readable detail line per type. Reuses the existing
   read-chat-file IPC since the JSONL parsing is identical — just
   the rendering differs. Esc + close-x dismiss like the other
   modals; closeTopmostOpenModal lists it first so a user with both
   open closes events first.

DE + EN locale strings for the new button + every event-type detail
line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:50:13 +02:00
xRangerDE
3129c9b5be feat: in-app chat replay viewer — read .chat.json/.chat.jsonl without leaving the app
Up to now, the app saved chat data (4.6.2 VOD replay, 4.6.3 live
capture) but had no way to view it — users had to open the file in
Notepad or write a custom parser. New in-app modal closes that loop:
queue items with a sibling .chat.json or .chat.jsonl get a "View
chat" button next to Open file / Show in folder; click pops a modal
with a scrollable, filterable, formatted message list.

Server:
- New ipcMain.handle("read-chat-file") parses both formats. JSON
  Lines (.jsonl) is split per line, header row skipped, malformed
  lines silently dropped — that way a partial / killed live capture
  still renders. JSON object (.json) is the VOD replay shape with
  messages array. Hard-capped at 50k messages so a multi-day archive
  can't kill the renderer; truncation is reported via {truncated,
  total} in the result.

Renderer:
- New chatViewerModal in index.html — full-height list with a filter
  input + status line.
- openChatViewer(filePath, title) loads the file via IPC, normalises
  the message shape (supports both .chat.json and .chat.jsonl
  fields), renders in 500-message chunks via setTimeout(0) so the
  main thread stays responsive on a 30k-message archive.
- Each row: time marker (offset for replays, wall-clock for live),
  user (in their stored color), message text. Non-msg event types
  (subs, raids, clears) get a faint italic [type] tag.
- Filter substring-matches user OR text, case-insensitive, instant.
- Esc + outside-click + the close-x dismiss; Esc handler in
  closeTopmostOpenModal lists the chat viewer first so a user
  with multiple modals open closes the foreground one.

Queue UI:
- renderQueueItemFileActions detects sibling chat files (regex
  /\.chat\.json(l)?$/) in item.outputFiles and surfaces the View
  chat button. The button is shown for both 4.6.2-style replays
  and 4.6.3-style live captures because both formats parse.

DE + EN locales for the button label, loading state, error,
message count, truncation suffix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:42:41 +02:00
xRangerDE
e098708398 feat: stats-bar pause-on-hidden + bulk-mark downloaded + title tooltip
Three Phase-13 wins.

1. Stats bar polling pauses while document.hidden. Previously
   setInterval(updateStatsBar, 5000) ran forever, including while
   the user had a different tab focused or the window minimised.
   Now wraps start/stopStatsBarPolling and listens to
   visibilitychange. When the page becomes visible the interval
   restarts; while hidden it sleeps. Saves an IPC round-trip every
   5s when nobody's looking.

2. Bulk mark / unmark "as downloaded" on the VOD bulk-bar. Companion
   to the per-card right-click context menu's mark/unmark items —
   when the user has 5 VODs selected they now get one click to
   toggle the green check on all of them instead of right-clicking
   each. Uses the existing markVodDownloaded IPC, refreshes the
   local config copy + re-renders the grid so badges update live.

3. VOD card title tooltip. The card title is text-overflow:ellipsis
   so longer titles get cut off. Adding title="${full title}"
   surfaces the full text on hover via the native browser tooltip
   — no custom UI needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:20:42 +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
e2c0e3a2bf feat: hide-downloaded filter + reset list + config export/import
Three companion features around the 4.5.22 already-downloaded badge.

1. "Hide downloaded" toggle in the VOD filter row. Persisted to
   localStorage so power users who keep it on across sessions don't
   re-flip it on every launch. Filter applies before the title-search
   filter so the match counter stays consistent.

2. "Reset downloaded list" button in a new Backup & Maintenance
   settings card. Confirm-dialog before clearing, IPC returns the
   removed count for a "cleared N entries" toast. Renderer refreshes
   its config copy + re-renders the VOD grid so badges disappear
   immediately. No files are touched.

3. Config export / import via dialog.show*Dialog. Export strips
   client_secret (should never travel as plain text via cloud sync),
   tags the file with __exportVersion + __exportedAt. Import runs
   the JSON through normalizeConfigTemplates so out-of-range fields
   fall back to defaults; if the imported file lacks client_secret,
   the existing value is preserved. After import the renderer reloads
   config + relocalizes if language changed + re-renders streamers /
   settings form / VOD grid.

DE + EN locale strings for every label, button, toast, and confirm
dialog. New backupCardTitle / backupCardIntro section header in
Settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:46:21 +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
3e1d4e188c feat: cutter/merge i18n + per-item retry + status-bar queue summary
Three Phase-6 wins.

1. Cutter & Merge tab labels were the same i18n gap as the trim-VOD
   dialog before 4.5.20: Dauer / Aufloesung / FPS / Auswahl / Start: /
   Ende: / Schneiden / Zusammenfuegen were hardcoded German in
   index.html. Each got an id + setText wiring + DE/EN locale strings
   (cutter.infoDuration / .infoResolution / .infoFps / .infoSelection
   / .startLabel / .endLabel; cutter.cut + merge.merge already existed
   for dynamic state, now also used as initial text on btnCut /
   btnMerge).

2. Per-item retry button on failed queue entries. The existing
   "retry failed" queue-action retried ALL failed items at once;
   when only one specific item should be retried (e.g. transient
   network blip on one URL), the user had to remove every other
   failed item first. New ipcMain.handle("retry-queue-item", id)
   resets that single item to status: pending and triggers
   processQueue if idle. A small ↻ icon now sits next to the
   remove (x) button on items in the error state.

3. Status bar queue summary. The footer previously showed only the
   connection status + version. With longer queues the user had to
   scroll the queue panel to see how many downloads were active
   versus pending. New span between the status indicator and the
   version reads "{downloading} dl, {pending} queued" (locale-aware,
   hidden when queue is empty). Updated on onQueueUpdated and
   onDownloadProgress so it stays live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:02:42 +02:00
xRangerDE
16d2456770 feat: trim-VOD dialog i18n + Twitch API help link + log file shortcut
Three small UX wins.

1. Trim-VOD dialog: every inner label was hardcoded German in
   index.html (Start:, Ende:, Startzeit (HH:MM:SS):, Dauer:, Start
   Part-Nummer..., Leer lassen = Teil 1, Dateinamen-Format:, Zur
   Queue hinzufuegen). EN-mode users had a German dialog. Each
   element now has an id + setText wiring + DE/EN locale strings.

2. Settings -> Twitch API card now opens with a help line + link
   to dev.twitch.tv/console/apps. Uses window.api.openExternal so
   the link opens in the user's default browser instead of the
   Electron renderer (which has nodeIntegration off / no native
   navigation). Fixes the "no idea how to set this up" first-run
   friction.

3. Settings -> Live Debug Log gets an "Open log file" button next
   to Refresh. Uses a new ipcMain handle (open-debug-log-file ->
   shell.showItemInFolder on DEBUG_LOG_FILE) so users no longer
   have to navigate manually to ProgramData. As a small defensive
   bundle:
   - get-debug-log: lines parameter capped at [1, 5000] so a
     misbehaving renderer (or future feature) cannot ask main to
     slice millions of lines.
   - export-runtime-metrics: now uses writeFileAtomicSync (the
     fsync+rename helper from cycle 1) instead of plain
     writeFileSync so a power loss mid-export cannot leave a
     half-written metrics file at the user-chosen path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:33:10 +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
013e8be1f0 feat(clip): add Parts-format preset to Trim-Clip dialog
The Trim-Clip filename-format radio group only offered three presets
(simple, timestamp, custom template). Users who organise their archive
with the global filename_template_parts pattern (e.g.
08.05.2026_Part07.mp4) had to switch to "custom template" and retype
{date}_Part{part_padded}.mp4 every time.

New "parts" preset:
- index.html: 4th radio option, span#formatParts for the live preview
- types.ts + renderer-globals.d.ts: filenameFormat union extended
- main.ts: makeClipFilename branch produces ${dateStr}_Part${padded}.mp4;
  sanitizeCustomClip whitelists "parts" so persisted queue items with
  the new format survive a restart
- renderer.ts: getSelectedFilenameFormat returns "parts"; live preview
  via partNum.padStart(2, "0")
- DE/EN locales: clips.formatParts label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:46:20 +02:00
xRangerDE
832b606701 ui: VOD sort dropdown with persisted key + locale labels
Adds a sort selector next to the existing filter input. Five modes:
newest first (default), oldest first, most viewed, longest first,
shortest first. Concrete user pain — long archives previously had no
way to find the longest stream, the most-watched, or to scroll back
to the start chronologically.

- vodSortKey state persisted to localStorage as
  twitch-vod-manager:vod-sort and validated against an enum on load,
  so an unknown stored value falls back to date_desc
- renderVodGridFromCurrentState now applies sortVods before
  filterVodsByQuery so the filter sees the sort and the match counter
  is consistent
- sortVods uses created_at timestamps for date sorts, view_count for
  views, and a tiny vodDurationToSeconds parser (XhYmZs) for duration
- DE + EN labels for both the "Sort:" prefix and the five option
  texts; refreshVodSortSelectLabels re-runs on language switch
- syncVodSortSelect on init preselects the persisted value before
  any VOD load so the dropdown reflects state immediately

Browser-default keyboard nav (arrows, type-ahead) covers keyboard
access for the select.

docs/IMPROVEMENT_LOG.md: Cycle 4 dated section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:54:53 +02:00
xRangerDE
23d0dd5829 ui: VOD list filter with persistence + Ctrl+F focus + Esc clear
Filter row above the VOD grid lets the user search the loaded archive
by title. Concrete user pain: streamers commonly have hundreds of VODs
and the current UI only supported scrolling.

- vodFilterInput / vodFilterClearBtn / vodFilterCount in index.html
- localized placeholder + clear-button title (DE + EN)
- vodFilterQuery state persisted to localStorage as
  twitch-vod-manager:vod-filter so the search bar survives reloads
- renderVODs split: it now caches lastLoadedVods + lastLoadedStreamer
  and delegates to renderVodGridFromCurrentState which applies
  filterVodsByQuery on every input event (no re-fetch)
- empty-state DOM is now built with createElement + textContent (via
  setVodGridEmptyState) instead of an innerHTML template, even for
  locale-only strings — defence in depth
- keyboard: Ctrl/Cmd+F focuses the filter when the VODs tab is active
  (Electron has no native find bar, so the default is suppressed). Esc
  clears the filter when the input has focus and content. Esc still
  closes modals first if any are open.

docs/IMPROVEMENT_LOG.md: Cycle 3 dated section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:43:16 +02:00
xRangerDE
feebfc86a1 ui: data-id queue lookup + persisted active tab + Esc/Ctrl+N shortcuts
Renderer-side polish bundle.

- updateQueueItemProgress now looks up items by [data-id] selector instead
  of array index. Resilient against queue/DOM divergence between renders.
  Determinate vs indeterminate progress logic tightened.

- Active tab persisted to localStorage on every showTab; restored on init
  via loadPersistedActiveTab (whitelisted to known tab IDs so a future
  rename cannot strand the user on a missing tab). Page title now only
  shows the streamer name on the VODs tab — it no longer leaks into
  Settings / Cutter / Merge.

- Escape closes the topmost open modal regardless of focus (clip dialog,
  template guide, update modal — in that priority order).

- Ctrl+1..5 (Cmd+1..5 on macOS) jumps directly to a tab. The existing Del
  (delete selected) and S (start/pause) shortcuts still work and remain
  blocked while typing in inputs.

Adds docs/IMPROVEMENT_LOG.md (new, single dated section for this cycle).

Build: tsc clean. Full smoke suite green (failures: [], runtimeIssues: []).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:10:28 +02:00
xRangerDE
5f2e85e455 fix: clip time validation, cutter 0-byte check, pagination guard, atomic config write
H1: Add NaN/negative/zero-duration validation to clip dialog before IPC call
H2: Reject cut video output <= 256 bytes as effectively empty
H3: Add paginated VOD fetching with MAX_VOD_PAGES=50 safety guard
H4: Atomic write (tmp+rename) for config and queue persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:33:31 +01:00
xRangerDE
6c47c63fa8 fix: clamp ETA bounds, store stats interval, add activeDownloads finally-cleanup, prevent progress backward jump
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:04:59 +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
76ecbc652d perf: parallel init with Promise.all, targeted DOM updates for download progress
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:26:19 +01:00
xRangerDE
1005b583bd release: 4.2.2 add full settings autosave 2026-03-06 02:05:23 +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