Commit Graph

22 Commits

Author SHA1 Message Date
xRangerDE
3e37d780c3 a11y: queue-item title + selector keyboard-accessible
Two more click-only divs in the queue item template were leaving
keyboard users stuck:

- .queue-selector — the "X" number-badge to the left of pending
  queue items that toggles bulk-select. Previously a div with onclick.
  Now role="checkbox" + tabindex + aria-checked tracking the selection
  state + Enter/Space keydown handler.

- .queue-item .title — the truncated VOD title that, when clicked,
  toggles the expanded detail panel underneath the row. Previously
  a div with onclick. Now role="button" + tabindex +
  aria-expanded reflecting the panel state + aria-controls pointing
  at the details panel ID + Enter/Space keydown handler.

Both pick up 2px purple focus-visible rings to match the rest of
the a11y family.

aria-expanded on a button is the conventional pattern for
"disclosure widget" controls (collapsible/expandable content),
so screen readers will now announce the title as "VOD title,
button, collapsed" or "expanded" as the user navigates and toggles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:46:40 +02:00
xRangerDE
35769959f4 a11y: remove-X spans become keyboard-accessible with aria-label
The two `<span class="remove">x</span>` glyphs (one per queue
item, one per streamer-list item) had no semantic role, no
aria-label, no tabindex — entirely mouse-only and screen-readers
just announced them as the bare "x" character.

Made both fully keyboard-accessible:
- role="button" + tabindex="0" so they enter the tab order and
  read as buttons to AT
- aria-label="Remove" / "Entfernen" via new
  streamers.removeAria locale key (DE + EN)
- Keydown handler on Enter + Space synthesizes the same removal
  callback (mirroring native button behaviour for synthetic
  buttons — Enter on real buttons fires click, Space does too)
- Focus-visible state: 2px red glow ring + force opacity:1 on
  the streamer-list .remove (which is normally opacity:0 until
  the streamer-item is hovered) so keyboard navigators can see
  the focused X

Both call sites preserved e.stopPropagation in the keydown handler
so Enter on a focused X doesn't bubble up to the row's click
handler (which would trigger streamer-select).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:18:18 +02:00
xRangerDE
fedf3a9945 i18n+polish: localize queue detail labels + style the retry button
Three queue-item detail labels were hardcoded German in the
renderer's HTML template: "Streamer:" / "Dauer:" / "Datum:". The
queue-details panel that expands when a user clicks a queue items
title would render these labels in German regardless of the
selected locale — same i18n gap as the empty states fixed in
4.6.37.

Added queue.detailStreamer / detailDuration / detailDate to both
locale tables ("Streamer:" / "Duration:" / "Date:" in EN,
"Streamer:" / "Dauer:" / "Datum:" in DE) and wrapped the labels
in a .queue-detail-label span so the colour distinction between
label and value (secondary vs primary text colour) is consistent.

The error-state retry button was a span with five inline-style
props and a unicode ↻ as its glyph — visually drab and not really
a button semantically (screen reader read it as text). Promoted
to a real <button class="queue-retry-btn"> with a proper hover
state (purple-tinted background + accent border + white text +
active-press scale-down). Subtler than .btn-pill but obviously
clickable.

Cursor:pointer on .queue-item .title moved from inline style on
the HTML template to the CSS rule — same effect, one less
attribute per queue item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:13:42 +02:00
xRangerDE
63f1cafe1a cleanup+polish: queue empty state — class-based + visual sibling to sidebar
The sidebar queue empty state was built via inline-style HTML
template: `<div style="color: var(--text-secondary); font-size:
12px; text-align: center; padding: 15px;">${UI_TEXT.queue.empty}
</div>`. Worked but had two issues:

1. Flat plain-text styling that did not match the
   .streamer-list-empty card-style empty hint sitting directly
   above it in the same sidebar — visually inconsistent.

2. innerHTML interpolation of a locale string. The string is
   safe (locale-controlled, not user input), but the lint hook
   pattern-matches innerHTML use anyway, leaving the file flagged
   on every audit pass.

Rebuilt via createElement / textContent so no innerHTML touches
the locale string, and extracted the styling into a .queue-empty
CSS class that mirrors the .streamer-list-empty card look
(dashed border + tinted bg + 6px radius + centred text). The two
sidebar empty states now read as a family instead of two
unrelated takes on "empty".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:47:14 +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
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
1f2b5e583c feat: --twitch-disable-ads + queue context menu + remove redundant flag
Three Phase-12 wins.

1. Streamlink --twitch-disable-ads is now a setting (default on, since
   most users hit this — Twitch mid-roll ads otherwise get embedded
   into the VOD output as black-screen audio gaps). Off only when the
   user explicitly opts out via the new checkbox in Download Settings.
   Applied in downloadVODPart args; clip downloads are unaffected
   (Twitch clips do not carry mid-roll ads).

2. Right-click context menu on queue items. Items vary by status:
   pending/paused -> Move to top, Move to bottom; failed -> Retry;
   completed -> Open file (when 1 output) / Show in folder; always
   -> Copy URL, Open on Twitch, Remove from queue. Move-to-top/
   bottom calls existing reorderQueue IPC. Menu auto-dismisses on
   outside-click / Escape / scroll, repositions to stay inside the
   viewport.

3. Removed the global currentDownloadCancelled flag. It was a
   leftover from before per-item tracking — every site that set it
   (pause-download / cancel-download / remove-from-queue) already
   added every active item to cancelledItemIds via the activeDownloads
   loop. The four read sites (downloadVODPart close handler,
   processOneQueueItem retry-loop guard, processDownloadMergeGroup
   phase 1 and phase 3 guards, splitMergedFile loop) now check
   cancelledItemIds.has(itemId) directly. splitMergedFile reads
   from its itemId parameter (added in cycle 1) so the per-item
   intent threads through correctly. Net: -8 lines, one less
   global flag to reason about, no behaviour change for the
   intended cases (per-item cancel via remove + bulk cancel via
   pause/cancel both still work because they each populate the
   per-item set).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:09:33 +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
933af6a6da feat: queue "Open file" / "Show in folder" + clickable finish notification
After a download completes there was no way to jump to the result
without manually navigating the download folder.

Server-side:
- DownloadResult and QueueItem gain optional outputFiles: string[]
  (single entry for VOD/clip, multi for parts/merge-group splits).
  Threaded through every downloadVOD / processDownloadMergeGroup
  branch into processOneQueueItem which attaches it to the queue
  item on success. Persisted via sanitizeQueueItem so the actions
  survive a queue file reload.
- New IPC handlers open-file (shell.openPath) and show-in-folder
  (shell.showItemInFolder), both with existence + type checks.
- The "downloads finished" Notification gets a click handler that
  brings the window to the foreground and opens the download folder.

Renderer-side:
- Expanded queue-item details now render an action row when
  status === completed and outputFiles is non-empty.
- "Open file" only shown when there is exactly one file (so multi-
  part downloads do not surprise the user by opening just part 1).
  "Show in folder" always shown.
- DE / EN locale strings + a graceful toast if the file was moved
  or deleted between completion and click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:19:29 +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
2b379e5e6a fix: correct dragstart cancel method and release claimed filenames after download
- Use dataTransfer.effectAllowed='none' instead of preventDefault() for dragstart
  (preventDefault does not cancel dragstart events per HTML spec)
- Clear claimedFilenames Set in processOneQueueItem finally block to prevent
  stale claims from blocking re-downloads of same VODs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:19:30 +01: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
4607ba9cc6 fix: persist expanded details across re-renders, guard drag-drop init against duplicates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:03:20 +01:00
xRangerDE
0133569104 fix: include selection state in queue render fingerprint so numbered selectors update visually
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:06:42 +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
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
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
6c082a87ab fix(merge-split): fix premature 100% progress, add ffmpeg preflight, clean partial splits, use locale metaLabel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:12:50 +01:00
xRangerDE
ad4e540952 feat(merge-split): add queue checkboxes and merge-group selection UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:55:33 +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