1472 lines
54 KiB
HTML
1472 lines
54 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MeTube — 下载管理</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: oklch(98% 0.005 250);
|
|
--surface: oklch(100% 0 0);
|
|
--fg: oklch(22% 0.02 240);
|
|
--muted: oklch(50% 0.018 240);
|
|
--border: oklch(90% 0.008 240);
|
|
--accent: oklch(58% 0.16 145);
|
|
|
|
--accent-strong: oklch(48% 0.14 145);
|
|
--accent-tint: oklch(95% 0.03 145);
|
|
--success: oklch(58% 0.16 145);
|
|
--warn: oklch(70% 0.15 75);
|
|
--danger: oklch(55% 0.19 25);
|
|
--danger-tint: oklch(95% 0.025 25);
|
|
|
|
--font-display: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
|
--font-body: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
|
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
|
|
|
|
--radius: 8px;
|
|
--sidebar-w: 236px;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; }
|
|
body {
|
|
font-family: var(--font-body);
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-size: 14px;
|
|
line-height: 1.55;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
button { font-family: inherit; cursor: pointer; }
|
|
input, select { font-family: inherit; }
|
|
:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
border-radius: 4px;
|
|
}
|
|
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
|
|
|
/* ============ App frame ============ */
|
|
.app {
|
|
display: grid;
|
|
grid-template-columns: var(--sidebar-w) 1fr;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ============ Sidebar ============ */
|
|
.sidebar {
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
}
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 16px 18px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.brand-mark {
|
|
width: 28px; height: 28px;
|
|
border-radius: 7px;
|
|
background: var(--fg);
|
|
color: var(--surface);
|
|
display: grid; place-items: center;
|
|
flex: none;
|
|
}
|
|
.brand-name {
|
|
font-family: var(--font-display);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.brand-ver {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-left: auto;
|
|
}
|
|
.nav-section { padding: 14px 10px 4px; }
|
|
.nav-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
color: var(--muted);
|
|
padding: 0 8px 6px;
|
|
}
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
width: 100%;
|
|
padding: 7px 8px;
|
|
border: none;
|
|
background: none;
|
|
border-radius: 6px;
|
|
font-size: 13.5px;
|
|
color: var(--fg);
|
|
text-align: left;
|
|
position: relative;
|
|
transition: background 0.12s ease;
|
|
}
|
|
.nav-item:hover { background: var(--bg); }
|
|
.nav-item.active { background: var(--accent-tint); font-weight: 550; }
|
|
.nav-item.active::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: -10px;
|
|
top: 6px; bottom: 6px;
|
|
width: 3px;
|
|
border-radius: 0 2px 2px 0;
|
|
background: var(--accent);
|
|
}
|
|
.nav-item svg { flex: none; color: var(--muted); }
|
|
.nav-item.active svg { color: var(--accent-strong); }
|
|
.nav-count {
|
|
margin-left: auto;
|
|
font-family: var(--font-mono);
|
|
font-size: 11.5px;
|
|
color: var(--muted);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 20px;
|
|
padding: 0 7px;
|
|
line-height: 18px;
|
|
}
|
|
.nav-item.active .nav-count {
|
|
background: var(--surface);
|
|
border-color: oklch(85% 0.05 145);
|
|
color: var(--accent-strong);
|
|
}
|
|
.nav-item .dot {
|
|
width: 7px; height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
flex: none;
|
|
animation: pulse 1.6s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse { 50% { opacity: 0.35; } }
|
|
|
|
.sidebar-foot {
|
|
margin-top: auto;
|
|
border-top: 1px solid var(--border);
|
|
padding: 10px;
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
.ghost-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 7px 10px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 6px;
|
|
font-size: 12.5px;
|
|
color: var(--fg);
|
|
transition: background 0.12s ease, border-color 0.12s ease;
|
|
}
|
|
.ghost-btn:hover { background: var(--bg); border-color: oklch(82% 0.01 240); }
|
|
.ghost-btn svg { color: var(--muted); }
|
|
|
|
/* ============ Main ============ */
|
|
.main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* Command bar */
|
|
.commandbar {
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 14px 22px 12px;
|
|
}
|
|
.cb-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: stretch;
|
|
}
|
|
.url-wrap {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--bg);
|
|
padding: 0 12px;
|
|
transition: border-color 0.12s ease, background 0.12s ease;
|
|
}
|
|
.url-wrap:focus-within {
|
|
border-color: var(--accent);
|
|
background: var(--surface);
|
|
}
|
|
.url-wrap svg { color: var(--muted); flex: none; }
|
|
.url-input {
|
|
flex: 1;
|
|
border: none;
|
|
background: none;
|
|
height: 40px;
|
|
font-size: 14px;
|
|
color: var(--fg);
|
|
outline: none;
|
|
font-family: var(--font-mono);
|
|
}
|
|
.url-input::placeholder { color: var(--muted); font-family: var(--font-body); }
|
|
.cb-chips {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-top: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
.chip {
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 20px;
|
|
padding: 4px 12px;
|
|
font-size: 12.5px;
|
|
color: var(--fg);
|
|
transition: background 0.12s ease, border-color 0.12s ease;
|
|
}
|
|
.chip:hover { background: var(--bg); }
|
|
.chip.on {
|
|
background: var(--fg);
|
|
border-color: var(--fg);
|
|
color: var(--surface);
|
|
font-weight: 550;
|
|
}
|
|
.chip-sep {
|
|
width: 1px;
|
|
height: 18px;
|
|
background: var(--border);
|
|
margin: 0 4px;
|
|
}
|
|
.cb-label { font-size: 12px; color: var(--muted); margin-right: 2px; }
|
|
.folder-pick {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 20px;
|
|
padding: 4px 12px;
|
|
font-size: 12.5px;
|
|
color: var(--fg);
|
|
}
|
|
.folder-pick:hover { background: var(--bg); }
|
|
.folder-pick svg { color: var(--muted); }
|
|
.advanced-link {
|
|
margin-left: auto;
|
|
border: none;
|
|
background: none;
|
|
font-size: 12.5px;
|
|
color: var(--muted);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 4px 6px;
|
|
border-radius: 6px;
|
|
}
|
|
.advanced-link:hover { background: var(--bg); color: var(--fg); }
|
|
|
|
.primary-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
background: var(--accent);
|
|
color: oklch(99% 0.01 145);
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
padding: 0 18px;
|
|
height: 40px;
|
|
font-size: 14px;
|
|
font-weight: 590;
|
|
letter-spacing: 0.02em;
|
|
transition: background 0.12s ease, transform 0.06s ease;
|
|
white-space: nowrap;
|
|
}
|
|
.primary-btn:hover { background: var(--accent-strong); }
|
|
.primary-btn:active { transform: translateY(1px); }
|
|
|
|
/* Stats strip */
|
|
.stats {
|
|
display: flex;
|
|
gap: 0;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.stat {
|
|
padding: 8px 22px;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 7px;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
.stat strong {
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--fg);
|
|
}
|
|
.stat .live { color: var(--accent-strong); font-weight: 500; }
|
|
|
|
/* Scrollable content */
|
|
.content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 18px 22px 90px;
|
|
}
|
|
|
|
/* Active downloads */
|
|
.active-panel { margin-bottom: 22px; }
|
|
.section-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.section-title {
|
|
font-family: var(--font-display);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
line-height: 1.35;
|
|
}
|
|
.section-meta { font-size: 12px; color: var(--muted); font-family: var(--font-mono); }
|
|
.active-list {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
.active-row {
|
|
display: grid;
|
|
grid-template-columns: 120px 1fr auto;
|
|
gap: 14px;
|
|
align-items: center;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 10px 14px 10px 10px;
|
|
}
|
|
.thumb {
|
|
border-radius: 5px;
|
|
overflow: hidden;
|
|
background: oklch(94% 0.01 240);
|
|
position: relative;
|
|
flex: none;
|
|
}
|
|
.thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
.thumb .dur {
|
|
position: absolute;
|
|
right: 4px; bottom: 4px;
|
|
background: oklch(20% 0.02 240 / 0.82);
|
|
color: oklch(96% 0.01 240);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
}
|
|
.active-title {
|
|
font-size: 13.5px;
|
|
font-weight: 550;
|
|
line-height: 1.4;
|
|
margin-bottom: 6px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 1;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.progress-track {
|
|
height: 5px;
|
|
border-radius: 3px;
|
|
background: oklch(93% 0.008 240);
|
|
overflow: hidden;
|
|
margin-bottom: 6px;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
background: var(--accent);
|
|
transition: width 0.6s ease;
|
|
}
|
|
.active-stats {
|
|
display: flex;
|
|
gap: 14px;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
.active-stats .num { color: var(--fg); }
|
|
.icon-btn {
|
|
width: 32px; height: 32px;
|
|
display: grid; place-items: center;
|
|
border: 1px solid transparent;
|
|
background: none;
|
|
border-radius: 6px;
|
|
color: var(--muted);
|
|
transition: background 0.12s ease, color 0.12s ease;
|
|
}
|
|
.icon-btn:hover { background: var(--danger-tint); color: var(--danger); }
|
|
.icon-btn.neutral:hover { background: var(--bg); color: var(--fg); }
|
|
|
|
/* List toolbar */
|
|
.list-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.filter-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 6px;
|
|
padding: 0 10px;
|
|
height: 34px;
|
|
width: 240px;
|
|
transition: border-color 0.12s ease;
|
|
}
|
|
.filter-wrap:focus-within { border-color: var(--accent); }
|
|
.filter-wrap svg { color: var(--muted); flex: none; }
|
|
.filter-wrap input {
|
|
border: none; background: none; outline: none;
|
|
font-size: 13px; color: var(--fg);
|
|
width: 100%;
|
|
}
|
|
.filter-wrap input::placeholder { color: var(--muted); }
|
|
.tool-select {
|
|
height: 34px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 6px;
|
|
font-size: 12.5px;
|
|
color: var(--fg);
|
|
padding: 0 8px;
|
|
}
|
|
.view-toggle {
|
|
display: flex;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
background: var(--surface);
|
|
}
|
|
.view-toggle button {
|
|
width: 34px; height: 32px;
|
|
border: none;
|
|
background: none;
|
|
display: grid; place-items: center;
|
|
color: var(--muted);
|
|
transition: background 0.12s ease, color 0.12s ease;
|
|
}
|
|
.view-toggle button + button { border-left: 1px solid var(--border); }
|
|
.view-toggle button.on { background: var(--fg); color: var(--surface); }
|
|
.view-toggle button:not(.on):hover { background: var(--bg); color: var(--fg); }
|
|
.result-count {
|
|
margin-left: auto;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Queue list rows */
|
|
.queue-list {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
}
|
|
.q-row {
|
|
display: grid;
|
|
grid-template-columns: 30px 88px 1fr auto auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
padding: 8px 14px 8px 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
transition: background 0.1s ease;
|
|
}
|
|
.q-row:last-child { border-bottom: none; }
|
|
.q-row:hover { background: var(--bg); }
|
|
.q-row.selected { background: var(--accent-tint); }
|
|
.q-check {
|
|
width: 17px; height: 17px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
margin-left: 6px;
|
|
}
|
|
.q-title {
|
|
font-size: 13.5px;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 1;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.q-meta {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
margin-top: 3px;
|
|
font-size: 11.5px;
|
|
color: var(--muted);
|
|
}
|
|
.badge {
|
|
font-size: 10.5px;
|
|
font-weight: 550;
|
|
padding: 1px 7px;
|
|
border-radius: 20px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg);
|
|
color: var(--muted);
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.badge.folder { color: var(--fg); background: var(--surface); }
|
|
.badge.fmt { font-family: var(--font-mono); font-weight: 500; }
|
|
.q-side {
|
|
text-align: right;
|
|
font-size: 11.5px;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
.q-side .num { color: var(--fg); font-size: 12px; }
|
|
.q-actions { display: flex; gap: 2px; opacity: 0; transition: opacity 0.12s ease; }
|
|
.q-row:hover .q-actions, .q-row.selected .q-actions { opacity: 1; }
|
|
|
|
/* Completed grid */
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(218px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
|
position: relative;
|
|
}
|
|
.card:hover {
|
|
border-color: oklch(80% 0.012 240);
|
|
box-shadow: 0 4px 14px oklch(30% 0.02 240 / 0.08);
|
|
}
|
|
.card .thumb { border-radius: 0; aspect-ratio: 16/9; }
|
|
.card-body { padding: 10px 12px 12px; }
|
|
.card-title {
|
|
font-size: 13px;
|
|
font-weight: 550;
|
|
line-height: 1.45;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
min-height: 38px;
|
|
}
|
|
.card-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
margin-top: 8px;
|
|
font-size: 11.5px;
|
|
color: var(--muted);
|
|
}
|
|
.card-meta .num { color: var(--fg); }
|
|
.card-actions {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 2px;
|
|
}
|
|
.card .icon-btn { width: 28px; height: 28px; }
|
|
.card-check {
|
|
position: absolute;
|
|
top: 8px; left: 8px;
|
|
z-index: 2;
|
|
width: 17px; height: 17px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: opacity 0.12s ease;
|
|
}
|
|
.card:hover .card-check, .card-check:checked { opacity: 1; }
|
|
.card.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
|
|
/* Empty state */
|
|
.empty {
|
|
text-align: center;
|
|
padding: 56px 20px;
|
|
border: 1px dashed var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
}
|
|
.empty svg { color: var(--muted); margin-bottom: 12px; }
|
|
.empty-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
.empty-sub { font-size: 12.5px; color: var(--muted); }
|
|
|
|
/* Batch bar */
|
|
.batchbar {
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: 18px;
|
|
transform: translate(-50%, 76px);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: var(--fg);
|
|
color: oklch(96% 0.008 240);
|
|
border-radius: 10px;
|
|
padding: 8px 8px 8px 16px;
|
|
box-shadow: 0 10px 30px oklch(20% 0.02 240 / 0.3);
|
|
transition: transform 0.2s cubic-bezier(0.2, 0.9, 0.3, 1.2);
|
|
z-index: 30;
|
|
}
|
|
.batchbar.show { transform: translate(-50%, 0); }
|
|
.batch-count {
|
|
font-size: 13px;
|
|
font-weight: 550;
|
|
margin-right: 8px;
|
|
white-space: nowrap;
|
|
}
|
|
.batch-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
border: none;
|
|
background: oklch(100% 0 0 / 0.1);
|
|
color: oklch(96% 0.008 240);
|
|
border-radius: 6px;
|
|
padding: 7px 12px;
|
|
font-size: 12.5px;
|
|
font-weight: 500;
|
|
transition: background 0.12s ease;
|
|
white-space: nowrap;
|
|
}
|
|
.batch-btn:hover { background: oklch(100% 0 0 / 0.2); }
|
|
.batch-btn.danger:hover { background: var(--danger); }
|
|
|
|
/* Slide-over */
|
|
.scrim {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: oklch(25% 0.02 240 / 0.35);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.18s ease;
|
|
z-index: 40;
|
|
}
|
|
.scrim.show { opacity: 1; pointer-events: auto; }
|
|
.slideover {
|
|
position: fixed;
|
|
top: 0; right: 0; bottom: 0;
|
|
width: 340px;
|
|
background: var(--surface);
|
|
border-left: 1px solid var(--border);
|
|
box-shadow: -12px 0 40px oklch(30% 0.02 240 / 0.15);
|
|
transform: translateX(100%);
|
|
transition: transform 0.22s ease;
|
|
z-index: 50;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.slideover.show { transform: translateX(0); }
|
|
.so-head {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 16px 18px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.so-title { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
|
|
.so-body { padding: 18px; overflow-y: auto; flex: 1; }
|
|
.so-group { margin-bottom: 24px; }
|
|
.so-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
color: var(--muted);
|
|
margin-bottom: 10px;
|
|
}
|
|
.so-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 9px 0;
|
|
font-size: 13.5px;
|
|
}
|
|
.so-row .sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
|
|
.switch {
|
|
width: 36px; height: 21px;
|
|
border-radius: 20px;
|
|
border: none;
|
|
background: oklch(85% 0.01 240);
|
|
position: relative;
|
|
transition: background 0.15s ease;
|
|
flex: none;
|
|
}
|
|
.switch::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2.5px; left: 3px;
|
|
width: 16px; height: 16px;
|
|
border-radius: 50%;
|
|
background: var(--surface);
|
|
box-shadow: 0 1px 3px oklch(20% 0.02 240 / 0.3);
|
|
transition: transform 0.15s ease;
|
|
}
|
|
.switch.on { background: var(--accent); }
|
|
.switch.on::after { transform: translateX(14px); }
|
|
.so-input {
|
|
width: 72px;
|
|
height: 30px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0 8px;
|
|
font-size: 13px;
|
|
font-family: var(--font-mono);
|
|
color: var(--fg);
|
|
text-align: right;
|
|
}
|
|
.so-btn {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
color: var(--fg);
|
|
text-align: left;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
transition: background 0.12s ease;
|
|
}
|
|
.so-btn:hover { background: var(--bg); }
|
|
.so-btn svg { color: var(--muted); }
|
|
.cookie-saved {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
font-size: 12.5px;
|
|
padding: 7px 10px;
|
|
background: var(--accent-tint);
|
|
border-radius: 6px;
|
|
color: var(--accent-strong);
|
|
margin-bottom: 8px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Toast */
|
|
.toasts {
|
|
position: fixed;
|
|
right: 20px; bottom: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
z-index: 60;
|
|
}
|
|
.toast {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: 8px;
|
|
padding: 11px 14px;
|
|
font-size: 13px;
|
|
box-shadow: 0 8px 24px oklch(30% 0.02 240 / 0.14);
|
|
min-width: 260px;
|
|
animation: toast-in 0.25s ease;
|
|
}
|
|
.toast.err { border-left-color: var(--danger); }
|
|
.toast svg { flex: none; }
|
|
.toast .t-act {
|
|
margin-left: auto;
|
|
border: none;
|
|
background: none;
|
|
color: var(--accent-strong);
|
|
font-size: 12.5px;
|
|
font-weight: 550;
|
|
padding: 2px 4px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
.toast .t-act:hover { background: var(--accent-tint); }
|
|
@keyframes toast-in {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 900px) {
|
|
.app { grid-template-columns: 1fr; }
|
|
.sidebar {
|
|
position: fixed;
|
|
z-index: 45;
|
|
width: 260px;
|
|
height: 100%;
|
|
transform: translateX(-100%);
|
|
transition: transform 0.2s ease;
|
|
box-shadow: 12px 0 40px oklch(30% 0.02 240 / 0.15);
|
|
}
|
|
.sidebar.open { transform: translateX(0); }
|
|
.menu-btn { display: grid !important; }
|
|
.cb-row { flex-wrap: wrap; }
|
|
.url-wrap { width: 100%; flex: none; }
|
|
.active-row { grid-template-columns: 96px 1fr; }
|
|
.active-row .row-actions { grid-column: 2; justify-self: end; }
|
|
.q-row { grid-template-columns: 30px 64px 1fr auto; }
|
|
.q-side { display: none; }
|
|
.stats { flex-wrap: wrap; }
|
|
.filter-wrap { width: 100%; }
|
|
.list-toolbar { flex-wrap: wrap; }
|
|
.result-count { margin-left: 0; width: 100%; }
|
|
}
|
|
.menu-btn {
|
|
display: none;
|
|
place-items: center;
|
|
width: 40px; height: 40px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
border-radius: var(--radius);
|
|
color: var(--fg);
|
|
flex: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="app" data-od-id="app">
|
|
|
|
<!-- ============ Sidebar ============ -->
|
|
<aside class="sidebar" id="sidebar" data-od-id="sidebar">
|
|
<div class="brand">
|
|
<span class="brand-mark">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" fill="currentColor" stroke="none"/></svg>
|
|
</span>
|
|
<span class="brand-name">MeTube</span>
|
|
<span class="brand-ver num">v1.13.2</span>
|
|
</div>
|
|
|
|
<div class="nav-section" data-od-id="nav-status">
|
|
<div class="nav-label">状态</div>
|
|
<button class="nav-item active" data-status="downloading">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
正在下载
|
|
<span class="nav-count num">2</span>
|
|
</button>
|
|
<button class="nav-item" data-status="queued">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15.5 14"/></svg>
|
|
排队中
|
|
<span class="nav-count num">230</span>
|
|
</button>
|
|
<button class="nav-item" data-status="completed">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
已完成
|
|
<span class="nav-count num">303</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="nav-section" data-od-id="nav-folders">
|
|
<div class="nav-label">文件夹</div>
|
|
<button class="nav-item active" data-folder="all">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
|
全部
|
|
<span class="nav-count num">535</span>
|
|
</button>
|
|
<button class="nav-item" data-folder="movies">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
movies
|
|
<span class="nav-count num">90</span>
|
|
</button>
|
|
<button class="nav-item" data-folder="shows">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
shows
|
|
<span class="nav-count num">86</span>
|
|
</button>
|
|
<button class="nav-item" data-folder="music">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
music
|
|
<span class="nav-count num">54</span>
|
|
</button>
|
|
<button class="nav-item" data-folder="uncategorized">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
未分类
|
|
<span class="nav-count num">305</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="sidebar-foot">
|
|
<button class="ghost-btn" id="openAdvanced">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
高级选项
|
|
</button>
|
|
<button class="ghost-btn" id="activityBtn" title="活动日志">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
日志
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ============ Main ============ -->
|
|
<main class="main">
|
|
|
|
<!-- Command bar -->
|
|
<div class="commandbar" data-od-id="command-bar">
|
|
<div class="cb-row">
|
|
<button class="menu-btn" id="menuBtn" aria-label="打开导航">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
</button>
|
|
<div class="url-wrap">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
<input class="url-input" id="urlInput" type="url" placeholder="粘贴视频或播放列表 URL…" aria-label="视频 URL">
|
|
</div>
|
|
<button class="primary-btn" id="addBtn" data-od-id="add-button">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
添加到队列
|
|
</button>
|
|
</div>
|
|
<div class="cb-chips">
|
|
<span class="cb-label">清晰度</span>
|
|
<button class="chip on" data-quality="best">最佳</button>
|
|
<button class="chip" data-quality="1080p">1080p</button>
|
|
<button class="chip" data-quality="720p">720p</button>
|
|
<button class="chip" data-quality="audio">仅音频</button>
|
|
<span class="chip-sep"></span>
|
|
<span class="cb-label">格式</span>
|
|
<button class="chip on" data-format="mp4">MP4</button>
|
|
<button class="chip" data-format="mkv">MKV</button>
|
|
<button class="chip" data-format="mp3">MP3</button>
|
|
<span class="chip-sep"></span>
|
|
<button class="folder-pick" id="folderPick">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
<span id="folderPickLabel">movies</span>
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</button>
|
|
<button class="advanced-link" id="openAdvanced2">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>
|
|
高级选项
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats strip -->
|
|
<div class="stats" data-od-id="stats-strip">
|
|
<div class="stat"><span class="live">● 下载中</span><strong id="statActive">2</strong></div>
|
|
<div class="stat">总速度 <strong id="statSpeed">12.4 MB/s</strong></div>
|
|
<div class="stat">队列 <strong>230</strong></div>
|
|
<div class="stat">已入库存储 <strong>412.6 GB</strong></div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="content" id="content">
|
|
|
|
<!-- Active downloads -->
|
|
<section class="active-panel" data-od-id="active-panel">
|
|
<div class="section-head">
|
|
<h2 class="section-title">正在下载</h2>
|
|
<span class="section-meta" id="activeMeta">12.4 MB/s · 预计 0:43 完成首个任务</span>
|
|
</div>
|
|
<div class="active-list" id="activeList"></div>
|
|
</section>
|
|
|
|
<!-- Library / queue -->
|
|
<section data-od-id="list-section">
|
|
<div class="section-head">
|
|
<h2 class="section-title" id="listTitle">排队中</h2>
|
|
<span class="section-meta" id="listSubtitle">movies · 90 项</span>
|
|
</div>
|
|
|
|
<div class="list-toolbar" data-od-id="list-toolbar">
|
|
<div class="filter-wrap">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
<input id="filterInput" type="search" placeholder="筛选标题…" aria-label="筛选标题">
|
|
</div>
|
|
<select class="tool-select" id="sortSelect" aria-label="排序方式">
|
|
<option value="date">按添加时间</option>
|
|
<option value="title">按标题</option>
|
|
<option value="size">按大小</option>
|
|
</select>
|
|
<div class="view-toggle" id="viewToggle" data-od-id="view-toggle">
|
|
<button data-view="list" class="on" aria-label="列表视图">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
|
</button>
|
|
<button data-view="grid" aria-label="网格视图">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
</button>
|
|
</div>
|
|
<button class="ghost-btn" id="selectAllBtn">全选</button>
|
|
<span class="result-count" id="resultCount">显示 90 / 90</span>
|
|
</div>
|
|
|
|
<div id="listContainer"></div>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- Batch bar -->
|
|
<div class="batchbar" id="batchbar" data-od-id="batch-bar">
|
|
<span class="batch-count" id="batchCount">已选 0 项</span>
|
|
<button class="batch-btn" data-batch="start">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6 3 20 12 6 21 6 3"/></svg>
|
|
开始
|
|
</button>
|
|
<button class="batch-btn" data-batch="move">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
移动到…
|
|
</button>
|
|
<button class="batch-btn danger" data-batch="delete">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
删除
|
|
</button>
|
|
<button class="batch-btn" data-batch="clear">取消选择</button>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Slide-over -->
|
|
<div class="scrim" id="scrim"></div>
|
|
<aside class="slideover" id="slideover" data-od-id="advanced-panel" aria-label="高级选项">
|
|
<div class="so-head">
|
|
<span class="so-title">高级选项</span>
|
|
<button class="icon-btn neutral" id="closeAdvanced" style="margin-left:auto" aria-label="关闭">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="so-body">
|
|
<div class="so-group">
|
|
<div class="so-label">下载行为</div>
|
|
<div class="so-row">
|
|
<div>自动开始<div class="sub">添加后立即开始下载</div></div>
|
|
<button class="switch on" role="switch" aria-checked="true" aria-label="自动开始"></button>
|
|
</div>
|
|
<div class="so-row">
|
|
<div>严格播放列表<div class="sub">仅下载完全匹配的播放列表项</div></div>
|
|
<button class="switch" role="switch" aria-checked="false" aria-label="严格播放列表"></button>
|
|
</div>
|
|
<div class="so-row">
|
|
<div>播放列表上限<div class="sub">单次最多展开的条目数</div></div>
|
|
<input class="so-input num" type="number" value="10" min="1" max="500">
|
|
</div>
|
|
<div class="so-row">
|
|
<div>文件名前缀<div class="sub">可选,如频道名</div></div>
|
|
<input class="so-input" style="width:120px;text-align:left;font-family:var(--font-body)" type="text" placeholder="无前缀">
|
|
</div>
|
|
</div>
|
|
<div class="so-group">
|
|
<div class="so-label">批量操作</div>
|
|
<button class="so-btn">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
|
导入 URL 列表
|
|
</button>
|
|
<button class="so-btn">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
导出全部 URL
|
|
</button>
|
|
<button class="so-btn">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
复制全部 URL
|
|
</button>
|
|
</div>
|
|
<div class="so-group">
|
|
<div class="so-label">Cookies</div>
|
|
<div class="cookie-saved">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
bilibili.com · 已保存
|
|
</div>
|
|
<button class="so-btn">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>
|
|
粘贴新的 Cookie…
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Toasts -->
|
|
<div class="toasts" id="toasts" aria-live="polite"></div>
|
|
|
|
<script>
|
|
/* ================= Sample data ================= */
|
|
const THUMBS = [
|
|
'assets/thumb-city.jpg', 'assets/thumb-coding.jpg', 'assets/thumb-cooking.jpg',
|
|
'assets/thumb-desk.jpg', 'assets/thumb-mountains.jpg', 'assets/thumb-ocean.jpg',
|
|
'assets/thumb-travel.jpg', 'assets/thumb-waveform.jpg'
|
|
];
|
|
|
|
const ACTIVE = [
|
|
{ id: 'a1', title: '【4K】东京城市漫游 — 雨后的新宿与涩谷街头实录', thumb: THUMBS[0], dur: '24:16', pct: 62, speed: '9.3 MB/s', eta: '0:43', size: '1.8 GB' },
|
|
{ id: 'a2', title: '深水地平线:北大西洋冬季航行的真实海况记录', thumb: THUMBS[5], dur: '41:02', pct: 18, speed: '3.1 MB/s', eta: '4:12', size: '3.4 GB' }
|
|
];
|
|
|
|
const QUEUED = [
|
|
{ id: 'q1', title: '银翼杀手 2049 — 摄影与灯光美学全解析', folder: 'movies', quality: '1080p', fmt: 'MP4', size: '2.4 GB', added: '2 天前', thumb: THUMBS[0] },
|
|
{ id: 'q2', title: '阿尔卑斯山 solo 徒步 7 日:装备清单与路线复盘', folder: 'movies', quality: '最佳', fmt: 'MKV', size: '5.1 GB', added: '2 天前', thumb: THUMBS[4] },
|
|
{ id: 'q3', title: '东京胶囊旅馆体验 — 一个人的关西旅行 EP03', folder: 'shows', quality: '1080p', fmt: 'MP4', size: '1.2 GB', added: '2 天前', thumb: THUMBS[6] },
|
|
{ id: 'q4', title: 'Rust 异步运行时原理:从 Future 到 Executor', folder: 'shows', quality: '720p', fmt: 'MP4', size: '860 MB', added: '3 天前', thumb: THUMBS[1] },
|
|
{ id: 'q5', title: '家庭厨房:意式手工意面从零开始(附配方)', folder: 'shows', quality: '1080p', fmt: 'MP4', size: '1.5 GB', added: '3 天前', thumb: THUMBS[2] },
|
|
{ id: 'q6', title: 'Lo-fi Hip Hop Radio — 专注学习 3 小时合辑', folder: 'music', quality: '仅音频', fmt: 'MP3', size: '168 MB', added: '3 天前', thumb: THUMBS[7] },
|
|
{ id: 'q7', title: '桌面搭建 2025:极简工作台与理线方案', folder: 'shows', quality: '最佳', fmt: 'MP4', size: '2.0 GB', added: '4 天前', thumb: THUMBS[3] },
|
|
{ id: 'q8', title: '冰岛环岛自驾:一号公路全段路况与营地指南', folder: 'movies', quality: '1080p', fmt: 'MKV', size: '4.6 GB', added: '4 天前', thumb: THUMBS[5] },
|
|
{ id: 'q9', title: 'City Pop 名盘 20 选 — 昭和都市之夜歌单', folder: 'music', quality: '仅音频', fmt: 'M4A', size: '142 MB', added: '5 天前', thumb: THUMBS[7] }
|
|
];
|
|
|
|
const COMPLETED = [
|
|
{ id: 'c1', title: '挪威峡湾邮轮记:盖朗厄尔到弗洛姆', folder: 'movies', quality: '1080p', fmt: 'MP4', size: '3.2 GB', added: '今天 09:12', thumb: THUMBS[5], dur: '38:44' },
|
|
{ id: 'c2', title: 'Vue 3 源码共读:响应式系统完整实现', folder: 'shows', quality: '720p', fmt: 'MP4', size: '1.1 GB', added: '昨天 22:03', thumb: THUMBS[1], dur: '52:10' },
|
|
{ id: 'c3', title: '深夜食堂复刻:玉子烧与味噌汤的家常做法', folder: 'shows', quality: '1080p', fmt: 'MP4', size: '980 MB', added: '昨天 20:47', thumb: THUMBS[2], dur: '18:26' },
|
|
{ id: 'c4', title: '摩洛哥撒哈拉沙漠团:三天两夜全记录', folder: 'movies', quality: '最佳', fmt: 'MKV', size: '4.8 GB', added: '2 天前', thumb: THUMBS[6], dur: '45:31' },
|
|
{ id: 'c5', title: 'Efficient Desk Setup — Cable Management Guide', folder: 'shows', quality: '1080p', fmt: 'MP4', size: '1.4 GB', added: '3 天前', thumb: THUMBS[3], dur: '12:08' },
|
|
{ id: 'c6', title: 'Ambient Focus Mix — Rainy Night Coding', folder: 'music', quality: '仅音频', fmt: 'MP3', size: '204 MB', added: '3 天前', thumb: THUMBS[7], dur: '1:02:44' },
|
|
{ id: 'c7', title: '尼泊尔 ABC 大本营徒步:高反预防与季节选择', folder: 'movies', quality: '1080p', fmt: 'MP4', size: '3.9 GB', added: '4 天前', thumb: THUMBS[4], dur: '33:17' },
|
|
{ id: 'c8', title: '纽约曼哈顿天际线延时摄影合集(4K HDR)', folder: 'movies', quality: '最佳', fmt: 'MKV', size: '6.2 GB', added: '5 天前', thumb: THUMBS[0], dur: '26:55' }
|
|
];
|
|
|
|
const FOLDER_NAMES = { all: '全部', movies: 'movies', shows: 'shows', music: 'music', uncategorized: '未分类' };
|
|
const STATUS_NAMES = { downloading: '正在下载', queued: '排队中', completed: '已完成' };
|
|
|
|
/* ================= State ================= */
|
|
const saved = JSON.parse(localStorage.getItem('metube-ui') || '{}');
|
|
const state = {
|
|
status: saved.status || 'queued',
|
|
folder: saved.folder || 'all',
|
|
view: saved.view || 'list',
|
|
filter: '',
|
|
sort: 'date',
|
|
selected: new Set()
|
|
};
|
|
function persist() {
|
|
localStorage.setItem('metube-ui', JSON.stringify({ status: state.status, folder: state.folder, view: state.view }));
|
|
}
|
|
|
|
/* ================= Toasts ================= */
|
|
const toastsEl = document.getElementById('toasts');
|
|
function toast(msg, opts = {}) {
|
|
const el = document.createElement('div');
|
|
el.className = 'toast' + (opts.error ? ' err' : '');
|
|
const icon = opts.error
|
|
? '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>'
|
|
: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--accent-strong)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
el.innerHTML = icon + '<span>' + msg + '</span>' + (opts.action ? '<button class="t-act">' + opts.action + '</button>' : '');
|
|
toastsEl.appendChild(el);
|
|
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity .3s'; setTimeout(() => el.remove(), 320); }, opts.error ? 6000 : 3600);
|
|
}
|
|
|
|
/* ================= Active downloads ================= */
|
|
const activeList = document.getElementById('activeList');
|
|
function renderActive() {
|
|
activeList.innerHTML = ACTIVE.map(d => `
|
|
<div class="active-row" data-od-id="active-row-${d.id}">
|
|
<div class="thumb" style="aspect-ratio:16/9"><img src="${d.thumb}" alt="${d.title}" loading="lazy"><span class="dur num">${d.dur}</span></div>
|
|
<div>
|
|
<div class="active-title">${d.title}</div>
|
|
<div class="progress-track"><div class="progress-fill" data-id="${d.id}" style="width:${d.pct}%"></div></div>
|
|
<div class="active-stats">
|
|
<span><span class="num" data-pct="${d.id}">${d.pct}</span>% · ${d.size}</span>
|
|
<span class="num">${d.speed}</span>
|
|
<span>ETA <span class="num">${d.eta}</span></span>
|
|
</div>
|
|
</div>
|
|
<div class="row-actions">
|
|
<button class="icon-btn" data-cancel="${d.id}" title="取消下载" aria-label="取消下载">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
renderActive();
|
|
|
|
/* Simulate live progress */
|
|
setInterval(() => {
|
|
ACTIVE.forEach(d => {
|
|
if (d.pct < 99) d.pct = Math.min(99, d.pct + (Math.random() * 1.4));
|
|
const fill = document.querySelector(`.progress-fill[data-id="${d.id}"]`);
|
|
const pct = document.querySelector(`[data-pct="${d.id}"]`);
|
|
if (fill) fill.style.width = d.pct + '%';
|
|
if (pct) pct.textContent = Math.floor(d.pct);
|
|
});
|
|
}, 1800);
|
|
|
|
activeList.addEventListener('click', e => {
|
|
const btn = e.target.closest('[data-cancel]');
|
|
if (!btn) return;
|
|
const item = ACTIVE.find(d => d.id === btn.dataset.cancel);
|
|
toast(`已取消「${item.title.slice(0, 22)}…」`, { error: true, action: '撤销' });
|
|
});
|
|
|
|
/* ================= List rendering ================= */
|
|
const listContainer = document.getElementById('listContainer');
|
|
const listTitle = document.getElementById('listTitle');
|
|
const listSubtitle = document.getElementById('listSubtitle');
|
|
const resultCount = document.getElementById('resultCount');
|
|
|
|
function currentItems() {
|
|
let items = state.status === 'completed' ? COMPLETED.slice()
|
|
: state.status === 'queued' ? QUEUED.slice() : [];
|
|
if (state.folder !== 'all') items = items.filter(i => i.folder === state.folder);
|
|
if (state.filter) items = items.filter(i => i.title.toLowerCase().includes(state.filter.toLowerCase()));
|
|
if (state.sort === 'title') items.sort((a, b) => a.title.localeCompare(b.title, 'zh'));
|
|
if (state.sort === 'size') items.sort((a, b) => parseFloat(b.size) - parseFloat(a.size));
|
|
return items;
|
|
}
|
|
|
|
function esc(s) { return s; }
|
|
|
|
function renderList() {
|
|
const items = currentItems();
|
|
const folderPart = state.folder === 'all' ? '全部文件夹' : FOLDER_NAMES[state.folder];
|
|
listTitle.textContent = STATUS_NAMES[state.status];
|
|
listSubtitle.textContent = folderPart + ' · ' + items.length + ' 项';
|
|
resultCount.textContent = `显示 ${items.length} / ${items.length}`;
|
|
|
|
if (state.status === 'downloading') {
|
|
listContainer.innerHTML = `
|
|
<div class="empty" data-od-id="empty-active">
|
|
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
<div class="empty-title">下载任务固定显示在上方</div>
|
|
<div class="empty-sub">进行中的 2 个任务已固定在顶部面板,这里不再重复列出</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
listContainer.innerHTML = `
|
|
<div class="empty">
|
|
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
<div class="empty-title">没有匹配的条目</div>
|
|
<div class="empty-sub">试试清除筛选条件,或切换到其他文件夹</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
if (state.view === 'grid' && state.status === 'completed') {
|
|
listContainer.innerHTML = `<div class="grid" data-od-id="completed-grid">` + items.map(i => `
|
|
<div class="card ${state.selected.has(i.id) ? 'selected' : ''}" data-od-id="card-${i.id}">
|
|
<input type="checkbox" class="card-check" data-select="${i.id}" ${state.selected.has(i.id) ? 'checked' : ''} aria-label="选择 ${i.title}">
|
|
<div class="thumb"><img src="${i.thumb}" alt="${i.title}" loading="lazy">${i.dur ? `<span class="dur num">${i.dur}</span>` : ''}</div>
|
|
<div class="card-body">
|
|
<div class="card-title">${i.title}</div>
|
|
<div class="card-meta">
|
|
<span class="badge folder">${i.folder}</span>
|
|
<span class="num">${i.size}</span>
|
|
<div class="card-actions">
|
|
<button class="icon-btn neutral" title="在文件夹中显示" aria-label="在文件夹中显示">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
</button>
|
|
<button class="icon-btn" title="删除" aria-label="删除" data-del="${i.id}">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`).join('') + '</div>';
|
|
return;
|
|
}
|
|
|
|
listContainer.innerHTML = `<div class="queue-list" data-od-id="queue-list">` + items.map(i => `
|
|
<div class="q-row ${state.selected.has(i.id) ? 'selected' : ''}" data-od-id="row-${i.id}">
|
|
<input type="checkbox" class="q-check" data-select="${i.id}" ${state.selected.has(i.id) ? 'checked' : ''} aria-label="选择 ${i.title}">
|
|
<div class="thumb" style="aspect-ratio:16/9"><img src="${i.thumb}" alt="${i.title}" loading="lazy"></div>
|
|
<div>
|
|
<div class="q-title">${i.title}</div>
|
|
<div class="q-meta">
|
|
<span class="badge folder">${i.folder}</span>
|
|
<span class="badge fmt">${i.quality} · ${i.fmt}</span>
|
|
<span>${i.added} 添加</span>
|
|
</div>
|
|
</div>
|
|
<div class="q-side"><span class="num">${i.size}</span></div>
|
|
<div class="q-actions">
|
|
${state.status === 'queued' ? `<button class="icon-btn neutral" title="立即开始" aria-label="立即开始" data-start="${i.id}">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6 3 20 12 6 21 6 3"/></svg>
|
|
</button>` : ''}
|
|
<button class="icon-btn" title="删除" aria-label="删除" data-del="${i.id}">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>`).join('') + '</div>';
|
|
}
|
|
|
|
/* ================= Batch bar ================= */
|
|
const batchbar = document.getElementById('batchbar');
|
|
const batchCount = document.getElementById('batchCount');
|
|
function updateBatchbar() {
|
|
const n = state.selected.size;
|
|
batchbar.classList.toggle('show', n > 0);
|
|
batchCount.textContent = `已选 ${n} 项`;
|
|
}
|
|
|
|
listContainer.addEventListener('change', e => {
|
|
const cb = e.target.closest('[data-select]');
|
|
if (!cb) return;
|
|
cb.checked ? state.selected.add(cb.dataset.select) : state.selected.delete(cb.dataset.select);
|
|
const row = cb.closest('.q-row, .card');
|
|
if (row) row.classList.toggle('selected', cb.checked);
|
|
updateBatchbar();
|
|
});
|
|
|
|
listContainer.addEventListener('click', e => {
|
|
const start = e.target.closest('[data-start]');
|
|
const del = e.target.closest('[data-del]');
|
|
if (start) toast('已开始下载该任务');
|
|
if (del) toast('已删除 1 项', { error: true, action: '撤销' });
|
|
});
|
|
|
|
batchbar.addEventListener('click', e => {
|
|
const btn = e.target.closest('[data-batch]');
|
|
if (!btn) return;
|
|
const n = state.selected.size;
|
|
const act = btn.dataset.batch;
|
|
if (act === 'clear') {
|
|
state.selected.clear();
|
|
renderList();
|
|
} else if (act === 'start') {
|
|
toast(`已开始 ${n} 个任务`);
|
|
state.selected.clear(); renderList();
|
|
} else if (act === 'delete') {
|
|
toast(`已删除 ${n} 项`, { error: true, action: '撤销' });
|
|
state.selected.clear(); renderList();
|
|
} else if (act === 'move') {
|
|
toast(`已将 ${n} 项移动到 movies`);
|
|
state.selected.clear(); renderList();
|
|
}
|
|
updateBatchbar();
|
|
});
|
|
|
|
document.getElementById('selectAllBtn').addEventListener('click', () => {
|
|
const items = currentItems();
|
|
const allSelected = items.every(i => state.selected.has(i.id));
|
|
state.selected.clear();
|
|
if (!allSelected) items.forEach(i => state.selected.add(i.id));
|
|
renderList();
|
|
updateBatchbar();
|
|
});
|
|
|
|
/* ================= Navigation ================= */
|
|
function bindNav(selector, key) {
|
|
document.querySelectorAll(selector).forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll(selector).forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
state[key] = btn.dataset.status || btn.dataset.folder;
|
|
state.selected.clear();
|
|
persist();
|
|
renderList();
|
|
updateBatchbar();
|
|
document.getElementById('sidebar').classList.remove('open');
|
|
});
|
|
});
|
|
}
|
|
bindNav('[data-status]', 'status');
|
|
bindNav('[data-folder]', 'folder');
|
|
|
|
/* Restore persisted nav state */
|
|
document.querySelectorAll('[data-status]').forEach(b => b.classList.toggle('active', b.dataset.status === state.status));
|
|
document.querySelectorAll('[data-folder]').forEach(b => b.classList.toggle('active', b.dataset.folder === state.folder));
|
|
|
|
/* Filter & sort */
|
|
document.getElementById('filterInput').addEventListener('input', e => {
|
|
state.filter = e.target.value.trim();
|
|
renderList();
|
|
});
|
|
document.getElementById('sortSelect').addEventListener('change', e => {
|
|
state.sort = e.target.value;
|
|
renderList();
|
|
});
|
|
|
|
/* View toggle */
|
|
document.getElementById('viewToggle').addEventListener('click', e => {
|
|
const btn = e.target.closest('[data-view]');
|
|
if (!btn) return;
|
|
document.querySelectorAll('#viewToggle button').forEach(b => b.classList.remove('on'));
|
|
btn.classList.add('on');
|
|
state.view = btn.dataset.view;
|
|
persist();
|
|
renderList();
|
|
});
|
|
document.querySelectorAll('#viewToggle button').forEach(b => b.classList.toggle('on', b.dataset.view === state.view));
|
|
|
|
/* Quality / format chips */
|
|
document.querySelectorAll('.cb-chips').forEach(group => {
|
|
group.addEventListener('click', e => {
|
|
const chip = e.target.closest('.chip');
|
|
if (!chip) return;
|
|
const attr = chip.dataset.quality ? 'quality' : 'format';
|
|
document.querySelectorAll(`.chip[data-${attr}]`).forEach(c => c.classList.remove('on'));
|
|
chip.classList.add('on');
|
|
});
|
|
});
|
|
|
|
/* Folder picker (cycles as a demo) */
|
|
const folderPick = document.getElementById('folderPick');
|
|
const folderPickLabel = document.getElementById('folderPickLabel');
|
|
const pickCycle = ['movies', 'shows', 'music', '未分类'];
|
|
folderPick.addEventListener('click', () => {
|
|
const i = pickCycle.indexOf(folderPickLabel.textContent);
|
|
folderPickLabel.textContent = pickCycle[(i + 1) % pickCycle.length];
|
|
toast('下载目录已切换为 ' + folderPickLabel.textContent);
|
|
});
|
|
|
|
/* Add */
|
|
document.getElementById('addBtn').addEventListener('click', () => {
|
|
const input = document.getElementById('urlInput');
|
|
const url = input.value.trim();
|
|
if (!url) {
|
|
toast('请先粘贴一个视频 URL', { error: true });
|
|
input.focus();
|
|
return;
|
|
}
|
|
if (!/^https?:\/\//.test(url)) {
|
|
toast('URL 格式不正确,请检查后重试', { error: true });
|
|
input.focus();
|
|
return;
|
|
}
|
|
input.value = '';
|
|
toast('已解析并加入队列', { action: '查看' });
|
|
});
|
|
document.getElementById('urlInput').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') document.getElementById('addBtn').click();
|
|
});
|
|
|
|
/* Slide-over */
|
|
const scrim = document.getElementById('scrim');
|
|
const slideover = document.getElementById('slideover');
|
|
function openSlideover() { scrim.classList.add('show'); slideover.classList.add('show'); }
|
|
function closeSlideover() { scrim.classList.remove('show'); slideover.classList.remove('show'); }
|
|
document.getElementById('openAdvanced').addEventListener('click', openSlideover);
|
|
document.getElementById('openAdvanced2').addEventListener('click', openSlideover);
|
|
document.getElementById('closeAdvanced').addEventListener('click', closeSlideover);
|
|
scrim.addEventListener('click', closeSlideover);
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSlideover(); });
|
|
|
|
document.querySelectorAll('.switch').forEach(sw => {
|
|
sw.addEventListener('click', () => {
|
|
sw.classList.toggle('on');
|
|
sw.setAttribute('aria-checked', sw.classList.contains('on'));
|
|
});
|
|
});
|
|
|
|
document.getElementById('activityBtn').addEventListener('click', () => {
|
|
toast('最近事件:precheck 完成 · 3 个任务待开始');
|
|
});
|
|
|
|
/* Mobile menu */
|
|
document.getElementById('menuBtn').addEventListener('click', () => {
|
|
document.getElementById('sidebar').classList.toggle('open');
|
|
});
|
|
|
|
/* Initial render */
|
|
renderList();
|
|
updateBatchbar();
|
|
</script>
|
|
</body>
|
|
</html>
|