Перенос дизайна в server-rendered веб-UI (web-ui)
Новая 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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// Клиентская мелочь без сборки и фреймворков.
|
||||
// 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);
|
||||
Reference in New Issue
Block a user