Redesign: light-table concept, dark background, readable stats

Shared style.css, grease-pencil swipe marks, log-scale outlier ruler,
paper-mount gallery cards with larger views/subscribers figures.
Add the sample outliers CSV.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
IgorVolochay
2026-11-14 22:30:43 +03:00
co-authored by Claude Sonnet 5.5
parent 874a53e492
commit 401833104b
5 changed files with 1141 additions and 287 deletions
+154 -185
View File
@@ -4,175 +4,141 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Thumbnail Tinder</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Onest:wght@400;500;600;700&family=Unbounded:wght@500;600&display=swap">
<link rel="stylesheet" href="style.css">
<style>
:root {
--bg: #0e0e12;
--surface: #1a1a21;
--surface-2: #24242d;
--border: #2e2e39;
--text: #f3f3f6;
--muted: #9a9aa8;
--like: #22c55e;
--nope: #ef4444;
--skip: #3b82f6;
--accent-1: #fd267a;
--accent-2: #ff6036;
--radius: 18px;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: radial-gradient(1200px 600px at 50% -10%, #2a1420 0%, var(--bg) 60%);
background-color: var(--bg);
color: var(--text);
font: 15px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
display: flex; flex-direction: column;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
button { font: inherit; color: inherit; cursor: pointer; border: 0; background: none; }
button:disabled { opacity: .35; cursor: default; }
a { color: inherit; }
body { height: 100%; display: flex; flex-direction: column; overflow: hidden; -webkit-tap-highlight-color: transparent; }
/* ---------- Top bar ---------- */
header {
display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
gap: 12px; padding: 12px 16px;
}
.brand { font-weight: 800; font-size: 18px; letter-spacing: -.02em; white-space: nowrap; }
.brand span {
background: linear-gradient(90deg, var(--accent-1), var(--accent-2));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
header { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; padding: 14px 20px; }
.top-right { display: flex; justify-content: flex-end; gap: 8px; }
.pill {
padding: 8px 14px; border-radius: 999px; background: var(--surface-2);
border: 1px solid var(--border); font-weight: 600; font-size: 14px; white-space: nowrap;
}
.pill:not(:disabled):hover { background: #2d2d38; }
.pill.primary {
background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); border-color: transparent;
}
#skipBtn { padding: 10px 22px; font-size: 15px; border-color: #3b82f655; }
#skipBtn:not(:disabled):hover { background: #1e3a6e; }
#skipBtn { padding: 10px 30px; }
.progress { padding: 0 16px; }
.progress-meta { display: flex; justify-content: space-between; color: var(--muted); font-size: 13px; margin-bottom: 6px; }
.progress-meta b { color: var(--text); font-weight: 600; }
.bar { height: 4px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
.bar > div { height: 100%; width: 0; background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); transition: width .25s; }
.progress { padding: 0 20px; }
.progress-meta { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 6px; font: 400 12px/1.4 var(--f-mono); color: var(--on-muted); }
.progress-meta b { color: var(--on); font-weight: 500; }
.bar { height: 3px; background: #ffffff2e; }
.bar > div { height: 100%; width: 0; background: var(--on); transition: width .25s; }
/* ---------- Stage ---------- */
main { flex: 1; display: flex; align-items: center; justify-content: center; padding: 16px; min-height: 0; }
/* all cards share one grid cell: the stack is as tall as the tallest card */
.stack { position: relative; display: grid; min-height: 360px;
/* keep card + controls inside the viewport: ~440px is header, info block and buttons */
width: min(100%, 600px, max(320px, calc((100dvh - 440px) * 16 / 9))); }
.stack {
position: relative; display: grid; min-height: 360px;
/* keep card + controls inside the viewport */
width: min(100%, 600px, max(360px, calc((100dvh - 470px) * 16 / 9)));
}
.stack > * { grid-area: 1 / 1; }
/* the card is a slide in a mount */
.card {
position: relative; display: flex; flex-direction: column; min-height: 0;
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
overflow: hidden; box-shadow: 0 20px 50px #0008;
position: relative; display: flex; flex-direction: column; min-height: 0; padding: 10px;
background: var(--mount); border: 1px solid var(--line); border-radius: 6px;
box-shadow: 0 1px 0 #fff inset, 0 26px 40px -26px #000a, 0 2px 5px #0002;
user-select: none; -webkit-user-select: none; touch-action: none;
will-change: transform; cursor: grab;
will-change: transform; cursor: grab; container-type: inline-size;
transition: transform .3s cubic-bezier(.2,.8,.2,1), opacity .3s;
}
.card.dragging { transition: none; cursor: grabbing; }
.card.behind { transform: scale(.95) translateY(14px); opacity: .6; pointer-events: none; }
.card.behind-2 { transform: scale(.9) translateY(28px); opacity: .3; pointer-events: none; }
.card.behind { transform: scale(.96) translateY(12px); opacity: .7; pointer-events: none; }
.card.behind-2 { transform: scale(.92) translateY(24px); opacity: .4; pointer-events: none; }
.thumb { position: relative; aspect-ratio: 16 / 9; background: #000; flex-shrink: 1; min-height: 0; }
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.thumb.short img { object-fit: contain; }
.chip {
position: absolute; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700;
background: #000b; backdrop-filter: blur(6px);
.thumb { position: relative; aspect-ratio: 16 / 9; background: #000; flex-shrink: 1; min-height: 0; border-radius: 2px; }
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; border-radius: 2px; }
.tags { position: absolute; left: 8px; top: 8px; display: flex; gap: 4px; }
.tag { padding: 4px 6px; border-radius: 2px; background: var(--mount); font: 500 11px/1 var(--f-mono); letter-spacing: .06em; text-transform: uppercase; }
.tag.short { background: var(--red); color: #fff; }
/* grease-pencil marks, drawn over the thumbnail while dragging */
.stamp { position: absolute; inset: -6px; opacity: 0; pointer-events: none; z-index: 2; }
.stamp svg { width: 100%; height: 100%; overflow: visible; }
.stamp path { fill: none; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.stamp path.u { stroke: #fff; stroke-width: 9; opacity: .75; }
.stamp path.m { stroke-width: 4.5; }
.stamp.like path.m { stroke: var(--red); }
.stamp.nope path.m { stroke: var(--ink); }
.stamp.skip { inset: auto; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(-6deg); }
.stamp.skip span {
display: block; padding: 8px 16px; background: #fffd; border: 3px solid var(--blue); border-radius: 3px;
color: var(--blue); font: 500 22px/1 var(--f-mono); letter-spacing: .1em; text-transform: uppercase;
}
.chip.found { top: 10px; right: 10px; text-transform: uppercase; letter-spacing: .04em; color: #ddd; }
.chip.short-badge { top: 10px; left: 10px; background: #ff0033cc; }
.stamp {
position: absolute; top: 28px; padding: 6px 14px; border: 4px solid; border-radius: 10px;
font-size: 34px; font-weight: 900; letter-spacing: .05em; opacity: 0; pointer-events: none;
text-shadow: 0 2px 8px #0008; z-index: 2;
}
.stamp.like { left: 22px; color: var(--like); border-color: var(--like); transform: rotate(-14deg); }
.stamp.nope { right: 22px; color: var(--nope); border-color: var(--nope); transform: rotate(14deg); }
.stamp.skip { left: 50%; top: 40%; color: var(--skip); border-color: var(--skip); transform: translateX(-50%); }
.info { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 12px; min-height: 0; overflow: hidden; }
.info { padding: 14px 6px 4px; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.title {
font-size: 19px; font-weight: 700; line-height: 1.3; margin: 0;
margin: 0; font: 700 20px/1.25 var(--f-body); letter-spacing: -.01em;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
text-decoration: none;
}
.title:hover { text-decoration: underline; }
.channel { color: var(--muted); font-size: 14px; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.channel:hover { color: var(--text); }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: auto; }
.stat { background: var(--surface-2); border-radius: 12px; padding: 10px 12px; min-width: 0; }
.stat .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
.stat .value { font-size: 20px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stat.score .value { color: #ffb547; }
.stat.score.hot .value { background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.title:hover { text-decoration: underline; text-underline-offset: 3px; }
.channel { color: var(--muted); font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.channel:hover { color: var(--ink); }
.facts { display: grid; grid-template-columns: 1fr 1fr 1.3fr; margin-top: 6px; border-top: 1.5px solid var(--ink); }
.fact { padding: 10px 12px 0 0; min-width: 0; }
.fact + .fact { padding-left: 14px; border-left: 1px solid var(--ink); }
.fact .label { font: 500 11.5px/1.2 var(--f-mono); letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.fact .value { margin-top: 3px; font: 700 28px/1.15 var(--f-body); letter-spacing: -.02em; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.fact.score .value { font: 600 28px/1.15 var(--f-display); letter-spacing: -.05em; }
/* log scale 1x … 1000x: shows where this video sits */
.ruler { position: relative; height: 26px; margin-top: 4px; font: 400 10px/1 var(--f-mono); color: var(--muted); }
.ruler::before { content: ''; position: absolute; left: 0; right: 0; top: 4px; height: 1px; background: var(--ink); opacity: .55; }
.ruler .tick { position: absolute; top: 1px; width: 1px; height: 7px; background: var(--ink); opacity: .55; }
.ruler span { position: absolute; top: 13px; }
.ruler .t0 { left: 0; }
.ruler .t1 { left: 33.33%; transform: translateX(-33%); }
.ruler .t2 { left: 66.67%; transform: translateX(-66%); }
.ruler .t3 { right: 0; }
.ruler .mark { position: absolute; top: 0; width: 9px; height: 9px; margin-left: -4.5px; border-radius: 50%; background: var(--ink); box-shadow: 0 0 0 2px var(--mount); text-decoration: none; }
@container (max-width: 440px) {
.title { font-size: 17px; }
.fact .value { font-size: 23px; }
.fact.score .value { font-size: 23px; }
.fact .label { font-size: 10.5px; letter-spacing: .02em; }
}
@container (max-width: 380px) {
.fact { padding-right: 8px; }
.fact + .fact { padding-left: 10px; }
.fact .value { font-size: 19px; }
.fact.score .value { font-size: 19px; }
.fact .label { font-size: 9.5px; }
}
/* ---------- Bottom controls ---------- */
footer { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 4px 16px 18px; }
.controls { display: flex; align-items: center; justify-content: center; gap: 22px; }
.round {
display: grid; place-items: center; border-radius: 50%;
background: var(--surface); border: 1px solid var(--border); box-shadow: 0 6px 20px #0006;
transition: transform .12s, background .15s;
footer { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 4px 20px 18px; }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; width: min(440px, 100%); }
.act {
display: flex; align-items: center; justify-content: center; gap: 10px; height: 54px;
border-radius: 6px; border: 1.5px solid var(--ink); background: var(--mount);
font-weight: 700; font-size: 16px; transition: transform .1s, background .15s, color .15s;
}
.round:not(:disabled):hover { transform: scale(1.08); }
.round:not(:disabled):active { transform: scale(.94); }
.round.big { width: 68px; height: 68px; }
.round.small { width: 48px; height: 48px; }
.round svg { width: 50%; height: 50%; }
#nopeBtn { color: var(--nope); }
#likeBtn { color: var(--like); }
#undoBtn { color: #facc15; }
.hint { color: var(--muted); font-size: 12px; }
.hint kbd { background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px; padding: 0 5px; font: inherit; }
.act:not(:disabled):hover { background: var(--ink); color: var(--mount); }
.act:not(:disabled):active { transform: scale(.97); }
.act.yes { background: var(--red); border-color: var(--red); color: #fff; }
.act.yes:not(:disabled):hover { background: #b9211a; border-color: #b9211a; }
.act:disabled { opacity: .3; }
.minor { display: flex; gap: 20px; }
.link { display: inline-flex; align-items: center; gap: 8px; padding: 4px 0; background: none; border: 0; color: var(--on-muted); font-size: 13px; }
.link:not(:disabled):hover { color: var(--on); }
.link:disabled { opacity: .35; }
/* ---------- Screens ---------- */
.screen {
position: relative; min-height: 420px; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 16px; text-align: center; padding: 24px;
background: var(--surface); border: 2px dashed var(--border); border-radius: var(--radius);
}
.screen.solid { border-style: solid; }
.screen h2 { margin: 0; font-size: 24px; }
.screen p { margin: 0; color: var(--muted); max-width: 380px; }
.screen.dragover { border-color: var(--accent-1); background: #2a1420; }
.screen .big-num { font-size: 56px; font-weight: 900; line-height: 1; }
.screen .row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.summary { display: flex; gap: 20px; color: var(--muted); }
.summary b { display: block; font-size: 22px; color: var(--text); }
.toast {
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(120%);
background: var(--surface-2); border: 1px solid var(--border); padding: 10px 16px; border-radius: 12px;
opacity: 0; transition: .25s; z-index: 10; pointer-events: none; max-width: calc(100% - 32px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.hidden { display: none !important; }
.card { container-type: inline-size; }
@container (max-width: 440px) {
.stat { padding: 8px 10px; }
.stat .value { font-size: 16px; }
.stat .label { font-size: 10px; letter-spacing: .03em; }
}
/* ---------- Done screen ---------- */
.summary { display: flex; gap: 28px; margin: 4px 0; font: 400 12px/1.3 var(--f-mono); letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.summary b { display: block; font: 600 30px/1.1 var(--f-display); letter-spacing: -.04em; color: var(--ink); }
.summary .like b { color: var(--red); }
.summary .skip b { color: var(--blue); }
@media (max-width: 560px) {
header { grid-template-columns: 1fr auto; }
header { grid-template-columns: auto 1fr; padding: 12px 14px; gap: 8px; }
.brand { display: none; }
.info { padding: 12px 14px 14px; gap: 8px; }
.title { font-size: 16px; }
.stat .value { font-size: 16px; }
.hint { display: none; }
header .btn { padding: 9px 12px; }
#skipBtn { padding: 9px 16px; }
.top-right { gap: 6px; }
.progress { padding: 0 14px; }
.info { padding: 12px 4px 2px; }
#newFileBtn { display: none; }
}
@media (max-height: 700px) {
.title { -webkit-line-clamp: 2; }
@@ -182,19 +148,19 @@
<body>
<header>
<div class="brand">🔥 Thumbnail <span>Tinder</span></div>
<button id="skipBtn" class="pill" disabled title="Пропустить (↑ или S)">Пропустить ⤴</button>
<a class="brand" href="index.html">Thumbnail Tinder</a>
<button id="skipBtn" class="btn" disabled title="Отложить видео (↑ или S)">Пропустить</button>
<div class="top-right">
<button id="exportBtn" class="pill primary" disabled>Скачать ♥ <span id="exportCount">0</span></button>
<button id="newFileBtn" class="pill hidden" title="Загрузить другой файл">Файл…</button>
<a class="pill" href="liked.html" title="Открыть галерею понравившихся" style="text-decoration:none">Галерея</a>
<button id="exportBtn" class="btn primary" disabled>Скачать CSV · <span id="exportCount">0</span></button>
<button id="newFileBtn" class="btn hidden" title="Загрузить другой файл">Другой файл</button>
<a class="btn" href="liked.html" title="Открыть галерею понравившихся">Галерея</a>
</div>
</header>
<div class="progress hidden" id="progress">
<div class="progress-meta">
<span id="fileName"></span>
<span><b id="doneCount">0</b> / <span id="totalCount">0</span> · ♥ <b id="likeCount">0</b> · ✕ <b id="nopeCount">0</b> · ⤴ <b id="skipCount">0</b></span>
<span><b id="doneCount">0</b> из <span id="totalCount">0</span> · нравится <b id="likeCount">0</b> · нет <b id="nopeCount">0</b> · отложено <b id="skipCount">0</b></span>
</div>
<div class="bar"><div id="barFill"></div></div>
</div>
@@ -202,32 +168,24 @@
<main>
<div class="stack" id="stack">
<div class="screen" id="dropScreen">
<div style="font-size:52px">🗂️</div>
<h2>Загрузите CSV с outliers</h2>
<p>Перетащите файл сюда или выберите его. Свайп вправо — нравится, влево — нет. В итоговый файл попадут только понравившиеся видео.</p>
<h2>Загрузите CSV из Outlier Saver</h2>
<p>Перетащите файл на страницу или выберите его. Понравившиеся видео сохранятся в новый CSV в том же формате.</p>
<div class="row">
<button class="pill primary" id="pickBtn">Выбрать файл</button>
<button class="btn primary" id="pickBtn">Выбрать файл</button>
</div>
</div>
</div>
</main>
<footer>
<div class="controls">
<button class="round small" id="undoBtn" disabled title="Отменить (Z)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg>
</button>
<button class="round big" id="nopeBtn" disabled title="Не нравится (←)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg>
</button>
<button class="round big" id="likeBtn" disabled title="Нравится (→)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 21s-7.5-4.6-10-9.3C.3 8.4 2.2 4 6.3 4c2.3 0 3.9 1.3 5.7 3.4C13.8 5.3 15.4 4 17.7 4 21.8 4 23.7 8.4 22 11.7 19.5 16.4 12 21 12 21z"/></svg>
</button>
<button class="round small" id="openBtn" disabled title="Открыть видео (Enter)" style="color:#cbd5e1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6"/><path d="M20 4 10 14"/><path d="M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5"/></svg>
</button>
<div class="actions">
<button class="act no" id="nopeBtn" disabled title="Не нравится (←)"><kbd>←</kbd>Не нравится</button>
<button class="act yes" id="likeBtn" disabled title="Нравится (→)">Нравится<kbd>→</kbd></button>
</div>
<div class="minor">
<button class="link" id="undoBtn" disabled title="Отменить последнее решение">Отменить <kbd>Z</kbd></button>
<button class="link" id="openBtn" disabled title="Открыть видео на YouTube">Открыть на YouTube <kbd>Enter</kbd></button>
</div>
<div class="hint"><kbd>←</kbd> нет · <kbd>→</kbd> нравится · <kbd>↑</kbd>/<kbd>S</kbd> пропустить · <kbd>Z</kbd> отменить · <kbd>Enter</kbd> открыть видео</div>
</footer>
<input type="file" id="fileInput" accept=".csv,text/csv" hidden>
@@ -326,7 +284,11 @@
if (v === '' || v == null || isNaN(+v)) return '—';
return +v >= 10000 ? compact.format(+v) : full.format(+v);
};
const fmtScore = (v) => (v === '' || isNaN(+v)) ? '—' : '×' + (+v >= 100 ? Math.round(+v) : +(+v).toFixed(1));
const fmtScore = (v) => {
if (v === '' || isNaN(+v)) return '—';
v = +v;
return '×' + (v >= 1000 ? compact.format(v) : v >= 100 ? Math.round(v) : +v.toFixed(1));
};
const foundOnLabel = { home: 'главная', search: 'поиск', watch: 'рекомендации', channel: 'канал' };
function thumbSources(r) {
@@ -366,24 +328,32 @@
const el = document.createElement('div');
el.className = 'card';
el.dataset.idx = r._i;
const logPos = (isFinite(score) && score > 1) ? Math.min(1, Math.log10(score) / 3) * 100 : 0;
el.innerHTML = `
<div class="thumb ${isShort ? 'short' : ''}">
<div class="thumb">
<img alt="" draggable="false">
${isShort ? '<span class="chip short-badge">SHORTS</span>' : ''}
${r.foundOn ? `<span class="chip found">${esc(foundOnLabel[r.foundOn] || r.foundOn)}</span>` : ''}
<div class="stamp like">НРАВИТСЯ</div>
<div class="stamp nope">НЕТ</div>
<div class="stamp skip">ПОТОМ</div>
<div class="tags">
${isShort ? '<span class="tag short">Shorts</span>' : ''}
${r.foundOn ? `<span class="tag">${esc(foundOnLabel[r.foundOn] || r.foundOn)}</span>` : ''}
</div>
<div class="stamp like"><svg viewBox="0 0 100 60" preserveAspectRatio="none" aria-hidden="true"><path class="u" d="M52 3 C82 1 98 14 97 31 C96 49 72 58 46 57 C19 56 3 44 4 28 C5 12 28 4 56 4 C68 4 78 7 85 12"/><path class="m" d="M52 3 C82 1 98 14 97 31 C96 49 72 58 46 57 C19 56 3 44 4 28 C5 12 28 4 56 4 C68 4 78 7 85 12"/></svg></div>
<div class="stamp nope"><svg viewBox="0 0 100 60" preserveAspectRatio="none" aria-hidden="true"><path class="u" d="M6 5 C30 20 70 42 95 56 M95 4 C70 20 30 42 5 55"/><path class="m" d="M6 5 C30 20 70 42 95 56 M95 4 C70 20 30 42 5 55"/></svg></div>
<div class="stamp skip"><span>Потом</span></div>
</div>
<div class="info">
<a class="title" href="${esc(r.url)}" target="_blank" rel="noopener" draggable="false">${esc(r.title)}</a>
${r.channelUrl
? `<a class="channel" href="${esc(r.channelUrl)}" target="_blank" rel="noopener" draggable="false">${esc(r.channelTitle)}</a>`
: `<span class="channel">${esc(r.channelTitle)}</span>`}
<div class="stats">
<div class="stat"><div class="label">Просмотры</div><div class="value" title="${esc(r.views)}">${fmtNum(r.views)}</div></div>
<div class="stat"><div class="label">Подписчики</div><div class="value" title="${esc(r.subscribers)}">${fmtNum(r.subscribers)}</div></div>
<div class="stat score ${score >= 10 ? 'hot' : ''}"><div class="label">Outlier</div><div class="value" title="${esc(r.outlierScore)}">${fmtScore(r.outlierScore)}</div></div>
<div class="facts">
<div class="fact"><div class="label">Просмотры</div><div class="value" title="${esc(r.views)}">${fmtNum(r.views)}</div></div>
<div class="fact"><div class="label">Подписчики</div><div class="value" title="${esc(r.subscribers)}">${fmtNum(r.subscribers)}</div></div>
<div class="fact score"><div class="label">Outlier score</div><div class="value" title="${esc(r.outlierScore)}">${fmtScore(r.outlierScore)}</div></div>
</div>
<div class="ruler" aria-hidden="true">
<i class="tick" style="left:0"></i><i class="tick" style="left:33.33%"></i><i class="tick" style="left:66.67%"></i><i class="tick" style="left:100%"></i>
<span class="t0">1×</span><span class="t1">10×</span><span class="t2">100×</span><span class="t3">1000×</span>
<u class="mark" style="left:${logPos}%"></u>
</div>
</div>`;
setImg(el.querySelector('img'), thumbSources(r));
@@ -442,18 +412,17 @@
const el = document.createElement('div');
el.className = 'screen solid';
el.innerHTML = `
<div style="font-size:48px">${c.like ? '🎉' : '🤷'}</div>
<h2>${c.skip ? 'Основной проход закончен' : 'Готово!'}</h2>
<h2>${c.skip ? 'Основной проход закончен' : (c.like ? 'Все видео оценены' : 'Понравившихся нет')}</h2>
<div class="summary">
<div><b style="color:var(--like)">${c.like}</b>нравится</div>
<div><b style="color:var(--nope)">${c.nope}</b>нет</div>
<div><b style="color:var(--skip)">${c.skip}</b>пропущено</div>
<div class="like"><b>${c.like}</b>нравится</div>
<div><b>${c.nope}</b>не нравится</div>
<div class="skip"><b>${c.skip}</b>отложено</div>
</div>
<p>${c.skip ? 'Пропущенные видео не попадут в файл, пока вы их не оцените.' : 'В файл попадут только понравившиеся видео — в том же формате, что и исходный.'}</p>
<p>${c.skip ? 'Отложенные видео не попадут в файл, пока вы их не оцените.' : 'В файл попадут только понравившиеся видео, в том же формате, что и в исходном.'}</p>
<div class="row">
${c.like ? '<button class="pill primary" data-act="export">Скачать CSV (' + c.like + ')</button>' : ''}
${c.skip ? '<button class="pill" data-act="review">Пересмотреть пропущенные (' + c.skip + ')</button>' : ''}
<button class="pill" data-act="restart">Начать заново</button>
${c.like ? '<button class="btn primary" data-act="export">Скачать CSV · ' + c.like + '</button>' : ''}
${c.skip ? '<button class="btn" data-act="review">Пересмотреть отложенные · ' + c.skip + '</button>' : ''}
<button class="btn" data-act="restart">Начать заново</button>
</div>`;
el.addEventListener('click', (e) => {
const act = e.target.closest('[data-act]')?.dataset.act;