Commit Graph

395 Commits

Author SHA1 Message Date
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
xRangerDE
ce01034586 release: 4.6.38 sidebar streamer-list empty-state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:24:51 +02:00
xRangerDE
6fdfa08ecb feat: sidebar empty-state when no streamers added yet
First-launch (or after-clearing-everything) opens the app with an
empty sidebar streamer list — just the "Streamer" section heading
and a blank area below. New users had no in-app indication of where
to add their first streamer. The "Add streamer..." input lives in
the TOP bar, which is non-obvious from the sidebar context.

renderStreamers now short-circuits on empty streamers[] and stamps
a small dashed-border hint card into the list with locale-driven
copy pointing the user at the top-right input ("No streamers yet.
Add one via the input at the top right." / "Noch keine Streamer.
Fuege oben rechts einen hinzu.").

The empty state styling (.streamer-list-empty) is intentionally
subtler than the full-page .empty-state used for the VOD grid —
dashed border + tinted background + small padding so it fits the
narrow sidebar rail without dominating it.

Also clears the streamer-section-counter on this branch and hides
the bulk-remove X button, since both would otherwise have stale
state from a previous non-empty render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:24:50 +02:00
xRangerDE
a7c251f016 release: 4.6.37 localize VOD/Merge empty-state strings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:20:29 +02:00
xRangerDE
5f514b1700 i18n: localize 3 empty-state strings (VOD grid + Merge tab)
Three empty-state texts hardcoded German in the HTML and never wired
through the locale system:

- VOD grid empty state: "Keine VODs" + "Wahle einen Streamer aus
  der Liste oder fuge einen neuen hinzu." Shown when no streamer is
  selected. English users were reading German strings here despite
  the rest of the app rendering in English.

- Merge tab empty state: "Keine Videos ausgewahlt." Shown in the
  Videos zusammenfugen tab before any files are added.

Existing locale tables already had `vods.noneTitle` /
`vods.noneText` / `merge.empty` in both EN and DE — they just
weren't being applied. Added IDs to the three elements
(vodGridEmptyTitle / vodGridEmptyText / mergeEmptyText) and
wired three setText calls in renderer-texts.applyText.

Zero new locale keys; pure plumbing fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:20:29 +02:00
xRangerDE
db32f01ddb release: 4.6.36 events viewer rows class-based + data-type pills
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:14:34 +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
4809da8957 release: 4.6.35 unify filter inputs + monospace template class
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:09:46 +02:00
xRangerDE
5da4cc9e64 cleanup: unify filter inputs + monospace template inputs into shared classes
Five filter-style text inputs (vodFilterInput, streamerListFilter,
archiveSearchQuery, chatViewerFilter) plus three monospace template
inputs (vod / parts / clip filename templates) were each carrying
their own ~80 chars of inline style declaring near-identical
background / border / radius / padding combinations.

Consolidated into three new utility classes:
- .filter-input — base flex-1 minWidth-180 filter look, used by
  vodFilterInput
- .filter-input.compact — small variant for the sidebar streamer
  filter (smaller padding, smaller font, no flex, percent-width
  with margin)
- .filter-input.flex-1-1-240 — larger variant for the archive
  search box (240px basis, 200px min, smaller radius/padding to
  fit the multi-control form-row it sits in)
- .input-monospace — applies the same monospace stack (Consolas /
  Segoe UI Mono / monospace) used by .chat-viewer-time and
  .viewer-modal-list-chat to text inputs that hold code-shaped
  values

Side effect: vodHideDownloadedToggle had a hardcoded
`accent-color: var(--accent); cursor:pointer;` inline style, which
was redundant after the global custom-checkbox styling landed in
4.6.26 (the checkbox is now ::after-driven, accent-color does
nothing). Removed.

Zero visual change. The inputs render identically because the
class CSS values match what was inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:09:45 +02:00
xRangerDE
a373410b89 release: 4.6.34 viewer modal style extraction + dead var
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:05:37 +02:00
xRangerDE
ee8f9425fc cleanup: extract events/chat viewer inline styles + drop dead var
The events-viewer and chat-viewer modals were each carrying ~5 inline
styled elements (modal sizing, status text, list container, filter
row + filter input) duplicated between the two modals. Edits to one
viewer left the other drifting visually.

Extracted to a shared .viewer-modal* family in styles.css:
- .viewer-modal sets the column flex layout
- .viewer-modal-events / .viewer-modal-chat set their own sizing
- .viewer-modal-title / .viewer-modal-status / .viewer-modal-list +
  inline + chat list variants for the data area
- .viewer-modal-filter-row + .viewer-modal-filter-input for the
  chat viewer's filter

Zero visual change; just stops the two viewers from drifting and
unblocks future polish (skeleton states inside the list, sticky
filter row, etc.) without an inline-edit-by-inline-edit grind.

Side: removed lastArchiveStatsScannedAt module variable in
renderer-stats.ts. It was assigned in refreshArchiveStats but never
read anywhere — leftover from an early plan to compare against a
previous timestamp before refreshing. The renderer-rendered "Last
scan" line reads stats.scannedAt directly. Dead, removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:05:37 +02:00
xRangerDE
0ae0f8bb7d release: 4.6.33 localize modal close aria-labels
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:00:29 +02:00
xRangerDE
b37244cccf a11y+i18n: localize modal close aria-labels + strip dead modal title fallbacks
Two related artifacts from the 4.6.31 a11y pass.

aria-label="Close" was hardcoded English on all five modal-close X
buttons — anyone running the German locale would still hear "Close
button" from their screen reader. Added a shared
.modal-close-localizable class on each X, plus a streamers.modalCloseAria
locale string ("Close dialog" / "Dialog schliessen"), plus a small
setAriaLabelAll helper in renderer-texts that resolves the class via
querySelectorAll and applies the localized label in one shot. Now all
five modals announce in the active language.

While editing the modal headers, also removed the dead "Stream events"
and "Chat replay" English fallback text from eventsViewerTitle and
chatViewerTitle. Both h2s get their textContent overwritten the
instant openEventsViewer / openChatViewer is called (with the
streamers name or a UI_TEXT fallback), so the inline English text was
never user-visible past first-paint and only mattered to a screen
reader if a user managed to focus an unopened modal. Empty <h2/> is
cheaper and removes the i18n drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:00:28 +02:00
xRangerDE
4956a68d9b release: 4.6.32 clip-cutter modal repaint + global radio styling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:56:08 +02:00
xRangerDE
0df8bf357d feat: clip-cutter modal themed + global radio button styling
Two interrelated changes shipped together.

Clip-cutter modal cleanup. The "VOD zuschneiden" modal was the last
big surface still painted in the apps PRE-purple colour palette:
hardcoded #2b2b2b modal bg, #E5A00D orange title, #1a1a1a slider
tracks (already overridden by the global rule but inline-styles still
sat there), #333 input bgs, #444 borders, plain "white" text, #888
labels, #aaa radio labels. All of it inline. The result: opening the
clip dialog was visually jumping back two themes.

Extracted everything to class-based styles using var() colours:
- .clip-modal* family of classes for layout
- Title now uses var(--text), no orange
- Inputs use var(--bg-elevated) + var(--border-soft) and pick up
  the global focus ring automatically
- The duration display ("Dauer: 00:01:00") now sits in a small
  green-tinted card to make it visually distinct from the input
  rows around it
- Radio labels go through a unified .clip-radio-row with a hover
  background tint, and the :has(input:checked) selector swaps the
  label text colour + weight when a radio is selected

Global radio button styling. The clip modal had four radio buttons
that were the only non-OS-themed control left in the app. Custom
.appearance:none + ::after-driven dot styling matching the new
checkbox visual: 16px circle, 1.5px border, hover purple tint,
checked fills the inner circle with the accent colour + a soft
purple shadow, focus-visible has the same 3px purple ring as every
other form control. Cascades globally so any future radio gets the
treatment for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:56:07 +02:00
xRangerDE
32decb4c01 release: 4.6.31 modal a11y — dialog roles + aria-labels
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:50:27 +02:00
xRangerDE
afef213b45 a11y: dialog roles + aria-labelledby + aria-label on modal closes
All five modal-overlay containers (update, clip-cutter, events-viewer,
chat-viewer, template-guide) were rendering as plain divs from an
accessibility perspective. Screen readers would announce nothing
distinguishing when one of them opened, and the close-X buttons would
read as "x button" with no semantic meaning.

Added on each .modal-overlay:
- role="dialog" — tells assistive tech this is a modal region
- aria-modal="true" — instructs the reader to ignore content outside
  the dialog while it is open (matches the keyboard escape + click-
  outside-to-dismiss behavior the renderer already implements)
- aria-labelledby="<existingTitleId>" — every modal already had a
  uniquely-IDd h2; pointed each dialog at its own title so the reader
  announces e.g. "Stream events dialog" on open

Added on each .modal-close button:
- aria-label="Close" — gives the X button a real semantic label
  independent of the visual character

Zero visual change, zero behavior change. Just makes the app actually
usable for someone running NVDA/JAWS/Orca/VoiceOver. WCAG 4.1.2 +
2.1.1 + 1.3.1 alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:50:26 +02:00
xRangerDE
5200126565 release: 4.6.30 dead code cleanup + profile type clarity
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:46:13 +02:00
xRangerDE
f93b07c87a cleanup: remove dead fetchOnlyFollowerCount + clarify profile inferred type
Two related artifacts left over from the avatar/banner GQL refactor
in 4.6.20:

- fetchOnlyFollowerCount was an early standalone helper from the
  iteration where Helix supplied core profile fields and a separate
  public-GQL roundtrip pulled just the follower count. The 4.6.19
  rewrite folded all of that into a single public-GQL query, so the
  helper has no callers. Removed.

- streamFromPublic was typed via
  `Awaited<ReturnType<typeof fetchPublicStreamerProfile>> extends ...`
  conditional inference because the inline stream shape was anonymous.
  That worked but read like a riddle. Hoisted the inline shapes to
  two named interfaces (PublicStreamInfo + PublicStreamerProfileResult)
  so the function signature is explicit and the local var is just
  `PublicStreamInfo | null`. Same type, an order of magnitude more
  obvious to anyone reading.

Both changes are zero-runtime-behavior; tests confirm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:46:13 +02:00
xRangerDE
2f91823161 release: 4.6.29 VOD bulk-bar slide-in + style extraction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:41:09 +02:00
xRangerDE
9115819bb0 feat: VOD bulk-action bar — slide-in animation + style extracted from inline
The bulk-action bar (the purple-tinted row that appears between the
VOD filter and the grid when 1+ VOD checkbox is ticked) was 100%
inline-styled in HTML, which meant:
- No animation when it appears — it just popped into existence
- No reusable styling for similar action surfaces later
- Layout debugging meant editing HTML, not CSS

Extracted to a proper .vod-bulk-bar class, plus .vod-bulk-count for
the "N selected" label and a .vod-bulk-spacer for the flex push.
The CSS rule also picks up a 4px soft purple shadow + a slightly
richer gradient background that matches the rest of the purple
surfaces in the app.

Animation: vod-bulk-bar-slide @keyframes fires every time the JS
flips display:none -> display:flex, because @keyframes restart on
each display change. 220ms, cubic-bezier(0.16, 1, 0.3, 1) for a
quick spring landing, 10px translateY-from + opacity 0->1. The
appear feels intentional now instead of jarring.

Disappear (display:flex -> none) still snaps because CSS cannot
transition through display:none — adding that would require a
class-toggle refactor and an explicit timer to defer the actual
removal. Not worth the complexity for the polish-grade improvement
this is going for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:41:08 +02:00
xRangerDE
fdeb1697de release: 4.6.28 active streamer highlight + dead scrollbar cleanup
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:36:32 +02:00
xRangerDE
b9f2b68596 feat+cleanup: active streamer highlight polish + dead scrollbar rule removed
Active streamer state in the sidebar was a flat purple-tinted
background with a 3px left border. Felt slightly weak as the primary
"this is what you're looking at" affordance, especially with the
similarly-tinted hover state immediately next to it.

Bumped to:
- Gradient background fading purple-strong to purple-faint across
  the row, so the active item has a directional emphasis matching
  the rest of the Twitch-purple language.
- 1px inset purple ring on top so the active state reads as a
  clearly-bordered card, not just a tinted background.
- A small purple right-edge marker (3px wide, 60% tall, centered)
  drawn via ::after — mirrors the existing left border and makes
  the selected row feel "framed".
- Streamer name in the active row goes 600 weight so the identity
  pops over the meta toggles next to it.

Cleanup side: the old generic ::-webkit-scrollbar rule block from
the early days of the app was still in the file at line 1497, even
though the newer purple-themed *::-webkit-scrollbar block further
down has been overriding it for several releases (later wins on
identical specificity). Replaced the old block with a one-line
comment explaining where the live rule lives, so the next person
greping for "scrollbar" doesn't get a misleading hit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:36:32 +02:00
xRangerDE
c7d0bb7e30 release: 4.6.27 range slider repaint + number input cleanup
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:31:29 +02:00
xRangerDE
227c4bdf82 feat: range slider repaint + number input spinner cleanup
Two leftover form-control oddities from the audit.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:27:04 +02:00
xRangerDE
f6333bf6f5 release: 4.6.25 streamer counter + duration badge + queue shimmer + chat polish
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:23:19 +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
8edbef0a60 release: 4.6.24 input focus + queue polish + toast + btn-icon fix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:18:12 +02:00
xRangerDE
17b715ab24 fix+feat: input focus ring + queue item polish + toast + button class collision
Polish round 3, plus a class-collision fix.

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

Polish bundle:

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:14:15 +02:00
xRangerDE
8928d1f8ed release: 4.6.22 sidebar live indicators + polished hover + empty-state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:11:27 +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
fa8c2b2658 release: 4.6.21 sticky header opaque + banner visible + button styles
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:04:23 +02:00
xRangerDE
30776c02b9 fix: sticky header opaque + banner visible + missing button styles
Three things from screenshot feedback against 4.6.20:

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:55:17 +02:00
xRangerDE
1b87a2611e release: 4.6.19 fix public-mode profile avatar (roles instead of broadcasterType)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:43:53 +02:00
xRangerDE
ec48592503 fix: public-mode profile shows letter X — GQL query referenced authed-only field
Root cause of the X-fallback in the new profile header when the app
runs without Twitch credentials ("public mode"): the GQL query in
fetchPublicStreamerProfile asked for `broadcasterType`, which exists
on the AUTHENTICATED Twitch GQL schema but NOT on the public one. The
public endpoint returned `errors[]` with "Cannot query field
broadcasterType on type User", which fetchPublicTwitchGql correctly
treats as a complete failure and returns null. That cascaded:
- avatarUrl stayed empty
- displayName fell back to the lowercase login
- description stayed empty
- partner/affiliate badge never rendered
- the renderer hit the letter-tile fallback path

Reproduced live against gql.twitch.tv with a curl-equivalent: the
exact query worked when broadcasterType was swapped for the public-
schema field roles{isPartner isAffiliate}. xrohat correctly comes
back as Partner, with the full 150x150 avatar URL, real displayName
"xRohat", and 1.25M follower count.

The 4.6.18 data-URL fetch fix is still right (Electron renderer img
loading against the Twitch CDN was its own minor headache) — it just
never got exercised because we never had a URL to fetch in the first
place. With this fix the data-URL path now activates on every
public-mode profile load, and avatars actually render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:43:53 +02:00
xRangerDE
f564567897 release: 4.6.18 fix profile avatar fallback (real Twitch picture now loads)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:41:04 +02:00
xRangerDE
b1fd73cbe6 fix: profile avatar shows fallback X instead of real picture
Reproduction: open the new profile header (4.6.17) for any streamer
with a real Twitch avatar. The fallback gradient letter tile renders
instead of the actual profile picture. Image works fine pasted into
the browser; only the renderer img tag fails.

Cause is Electron renderer image loading against the Twitch CDN
(static-cdn.jtvnw.net) — undocumented but reproducible: the same
HTTPS URL that loads fine in DevTools fails silently from the live
page, firing the img.onerror handler which (by design) swaps to the
letter-tile fallback.

Fix: fetch the avatar bytes in the main process via axios (Node http
client, no renderer / CSP / referrer-policy / CORS shenanigans),
convert to base64 data URL, and put THAT in the profile.avatarUrl
field. The renderer just renders the data URL via img src — same
code path, but the URL is now data:image/png;base64,... so no
external fetch is involved.

Bytes cached by source URL in a small FIFO map (256 entries) so the
same avatar across cache misses only downloads once. Profile cache
itself is unchanged, just stores the data URL now instead of the
remote URL. On a clean restart the user sees the fix on first
streamer click; mid-session a click on Refresh (top-right of the
header) forces a re-fetch through the new path.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:38:38 +02:00
xRangerDE
a43fc6689c release: 4.6.16 auto-merge resumed live-recording parts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:29:55 +02:00