Files
jellybit/web/templates/index.html
T
avandClaude Opus 4.8 c0b5ab7295 UI/UX списка и карточки загрузки: серверные фильтр/поиск/пагинация, матч-ссылка, имя раздачи (web-ui-list-detail)
- Список: серверные фильтр по группе состояний, поиск и пагинация (GET
  f/q/page/all, по 25), сортировка по времени добавления в qBittorrent
  (added_on) с фолбеком на created_at и tie-break по id.
- Заголовок загрузки = имя раздачи (display_name) → распознанное название →
  усечённый источник; сырой magnet вынесен в блок «Информация о торренте».
- Матч метабазы показан ссылкой на запись (страница загрузки и ревью);
  URL берётся у выбранного кандидата либо строится по provider+id и типу.
- Полировка вёрстки; клиентская JS-фильтрация убрана (всё серверное, без JS).
- Миграция 0005 (display_name, source_added_at); воркер однократно
  фиксирует source_added_at при поллинге/усыновлении; ER-схема обновлена.
- OpenSpec: дельты влиты в specs/{web-ui,ingest}, change заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:50:24 +03:00

110 lines
4.9 KiB
HTML

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jellybit — загрузки</title>
<link rel="stylesheet" href="{{asset "css/jellybit.css"}}">
<script src="{{asset "vendor/htmx.min.js"}}" defer></script>
<script src="{{asset "js/app.js"}}" defer></script>
</head>
<body>
{{template "header" "downloads"}}
<main class="main">
<h1 class="page-title">Загрузки</h1>
<p class="page-sub">Кинь magnet, ссылку, .torrent или сообщение бота — остальное jellybit сделает сам.</p>
{{if .Error}}<div class="result-banner" style="background:var(--st-err-bg);border-color:color-mix(in srgb,var(--st-err) 35%,transparent)"><h2 style="color:var(--st-err)">{{.Error}}</h2></div>{{end}}
<!-- Единое окно добавления -->
<form class="add-box" method="post" action="/ui/downloads">
<div class="add-row">
<input class="input" type="text" name="source" autocomplete="off" required
placeholder="magnet:?xt=urn:btih:… · ссылка · .torrent · или вставь текст бота">
<button class="btn btn-primary btn-lg" type="submit">Добавить</button>
</div>
<p class="add-hint">Можно вставить целиком сообщение трекер-бота — magnet и контекст разберутся автоматически.</p>
</form>
{{if or .Downloads .Searching}}
<!-- Панель фильтров (серверные: фильтр/поиск/пагинация в query) -->
<div class="toolbar" id="list-toolbar">
<form class="search" method="get" action="/">
<span class="ico"></span>
<input class="input" type="search" name="q" value="{{.Query}}"
placeholder="Поиск по названию или infohash…">
{{if ne .Filter "all"}}<input type="hidden" name="f" value="{{.Filter}}">{{end}}
{{if .ShowAll}}<input type="hidden" name="all" value="1">{{end}}
</form>
<div class="filter-chips" id="chips">
{{range .Chips}}<a class="chip{{if .Active}} active{{end}}" href="{{.URL}}">{{.Label}}</a>{{end}}
</div>
</div>
<div class="toggle-line">
<a href="{{.ShowURL}}">{{if .ShowAll}}☑{{else}}☐{{end}} Показать всё, включая удалённые</a>
</div>
{{end}}
{{if .Downloads}}
<!-- Список карточек -->
<div id="list">
{{range .Downloads}}
<article class="card">
<div class="card-top">
<div>
<div class="card-title"><a href="/download/{{.ID}}" style="color:inherit">{{.Title}}</a></div>
{{if .Infohash}}
<div class="hash-row">
<span class="hash">{{.InfohashShort}}</span>
<button class="copy-btn" type="button" onclick="copyHash(this,'{{.Infohash}}')">⧉ копировать</button>
</div>
{{end}}
</div>
{{template "status_badge" .State}}
</div>
{{if .IsDownloading}}{{template "progress" .Progress}}{{end}}
{{if .Context}}
<details class="spoiler">
<summary>контекст</summary>
<div class="spoiler-body">«{{.Context}}»</div>
</details>
{{end}}
<div class="card-foot">
<span class="card-meta">{{if .Note}}{{.Note}}{{else if .Error}}{{.Error}}{{end}}</span>
<div class="card-acts">
{{if .Reviewable}}<a class="btn btn-primary btn-sm" href="/review/{{.ID}}">Ревью →</a>{{end}}
{{if .Undoable}}<form method="post" action="/ui/downloads/{{.ID}}/undo"><button class="btn btn-sm btn-ghost" type="submit">Откатить</button></form>{{end}}
{{if .Relinkable}}<form method="post" action="/ui/downloads/{{.ID}}/relink"><button class="btn btn-sm" type="submit">Привязать заново</button></form>{{end}}
{{if .Retriable}}<form method="post" action="/ui/downloads/{{.ID}}/retry"><button class="btn btn-sm" type="submit">Повторить</button></form>{{end}}
{{if not .Terminal}}<form method="post" action="/ui/downloads/{{.ID}}/cancel"><button class="btn btn-sm btn-danger" type="submit">{{if .Reviewable}}Отклонить{{else}}Отменить{{end}}</button></form>{{end}}
</div>
</div>
</article>
{{end}}
</div>
{{if gt .Pages 1}}
<nav class="pager">
{{if .PrevURL}}<a class="btn btn-sm" href="{{.PrevURL}}"></a>{{end}}
{{range .PageLinks}}<a class="btn btn-sm{{if .Active}} btn-primary{{end}}" href="{{.URL}}">{{.Num}}</a>{{end}}
{{if .NextURL}}<a class="btn btn-sm" href="{{.NextURL}}"></a>{{end}}
</nav>
{{end}}
{{else}}
<div class="empty-state">
<div class="big">🍿</div>
{{if .Searching}}Ничего не найдено. Поменяй фильтр или запрос.{{else}}Пока пусто. Добавь первую загрузку выше.{{end}}
</div>
{{end}}
</main>
</body>
</html>