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:
co-authored by
Claude Sonnet 5.5
parent
874a53e492
commit
401833104b
+154
-185
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user