Compare commits
2 Commits
8edbef0a60
...
f6333bf6f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6333bf6f5 | ||
|
|
f7a54a2007 |
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.24",
|
"version": "4.6.25",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.24",
|
"version": "4.6.25",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.24",
|
"version": "4.6.25",
|
||||||
"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",
|
||||||
|
|||||||
@ -240,7 +240,10 @@
|
|||||||
</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;">
|
||||||
|
|||||||
@ -257,6 +257,7 @@ 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">
|
||||||
@ -425,6 +426,20 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@ -464,44 +464,41 @@ 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.style.padding = '2px 0';
|
row.className = 'chat-viewer-row' + (!isMessageType ? ' is-system' : '');
|
||||||
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.style.color = 'var(--text-secondary)';
|
tSpan.className = 'chat-viewer-time';
|
||||||
tSpan.style.marginRight = '6px';
|
tSpan.textContent = time;
|
||||||
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.style.fontWeight = '600';
|
uSpan.className = 'chat-viewer-user';
|
||||||
uSpan.style.color = m.color || 'var(--accent)';
|
// Per-user IRC color is preserved; the class supplies weight.
|
||||||
uSpan.style.marginRight = '4px';
|
if (m.color) uSpan.style.color = m.color;
|
||||||
|
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);
|
||||||
|
|||||||
127
src/styles.css
127
src/styles.css
@ -459,17 +459,36 @@ select:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.queue-progress-wrap {
|
.queue-progress-wrap {
|
||||||
height: 4px;
|
height: 5px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(255,255,255,0.12);
|
background: rgba(255,255,255,0.10);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-progress-bar {
|
.queue-progress-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0;
|
width: 0;
|
||||||
background: var(--accent);
|
background: linear-gradient(90deg, var(--accent) 0%, #b97aff 100%);
|
||||||
transition: width 0.2s ease;
|
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 {
|
.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);
|
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
|
||||||
============================================
|
============================================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user