${safeDisplayTitle}
@@ -425,6 +426,20 @@ function renderStreamers(): void {
// Compact title margin when filter is shown — avoids double gap.
if (sectionTitle) sectionTitle.style.marginBottom = showFilter ? '4px' : '';
+ // Section counter — "X · Y live". Updates on every re-render, so it
+ // stays accurate after add/remove/live-status changes.
+ const counter = document.getElementById('streamerSectionCounter');
+ if (counter) {
+ const liveCount = all.reduce((n, s) => n + (liveStatusByLogin.get(s.toLowerCase()) === true ? 1 : 0), 0);
+ if (all.length === 0) {
+ counter.textContent = '';
+ } else if (liveCount > 0) {
+ counter.innerHTML = `${all.length} · ${liveCount} live`;
+ } else {
+ counter.textContent = String(all.length);
+ }
+ }
+
const q = (streamerListFilterQuery || '').trim().toLowerCase();
const visible = q ? all.filter((s) => s.toLowerCase().includes(q)) : all;
diff --git a/src/renderer.ts b/src/renderer.ts
index 5ab3438..1173688 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -464,44 +464,41 @@ function renderChatViewerList(messages: ChatViewerMessage[]): void {
const end = Math.min(idx + CHUNK, messages.length);
for (let i = idx; i < end; i++) {
const m = messages[i];
+ const isMessageType = m.type === 'msg' || !m.type;
const row = document.createElement('div');
- row.style.padding = '2px 0';
- row.style.lineHeight = '1.5';
+ row.className = 'chat-viewer-row' + (!isMessageType ? ' is-system' : '');
+
+ // System events (subs, raids, deletions) lead with a faint tag.
+ if (!isMessageType) {
+ const tag = document.createElement('span');
+ tag.className = 'chat-viewer-tag';
+ tag.textContent = m.type || 'event';
+ row.appendChild(tag);
+ }
const time = formatChatTimeMarker(m);
if (time) {
const tSpan = document.createElement('span');
- tSpan.style.color = 'var(--text-secondary)';
- tSpan.style.marginRight = '6px';
- tSpan.textContent = `[${time}]`;
+ tSpan.className = 'chat-viewer-time';
+ tSpan.textContent = time;
row.appendChild(tSpan);
}
const user = m.u || m.user || m.login || '';
if (user) {
const uSpan = document.createElement('span');
- uSpan.style.fontWeight = '600';
- uSpan.style.color = m.color || 'var(--accent)';
- uSpan.style.marginRight = '4px';
+ uSpan.className = 'chat-viewer-user';
+ // Per-user IRC color is preserved; the class supplies weight.
+ if (m.color) uSpan.style.color = m.color;
+ else uSpan.style.color = 'var(--accent)';
uSpan.textContent = `${user}:`;
row.appendChild(uSpan);
}
const msgSpan = document.createElement('span');
- msgSpan.textContent = m.msg || m.text || '';
+ msgSpan.textContent = ' ' + (m.msg || m.text || '');
row.appendChild(msgSpan);
- // System events (subs, raids, deletions) get a faint bracketed prefix
- const isMessageType = m.type === 'msg' || !m.type;
- if (!isMessageType) {
- const tag = document.createElement('span');
- tag.style.color = 'var(--text-secondary)';
- tag.style.fontStyle = 'italic';
- tag.style.marginRight = '4px';
- tag.textContent = `[${m.type}]`;
- row.insertBefore(tag, row.firstChild);
- }
-
fragment.appendChild(row);
}
list.appendChild(fragment);
diff --git a/src/styles.css b/src/styles.css
index 38885bf..ef11f15 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -459,17 +459,36 @@ select:focus {
}
.queue-progress-wrap {
- height: 4px;
+ height: 5px;
border-radius: 999px;
overflow: hidden;
- background: rgba(255,255,255,0.12);
+ background: rgba(255,255,255,0.10);
+ position: relative;
}
.queue-progress-bar {
height: 100%;
width: 0;
- background: var(--accent);
- transition: width 0.2s ease;
+ background: linear-gradient(90deg, var(--accent) 0%, #b97aff 100%);
+ transition: width 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+/* Moving shimmer overlay — implies "active" without needing a different
+ border or pulse rule. Only visible because the bar is purple. */
+.queue-progress-bar::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.35) 50%, transparent 100%);
+ transform: translateX(-100%);
+ animation: queue-progress-shimmer 1.8s ease-in-out infinite;
+}
+
+@keyframes queue-progress-shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
}
.queue-progress-bar.indeterminate {
@@ -2315,6 +2334,106 @@ body.theme-light .modal {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 70, 70, 0.25);
}
+/* ============================================
+ STREAMER SECTION COUNTER
+ ============================================
+ Tiny "X · Y live" line next to the "Streamer" section heading.
+ Updated by renderStreamers on every redraw. */
+.streamer-section-counter {
+ font-size: 11px;
+ color: var(--text-secondary);
+ font-weight: 400;
+ letter-spacing: 0.2px;
+}
+
+.streamer-section-counter-divider {
+ opacity: 0.5;
+ margin: 0 1px;
+}
+
+.streamer-section-counter-live {
+ color: #e91916;
+ font-weight: 600;
+}
+
+/* ============================================
+ VOD DURATION BADGE — Twitch-style pill on the thumbnail
+ ============================================ */
+.vod-duration-badge {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ background: rgba(0, 0, 0, 0.78);
+ color: #fff;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 3px 7px;
+ border-radius: 3px;
+ z-index: 1;
+ letter-spacing: 0.3px;
+ backdrop-filter: blur(2px);
+ pointer-events: none;
+}
+
+.vod-card:hover .vod-duration-badge {
+ background: rgba(0, 0, 0, 0.88);
+}
+
+.vod-card.preview-active .vod-duration-badge {
+ opacity: 0;
+ transition: opacity 0.2s;
+}
+
+/* ============================================
+ CHAT VIEWER — Twitch-chat-like message rows
+ ============================================
+ Replaces the inline-style chat row inside the chat-viewer modal
+ with proper class-based styling. Renderer still uses inline
+ per-message colour for the username (driven by Twitch's IRC color
+ metadata). */
+.chat-viewer-row {
+ padding: 4px 8px;
+ line-height: 1.55;
+ border-radius: 4px;
+ transition: background 0.12s;
+ font-size: 13px;
+ word-wrap: break-word;
+}
+
+.chat-viewer-row:hover {
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.chat-viewer-row .chat-viewer-time {
+ color: var(--text-secondary);
+ margin-right: 8px;
+ font-size: 10px;
+ opacity: 0.7;
+ font-family: 'Segoe UI Mono', 'Consolas', monospace;
+}
+
+.chat-viewer-row .chat-viewer-user {
+ font-weight: 700;
+ margin-right: 4px;
+}
+
+.chat-viewer-row .chat-viewer-tag {
+ color: var(--accent);
+ font-style: italic;
+ font-size: 11px;
+ margin-right: 6px;
+ background: rgba(145, 70, 255, 0.12);
+ padding: 1px 6px;
+ border-radius: 3px;
+ border: 1px solid rgba(145, 70, 255, 0.3);
+}
+
+.chat-viewer-row.is-system {
+ background: rgba(145, 70, 255, 0.05);
+ border-left: 2px solid rgba(145, 70, 255, 0.45);
+ padding-left: 10px;
+}
+
/* ============================================
STREAMER PROFILE HEADER
============================================