Commit Graph

340 Commits

Author SHA1 Message Date
xRangerDE
c6ae0cadbd release: 4.6.63 .select-compact for 4 inline-styled selects
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:22:16 +02:00
xRangerDE
274d3874f5 cleanup: extract .select-compact class — 4 selects outside .form-group
The vodSortSelect + the 3 archive-search selects all sat OUTSIDE
the .form-group container, which meant the existing
.form-group select rule did not apply to them. Each carried the
same ~110 chars of inline style hard-coding bg / border / radius /
padding / color.

Extracted to a shared .select-compact class (6px radius, 7px
padding, var(--bg-card) base, var(--border-soft) border) and
swapped all four call sites. Visual consistency between the
filter-row select in the VODs tab and the multi-select control
strip in the Archive search — same heights, same border treatment.

Minor visual change for the 3 archive selects (4px -> 6px
border-radius, 6px -> 7px vertical padding) so the controls now
match the rest of the apps button + input family. Worth the 1px
delta for the consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:22:16 +02:00
xRangerDE
1c62cf4a92 release: 4.6.62 open-file blocks executable extensions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:16:46 +02:00
xRangerDE
32e0b1ab7d security: open-file IPC blocks executable extensions
Companion to 4.6.61. The open-file IPC handler (used by the
"Open file" buttons in the queue + archive) was previously a
plain shell.openPath call with only an existsSync check:

  if (typeof filePath !== "string" || !filePath) return false;
  if (!fs.existsSync(filePath)) return false;
  const result = await shell.openPath(filePath);

shell.openPath happily launches any path the OS knows how to
execute. An XSS landing through e.g. a smuggled queue item URL
that reached the renderer-side openFile global function could
pass `C:\\Windows\\System32\\calc.exe` and the IPC would launch
calc.

Added a deny-list of obvious shell-execution extensions (.exe,
.bat, .cmd, .com, .ps1, .vbs, .vbe, .js, .jse, .wsf, .wsh, .scr,
.msi, .msp, .lnk, .cpl, .reg, .hta, .jar, .application). Rejected
calls log to debug + return false to the renderer. Media + text +
image extensions remain unaffected — those open in their normal
default-app viewers, which is the intended use case.

show-in-folder + open-folder stay permissive on extension since
they only open File Explorer (no execution).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:16:46 +02:00
xRangerDE
73eaccb483 release: 4.6.61 scheme-validate open-external IPC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:12:51 +02:00
xRangerDE
c6f423b5ac security: scheme-validate URLs handed to shell.openExternal
The open-external IPC was a pass-through:

  ipcMain.handle("open-external", async (_, url) =>
    await shell.openExternal(url));

shell.openExternal on Windows happily resolves any URL scheme the OS
knows how to launch — including file:// paths, ms-settings:, shell:,
javascript:, and assorted protocol handlers. The renderer is
contextIsolated + nodeIntegration: false so direct exploits are
blocked, but an XSS landing through (for example) a streamer name
that smuggled HTML into a renderer template would have a clean path
through this IPC to launch arbitrary local executables via the OS
shell.

Validation gate: reject anything that isn't an http:// or https://
URL. Trim before the test so a smuggled leading/trailing whitespace
attempt does not slip through. Rejected requests get a debug-log
entry (truncated to 200 chars so a megabyte payload doesnt nuke the
log) and return silently — the renderer caller already swallows
the promise without checking, so silent-drop matches existing
behaviour.

Defence-in-depth. No known active exploit; just removing an
unnecessary surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:12:51 +02:00
xRangerDE
7e60d0e920 release: 4.6.60 bound renderer storyboard cache to 100 entries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:08:43 +02:00
xRangerDE
976ca40963 perf: bound the renderer-side VOD storyboard cache (FIFO 100)
vodStoryboardClientCache was a plain Map<vodId, VodStoryboard | null>
with no eviction. Every VOD ever hovered cached its first sprite
data URL — about 50-200 KB each. Browsing a long-running streamer's
2000-VOD archive could leave the renderer holding 100-400 MB of
hover-only sprite data permanently, with no signal to the user
that it was happening.

Wrapped writes in a rememberStoryboard helper that caps the cache
at 100 entries with FIFO eviction (Map iterator is insertion-ordered
so .keys().next().value is always the oldest). Cache hit / miss
semantics unchanged for the live set — only the dropped-off-the-
back entries get re-fetched if the user scrolls back to a VOD they
hovered hundreds of cards ago, and that re-fetch is fast because
the main-process side has its own metadata cache that survives the
renderer-side eviction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:08:42 +02:00
xRangerDE
96683afa14 release: 4.6.59 localize clip-cutter "Invalid time values" alert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:00:28 +02:00
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
8ef2ce50e7 release: 4.6.58 merge-tab empty state DOM-built
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:56:13 +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
1b8624d88a release: 4.6.57 live-status poller — eviction on empty list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:51:42 +02:00
xRangerDE
77e4c84c45 fix: live-status poller — eviction now runs even when watch list is empty
Subtle leak in runLiveStatusBatchPoll: the eviction pass (which
removes liveStatusByLogin entries for streamers no longer in
config.streamers) ran INSIDE the fetch branch — but the fetch
branch is skipped early when logins.length === 0.

Concretely: if a user had 3 streamers all marked live, then
removed all 3, the poll would early-return at length-check,
leaving stale liveStatusByLogin entries forever (until app
restart) — main-process memory + an inaccurate
get-live-status-snapshot IPC response.

Renderer wasn't visibly affected because renderStreamers only
looks up entries for streamers in the rendered list, but the
underlying state was wrong.

Restructured so the eviction pass always runs first based on the
current watch list, then the fetch + diff only runs when the list
is non-empty. Empty-list case still emits "removed -> offline"
changes to the renderer so its parallel map stays in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:51:42 +02:00
xRangerDE
4518f8867a release: 4.6.56 queue-item title + selector keyboard-accessible
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:46:41 +02:00
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
e95be22a02 release: 4.6.55 profile header avatar wrap + live card keyboard-accessible
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:41:22 +02:00
xRangerDE
96113dc267 a11y: streamer-profile header — avatar wrap + live card keyboard-activatable
Two click-only divs in the new profile header (4.6.17+) had no
keyboard equivalent:

- .streamer-profile-avatar-wrap (clicking the avatar opens the
  channel on twitch.tv) — the only way to trigger that action
  besides the "Open on Twitch" button in the action column, so
  keyboard users were missing a primary affordance
- .streamer-profile-live-card (clicking anywhere on the live
  preview card starts a live recording) — the embedded Record-now
  button inside the card already covered keyboard activation, so
  this one is more about completeness than necessity

Both got:
- role="button" + tabindex="0"
- aria-label = the existing tooltip locale string (so AT reads
  the same text shown to sighted users on hover)
- An inline onkeydown that re-fires the same onclick handler on
  Enter / Space. The live-card additionally checks
  event.target === event.currentTarget so a focused inner button
  pressing Enter doesn't double-fire the wrapper handler

CSS adds focus-visible rings:
- Purple ring on the avatar wrap (matching the existing avatar's
  purple border)
- Red ring + glow on the live card (matching the existing card
  border colour)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:41:21 +02:00
xRangerDE
5e369fef35 release: 4.6.54 VOD cards keyboard-activatable
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:37:15 +02:00
xRangerDE
76be8d3949 a11y: VOD cards keyboard-activatable — opens VOD on Twitch via Enter/Space
Following the chip + row + nav a11y passes, the VOD cards in the
main grid were the last big mouse-only surface in the sidebar +
main panel flow. Each card already delegated click via a single
vodGrid handler — but the card div itself was unfocusable, so a
keyboard user could only reach the +Queue / Trim VOD buttons
inside, never the card thumbnail click that opens the VOD page on
Twitch.

Added on each .vod-card:
- role="button" + tabindex="0"
- aria-label set to the VOD title so AT announces it correctly
  ("32h37m9s VOD: Cyborg Watchparty button") instead of reading
  the whole card content row by row

Added to the existing delegated vodGrid handler:
- A keydown branch that opens the VOD on Twitch when Enter / Space
  fires on a focused .vod-card and the event target is the card
  itself (not a child action button or checkbox — those have
  their own native button / checkbox semantics that handle Enter
  / Space correctly already)

CSS adds a 3px purple focus-visible ring + accent-coloured border
on the focused card, mirroring the hover state's purple glow.

Tab order through the VOD grid now goes: VOD card -> checkbox -> Trim
button -> +Queue button -> next VOD card. Predictable enough for
keyboard navigation through a 38-VOD streamer profile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:37:14 +02:00
xRangerDE
0b99014de3 release: 4.6.53 streamer-item row keyboard-accessible + aria-current
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:32:04 +02:00
xRangerDE
26b03da765 a11y: streamer-item row itself keyboard-accessible + aria-current
The previous a11y pass made all four chip-buttons inside a streamer
row (AUTO / VOD / REC / remove) keyboard-accessible, but the row
itself — the parent .streamer-item div whose click selects the
streamer — was still mouse-only. A keyboard user could focus the
chips but never the row, so they could never select a streamer
without a mouse.

Made the row a focusable role="button":
- role + tabindex on the .streamer-item div
- aria-label set to the streamer's name (so AT announces "xrohat
  button" rather than reading every chip child)
- aria-current="true" on the currently selected row (mirroring
  the visual .active state) so AT understands which row is the
  current selection
- A keydown handler on the row that fires selectStreamer on
  Enter / Space, but ONLY when the row itself (not a chip child)
  is the event target. The chips already preventDefault +
  stopPropagation on their own keydowns so they never reach this
  handler — and even if they did, the e.target check guards.

Focus-visible adds an inset 2px purple ring (inset to match the
row's left-border-marker styling for the active state). Tab order
through the sidebar is now: nav-items → streamer row → AUTO →
VOD → REC → remove-X → next streamer row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:32:03 +02:00
xRangerDE
78eeb8f3dc release: 4.6.52 sidebar nav-items keyboard-accessible + aria-current
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:27:53 +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
a82a8f97f7 release: 4.6.51 AUTO/VOD/REC chip a11y — role + aria-pressed + keyboard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:23:30 +02:00
xRangerDE
1d4b6718b9 a11y: AUTO / VOD / REC streamer chips keyboard-accessible
The three streamer-row action chips (AUTO toggle, VOD toggle, REC
one-shot) were spans wired only with click listeners. Same a11y
gap as the .remove X chips in 4.6.50: no role, no tabindex, no
keyboard activation, no semantic state for toggles. Screen readers
read them as raw text "AUTO", "VOD", "REC" with no clue they were
interactive controls.

Factored a wireChipButton helper inside renderStreamers and ran all
three chips through it. The helper stamps:
- role="button" + tabindex="0"
- aria-label (locale-driven, picked up the existing
  autoRecordTitle / autoVodTitle / recordLiveTitle locale keys
  that were previously only used for the visual title-tooltip)
- aria-pressed="true"/"false" for the AUTO and VOD toggles so
  AT announces the on/off state
- A keydown handler that synthesises the same click handler on
  Enter / Space and stops propagation so the row's click handler
  (streamer-select) does not also fire

CSS adds three focus-visible rings (green for AUTO, blue for VOD,
red for REC) matching each chips active-state colour palette.
Keyboard navigators tabbing through a streamer item now see the
ring on the focused chip clearly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:23:29 +02:00
xRangerDE
6086cd51c1 release: 4.6.50 remove-X buttons keyboard-accessible + aria-labeled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:18:18 +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
5f7ce36845 release: 4.6.49 queue detail labels localized + retry button styled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:13:43 +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
edf3836b26 release: 4.6.48 update-banner progress bar CSS extraction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:08:00 +02:00
xRangerDE
ce469b856c cleanup: update-banner progress bar — extract inline styles
The update banner that shows during an auto-update download was
hosting three inline-styled divs in a row for its progress
indicator:

  <div style="display: none; flex: 1; margin: 0 15px;">
    <div style="background: rgba(0,0,0,0.3); border-radius: 4px;
                height: 8px; overflow: hidden;">
      <div id="updateProgressBar"
           style="background: white; height: 100%; width: 0%;
                  transition: width 0.3s;">

Renderer-updates.ts kept reaching in to mutate
updateProgressBar.style.width as the download advanced — works,
but the bar's static look-and-feel (rounded, dark track, 8px tall,
white fill, 0.3s transition) was buried in HTML attributes
instead of CSS.

Extracted to:
- .update-banner-progress-wrap (the flex:1 container with side
  margin)
- .update-banner-progress-track (the rounded dark track)
- .update-banner-progress-bar (the white fill, 0% default, transition)

The JS continues to drive width via style.width, which now layers
on top of the class's transition for the smooth animation. Zero
visual change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:07:59 +02:00
xRangerDE
144088c01f release: 4.6.47 unified template-lint + retire hard-coded shades
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:04:31 +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
9d4f5fd9a3 release: 4.6.46 inline-toggle class for compact filter-row toggles
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:58:53 +02:00
xRangerDE
1123b9ac46 cleanup: extract .inline-toggle class — compact filter-row toggle pattern
The "Hide downloaded" checkbox in the VOD filter row was carrying
the inline style:

  display:flex; align-items:center; gap:6px;
  color: var(--text-secondary); font-size:12px;
  cursor:pointer; user-select:none

This is a distinct visual variant of the existing .toggle-row class
(filter-row context, smaller gap, smaller font, secondary text
color) so a separate .inline-toggle class is the cleaner fit
rather than overloading .toggle-row with another modifier.

One site cleaned up now, the class is reusable for future
tool-row inline toggles without copy-pasting the seven inline
properties.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:58:52 +02:00
xRangerDE
f473f9e343 release: 4.6.45 fix duration badge overlapping + Queue button
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:55:55 +02:00
xRangerDE
38a50b7a32 fix: VOD duration badge overlapped the + Queue button
Reported via screenshot against 4.6.44. The Twitch-style "32h37m9s"
duration pill introduced in 4.6.20 was anchored at bottom: 8px right:
8px inside .vod-card. But .vod-card is the WHOLE card — thumbnail
plus info + action buttons row below — so the badge sat at the
bottom-right of that whole stack, landing on top of the rightmost
action button ("+ Queue") and obscuring it. The user couldnt click
through the badge.

Fix wraps the .vod-thumbnail in a .vod-thumb-wrap div with
position:relative so the badge's absolute positioning anchors to
the thumbnail's bounding box. The badge now sits at the bottom-
right of the actual image, exactly where the Twitch convention
puts it.

.vod-thumb-wrap also gets line-height: 0 to kill the inline
baseline whitespace that would otherwise show as a thin gap
between the image and the .vod-info section below.

The storyboard hover preview overlay (renderer-vod-hover.ts) is
unaffected — it still appendChild's into .vod-card and uses
aspect-ratio: 16/9 to size itself, which matches the thumbnail
zone above the .vod-thumb-wrap container exactly. Verified end-to-
end pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:55:54 +02:00
xRangerDE
10513f7399 release: 4.6.44 toggle-row class — 17 inline copies unified
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:53:03 +02:00
xRangerDE
ac42ec3686 cleanup: extract 17 toggle-row inline styles into one .toggle-row class
The Settings tab has 17 checkbox toggles, each wrapping the input +
its label in a `<label style="display:flex; align-items:center;
gap:8px; margin-top: 8px;">`. The first toggle in a group skipped
the margin-top; one indented sub-toggle added margin-left: 22px.

All 18 sites were carrying the same ~50 chars of inline style.

Extracted to .toggle-row + the adjacent-sibling combinator
.toggle-row + .toggle-row { margin-top: 8px }, which automatically
adds the gap between consecutive toggles without needing the
per-instance override. The one indented case becomes
.toggle-row.indented (single rule, single margin-left).

Replacements done via Edit with replace_all (13 + 2 + 1 = 16
exact-match replacements + the one manual indented variant). Zero
visual change.

This will pay off on the next Settings card that adds a toggle —
class="toggle-row" is six characters of meaning vs the previous
~50 character inline-style copy-paste.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:53:02 +02:00
xRangerDE
d99fff5923 release: 4.6.43 queue empty state — class-based card
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:47:14 +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
7909beb516 release: 4.6.42 form utility classes extraction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:43:23 +02:00
xRangerDE
5d61094226 cleanup: extract recurring inline form patterns into utility classes
Audit turned up two ~6-occurrence inline-style patterns in Settings:

- `style="font-size:12px; color:var(--text-secondary);"` on small
  sub-labels above stacked inputs (autoCleanupDaysLabel,
  TargetLabel, ActionLabel, autoVodPollMinutesLabel,
  autoVodMaxAgeHoursLabel, statsLastScannedLabel)

- `style="display:flex; flex-direction:column; gap:4px; flex:1;
  min-width:NNNpx;"` on labels that wrap a sublabel + control as a
  vertical pair in a flex row (3 of these in the auto-cleanup
  grid)

Both lifted to .form-sublabel and .form-stack utility classes.
Plus a .form-note for block-level secondary-coloured text (the
cleanupReport "X files would be freed" panel). The min-width
values stay inline since they vary per call site (120 / 160 / 200).

Zero visual change — the class values match what was inline.
Future edits to "what does a small label look like" go through
one selector instead of grep + sed across six sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:43:23 +02:00
xRangerDE
e68db24e10 release: 4.6.41 storage stats table CSS extraction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:38:22 +02:00
xRangerDE
f1b4e6c39a cleanup: storage stats table — extract inline element.style.* into CSS
renderStorageStats was building the storage-stats table in
Einstellungen by setting ~10 style props per element with
.style.padding / .style.color / .style.borderBottom / etc. Verbose
in TypeScript and harder to retheme than CSS-class-based markup.

Extracted to .storage-stats-table family in styles.css:
- Header cells become uppercase 10px tracking-wide labels (matches
  the look of the .vod-bulk-count "X selected" label and the
  archive search type-pill chips)
- Body rows pick up a hover-background tint for scannability
- Numbers use font-variant-numeric: tabular-nums so file counts
  and byte sizes don't jitter as values change between scans
- Last row drops the bottom border so the table doesn't end on
  a dangling line

Open-folder button was using .btn-secondary with inline font-size
+ padding overrides — swapped to the .btn-pill class, which is
already the small/compact action button used in vod-bulk-bar and
the archive search results. Visual consistency across the app.

The "Other folders" subheading (.storage-stats-section) gets the
same uppercase-tracking look as the table headers — small
consistency win that ties the section together visually.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:38:21 +02:00
xRangerDE
a7e189fef9 release: 4.6.40 live-status IPC payload trim
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:33:10 +02:00
xRangerDE
dd08f33dc6 perf: trim live-status batch IPC payload + skip empty broadcasts
The live-status batch poller (60s cadence, every streamer in the
watch list) was sending two things on every tick:
- `changes` — the diff vs. the previous tick, used by the renderer
- `snapshot` — the full Map<login, boolean> serialized as a record

Renderer destructures only `changes` (renderer-streamers.ts line 20).
The snapshot field was wire-noise. For a typical 30-50 streamer
watch list, that snapshot is ~1.5KB of JSON every minute, never
read on the other side. Dropped from the broadcast payload.

Initial-state sync still works: the renderer's
initLiveStatusSubscription calls window.api.getLiveStatusSnapshot()
once at boot to pre-fill its map. The broadcast is only for diffs.

Also added a short-circuit on the main side: if changes.length === 0
(every streamer's live status matched the cached value this tick),
don't broadcast at all. The renderer would just iterate an empty
array and trigger a no-op render; saves the wakeup entirely.

Type signature updates ride through preload.ts +
renderer-globals.d.ts so the API contract stays accurate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:33:09 +02:00
xRangerDE
336fc77c85 release: 4.6.39 status bar cleanup + drop stale version hardcode
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:28:29 +02:00
xRangerDE
e09efd4a33 cleanup: status bar — drop stale v4.1.13 hardcode + extract inline styles
The bottom status-bar version span had "v4.1.13" hardcoded as initial
content from a release several versions ago. init() overwrites it via
window.api.getVersion() on app boot, so the user only ever sees this
for the millisecond between paint and IPC return — but during that
window we were lying about the version, and if init() ever crashed
the user would be stuck looking at v4.1.13 in the corner forever.

Cleared the initial content to empty so the span just doesn't render
text until the real version arrives.

Side effect: extracted the inline `style="color:var(--text-secondary);
font-size:12px; margin-left:auto; padding-right:12px"` from
statusBarQueueSummary to a .status-bar-queue-summary class, plus a
new .status-bar-version class for the version pill. Both consistent
with the rest of the status-bar styling. Bonus: queue summary picks
up font-variant-numeric: tabular-nums so the "X B | Y avg | Z done"
numbers don't jitter horizontally as values update during a
download.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:28:28 +02:00