{% extends "base.html" %} {% import '_start_page_form.html' as startpage %} {% block styles %} {% if devinfo_importants %}{% endif %} {% endblock %} {% block content %} {# ── Dev Info banners ────────────────────────────────────────────────── "warning", "important" and "release" posts all land here (app.py's index() builds devinfo_banners from the three). "important" additionally raises the modal further down; the banner is what remains on the page after the modal has been confirmed, so the message stays visible instead of vanishing. "release" shows the announced version and a shortcut into Settings -> Updates; the full text and the changelog live on the Dev Infos page. #} {% if devinfo_banners %}
{% for w in devinfo_banners %}
{% if w.type == 'release' %} {% elif w.type == 'important' %} {% else %} {% endif %}
{% if w.type == 'important' %} {{ _('Important') }} {% elif w.type == 'release' %} {{ _('Release') }} {% endif %}
{{ w.title }}
{# A release banner stays deliberately short: the headline plus the version it announces. The text of the post and the changelog are one click away on the Dev Infos page -- a home page banner is not the place for release notes. #} {% if w.type == 'release' %}
{{ w.release_name or w.release_tag }} {% if w.release_name and w.release_tag and w.release_name != w.release_tag %} {{ w.release_tag }} {% endif %}
{# Deep link into the Updates tab: settings.js's restoreTab() reads the hash and opens that panel, so this lands the user on the update check/install controls rather than the top of Settings. #} {{ _('Go to updates') }} {{ _('Read changelog') }}
{% else %}
{{ w.body_html|safe }}
{% endif %}
{% endfor %}
{% endif %} {# ── "Important" modal ───────────────────────────────────────────────── Rendered only when there is at least one *unread* important post (app.py filters on is_read). Confirming it marks that post as read and navigates to the Dev Infos page so the full text can actually be read -- see static/devinfo_important.js. Server-rendered rather than fetched, so it is on screen with the first paint instead of after a round trip. #} {% if devinfo_importants %} {% endif %}
{# ── "try the new home page" ─────────────────────────────────────────── Only rendered on the classic layout, and only until this account has either dismissed it or switched over once (app.py decides, see show_new_home_promo). Not rendered at all otherwise: a banner that the client hides after the fact still ships its text to everyone and still flashes on a slow paint. The switch is per ACCOUNT. Trying the new layout does not change what anybody else sees, which is the whole reason this is an invitation and not an announcement. #} {% if show_new_home_promo %}
{{ _('Beta') }}
{{ _('Try the new home page') }}
{{ _('Rows grouped by question instead of by source, with your watch progress and watchlist on top. Only you see the change — switch back at any time in Settings.') }}
{% endif %}

{{ _('What do you want to download?') }}

{{ _('One title is enough — MediaForge asks every active source at once.') }}

{# The Seerr modal reuses .search-bar, so the home-only pill styling hangs off the extra .home-searchbar class instead of touching .search-bar itself. #} {# The panel-bar keys below (panel_*, hp_*) live in this template rather than in routes/home_panels.py because babel.cfg extracts Jinja templates ONLY -- a _() in Python source never reaches a catalogue. The server sends the key, static/home_panels.js looks it up here. Placeholders are written as "{}" and not "%s": Flask-Babel installs newstyle gettext, which rewrites %s to {} in the rendered string while the catalogue keeps %s -- "{}" keeps template, catalogue and JS in step. NOTE: do not put a Jinja comment inside the dict below -- a comment is not allowed inside an expression and takes the whole page down with a TemplateSyntaxError. (Nor inside this one: the closing delimiter of a nested comment ends the outer one and the rest leaks onto the page.) #} {# Every string the home page renders client-side, translated here so it goes through the same Flask-Babel catalogue as the rest of the app instead of a hardcoded de/en pair in JavaScript. Rendered for both home variants -- the search box below is shared. #} {# The panel bar: buttons under the search field, one panel below them. Both are empty until /api/home-panels answers -- which panels an account may even see is decided server-side (Storage and System are admin-only), so rendering the buttons here would mean rendering buttons that 403. The bar hides itself when nothing is available. #} {% if new_home %}
{% endif %} {# Which sources a search actually hits. Filled by renderSourceChips() in app.js once /api/settings answered; empty (and invisible) until then. The new home page does NOT render this: its own chip row (below) says the same thing *and* filters, and two rows of identical source names stacked on top of each other is a puzzle, not a control. #} {% if not new_home %}
{# The way in to "which home page do I want", on the classic layout too. Not optional politeness: /settings redirects a non-admin to "/", so this modal is the ONLY place a normal account can pick a layout -- and the promo banner above is gone for good once it has been answered. Without this, dismissing the banner would be a one-way door onto v1. #}
{% endif %} {# Only rendered while something is downloading -- see renderHomeRunStrip(), fed by the queue poll that runs anyway. #}
{{ _('Searching...') }}
{# The way back out of a search. Its OWN element, above #results and not inside it: renderResults()/renderResultsBoth() replace #results.innerHTML wholesale when the answers arrive, so a header rendered in there survived exactly until the first result landed. #}
{# Dashboard widgets from enabled extensions (see register_thirdparty(dashboard_widget_template=...) in web/thirdparties/registry.py) — each entry's own template is included as-is, so the widget's markup/behaviour is entirely up to the extension; this just provides the slot + consistent spacing (.browse-provider-block, reused for its margin only — no provider dot styling implied). Nothing renders here if no extension registered one. #} {% for widget in dashboard_widgets %}
{# "ignore missing" -- a widget template that was removed/renamed after being registered (e.g. mid-upgrade) silently disappears instead of taking down the entire home page for every user. #} {% include widget.template ignore missing %}
{% endfor %} {# Settings -> General -> "Use the new home page": one row per question, every enabled source mixed into it, instead of one block per source. The classic block below stays the default and is what modules extend. #} {% if new_home %}
{# The chip row and the way in to "which rows do I even want". /settings is admin-only, so this is where a normal account reaches its own Start Page settings -- the same controls, same code, same storage. #}
{# Density and modes are rendered by static/home_2_1.js: both depend on a stored preference, and a server-rendered default would flash the wrong one on every load. #}
{# Filled by static/home_2_1.js and invisible on an instance that is fully set up. The monthly recap is NOT here: it moved into the panel bar above (next to System), because a recap is something you go and look at rather than something that interrupts the page you opened to search on. #} {# Personal rows first: what you were doing beats what a site published. Each one hides itself when its source has nothing to say (no playback position yet, no favourites, calendar switched off), so a fresh install shows exactly the discovery rows below and no empty furniture. #} {# The only row that asks something of you rather than offering something, which is why its default position is *behind* the discovery rows (see _FEED_DEFAULT_ORDER in routes/browse.py) even though its markup sits with the other personal rows. #}

{{ _('New this week') }}

{{ _('Popular right now') }}

{{ _('Movies') }}

{% else %}
AniWorld

{{ _('New Anime') }}

{{ _('Popular Anime') }}

SerienStream

{{ _('New Series') }}

{{ _('Popular Series') }}

FilmPalast

{{ _('New Movies') }}

MegaKino

{{ _('New Movies') }}

{{ _('Popular Movies') }}

{{ _('New Series') }}

{{ _('Popular Series') }}

{% endif %}
{# Start Page settings for this account. Same macro the Settings tab uses -- an admin sees the instance defaults there as well, everyone sees their own here. Rendered on BOTH layouts: /settings redirects a non-admin, so this is the only place a normal account can switch between them at all. #} {# Kids-mode PIN. Its own overlay rather than window.prompt(): a system prompt cannot mask the digits, is unstyled, blocks the tab, and on a phone appears as a sheet that does not look like it belongs to MediaForge. #} {% if new_home %} {% endif %} {% include "shared_modals.html" %} {% endblock %} {% block scripts %} {% if new_home %} {% endif %} {{ startpage.start_page_i18n() }} {% if devinfo_importants %}{% endif %} {% if show_new_home_promo %} {% endif %} {% endblock %}