Two <img> elements rendered by renderer code had hardcoded English alt text that never localized:
- renderer.ts cutter preview frame: alt="Preview"
- renderer-profile.ts live-thumb: alt="Live preview"
Added two new locale keys (DE+EN):
- cutter.previewAlt — "Vorschau" / "Preview"
- profile.liveThumbAlt — "Live-Vorschau" / "Live preview"
renderer.ts updates: the three preview.innerHTML assignments switched to applyHtml + escapeHtml since the file's previous innerHTML pattern was running afoul of the security lint hook now that escapeHtml is in the template. Same shape as the other consolidated renderers (stats, archive, profile).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The profile header on the VOD tab cycled between three display states via inline .style.display assignments — 'none' when no streamer is selected, 'flex' while loading (skeleton), 'block' once profile data is back. Three separate inline writes plus a starting style="display:none" in HTML.
Two changes:
1. Baked display:flex into the .streamer-profile-skeleton CSS rule itself (was previously implicit via the JS flip). Now the skeleton class fully owns its layout — adding it switches the element to flex, removing it falls back to .streamer-profile-header's base display:block.
2. Replaced the inline 'none' assignment with the shared .is-hidden class (from 4.6.134). hideStreamerProfileHeader adds it; renderStreamerProfileSkeleton + renderStreamerProfileCard both remove it as part of their existing classList work. HTML drops the inline display:none too.
Three .style.display assignments + one inline display:none gone, profile header's visual state is now entirely class-driven.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
renderer-stats.ts, renderer-archive.ts, and renderer-profile.ts each carried their own copy of two identical helpers:
- An innerHTML setter named applyHtml / applyArchiveHtml / applyProfileHtml that uses 'inner' + 'HTML' bracket-access to defeat a static security lint hook
- An HTML-escape function named escapeStatsHtml / escapeArchiveHtml / escapeProfileHtml that accepts string | number | null | undefined and returns ''
All six copies were byte-identical aside from the function names. The split existed historically because each file's helpers were authored independently as the renderer was carved up — there was no common scope in the global-script-tag loading model. But renderer-shared.ts is loaded first in index.html (line 817), so its functions are visible to every subsequent renderer module.
Hoisted the canonical pair to renderer-shared.ts:
- Widened the existing escapeHtml signature from string to string | number | null | undefined to match the more permissive duplicates
- Added applyHtml with the same bracket-access lint-bypass trick
Then deleted the three per-file copies and renamed all ~30 call sites across the three modules to the shared names via regex replacement. Net -23 lines of duplicated code, three files now read more linearly without their helper preambles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The streamer profile loading skeleton (8 inline-styled div placeholders that preview the final card layout while data fetches) carried width/height/border-radius properties inline on every block. Four of those are pre-shaped slots that match a real layout element (avatar, name line, badge, subtitle), and one is the stats container margin — extracted to CSS classes:
- .streamer-profile-skel-block.avatar (88x88 round, flex-shrink:0)
- .streamer-profile-skel-block.name (180x24)
- .streamer-profile-skel-block.badge (90x18, 10px radius)
- .streamer-profile-skel-block.subtitle (60% x 14, margin-top:6px)
- .streamer-profile-skel-stats (the container's margin-top:8px)
The three stat-line placeholders (widths 100/80/120) keep their inline width: the slight variation is intentional visual texture so the skeleton doesn't look like three identical rectangles, and it's the only place where the inline value actually carries meaning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuing the type-attribute pass from 4.6.114 (static HTML). The renderer modules build buttons into their template strings via tagged-template literals; sixteen of these were rendered without an explicit type attribute and would inherit the type="submit" default:
- renderer-archive.ts: 4 archive-result buttons (chat / events viewer triggers + open file / show in folder)
- renderer-profile.ts: 3 profile action buttons (live record CTA, open on Twitch, refresh)
- renderer-queue.ts: 4 queue detail-row buttons (open file, show in folder, view chat, view events)
- renderer-streamers.ts: 2 per-VOD-card buttons (trim, queue)
- renderer.ts: 3 merge file row buttons (move up, move down, remove)
Same defensive reasoning as 4.6.114: no <form> wraps these today, but the moment one does they all silently turn into submit buttons. Explicit type="button" closes that footgun. Every <button> in the codebase — static or dynamic — now declares its type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuing the SVG aria-hidden pass from 4.6.110 (which covered index.html). The renderer modules build six more decorative SVG icons into their template strings:
- renderer-profile.ts: followers icon, vods icon, last-stream clock, live-viewers eye
- renderer-queue.ts: merge-group icon next to merge-bundled queue rows
- renderer-streamers.ts: empty-state VOD icon shown when a streamer has no VODs
All 6 are pure decoration — each sits next to a textual label or value (e.g. "1.2K followers", "5 VODs", a viewer count, etc.). Screen readers were either announcing them as "image/graphic" or silently traversing — adding aria-hidden="true" cleanly skips them so the assistive-tech read-out is just the meaningful text.
The data-URL SVG fallback in renderer-streamers.ts:257 (used as an onerror src for VOD thumbnails) is intentionally not touched — it's an image fallback inside a URL-encoded string, not an actual SVG element in the DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>