feat(web): one row primitive and a mobile shell
Every list now uses the row primitive with a fixed action column, so actions never wrap. Rows use icons, headers use words. Status slots, skeletons and a fixed hero grid stop the page jumping. Below 40rem a bottom tab bar holds the four destinations. Tooltip-only meaning moves to tap-to-explain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ce6d38d044
commit
52b11fda93
+297
-332
@@ -69,72 +69,97 @@
|
||||
/settings, its lamps still driven by the health poll, and the master
|
||||
lamp stays on the rail so the one-glance read survives the move. Where
|
||||
earlier notes here say "returns to the board", read "the library".
|
||||
|
||||
REVISION 2 (mobile first): the phone is the base layout. One sticky
|
||||
one-line rail, and below 40rem a bottom tab bar carrying the four
|
||||
destinations — library, search, queues, settings — which move into the
|
||||
rail from 40rem. Every list is one row primitive (lead | body | actions)
|
||||
whose action column never wraps; icons in rows, words in headers and
|
||||
panels; every slot reserves its final size before data arrives. Detail
|
||||
pages open with a back control and the title, then one action bar.
|
||||
-->
|
||||
<header class="rail">
|
||||
<header class="rail" id="rail">
|
||||
<div class="rail-inner">
|
||||
<div class="rail-id">
|
||||
<span class="lamp" id="master-lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<a class="wordmark" id="wordmark" href="/">arr</a>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-icon"
|
||||
id="nav-queues"
|
||||
aria-label="queues needing attention"
|
||||
title="queues needing attention"
|
||||
hidden
|
||||
>
|
||||
<span class="nav-count readout" id="queues-count" hidden></span>
|
||||
</button>
|
||||
<div class="rail-search">
|
||||
<nav class="rail-nav" aria-label="destinations">
|
||||
<a class="nav-link" href="/" data-dest="library">library</a>
|
||||
<a class="nav-link" href="/queues" data-dest="queues"
|
||||
>queues<span class="nav-count readout" data-role="queues-count" hidden></span
|
||||
></a>
|
||||
<a class="nav-link" href="/settings" data-dest="settings">settings</a>
|
||||
</nav>
|
||||
<div class="rail-search" id="rail-search">
|
||||
<span class="rail-search-glyph" data-glyph="search" aria-hidden="true"></span>
|
||||
<input
|
||||
id="search"
|
||||
class="search-input readout"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
placeholder="search · title / tmdb id / tt… / magnet"
|
||||
enterkeyhint="search"
|
||||
placeholder="title, tmdb id, tt…, magnet"
|
||||
aria-label="unified search — titles, TMDB or IMDb ids, magnets and .torrent links"
|
||||
/>
|
||||
<span class="search-hint readout" id="search-hint" aria-hidden="true">/</span>
|
||||
</div>
|
||||
<div class="rail-meta">
|
||||
<span class="readout dim" id="version" data-testid="version">v —</span>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-icon"
|
||||
id="nav-settings"
|
||||
aria-label="settings"
|
||||
title="settings"
|
||||
aria-pressed="false"
|
||||
class="control control-icon rail-search-close"
|
||||
id="search-close"
|
||||
aria-label="close search"
|
||||
data-glyph="close"
|
||||
></button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-icon rail-search-open"
|
||||
id="search-open"
|
||||
aria-label="search"
|
||||
data-glyph="search"
|
||||
></button>
|
||||
<span class="readout dim rail-version" id="version" data-testid="version">v —</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="deck" id="deck" hidden>
|
||||
<p class="deck-status readout" id="deck-status" role="status" hidden></p>
|
||||
<nav class="tabbar" id="tabbar" aria-label="destinations">
|
||||
<a class="tab" href="/" data-dest="library"
|
||||
><span data-glyph="library" aria-hidden="true"></span><span class="tab-label">library</span></a
|
||||
>
|
||||
<a class="tab" href="/search" data-dest="search"
|
||||
><span data-glyph="search" aria-hidden="true"></span><span class="tab-label">search</span></a
|
||||
>
|
||||
<a class="tab" href="/queues" data-dest="queues"
|
||||
><span data-glyph="warning" aria-hidden="true"></span><span class="tab-label">queues</span
|
||||
><span class="tab-badge readout" data-role="queues-count" hidden></span
|
||||
></a>
|
||||
<a class="tab" href="/settings" data-dest="settings"
|
||||
><span data-glyph="cog" aria-hidden="true"></span><span class="tab-label">settings</span></a
|
||||
>
|
||||
</nav>
|
||||
|
||||
<div class="deck-filter" id="deck-filter" role="toolbar" aria-label="show only one kind" hidden>
|
||||
<button type="button" class="control control-filter" data-kind="all" aria-pressed="true">
|
||||
<main class="deck" id="deck" hidden aria-label="search results">
|
||||
<header class="page-bar">
|
||||
<h2 class="page-title">search</h2>
|
||||
<p class="head-status readout" id="deck-status" role="status" data-empty="true"></p>
|
||||
</header>
|
||||
|
||||
<div class="segmented deck-filter" id="deck-filter" role="toolbar" aria-label="show only one kind" hidden>
|
||||
<button type="button" class="seg" data-kind="all" aria-pressed="true">
|
||||
all<span class="filter-count" data-role="count"></span>
|
||||
</button>
|
||||
<button type="button" class="control control-filter" data-kind="movie" aria-pressed="false">
|
||||
<button type="button" class="seg" data-kind="movie" aria-pressed="false">
|
||||
movies<span class="filter-count" data-role="count"></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-filter"
|
||||
data-kind="series"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<button type="button" class="seg" data-kind="series" aria-pressed="false">
|
||||
series<span class="filter-count" data-role="count"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="deck-group" id="group-library" hidden aria-labelledby="label-library">
|
||||
<header class="deck-head">
|
||||
<h2 class="deck-label" id="label-library">in library</h2>
|
||||
<h3 class="deck-label" id="label-library">in library</h3>
|
||||
<span class="deck-count readout" id="count-library"></span>
|
||||
</header>
|
||||
<ul class="deck-rows" id="rows-library"></ul>
|
||||
@@ -142,7 +167,7 @@
|
||||
|
||||
<section class="deck-group" id="group-tmdb" hidden aria-labelledby="label-tmdb">
|
||||
<header class="deck-head">
|
||||
<h2 class="deck-label" id="label-tmdb">on tmdb</h2>
|
||||
<h3 class="deck-label" id="label-tmdb">on tmdb</h3>
|
||||
<span class="deck-count readout" id="count-tmdb"></span>
|
||||
</header>
|
||||
<ul class="deck-rows" id="rows-tmdb"></ul>
|
||||
@@ -150,22 +175,22 @@
|
||||
|
||||
<section class="deck-group" id="group-manual" hidden aria-labelledby="label-manual">
|
||||
<header class="deck-head">
|
||||
<h2 class="deck-label" id="label-manual">manual grab</h2>
|
||||
<h3 class="deck-label" id="label-manual">manual grab</h3>
|
||||
</header>
|
||||
<article class="module intake" id="manual-intake"></article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main class="deck" id="library" hidden aria-label="library">
|
||||
<header class="deck-head library-bar">
|
||||
<h2 class="deck-label">library</h2>
|
||||
<header class="page-bar">
|
||||
<h2 class="page-title">library</h2>
|
||||
<span class="deck-count readout" id="library-summary"></span>
|
||||
<div class="view-toggle">
|
||||
<button type="button" class="view-btn" id="library-view-grid" aria-pressed="true">grid</button>
|
||||
<button type="button" class="view-btn" id="library-view-list" aria-pressed="false">list</button>
|
||||
</div>
|
||||
<p class="head-status readout" id="library-status" role="status" data-empty="true"></p>
|
||||
<fieldset class="segmented" aria-label="layout">
|
||||
<button type="button" class="seg seg-icon" id="library-view-grid" aria-pressed="true" aria-label="poster grid" data-glyph="grid"></button>
|
||||
<button type="button" class="seg seg-icon" id="library-view-list" aria-pressed="false" aria-label="list" data-glyph="list"></button>
|
||||
</fieldset>
|
||||
</header>
|
||||
<p class="deck-status readout" id="library-status" role="status" hidden></p>
|
||||
|
||||
<div id="library-libraries"></div>
|
||||
|
||||
@@ -187,22 +212,24 @@
|
||||
</main>
|
||||
|
||||
<main class="deck" id="queues" hidden aria-label="attention queues">
|
||||
<header class="deck-head library-bar">
|
||||
<h2 class="deck-label">attention</h2>
|
||||
<header class="page-bar">
|
||||
<h2 class="page-title">queues</h2>
|
||||
<span class="deck-count readout" id="queues-summary"></span>
|
||||
<p class="head-status readout" id="queues-status" role="status" data-empty="true"></p>
|
||||
</header>
|
||||
<p class="deck-status readout" id="queues-status" role="status" hidden></p>
|
||||
|
||||
<section class="deck-group" id="queue-no-pt" hidden aria-labelledby="label-no-pt">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-no-pt">no pt source</h3>
|
||||
<span class="deck-count readout" id="count-no-pt"></span>
|
||||
</header>
|
||||
<p class="queue-note readout dim">
|
||||
kids titles with no qualifying pt release — parked here, sometimes for months, by
|
||||
design. allow english writes a movie's override and its search proceeds; series wait
|
||||
for the detail view.
|
||||
</p>
|
||||
<div class="group-note" data-explain-group>
|
||||
<p>Kids titles with no Portuguese release yet.</p>
|
||||
<p class="group-note-more" hidden>
|
||||
They can sit here for months, by design. Allow English writes the movie's override
|
||||
and its search goes ahead. A series waits for its own page.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-no-pt"></ul>
|
||||
</section>
|
||||
|
||||
@@ -211,10 +238,12 @@
|
||||
<h3 class="deck-label" id="label-decision">needs decision</h3>
|
||||
<span class="deck-count readout" id="count-decision"></span>
|
||||
</header>
|
||||
<p class="queue-note readout dim">
|
||||
hard-failed twice on different releases — open the movie's page and decide by hand;
|
||||
series entries are display-only for now.
|
||||
</p>
|
||||
<div class="group-note" data-explain-group>
|
||||
<p>Titles where two different releases failed.</p>
|
||||
<p class="group-note-more" hidden>
|
||||
Open the movie and pick a release by hand. Series entries are display-only for now.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-decision"></ul>
|
||||
</section>
|
||||
|
||||
@@ -223,11 +252,13 @@
|
||||
<h3 class="deck-label" id="label-subtitles">missing subtitles</h3>
|
||||
<span class="deck-count readout" id="count-subtitles"></span>
|
||||
</header>
|
||||
<p class="queue-note readout dim">
|
||||
a wanted language with no subtitle, and why — no candidate, a translation or provider
|
||||
failure, the daily budget, or a sync `alass` flagged. open the title to search,
|
||||
translate or retry by hand.
|
||||
</p>
|
||||
<div class="group-note" data-explain-group>
|
||||
<p>A wanted subtitle language is missing.</p>
|
||||
<p class="group-note-more" hidden>
|
||||
Each chip says why: no candidate, a translation or provider failure, the daily budget,
|
||||
or a timing sync that looked wrong. Open the title to search, translate or retry.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-subtitles"></ul>
|
||||
</section>
|
||||
</main>
|
||||
@@ -248,77 +279,67 @@
|
||||
deck section in view.
|
||||
-->
|
||||
|
||||
<main class="deck" id="movie" hidden aria-label="movie detail">
|
||||
<p class="deck-status readout" id="movie-status" role="status" hidden></p>
|
||||
|
||||
<section class="module movie-hero" id="movie-hero" aria-label="title metadata">
|
||||
<div class="movie-body">
|
||||
<img class="movie-poster" id="movie-poster" alt="" hidden />
|
||||
<div class="movie-info">
|
||||
<div class="movie-idline">
|
||||
<h2 class="movie-title" id="movie-title">—</h2>
|
||||
<span class="movie-year readout dim" id="movie-year"></span>
|
||||
</div>
|
||||
<span class="row-chips movie-chips" id="movie-chips"></span>
|
||||
<p class="movie-rating readout" id="movie-rating" hidden></p>
|
||||
<p class="movie-meta readout dim" id="movie-meta" hidden></p>
|
||||
<p class="movie-tagline" id="movie-tagline" hidden></p>
|
||||
<p class="movie-overview" id="movie-overview" hidden></p>
|
||||
<div class="movie-actions" id="movie-actions"></div>
|
||||
<main class="deck detail" id="movie" hidden aria-label="movie detail">
|
||||
<section class="hero" id="movie-hero" aria-label="title metadata">
|
||||
<header class="page-head">
|
||||
<button type="button" class="control control-icon" id="movie-back" aria-label="back" data-glyph="back"></button>
|
||||
<h2 class="page-title" id="movie-title" tabindex="-1"> </h2>
|
||||
<span class="page-year readout" id="movie-year"></span>
|
||||
</header>
|
||||
<div class="hero-body">
|
||||
<div class="hero-poster"><img class="movie-poster" id="movie-poster" alt="" /></div>
|
||||
<div class="hero-info">
|
||||
<span class="hero-chips" id="movie-chips"></span>
|
||||
<p class="hero-rating readout" id="movie-rating"></p>
|
||||
<p class="hero-meta readout" id="movie-meta"></p>
|
||||
<p class="hero-tagline" id="movie-tagline"></p>
|
||||
<p class="hero-overview" id="movie-overview"></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="r-note hero-note readout" id="movie-note" role="status" hidden></p>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="movie-controls" aria-label="library controls">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label">library</h3>
|
||||
</header>
|
||||
<div class="movie-controls">
|
||||
<button type="button" class="control control-quiet" id="movie-wanted" aria-pressed="false">
|
||||
wanted
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet"
|
||||
id="movie-blocked"
|
||||
aria-pressed="false"
|
||||
>
|
||||
no targeted search
|
||||
</button>
|
||||
<label class="movie-root-field">
|
||||
<span class="field-label readout dim" id="movie-root-label">root</span>
|
||||
<select class="form-input movie-root" id="movie-root" aria-labelledby="movie-root-label">
|
||||
</select>
|
||||
</label>
|
||||
<span class="movie-controls-space"></span>
|
||||
<button type="button" class="control" id="movie-sweep">search indexers</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet control-icon"
|
||||
data-kind="delete"
|
||||
id="movie-remove"
|
||||
aria-expanded="false"
|
||||
aria-controls="remove-panel"
|
||||
aria-label="remove from the library"
|
||||
>
|
||||
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M2.8 4.3h10.4" />
|
||||
<path d="M5.7 4.3V3.1c0-.5.4-.9.9-.9h2.8c.5 0 .9.4.9.9v1.2" />
|
||||
<path d="M4.2 4.3l.5 8.5c0 .8.7 1.4 1.5 1.4h3.6c.8 0 1.5-.6 1.5-1.4l.5-8.5" />
|
||||
<path d="M6.6 7v4.2" />
|
||||
<path d="M9.4 7v4.2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="control-note readout dim" id="movie-blocked-note" hidden>
|
||||
targeted search is off for this title. RSS still matches it, so a release can still arrive.
|
||||
<div class="actionbar">
|
||||
<button type="button" class="control" id="movie-sweep">search indexers</button>
|
||||
<span class="actionbar-links" id="movie-actions"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-icon"
|
||||
id="movie-more"
|
||||
aria-label="library settings for this title"
|
||||
aria-expanded="false"
|
||||
aria-controls="movie-more-panel"
|
||||
data-glyph="more"
|
||||
></button>
|
||||
</div>
|
||||
<div class="more-panel" id="movie-more-panel" hidden>
|
||||
<div class="more-links" id="movie-links"></div>
|
||||
<div class="more-toggles" id="movie-toggles"></div>
|
||||
<p class="control-note readout" id="movie-blocked-note" hidden>
|
||||
Targeted search is off. RSS still matches this title, so a release can still arrive.
|
||||
</p>
|
||||
<label class="field more-field">
|
||||
<span class="field-label" id="movie-root-label">root</span>
|
||||
<select class="form-input" id="movie-root" aria-labelledby="movie-root-label"></select>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet"
|
||||
data-kind="delete"
|
||||
id="movie-remove"
|
||||
aria-expanded="false"
|
||||
aria-controls="remove-panel"
|
||||
>
|
||||
remove from library
|
||||
</button>
|
||||
<div class="remove-panel" id="remove-panel" hidden></div>
|
||||
</section>
|
||||
</div>
|
||||
<p class="deck-status readout" id="movie-status" role="status" data-empty="true"></p>
|
||||
|
||||
<section class="deck-group" id="movie-files" hidden aria-labelledby="label-disk">
|
||||
<section class="deck-group" id="movie-files" aria-labelledby="label-disk">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-disk">on disk</h3>
|
||||
<h3 class="section-title" id="label-disk">on disk</h3>
|
||||
<span class="deck-count readout" id="count-disk"></span>
|
||||
</header>
|
||||
<ul class="deck-rows" id="rows-disk"></ul>
|
||||
@@ -326,185 +347,122 @@
|
||||
|
||||
<section class="deck-group" id="movie-releases" aria-labelledby="label-releases">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-releases">releases</h3>
|
||||
<h3 class="section-title" id="label-releases">releases</h3>
|
||||
</header>
|
||||
<p class="deck-status readout" id="releases-status" role="status" hidden></p>
|
||||
|
||||
<section class="deck-group" id="bucket-eligible" hidden aria-labelledby="label-eligible">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-eligible">eligible</h3>
|
||||
<span class="deck-count readout" id="count-eligible"></span>
|
||||
</header>
|
||||
<div class="colhead" aria-hidden="true">
|
||||
<span class="cw cw-score">score</span>
|
||||
<span class="cw cw-res">res</span>
|
||||
<span class="cw cw-src">source</span>
|
||||
<span class="cw cw-hdr">hdr</span>
|
||||
<span class="cw cw-aud">audio</span>
|
||||
<span class="cw cw-size">size</span>
|
||||
<span class="cw cw-seed">seed</span>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-eligible"></ul>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="bucket-waived" hidden aria-labelledby="label-waived">
|
||||
<header class="deck-head bucket-head">
|
||||
<h3 class="deck-label" id="label-waived">below policy</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="bucket-toggle readout"
|
||||
id="toggle-waived"
|
||||
aria-expanded="false"
|
||||
aria-controls="wrap-waived"
|
||||
></button>
|
||||
</header>
|
||||
<div id="wrap-waived" hidden>
|
||||
<div class="colhead" aria-hidden="true">
|
||||
<span class="cw cw-score">score</span>
|
||||
<span class="cw cw-res">res</span>
|
||||
<span class="cw cw-src">source</span>
|
||||
<span class="cw cw-hdr">hdr</span>
|
||||
<span class="cw cw-aud">audio</span>
|
||||
<span class="cw cw-size">size</span>
|
||||
<span class="cw cw-seed">seed</span>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-waived"></ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="bucket-rejected" hidden aria-labelledby="label-rejected">
|
||||
<header class="deck-head bucket-head">
|
||||
<h3 class="deck-label" id="label-rejected">rejected</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="bucket-toggle readout"
|
||||
id="toggle-rejected"
|
||||
aria-expanded="false"
|
||||
aria-controls="wrap-rejected"
|
||||
></button>
|
||||
</header>
|
||||
<div id="wrap-rejected" hidden>
|
||||
<div class="colhead" aria-hidden="true">
|
||||
<span class="cw cw-score">score</span>
|
||||
<span class="cw cw-res">res</span>
|
||||
<span class="cw cw-src">source</span>
|
||||
<span class="cw cw-hdr">hdr</span>
|
||||
<span class="cw cw-aud">audio</span>
|
||||
<span class="cw cw-size">size</span>
|
||||
<span class="cw cw-seed">seed</span>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-rejected"></ul>
|
||||
</div>
|
||||
</section>
|
||||
<p class="deck-status readout" id="releases-status" role="status" data-empty="true"></p>
|
||||
<div class="buckets" id="movie-buckets"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main class="deck" id="settings" hidden aria-label="settings">
|
||||
<header class="deck-head library-bar">
|
||||
<h2 class="deck-label">settings</h2>
|
||||
<header class="page-bar">
|
||||
<h2 class="page-title">settings</h2>
|
||||
<span class="deck-count readout" id="settings-summary"></span>
|
||||
<p class="head-status readout" id="settings-status" role="status" data-empty="true"></p>
|
||||
</header>
|
||||
<p class="deck-status readout" id="settings-status" role="status" hidden></p>
|
||||
|
||||
<section class="deck-group" id="group-chain" aria-labelledby="label-chain">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-chain">signal chain</h3>
|
||||
<h3 class="section-title" id="label-chain">signal chain</h3>
|
||||
</header>
|
||||
<section class="chain" aria-label="signal chain">
|
||||
<article class="module area-tmdb" data-check="tmdb">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">tmdb</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">metadata in</p>
|
||||
<p class="module-detail readout" data-role="detail">titles, languages, release dates</p>
|
||||
</article>
|
||||
<section class="chain-wrap" aria-label="signal chain">
|
||||
<div class="chain">
|
||||
<article class="module area-tmdb" data-check="tmdb">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h4 class="module-name">tmdb</h4>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">metadata in</p>
|
||||
<p class="module-detail readout" data-role="detail">titles, languages, release dates</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t1" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="tmdb" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
<div class="bus area-t1" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="tmdb" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-prowlarr" data-check="prowlarr">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h4 class="module-name">prowlarr</h4>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">indexers in</p>
|
||||
<p class="module-detail readout" data-role="detail">torznab search and rss</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t2" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="prowlarr" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module module-master area-master" data-check="master">
|
||||
<header class="module-head">
|
||||
<span class="lamp lamp-big" data-state="probing" aria-hidden="true"></span>
|
||||
<h4 class="module-name">arr</h4>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">reconcile core</p>
|
||||
<p class="module-detail readout" data-role="detail">decide · find · fetch · import</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-out" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="master" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-qbittorrent" data-check="qbit">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h4 class="module-name">qbittorrent</h4>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">torrents out</p>
|
||||
<p class="module-detail readout" data-role="detail">webui · grab and seed</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article class="module area-prowlarr" data-check="prowlarr">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">prowlarr</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">indexers in</p>
|
||||
<p class="module-detail readout" data-role="detail">torznab search and rss</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t2" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="prowlarr" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module module-master area-master" data-check="master">
|
||||
<header class="module-head">
|
||||
<span class="lamp lamp-big" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">arr</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">reconcile core</p>
|
||||
<p class="module-detail readout" data-role="detail">decide · find · fetch · import</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-out" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="master" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-qbittorrent" data-check="qbit">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">qbittorrent</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">torrents out</p>
|
||||
<p class="module-detail readout" data-role="detail">webui · grab and seed</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="chain-lane" id="chain-subs" aria-label="subtitle lane" hidden></section>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="group-roots" hidden aria-labelledby="label-roots">
|
||||
<header class="deck-head bucket-head">
|
||||
<h3 class="deck-label" id="label-roots">roots</h3>
|
||||
<button type="button" class="control control-quiet readout" id="root-add">add root</button>
|
||||
<header class="deck-head">
|
||||
<h3 class="section-title" id="label-roots">roots</h3>
|
||||
<button type="button" class="control control-quiet head-action" id="root-add">add root</button>
|
||||
</header>
|
||||
<ul class="deck-rows" id="rows-roots"></ul>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="group-policies" hidden aria-labelledby="label-policies">
|
||||
<header class="deck-head bucket-head">
|
||||
<h3 class="deck-label" id="label-policies">policies</h3>
|
||||
<button type="button" class="control control-quiet readout" id="policy-add">
|
||||
add policy
|
||||
</button>
|
||||
<header class="deck-head">
|
||||
<h3 class="section-title" id="label-policies">policies</h3>
|
||||
<button type="button" class="control control-quiet head-action" id="policy-add">add policy</button>
|
||||
</header>
|
||||
<p class="queue-note readout dim">
|
||||
a policy edit rescores every title's releases immediately — open any title to check it.
|
||||
</p>
|
||||
<div class="group-note">
|
||||
<p>Editing a policy rescores every title's releases straight away.</p>
|
||||
</div>
|
||||
<ul class="deck-rows" id="rows-policies"></ul>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="group-subtitles" hidden aria-labelledby="label-settings-subtitles">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-settings-subtitles">subtitles</h3>
|
||||
<h3 class="section-title" id="label-settings-subtitles">subtitles</h3>
|
||||
</header>
|
||||
<p class="queue-note readout dim">
|
||||
provider credentials, translator keys and the remote-command template
|
||||
live in config/env (§10) and are never shown here.
|
||||
</p>
|
||||
<div class="group-note" data-explain-group>
|
||||
<p>Provider logins and translator keys are not shown here.</p>
|
||||
<p class="group-note-more" hidden>
|
||||
They live in the daemon's config and environment (§10), along with the remote-command
|
||||
template.
|
||||
</p>
|
||||
</div>
|
||||
<div id="subtitles-body"></div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -521,78 +479,86 @@
|
||||
something: an unaired episode offers no search button at all.
|
||||
-->
|
||||
|
||||
<main class="deck releases" id="series" hidden aria-label="series detail">
|
||||
<p class="deck-status readout" id="series-status" role="status" hidden></p>
|
||||
|
||||
<section class="module movie-hero" id="series-hero" aria-label="title metadata">
|
||||
<div class="movie-body">
|
||||
<img class="movie-poster" id="series-poster" alt="" hidden />
|
||||
<div class="movie-info">
|
||||
<div class="movie-idline">
|
||||
<h2 class="movie-title" id="series-title">—</h2>
|
||||
<span class="movie-year readout dim" id="series-year"></span>
|
||||
</div>
|
||||
<main class="deck detail" id="series" hidden aria-label="series detail">
|
||||
<section class="hero" id="series-hero" aria-label="title metadata">
|
||||
<header class="page-head">
|
||||
<button type="button" class="control control-icon" id="series-back" aria-label="back" data-glyph="back"></button>
|
||||
<h2 class="page-title" id="series-title" tabindex="-1"> </h2>
|
||||
<span class="page-year readout" id="series-year"></span>
|
||||
</header>
|
||||
<div class="hero-body">
|
||||
<div class="hero-poster"><img class="movie-poster" id="series-poster" alt="" /></div>
|
||||
<div class="hero-info">
|
||||
<!-- audience, episode counts and §4.2 status: operational state,
|
||||
outranking the artwork, folded into this one header (issue 150) -->
|
||||
<span class="row-chips movie-chips" id="series-chips"></span>
|
||||
<p class="movie-rating readout" id="series-rating" hidden></p>
|
||||
<p class="movie-meta readout dim" id="series-meta" hidden></p>
|
||||
<p class="movie-tagline" id="series-tagline" hidden></p>
|
||||
<p class="movie-overview" id="series-overview" hidden></p>
|
||||
<div class="movie-actions" id="series-actions"></div>
|
||||
<span class="hero-chips" id="series-chips"></span>
|
||||
<p class="hero-rating readout" id="series-rating"></p>
|
||||
<p class="hero-meta readout" id="series-meta"></p>
|
||||
<p class="hero-tagline" id="series-tagline"></p>
|
||||
<p class="hero-overview" id="series-overview"></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="r-note hero-note readout" id="series-note" role="status" hidden></p>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="series-library" aria-label="library controls">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label">library</h3>
|
||||
</header>
|
||||
<div class="movie-controls">
|
||||
<label class="movie-root-field">
|
||||
<span class="field-label readout dim" id="series-root-label">root</span>
|
||||
<select
|
||||
class="form-input movie-root"
|
||||
id="series-root"
|
||||
data-focus-id="series-root"
|
||||
aria-labelledby="series-root-label"
|
||||
></select>
|
||||
</label>
|
||||
<span class="movie-controls-space"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet control-icon"
|
||||
data-kind="delete"
|
||||
id="series-remove"
|
||||
aria-expanded="false"
|
||||
aria-controls="series-remove-panel"
|
||||
aria-label="remove from the library"
|
||||
>
|
||||
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M2.8 4.3h10.4" />
|
||||
<path d="M5.7 4.3V3.1c0-.5.4-.9.9-.9h2.8c.5 0 .9.4.9.9v1.2" />
|
||||
<path d="M4.2 4.3l.5 8.5c0 .8.7 1.4 1.5 1.4h3.6c.8 0 1.5-.6 1.5-1.4l.5-8.5" />
|
||||
<path d="M6.6 7v4.2" />
|
||||
<path d="M9.4 7v4.2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="actionbar">
|
||||
<span class="actionbar-links" id="series-actions"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-icon"
|
||||
id="series-more"
|
||||
aria-label="library settings for this title"
|
||||
aria-expanded="false"
|
||||
aria-controls="series-more-panel"
|
||||
data-glyph="more"
|
||||
></button>
|
||||
</div>
|
||||
<div class="more-panel" id="series-more-panel" hidden>
|
||||
<div class="more-links" id="series-links"></div>
|
||||
<label class="field more-field">
|
||||
<span class="field-label" id="series-root-label">root</span>
|
||||
<select
|
||||
class="form-input"
|
||||
id="series-root"
|
||||
data-focus-id="series-root"
|
||||
aria-labelledby="series-root-label"
|
||||
></select>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet"
|
||||
data-kind="delete"
|
||||
id="series-remove"
|
||||
aria-expanded="false"
|
||||
aria-controls="series-remove-panel"
|
||||
>
|
||||
remove from library
|
||||
</button>
|
||||
<div class="remove-panel" id="series-remove-panel" hidden></div>
|
||||
</section>
|
||||
</div>
|
||||
<p class="deck-status readout" id="series-status" role="status" data-empty="true"></p>
|
||||
|
||||
<ul class="deck-rows seasons" id="rows-seasons"></ul>
|
||||
<section class="deck-group" aria-labelledby="label-seasons">
|
||||
<header class="deck-head">
|
||||
<h3 class="section-title" id="label-seasons">seasons</h3>
|
||||
</header>
|
||||
<ul class="deck-rows seasons" id="rows-seasons"></ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main class="deck releases" id="tv-releases" hidden aria-label="season and episode releases">
|
||||
<header class="releases-head">
|
||||
<button type="button" class="control" id="tv-releases-back">back</button>
|
||||
<div class="releases-id">
|
||||
<h2 class="releases-title" id="tv-releases-title">—</h2>
|
||||
<span class="releases-year readout dim" id="tv-releases-sub"></span>
|
||||
<main class="deck detail" id="tv-releases" hidden aria-label="season and episode releases">
|
||||
<header class="page-head">
|
||||
<button type="button" class="control control-icon" id="tv-releases-back" aria-label="back" data-glyph="back"></button>
|
||||
<div class="page-id">
|
||||
<h2 class="page-title" id="tv-releases-title"> </h2>
|
||||
<span class="page-sub readout" id="tv-releases-sub"></span>
|
||||
</div>
|
||||
<button type="button" class="control" id="tv-releases-sweep">re-search</button>
|
||||
</header>
|
||||
<p class="deck-status readout" id="tv-releases-status" role="status" hidden></p>
|
||||
<div class="actionbar">
|
||||
<button type="button" class="control" id="tv-releases-sweep">search indexers</button>
|
||||
</div>
|
||||
<p class="deck-status readout" id="tv-releases-status" role="status" data-empty="true"></p>
|
||||
<!-- issue 227: a pack that downloaded in full and was condemned at
|
||||
import (§5.7). It outlives the status line because it is a fact
|
||||
about the season, not about the request in flight. -->
|
||||
@@ -602,12 +568,11 @@
|
||||
<p class="notice-line" id="tv-failure-what"></p>
|
||||
<p class="notice-line" id="tv-failure-next"></p>
|
||||
<p class="notice-line dim">
|
||||
nothing was imported, and the torrent was left where it is: nothing is
|
||||
deleted early to satisfy the library (§7.3).
|
||||
Nothing was imported, and the torrent was left where it is: nothing is deleted early to
|
||||
satisfy the library (§7.3).
|
||||
</p>
|
||||
</section>
|
||||
<div id="tv-buckets"></div>
|
||||
<div class="buckets" id="tv-buckets"></div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1290
-1390
File diff suppressed because it is too large
Load Diff
@@ -115,4 +115,24 @@ export function navigate(route: Route, options: { replace?: boolean } = {}) {
|
||||
} else {
|
||||
history.pushState(null, "", path);
|
||||
}
|
||||
window.dispatchEvent(new Event(ROUTE_EVENT));
|
||||
}
|
||||
|
||||
/** Fired after every `navigate` and on popstate: the shell repaints its active tab. */
|
||||
export const ROUTE_EVENT = "arr:route";
|
||||
|
||||
/** Which of the four destinations a route belongs to. */
|
||||
export type Destination = "library" | "search" | "queues" | "settings";
|
||||
|
||||
export function destinationOf(route: Route): Destination {
|
||||
switch (route.kind) {
|
||||
case "queues":
|
||||
return "queues";
|
||||
case "settings":
|
||||
return "settings";
|
||||
case "search":
|
||||
return "search";
|
||||
default:
|
||||
return "library";
|
||||
}
|
||||
}
|
||||
|
||||
+82
-118
@@ -6,6 +6,7 @@ import { armedDeleteIcon, icon } from "./icons";
|
||||
import { navigate } from "./router";
|
||||
import type { Root } from "./search";
|
||||
import { forgetSubtitleOptions } from "./subtitles";
|
||||
import { control, type RowDom, row, setSlot, chip as uiChip } from "./ui";
|
||||
|
||||
/** One policy row: the JSON columns as the API returns them. */
|
||||
export interface PolicyDoc {
|
||||
@@ -166,13 +167,7 @@ function el<K extends keyof HTMLElementTagNameMap>(
|
||||
}
|
||||
|
||||
function chip(text: string): HTMLSpanElement {
|
||||
return el("span", "chip readout", text);
|
||||
}
|
||||
|
||||
function rowTitle(title: string): HTMLElement {
|
||||
const wrap = el("span", "row-id");
|
||||
wrap.append(el("span", "row-title", title));
|
||||
return wrap;
|
||||
return uiChip(text);
|
||||
}
|
||||
|
||||
function field(labelText: string, control: HTMLElement): HTMLDivElement {
|
||||
@@ -226,10 +221,9 @@ function listValue(input: HTMLInputElement): string[] {
|
||||
|
||||
/** Icon-only edit toggle for one row; the label names the row it opens. */
|
||||
function editIcon(label: string): HTMLButtonElement {
|
||||
const button = el("button", "control control-quiet control-icon");
|
||||
const button = el("button", "control control-icon");
|
||||
button.type = "button";
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = label;
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
button.append(icon("pencil"));
|
||||
return button;
|
||||
@@ -243,8 +237,6 @@ interface SettingsView {
|
||||
export function settingsMain(views: { hide: () => void }[], goHome: () => void): SettingsView {
|
||||
const view = must<HTMLElement>("#settings");
|
||||
const deck = must<HTMLElement>("#deck");
|
||||
const nav = must<HTMLButtonElement>("#nav-settings");
|
||||
nav.append(icon("cog"));
|
||||
const summary = must<HTMLElement>("#settings-summary");
|
||||
const status = must<HTMLElement>("#settings-status");
|
||||
const rootsSection = must<HTMLElement>("#group-roots");
|
||||
@@ -261,13 +253,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
let sequence = 0;
|
||||
|
||||
function setStatus(text: string | null, tone?: "fault") {
|
||||
status.hidden = text === null;
|
||||
status.textContent = text ?? "";
|
||||
if (tone) {
|
||||
status.dataset.tone = tone;
|
||||
} else {
|
||||
delete status.dataset.tone;
|
||||
}
|
||||
setSlot(status, text, tone);
|
||||
}
|
||||
|
||||
function clearGroups() {
|
||||
@@ -327,54 +313,65 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
|
||||
/* -- roots -------------------------------------------------------------- */
|
||||
|
||||
/** The row's line: identity, chips right, the icon action pair, feedback. */
|
||||
function rowLine(
|
||||
item: HTMLLIElement,
|
||||
/**
|
||||
* One settings row: identity, chips, then the edit and delete pair in the
|
||||
* row's fixed action column. The edit panel opens in the row's tray.
|
||||
*/
|
||||
function settingsRow(
|
||||
title: string,
|
||||
chips: HTMLSpanElement,
|
||||
del: HTMLButtonElement,
|
||||
edit: HTMLButtonElement,
|
||||
): HTMLParagraphElement {
|
||||
const actions = el("div", "settings-row-actions");
|
||||
actions.append(edit, del);
|
||||
const note = el("p", "add-note readout");
|
||||
note.setAttribute("role", "status");
|
||||
note.hidden = true;
|
||||
item.append(rowTitle(title), chips, actions, note);
|
||||
return note;
|
||||
}
|
||||
|
||||
function rootRow(root: Root): HTMLLIElement {
|
||||
const item = el("li", "queue-item settings-row");
|
||||
const chips = el("span", "row-chips");
|
||||
chips.append(chip(root.kind), chip(root.audience), chip(root.policy_name));
|
||||
|
||||
const del = armedDeleteIcon(`delete root ${root.path}`, () => {
|
||||
void deleteRow("roots", root.id).then((detail) => {
|
||||
if (detail !== null) {
|
||||
note.hidden = false;
|
||||
note.dataset.tone = "fault";
|
||||
note.textContent = detail;
|
||||
return;
|
||||
}
|
||||
void load();
|
||||
});
|
||||
chips: HTMLSpanElement[],
|
||||
label: string,
|
||||
remove: () => Promise<string | null>,
|
||||
panel: () => HTMLElement,
|
||||
): HTMLLIElement {
|
||||
let say: RowDom["say"] = () => {};
|
||||
const del = armedDeleteIcon(
|
||||
`delete ${label}`,
|
||||
() => {
|
||||
void remove().then((detail) => {
|
||||
if (detail !== null) {
|
||||
del.disabled = false;
|
||||
say(detail, "fault");
|
||||
return;
|
||||
}
|
||||
void load();
|
||||
});
|
||||
},
|
||||
{ explain: (text) => say(text, "warn") },
|
||||
);
|
||||
const edit = editIcon(`edit ${label}`);
|
||||
const parts = row({
|
||||
kind: "setting",
|
||||
title,
|
||||
meta: chips,
|
||||
actions: [
|
||||
{ node: edit, width: "icon" },
|
||||
{ node: del, width: "icon" },
|
||||
],
|
||||
});
|
||||
|
||||
const edit = editIcon(`edit root ${root.path}`);
|
||||
say = parts.say;
|
||||
parts.body.querySelector(".r-title")?.classList.add("readout");
|
||||
edit.addEventListener("click", () => {
|
||||
const existing = item.querySelector(".edit-panel");
|
||||
const existing = parts.tray.querySelector(".edit-panel");
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
edit.setAttribute("aria-expanded", "false");
|
||||
return;
|
||||
}
|
||||
item.append(rootPanel(root));
|
||||
parts.tray.append(panel());
|
||||
edit.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
return parts.item;
|
||||
}
|
||||
|
||||
const note = rowLine(item, root.path, chips, del, edit);
|
||||
return item;
|
||||
function rootRow(root: Root): HTMLLIElement {
|
||||
return settingsRow(
|
||||
root.path,
|
||||
[chip(root.kind), chip(root.audience), chip(root.policy_name)],
|
||||
`root ${root.path}`,
|
||||
() => deleteRow("roots", root.id),
|
||||
() => rootPanel(root),
|
||||
);
|
||||
}
|
||||
|
||||
function rootPanel(root: Root | null): HTMLElement {
|
||||
@@ -407,8 +404,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
note.setAttribute("role", "status");
|
||||
note.hidden = true;
|
||||
|
||||
const save = el("button", "control readout", root === null ? "create root" : "save root");
|
||||
save.type = "button";
|
||||
const save = control(root === null ? "create root" : "save root");
|
||||
save.addEventListener("click", () => {
|
||||
save.disabled = true;
|
||||
note.hidden = false;
|
||||
@@ -432,8 +428,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
});
|
||||
});
|
||||
|
||||
const cancel = el("button", "control control-quiet readout", "cancel");
|
||||
cancel.type = "button";
|
||||
const cancel = control("cancel", { quiet: true });
|
||||
cancel.addEventListener("click", () => {
|
||||
panel.remove();
|
||||
});
|
||||
@@ -455,39 +450,18 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
}
|
||||
|
||||
function policyRow(policy: PolicyDoc): HTMLLIElement {
|
||||
const item = el("li", "queue-item settings-row");
|
||||
const chips = el("span", "row-chips");
|
||||
chips.append(chip(policy.resolution_pref.join(" › ") || "no resolutions"));
|
||||
chips.append(chip(requiredAudioLabel(policy)));
|
||||
const bandCount = Object.keys(policy.size_bands).length;
|
||||
chips.append(chip(`${bandCount} band${bandCount === 1 ? "" : "s"}`));
|
||||
|
||||
const del = armedDeleteIcon(`delete policy ${policy.name}`, () => {
|
||||
void deleteRow("policies", policy.id).then((detail) => {
|
||||
if (detail !== null) {
|
||||
note.hidden = false;
|
||||
note.dataset.tone = "fault";
|
||||
note.textContent = detail;
|
||||
return;
|
||||
}
|
||||
void load();
|
||||
});
|
||||
});
|
||||
|
||||
const edit = editIcon(`edit policy ${policy.name}`);
|
||||
edit.addEventListener("click", () => {
|
||||
const existing = item.querySelector(".edit-panel");
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
edit.setAttribute("aria-expanded", "false");
|
||||
return;
|
||||
}
|
||||
item.append(policyPanel(policy));
|
||||
edit.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
const note = rowLine(item, policy.name, chips, del, edit);
|
||||
return item;
|
||||
return settingsRow(
|
||||
policy.name,
|
||||
[
|
||||
chip(policy.resolution_pref.join(" › ") || "no resolutions"),
|
||||
chip(requiredAudioLabel(policy)),
|
||||
chip(`${bandCount} band${bandCount === 1 ? "" : "s"}`),
|
||||
],
|
||||
`policy ${policy.name}`,
|
||||
() => deleteRow("policies", policy.id),
|
||||
() => policyPanel(policy),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -635,8 +609,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
scoreInputs[key] = input;
|
||||
}
|
||||
|
||||
const save = el("button", "control readout", policy === null ? "create policy" : "save policy");
|
||||
save.type = "button";
|
||||
const save = control(policy === null ? "create policy" : "save policy");
|
||||
save.addEventListener("click", () => {
|
||||
const bad = panel.querySelector<HTMLElement>("[data-invalid]");
|
||||
if (bad !== null) {
|
||||
@@ -704,8 +677,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
});
|
||||
});
|
||||
|
||||
const cancel = el("button", "control control-quiet readout", "cancel");
|
||||
cancel.type = "button";
|
||||
const cancel = control("cancel", { quiet: true });
|
||||
cancel.addEventListener("click", () => {
|
||||
panel.remove();
|
||||
});
|
||||
@@ -730,11 +702,10 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
* ordinal selector would quietly disable the wrong control.
|
||||
*/
|
||||
function moveButton(direction: "up" | "down", label: string): HTMLButtonElement {
|
||||
const button = el("button", "control control-quiet control-icon provider-move");
|
||||
const button = el("button", "control control-icon provider-move");
|
||||
button.type = "button";
|
||||
button.dataset.move = direction;
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = label;
|
||||
button.append(icon(direction === "up" ? "chevronUp" : "chevronDown"));
|
||||
return button;
|
||||
}
|
||||
@@ -750,23 +721,31 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
enabled: boolean,
|
||||
budget: number,
|
||||
): { item: HTMLLIElement; enabledInput: HTMLInputElement; budgetInput: HTMLInputElement } {
|
||||
const item = el("li", "provider-row");
|
||||
item.dataset.provider = provider;
|
||||
|
||||
const enabledInput = document.createElement("input");
|
||||
enabledInput.type = "checkbox";
|
||||
enabledInput.className = "provider-toggle";
|
||||
enabledInput.checked = enabled;
|
||||
enabledInput.setAttribute("aria-label", `enable ${provider}`);
|
||||
|
||||
const name = el("span", "row-title provider-name", provider);
|
||||
|
||||
const budgetInput = numberControl(budget);
|
||||
budgetInput.classList.add("provider-budget");
|
||||
budgetInput.setAttribute("aria-label", `${provider} daily budget`);
|
||||
|
||||
const up = moveButton("up", `move ${provider} up`);
|
||||
const down = moveButton("down", `move ${provider} down`);
|
||||
const toggleWrap = el("label", "provider-check");
|
||||
toggleWrap.append(enabledInput);
|
||||
const { item } = row({
|
||||
kind: "provider",
|
||||
lead: toggleWrap,
|
||||
title: provider,
|
||||
actions: [
|
||||
{ node: budgetInput, width: "word" },
|
||||
{ node: up, width: "icon" },
|
||||
{ node: down, width: "icon" },
|
||||
],
|
||||
});
|
||||
item.dataset.provider = provider;
|
||||
|
||||
up.addEventListener("click", () => {
|
||||
const previous = item.previousElementSibling;
|
||||
@@ -783,7 +762,6 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
}
|
||||
});
|
||||
|
||||
item.append(enabledInput, name, budgetInput, up, down);
|
||||
return { item, enabledInput, budgetInput };
|
||||
}
|
||||
|
||||
@@ -885,8 +863,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
note.setAttribute("role", "status");
|
||||
note.hidden = true;
|
||||
|
||||
const save = el("button", "control readout", "save subtitles");
|
||||
save.type = "button";
|
||||
const save = control("save subtitles");
|
||||
save.addEventListener("click", () => {
|
||||
const providerOrder = [...list.children].map(
|
||||
(item) => (item as HTMLElement).dataset.provider ?? "",
|
||||
@@ -956,32 +933,19 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
||||
}
|
||||
deck.hidden = true;
|
||||
view.hidden = false;
|
||||
nav.setAttribute("aria-pressed", "true");
|
||||
void load();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
view.hidden = true;
|
||||
nav.setAttribute("aria-pressed", "false");
|
||||
sequence += 1;
|
||||
}
|
||||
|
||||
function close() {
|
||||
hide();
|
||||
goHome();
|
||||
nav.focus();
|
||||
}
|
||||
|
||||
nav.addEventListener("click", () => {
|
||||
if (view.hidden) {
|
||||
navigate({ kind: "settings" });
|
||||
open();
|
||||
} else {
|
||||
navigate({ kind: "library" });
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
must<HTMLButtonElement>("#root-add").addEventListener("click", () => {
|
||||
rootsSection.append(rootPanel(null));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user