{% extends "base.html" %} {% block title %}{{ _('Catalogue') }} — MediaForge{% endblock %} {% block styles %} {% endblock %} {% block content %}
{# Two rows on purpose: what you PRESS on the first, what you NARROW BY on the second. With source chips, status chips, a count and four buttons on one line the wrap order changed with every source a user enabled. #}
{# The list is merged; these only narrow it down. Sources on the left, the three row badges as filters on the right — same chip, same "on by default, click to hide" rule for both groups. #}
{# Freshness and background work, in the one place the work is about. The lists come from the database now, so the page never waits on a source site -- but "instantly served from yesterday's copy" has to SAY so, and a refresh or an id resolution nobody can see is one the user assumes is broken. Hidden entirely when nothing is running and everything is fresh. #} {# The rail lives beside the viewport rather than inside it: overlaying the scrollbar means the two fight for the same 16px on desktop, and a finger resting on the rail must not also scroll the list underneath. #}
{# Shown while dragging the rail — on a phone the finger covers the letter it is on, so the current one is repeated to its left. #}
{# The bar is only in the way when there is nothing to act on, so on a phone it is not there at all until something is marked (see .is-empty in catalogue.css). Collapsed it is one line: the count, the primary action and a toggle; language, hoster, the missing-episodes switch, Auto-Sync and the warning all live behind that toggle. Desktop has the room and shows the lot. #}
0 {{ _('selected') }} {# Phone only: opens the sheet. The label carries the current option values, so collapsing them never hides what is set. #}
{# Filled from the same lists the start page uses (lang_labels / sto_lang_labels / supported_providers), passed in by routes/catalogue.py -- they are NOT global context variables, which is exactly why these two dropdowns came up empty at first. #}
{# Which site a merged row downloads from when the user has not pressed one of its pills. Only matters for titles both sites carry; filled from the source list in JS. #}
{# Details — cover, metadata and the episode list for ONE series. Opening it never touches the selection: that is the whole point of a separate modal here rather than the start page's download modal, which owns its own language/provider state and would fight with the bar above. It is the DOWNLOAD MODAL'S OWN MARKUP, class for class: .modal, its .modal-backdrop, its .modal-header with the poster column and .modal-meta, its .genres and .desc. Those rules live in modals.css, which base.html loads on every page, so this dialog is not "styled after" that one -- it IS that one, and the two cannot drift. The previous version was a private copy (.cat-modal-backdrop, a scrim on the card, a blurred-poster fallback, its own gradient stops). Every one of those was a decision the download modal had already made differently, which is exactly why it kept coming out looking like something else. What is left below is only what this dialog has and that one does not: a scrolling episode list and a footer. There is deliberately no link out to the source site. #} {% endblock %} {% block scripts %} {# app.js FIRST, and not optional. It owns the answer to "do I already have this": the download-folder list, the server-side alias index, the Plex/Jellyfin index and the three-stage matching in downloadedFolderFor(). This page was reading those globals without ever loading the file that defines them, so every "In library" check silently answered no — on a library with thousands of titles the page reported zero. Loading it is what the other non-start pages do (favourites, calendar, seerr, advanced-search), and it is the only way this page and the start page cannot disagree about what "downloaded" means. Reimplementing the matching here would have been a second opinion that drifts. #} {% endblock %}