diff --git a/src/index.html b/src/index.html index 07678e9..1f43581 100644 --- a/src/index.html +++ b/src/index.html @@ -240,7 +240,10 @@
- Streamer + + Streamer + +
diff --git a/src/renderer-streamers.ts b/src/renderer-streamers.ts index 5f82d2f..f61d491 100644 --- a/src/renderer-streamers.ts +++ b/src/renderer-streamers.ts @@ -257,6 +257,7 @@ function buildVodCardHtml(vod: VOD, streamer: string, downloadedIds?: Set ${downloadedBadge} +
${escapeHtml(vod.duration)}
${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 ============================================