Новая capability web-ui: презентационный перенос готового дизайна
(семантический HTML + единый jellybit.css, тёмная тема по настройке ОС)
в html/template без шага сборки.
- Встроенная (go:embed) отдача статики под /static с Cache-Control и
cache-busting (?v=<hash> по содержимому css/js).
- Шрифты IBM Plex self-hosted (@font-face, cyrillic+latin), без CDN.
- Наколеночный менеджер зависимостей: вендор (htmx + шрифты) не хранится
в репо (gitignore), идемпотентно добывается `task assets` по
web/assets.manifest с проверкой sha256; task build/run зависят от assets.
- Общие партиалы: шапка, бейдж статуса (карта всех 14 состояний),
виджет «файл источника → раскладка» (общий для review и download).
- Страницы: список с фильтром/поиском, ревью, новая страница просмотра
загрузки (/download/{id}). deleted скрыт по умолчанию.
- Превью раскладки берётся из единой логики internal/layout
(buildFileRows), без дублирования правил имён в шаблонах.
- Убраны meta-refresh и инлайн-стили; copyHash на vanilla с fallback
на execCommand и честной индикацией (целевой деплой — HTTP LAN).
Вне scope (отдельный change): живые обновления прогресса и раздел
раздачи, клиентский режим ручной раскладки файл→серия (с Alpine.js).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
JavaScript
76 lines
2.9 KiB
JavaScript
// Клиентская мелочь без сборки и фреймворков.
|
|
// htmx тянет серверные фрагменты, здесь — только то, что серверу знать не нужно.
|
|
|
|
// Копирование infohash в буфер обмена. На целевом деплое (домашний LAN по HTTP)
|
|
// navigator.clipboard недоступен — secure context только на HTTPS/localhost,
|
|
// поэтому есть fallback на execCommand, а индикация честная: «✓» лишь при успехе.
|
|
function copyHash(btn, full) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(full).then(
|
|
() => flashCopy(btn, true),
|
|
() => flashCopy(btn, legacyCopy(full)),
|
|
);
|
|
} else {
|
|
flashCopy(btn, legacyCopy(full));
|
|
}
|
|
}
|
|
|
|
function legacyCopy(text) {
|
|
try {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.focus();
|
|
ta.select();
|
|
const ok = document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
return ok;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function flashCopy(btn, ok) {
|
|
const old = btn.dataset.label || btn.textContent;
|
|
btn.dataset.label = old;
|
|
btn.textContent = ok ? '✓ скопировано' : '✗ не вышло';
|
|
btn.classList.toggle('copied', ok);
|
|
setTimeout(() => { btn.textContent = old; btn.classList.remove('copied'); }, 1400);
|
|
}
|
|
|
|
// --- Фильтр/поиск по списку загрузок (по уже отрисованным карточкам) ---
|
|
|
|
let activeFilter = 'all';
|
|
|
|
function setFilter(el) {
|
|
document.querySelectorAll('#chips .chip').forEach((c) => c.classList.remove('active'));
|
|
el.classList.add('active');
|
|
activeFilter = el.dataset.f;
|
|
applyFilters();
|
|
}
|
|
|
|
function applyFilters() {
|
|
const searchEl = document.querySelector('#list-toolbar input[type=search]');
|
|
const q = (searchEl ? searchEl.value : '').toLowerCase().trim();
|
|
const showAllEl = document.getElementById('showAll');
|
|
const showAll = showAllEl ? showAllEl.checked : false;
|
|
let visible = 0;
|
|
document.querySelectorAll('#list .card').forEach((card) => {
|
|
const grp = card.dataset.group;
|
|
const isDeleted = card.dataset.deleted === '1';
|
|
let ok = true;
|
|
if (isDeleted && !showAll) ok = false;
|
|
if (ok && activeFilter !== 'all' && grp !== activeFilter) ok = false;
|
|
if (ok && q && !(card.dataset.text || '').includes(q)) ok = false;
|
|
card.style.display = ok ? '' : 'none';
|
|
if (ok) visible += 1;
|
|
});
|
|
const empty = document.getElementById('empty');
|
|
if (empty) empty.style.display = visible ? 'none' : '';
|
|
}
|
|
|
|
// Применить начальный фильтр (скрыть удалённые) после загрузки.
|
|
document.addEventListener('DOMContentLoaded', applyFilters);
|