Compare commits

..

No commits in common. "f6333bf6f52823143ada0f4ff8baace9e3c28648" and "8edbef0a60bcd9713448e018cb755883abfbfe27" have entirely different histories.

6 changed files with 28 additions and 162 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.25", "version": "4.6.24",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.25", "version": "4.6.24",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.25", "version": "4.6.24",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -240,10 +240,7 @@
</nav> </nav>
<div class="section-title" id="streamerSectionTitle" style="display:flex; align-items:center; gap:6px; justify-content:space-between;"> <div class="section-title" id="streamerSectionTitle" style="display:flex; align-items:center; gap:6px; justify-content:space-between;">
<span style="display:flex; align-items:baseline; gap:8px;">
<span id="streamerSectionTitleText">Streamer</span> <span id="streamerSectionTitleText">Streamer</span>
<span id="streamerSectionCounter" class="streamer-section-counter"></span>
</span>
<button id="btnStreamerBulkRemove" class="btn-close" type="button" onclick="bulkRemoveStreamers()" title="Bulk remove" style="display:none;">x</button> <button id="btnStreamerBulkRemove" class="btn-close" type="button" onclick="bulkRemoveStreamers()" title="Bulk remove" style="display:none;">x</button>
</div> </div>
<input type="text" id="streamerListFilter" placeholder="Filter..." oninput="onStreamerListFilterChange()" style="display:none; width:calc(100% - 16px); margin:0 8px 8px; background:var(--bg-card); border:1px solid var(--border-soft); border-radius:4px; padding:4px 8px; color:var(--text); font-size:12px;"> <input type="text" id="streamerListFilter" placeholder="Filter..." oninput="onStreamerListFilterChange()" style="display:none; width:calc(100% - 16px); margin:0 8px 8px; background:var(--bg-card); border:1px solid var(--border-soft); border-radius:4px; padding:4px 8px; color:var(--text); font-size:12px;">

View File

@ -257,7 +257,6 @@ function buildVodCardHtml(vod: VOD, streamer: string, downloadedIds?: Set<string
<input type="checkbox" class="vod-select-checkbox" data-vod-url="${safeUrlAttr}" ${isChecked ? 'checked' : ''} title="${escapeHtml(UI_TEXT.vods.bulkSelectedCount.replace('{count}', '0').replace(/[0-9]/g, '').trim() || 'Select')}" style="position:absolute; top:8px; left:8px; width:18px; height:18px; accent-color:#9146FF; cursor:pointer; z-index:2;"> <input type="checkbox" class="vod-select-checkbox" data-vod-url="${safeUrlAttr}" ${isChecked ? 'checked' : ''} title="${escapeHtml(UI_TEXT.vods.bulkSelectedCount.replace('{count}', '0').replace(/[0-9]/g, '').trim() || 'Select')}" style="position:absolute; top:8px; left:8px; width:18px; height:18px; accent-color:#9146FF; cursor:pointer; z-index:2;">
${downloadedBadge} ${downloadedBadge}
<img class="vod-thumbnail" loading="lazy" decoding="async" src="${thumb}" alt="" title="${escapeHtml(UI_TEXT.vods.openOnTwitch)}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 180%22><rect fill=%22%23333%22 width=%22320%22 height=%22180%22/></svg>'"> <img class="vod-thumbnail" loading="lazy" decoding="async" src="${thumb}" alt="" title="${escapeHtml(UI_TEXT.vods.openOnTwitch)}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 180%22><rect fill=%22%23333%22 width=%22320%22 height=%22180%22/></svg>'">
<div class="vod-duration-badge">${escapeHtml(vod.duration)}</div>
<div class="vod-info"> <div class="vod-info">
<div class="vod-title" title="${escapeHtml(vod.title || '')}">${safeDisplayTitle}</div> <div class="vod-title" title="${escapeHtml(vod.title || '')}">${safeDisplayTitle}</div>
<div class="vod-meta"> <div class="vod-meta">
@ -426,20 +425,6 @@ function renderStreamers(): void {
// Compact title margin when filter is shown — avoids double gap. // Compact title margin when filter is shown — avoids double gap.
if (sectionTitle) sectionTitle.style.marginBottom = showFilter ? '4px' : ''; 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} <span class="streamer-section-counter-divider">·</span> <span class="streamer-section-counter-live">${liveCount} live</span>`;
} else {
counter.textContent = String(all.length);
}
}
const q = (streamerListFilterQuery || '').trim().toLowerCase(); const q = (streamerListFilterQuery || '').trim().toLowerCase();
const visible = q ? all.filter((s) => s.toLowerCase().includes(q)) : all; const visible = q ? all.filter((s) => s.toLowerCase().includes(q)) : all;

View File

@ -464,41 +464,44 @@ function renderChatViewerList(messages: ChatViewerMessage[]): void {
const end = Math.min(idx + CHUNK, messages.length); const end = Math.min(idx + CHUNK, messages.length);
for (let i = idx; i < end; i++) { for (let i = idx; i < end; i++) {
const m = messages[i]; const m = messages[i];
const isMessageType = m.type === 'msg' || !m.type;
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'chat-viewer-row' + (!isMessageType ? ' is-system' : ''); row.style.padding = '2px 0';
row.style.lineHeight = '1.5';
// 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); const time = formatChatTimeMarker(m);
if (time) { if (time) {
const tSpan = document.createElement('span'); const tSpan = document.createElement('span');
tSpan.className = 'chat-viewer-time'; tSpan.style.color = 'var(--text-secondary)';
tSpan.textContent = time; tSpan.style.marginRight = '6px';
tSpan.textContent = `[${time}]`;
row.appendChild(tSpan); row.appendChild(tSpan);
} }
const user = m.u || m.user || m.login || ''; const user = m.u || m.user || m.login || '';
if (user) { if (user) {
const uSpan = document.createElement('span'); const uSpan = document.createElement('span');
uSpan.className = 'chat-viewer-user'; uSpan.style.fontWeight = '600';
// Per-user IRC color is preserved; the class supplies weight. uSpan.style.color = m.color || 'var(--accent)';
if (m.color) uSpan.style.color = m.color; uSpan.style.marginRight = '4px';
else uSpan.style.color = 'var(--accent)';
uSpan.textContent = `${user}:`; uSpan.textContent = `${user}:`;
row.appendChild(uSpan); row.appendChild(uSpan);
} }
const msgSpan = document.createElement('span'); const msgSpan = document.createElement('span');
msgSpan.textContent = ' ' + (m.msg || m.text || ''); msgSpan.textContent = m.msg || m.text || '';
row.appendChild(msgSpan); 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); fragment.appendChild(row);
} }
list.appendChild(fragment); list.appendChild(fragment);

View File

@ -459,36 +459,17 @@ select:focus {
} }
.queue-progress-wrap { .queue-progress-wrap {
height: 5px; height: 4px;
border-radius: 999px; border-radius: 999px;
overflow: hidden; overflow: hidden;
background: rgba(255,255,255,0.10); background: rgba(255,255,255,0.12);
position: relative;
} }
.queue-progress-bar { .queue-progress-bar {
height: 100%; height: 100%;
width: 0; width: 0;
background: linear-gradient(90deg, var(--accent) 0%, #b97aff 100%); background: var(--accent);
transition: width 0.3s ease; transition: width 0.2s 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 { .queue-progress-bar.indeterminate {
@ -2334,106 +2315,6 @@ 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); 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 STREAMER PROFILE HEADER
============================================ ============================================