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
+75
-102
@@ -4,130 +4,102 @@
|
||||
<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;
|
||||
--accent-1: #fd267a;
|
||||
--accent-2: #ff6036;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
body {
|
||||
background: radial-gradient(1200px 600px at 50% -10%, #2a1420 0%, var(--bg) 60%) no-repeat;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
font: 15px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
button, select { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; border: 0; background: none; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
header {
|
||||
position: sticky; top: 0; z-index: 5;
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||
padding: 12px 24px; background: #0e0e12e6; backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; gap: 12px 16px; flex-wrap: wrap;
|
||||
padding: 12px 24px; background: #1B1B1Fee; backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid #ffffff2e;
|
||||
}
|
||||
.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; }
|
||||
.meta { color: var(--muted); font-size: 14px; }
|
||||
.meta b { color: var(--text); }
|
||||
.heading { font: 600 14px/1 var(--f-display); letter-spacing: -.02em; color: var(--on); }
|
||||
.meta { font: 400 12px/1.4 var(--f-mono); color: var(--on-muted); }
|
||||
.meta b { color: var(--on); font-weight: 500; }
|
||||
.spacer { flex: 1; }
|
||||
.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;
|
||||
@media (max-width: 520px) {
|
||||
header { padding: 10px 12px; gap: 8px; }
|
||||
header .btn { padding: 8px 12px; font-size: 13px; }
|
||||
.heading { flex-basis: 100%; }
|
||||
.spacer { display: none; }
|
||||
}
|
||||
.pill:hover { background: #2d2d38; }
|
||||
select.pill { appearance: none; padding-right: 14px; cursor: pointer; }
|
||||
select.pill option { background: var(--surface); }
|
||||
|
||||
/* ---------- Grid: 2 → 3 → 4 columns, like the YouTube home page ---------- */
|
||||
/* ---------- Contact sheet: 2 → 3 → 4 columns ---------- */
|
||||
.grid {
|
||||
display: grid; gap: 28px 16px; padding: 24px;
|
||||
display: grid; gap: 30px 18px; padding: 24px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
max-width: 1800px; margin: 0 auto;
|
||||
}
|
||||
@media (min-width: 900px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (min-width: 1300px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||||
@media (max-width: 520px) { .grid { padding: 12px; gap: 20px 10px; } header { padding: 10px 12px; } }
|
||||
@media (max-width: 520px) { .grid { padding: 12px; gap: 22px 10px; } }
|
||||
|
||||
.item { display: block; min-width: 0; }
|
||||
.thumb {
|
||||
display: block; position: relative; aspect-ratio: 16 / 9; border-radius: 12px; overflow: hidden;
|
||||
background: var(--surface); transition: border-radius .15s;
|
||||
/* each video is one paper mount: print on top, caption below */
|
||||
.item {
|
||||
display: flex; flex-direction: column; min-width: 0; padding: 6px 6px 14px; background: var(--mount); border-radius: 4px;
|
||||
box-shadow: 0 1px 0 #fff inset, 0 18px 26px -18px #000c, 0 1px 3px #0003;
|
||||
transition: transform .15s, box-shadow .15s;
|
||||
}
|
||||
.item:hover .thumb { border-radius: 0; }
|
||||
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.badge {
|
||||
position: absolute; right: 6px; bottom: 6px; padding: 2px 7px; border-radius: 6px;
|
||||
font-size: 12px; font-weight: 800; background: #000c; color: #ffb547;
|
||||
.item:hover { transform: translateY(-3px); box-shadow: 0 1px 0 #fff inset, 0 24px 30px -18px #000d, 0 1px 3px #0003; }
|
||||
.print { position: relative; display: block; }
|
||||
.frame { aspect-ratio: 16 / 9; background: #000; border-radius: 2px; overflow: hidden; }
|
||||
.frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.tag {
|
||||
position: absolute; left: 12px; top: 12px; padding: 4px 6px; border-radius: 2px;
|
||||
background: var(--red); color: #fff; font: 500 11px/1 var(--f-mono); letter-spacing: .06em; text-transform: uppercase;
|
||||
}
|
||||
.badge.hot { background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); color: #fff; }
|
||||
.shorts { position: absolute; left: 6px; top: 6px; padding: 2px 7px; border-radius: 6px; font-size: 11px; font-weight: 800; background: #ff0033cc; }
|
||||
|
||||
.details { display: flex; gap: 10px; padding-top: 10px; }
|
||||
.avatar {
|
||||
flex: none; width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-weight: 700; font-size: 15px; color: #fff; background: var(--hue, #555);
|
||||
}
|
||||
.text { min-width: 0; }
|
||||
.cap { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: 1fr auto; gap: 8px 12px; align-items: start; padding: 12px 6px 0; }
|
||||
.title {
|
||||
margin: 0 0 4px; font-size: 15px; font-weight: 600; line-height: 1.3;
|
||||
margin: 0 0 3px; font: 600 15px/1.3 var(--f-body);
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.sub { color: var(--muted); font-size: 13px; line-height: 1.45; }
|
||||
.channel { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
a.channel:hover { color: var(--text); }
|
||||
.nums b { color: var(--text); font-weight: 600; }
|
||||
.dot { margin: 0 4px; }
|
||||
a:hover > .title { text-decoration: underline; text-underline-offset: 3px; }
|
||||
.channel { display: block; color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
a.channel:hover { color: var(--ink); }
|
||||
.stats { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; margin-top: 4px; border-top: 1.5px solid var(--ink); }
|
||||
.stats div { padding: 8px 10px 0 0; min-width: 0; }
|
||||
.stats div + div { padding-left: 12px; border-left: 1px solid var(--ink); }
|
||||
.stats span { display: block; font: 500 11px/1.2 var(--f-mono); letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
||||
.stats b { display: block; margin-top: 2px; font: 700 22px/1.2 var(--f-body); letter-spacing: -.02em; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
@media (max-width: 520px) { .stats b { font-size: 16px; } .stats span { font-size: 9.5px; letter-spacing: .02em; } .stats div + div { padding-left: 8px; } }
|
||||
|
||||
/* every video on this sheet was chosen, so every score gets the red pencil circle */
|
||||
.score {
|
||||
display: inline-block; margin-top: 2px; padding: 7px 11px; white-space: nowrap;
|
||||
border: 2.5px solid var(--red); border-radius: 52% 48% 55% 45% / 50% 56% 44% 50%;
|
||||
transform: rotate(-5deg);
|
||||
font: 600 14px/1 var(--f-display); letter-spacing: -.04em; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@media (max-width: 520px) { .cap { grid-template-columns: 1fr; } .score { justify-self: start; } }
|
||||
|
||||
/* ---------- Empty ---------- */
|
||||
.empty {
|
||||
margin: 12vh auto 0; width: min(560px, calc(100% - 32px)); padding: 40px 24px;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center;
|
||||
background: var(--surface); border: 2px dashed var(--border); border-radius: 18px;
|
||||
}
|
||||
.empty.dragover { border-color: var(--accent-1); background: #2a1420; }
|
||||
.empty h2 { margin: 0; font-size: 24px; }
|
||||
.empty p { margin: 0; color: var(--muted); max-width: 400px; }
|
||||
.pill.primary { background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); border-color: transparent; }
|
||||
|
||||
.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; }
|
||||
.empty { margin: 12vh auto 0; width: min(560px, calc(100% - 32px)); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="brand">🔥 Thumbnail <span>Tinder</span></div>
|
||||
<span class="heading">Понравившиеся</span>
|
||||
<div class="meta hidden" id="meta"><b id="count">0</b> видео · <span id="fileName"></span></div>
|
||||
<div class="spacer"></div>
|
||||
<select class="pill hidden" id="sort" title="Сортировка">
|
||||
<select class="btn hidden" id="sort" aria-label="Сортировка">
|
||||
<option value="file">Как в файле</option>
|
||||
<option value="score">По outlier score</option>
|
||||
<option value="views">По просмотрам</option>
|
||||
<option value="subs">По подписчикам</option>
|
||||
<option value="date">Сначала новые</option>
|
||||
</select>
|
||||
<button class="pill hidden" id="newFileBtn">Файл…</button>
|
||||
<a class="pill" href="index.html">← Отбор</a>
|
||||
<button class="btn hidden" id="newFileBtn">Другой файл</button>
|
||||
<a class="btn" href="index.html">К отбору</a>
|
||||
</header>
|
||||
|
||||
<div class="empty" id="empty">
|
||||
<div style="font-size:52px">❤️</div>
|
||||
<h2>Загрузите отобранный CSV</h2>
|
||||
<p>Файл <b>…-liked.csv</b>, который вы скачали из Thumbnail Tinder (подойдёт и любой CSV из расширения). Видео покажутся сеткой, как на главной YouTube.</p>
|
||||
<button class="pill primary" id="pickBtn">Выбрать файл</button>
|
||||
<div class="screen empty" id="empty">
|
||||
<h2>Загрузите CSV с понравившимися</h2>
|
||||
<p>Выберите файл …-liked.csv, который вы скачали в приложении для отбора. Подойдёт и любой CSV из расширения.</p>
|
||||
<div class="row"><button class="btn primary" id="pickBtn">Выбрать файл</button></div>
|
||||
</div>
|
||||
|
||||
<div class="grid hidden" id="grid"></div>
|
||||
@@ -176,10 +148,12 @@
|
||||
const compact = new Intl.NumberFormat('ru-RU', { notation: 'compact', maximumFractionDigits: 1 });
|
||||
const full = new Intl.NumberFormat('ru-RU');
|
||||
const fmtNum = (v) => (v === '' || isNaN(+v)) ? '—' : (+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 esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
const hue = (s) => { let h = 0; for (const ch of s) h = (h * 31 + ch.codePointAt(0)) % 360; return `hsl(${h} 45% 40%)`; };
|
||||
const initial = (s) => (Array.from((s || '?').trim())[0] || '?').toUpperCase();
|
||||
|
||||
function thumbSources(r) {
|
||||
const id = r.videoId, orig = r.thumbnailUrl, list = [];
|
||||
@@ -212,7 +186,6 @@
|
||||
grid.innerHTML = '';
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const r of list) {
|
||||
const score = +r.outlierScore;
|
||||
const short = String(r.isShort).toLowerCase() === 'true';
|
||||
const ch = r.channelUrl
|
||||
? `<a class="channel" href="${esc(r.channelUrl)}" target="_blank" rel="noopener">${esc(r.channelTitle)}</a>`
|
||||
@@ -220,19 +193,19 @@
|
||||
const el = document.createElement('div');
|
||||
el.className = 'item';
|
||||
el.innerHTML = `
|
||||
<a href="${esc(r.url)}" target="_blank" rel="noopener" class="thumb">
|
||||
<img alt="" loading="lazy" draggable="false">
|
||||
${short ? '<span class="shorts">SHORTS</span>' : ''}
|
||||
<span class="badge ${score >= 10 ? 'hot' : ''}" title="Outlier score">${fmtScore(r.outlierScore)}</span>
|
||||
<a href="${esc(r.url)}" target="_blank" rel="noopener" class="print" aria-label="${esc(r.title)}">
|
||||
<div class="frame"><img alt="" loading="lazy" draggable="false"></div>
|
||||
${short ? '<span class="tag">Shorts</span>' : ''}
|
||||
</a>
|
||||
<div class="details">
|
||||
<div class="avatar" style="--hue:${hue(r.channelTitle || '')}">${esc(initial(r.channelTitle))}</div>
|
||||
<div class="text">
|
||||
<div class="cap">
|
||||
<div>
|
||||
<a href="${esc(r.url)}" target="_blank" rel="noopener"><h3 class="title" title="${esc(r.title)}">${esc(r.title)}</h3></a>
|
||||
<div class="sub">
|
||||
${ch}
|
||||
<div class="nums"><b>${fmtNum(r.views)}</b> просмотров<span class="dot">•</span><b>${fmtNum(r.subscribers)}</b> подписчиков</div>
|
||||
</div>
|
||||
${ch}
|
||||
</div>
|
||||
<div class="score" title="Outlier score: ${esc(r.outlierScore)}">${fmtScore(r.outlierScore)}</div>
|
||||
<div class="stats">
|
||||
<div><span>Просмотры</span><b>${fmtNum(r.views)}</b></div>
|
||||
<div><span>Подписчики</span><b>${fmtNum(r.subscribers)}</b></div>
|
||||
</div>
|
||||
</div>`;
|
||||
setImg(el.querySelector('img'), thumbSources(r));
|
||||
|
||||
Reference in New Issue
Block a user