Skip to content

feat(airing-schedule): add the Airing Schedule utility (part 1 of 3) - #1500

Open
revam wants to merge 10 commits into
masterfrom
feat/airing-schedule-part-1
Open

revam wants to merge 10 commits into
masterfrom
feat/airing-schedule-part-1

Conversation

@revam

@revam revam commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Why

Part 1 of 3, split from #1497. The server now has an airing schedule service (/api/v3/AiringSchedule/*): provider plugins (AnimeSchedule.net, Syoboi Calendar, TVmaze and others) supply timed broadcast and streaming airings, with channels, delays and sub/dub tracks. The WebUI had no way to see those airings.

This part adds the Airing Schedule utility page. It stands on its own: nothing outside this part is needed to review or run it.

Stack: this PR targets master. #1501 (feat/airing-schedule-part-2, the dashboard widget) and #1502 (feat/airing-schedule-part-3, the settings page) build on this branch.

Server: needs the airing schedule routes from current core master (6.0 alpha, unreleased). Against an older core the page fails to load.

What's in this part

Utilities > Airing Schedule (/webui/utilities/airing-schedule), with Season (the default), Month, Week and Agenda views.

Toolbar and header

  • Panel title: Airing Schedule | <period>; the period slides out and in the direction of travel (forward for later, back for earlier), and swaps at once on a view switch or with reduced motion. The header's right side counts the anime, seasons or airings shown.
  • Toolbar: Refresh (R), My Collection / All Series, Every Channel, Hide Estimates, Show Reruns. Beside it, in boxes of their own that wrap together: the view switcher (Season | Month | Week | Agenda), the navigation (previous, today or current season, next) and a settings gear.
  • The gear opens Airing Schedule Settings, a modal in two tabs. General holds the airing kind (All Kinds by default, left out of the URL), the provider and the channel list (toggles, country badges, counts, search past eight channels, Show All/Hide All). Channel changes last for the session and are never saved; the channels hidden on the settings page start off.
  • Every filter but the channels and Every Channel lives in the URL. Without view in the URL the page opens on the last view used (Redux, for the session). Series without files are always shown (the "Include Series Without Files" option and includeMissing are gone).

Month, Week, Agenda (read from GET AiringSchedule/Calendar, grouped and sorted on the server)

  • Month always draws six whole weeks from the week of the 1st, fetched as one request, so the grid keeps its height; days of other months dim their date and background, not their airings. A day's +N more opens that day's week.
  • Month and week keep their grid while a period loads: each day shows three skeleton entries, then fades its entries in.
  • The agenda is a virtual list on the page's scroll container (headings and entries as rows).
  • One entry per episode and local day, led by the server's preferred airing; with Every Channel on, +N more lists the others in place (a popover in the month's rows). Reruns only with Show Reruns, with a badge. Date-only episodes (AniDB air date) are all-day entries. Delayed airings show at their new slot. Hover shortcut on a channel to hide it, with Undo.
  • Now: a Now badge on an airing while it is on air (AiredAt until EndsAt), tooltip "On air until HH:mm".
  • Local files: the collection's files icon, muted, when the airing's episode has local files (VideoCount), with the count in its tooltip.
  • Unresolved airings: an airing for an episode AniDB does not list yet (IsResolved: false) reads Ep N from its Number, with a muted "Not on AniDB yet" hint, links to its series and never shows the files icon.

Season

  • Slider: a strip of seasons that keeps the shown one centred, two on each side and the outer ones half cut and faded. Moved by a click, the arrow keys or a swipe; next stops at the last announced season. Read from GET AiringSchedule/Season/ByYear without images.
  • Sticky: the slider sticks to the top of the page's scroll container on a frosted bar (data-stuck, judged from the layout on scroll and resize).
  • Season browser: clicking the shown season's pill swaps the anime for every year's seasons (image and count per season, newest year first), with a "Click/Tap to open/close" hint under the pill that flips over and hides while stuck. Opening pushes a history entry (browse=true), so Back closes it; closing steps back over that entry; a pick replaces it, so Back then returns to the page before the browser opened. Esc closes it.
  • Scroll: closing returns to where the page was while the same season is shown; otherwise the page lands just under the slider. Opening while scrolled past the slider keeps it pinned and centres the shown season's year only when it is out of view. Nothing scrolls on load: lists mount where the page already is.
  • Cards (from POST AiringSchedule/Season/{year}/{season}/Sections): the server's sections (TV & Web, Half Length, Continuing, Movies, OVAs & Specials) and order, or the user's layout; empty sections are hidden; countdown to the next new episode, its regular slot and provider icon, the episode length, "Airing now" with "ends in ..." while on air and "Aired 30m ago" after; the files icon; source, overview, top tags, Missing Files / In Collection.
  • Layout: cards in 1 to 3 columns and the browser in 1 to 4, at fixed width steps (getGridColumnCount), with minmax(0, 1fr) so phones never scroll sideways. Both are virtual lists on the page's scroll container.
  • Recently aired: a "Show Recently Aired" switch and its hours (1 to 24, 12 by default) in the gear's modal, kept in WebUI_Settings.airingSchedule.recentlyAired. While on, the current and previous seasons' sections are read with at that many hours back, rounded down to 15 minutes, so a card keeps an episode that just aired ("Aired 2h ago", by the clock) and moves on as the next 15 minute step is read. The season list and the calendar views never send it.
  • Oldest year: in the page's settings modal (the gear), kept in WebUI_Settings.airingSchedule.oldestSeasonYear and sent as fromYear.
  • Section editor: the modal's Sections tab lists the layout's sections to drag into order, each with a summary and a gear that opens an inline editor: title, anime types (none = every type, a rest group), and Continuing and Half Length as Any / Yes / No. Add, remove and Reset to Default. Edits save on their own after 500 ms to WebUI_Settings.airingSchedule.sections (null follows the server default from GET AiringSchedule/Season/Sections/Default, which the first edit copies). Muted hints flag a section after a rest group and a layout with no rest group. The sections read always POSTs { Sections }, null for the default.
  • Episode types: an "Episode Types" multi-select in the General tab, kept in WebUI_Settings.airingSchedule.episodeTypes (null for every type). It is sent as episodeType on the season sections read and as type on the calendar reads.

Loading and data

  • Skeletons of each view's cards in place of spinners, fading out downwards in three steps (full, faint, gone) under a mask, pulsing unless motion is reduced; they fade in after 150 ms so quick loads never flash them, and the content fades in where they were. The season skeleton fills the view plus a row (at least five).
  • Stale times: calendar and season sections 5 minutes, seasons by year an hour, providers once (shared through AiringProvidersContext). airing:provider.swept and airing:episode.aired invalidate the calendar and the sections.
  • One shared 30-second clock (useNow) for every countdown and on-air mark.

Debug builds only: ?clockOffset=
A signed offset (-90m, 7h40m, -1d6h) shifts the page's clock (still ticking): today, the current season, the default period and the slider. While shifted, the season, years and sections reads send at with the shifted time so the server agrees, and the header shows the shifted clock. Ignored in production builds.

Look first at: src/pages/utilities/AiringSchedule.tsx, src/components/Utilities/AiringSchedule/ (SeasonView, SeasonSlider, SeasonBrowser, SeasonCard, CalendarViews, AiringEntry, Skeleton), src/core/utilities/seasonSlider.ts, seasonGrid.ts, airingSchedule.ts, clock.ts, src/hooks/useNow.ts.

Shared with the other parts

Things that live in this part but exist for, or are also used by, parts 2 and 3 — review them knowing they have other consumers:

  • src/components/Dialogs/LanguagesModal.tsx — the opt-in selection API (single pick via multiple: false, noneLabel, exclude). No existing call site changes. Part 3's Preferred Tracks language pick is its first new consumer.
  • src/core/react-query/airing-schedule/ (queries.ts, types.ts, helpers.ts) and src/core/types/api/airing-schedule.ts — the calendar/provider read layer. Part 2 reads useAiringCalendarQuery for the dashboard widget; part 3 adds the configuration and channel queries on top.
  • src/core/utilities/airingChannels.ts (+ tests/core/utilities/airingChannels.test.ts) — channel search/grouping shared by this part's channel filter and part 3's channel administration (which extends it).
  • src/components/AiringProviderIcon.tsx and src/components/AiringChannelCountryBadge.tsx — used by the views here, by part 2's widget cards, and by part 3's provider and channel lists.
  • src/core/utilities/airingSchedule.ts (CalendarEntry grouping, toAiringRequestDates, getLocalTimeZone) — also drives part 2's timeline.
  • src/components/Input/MultiStateButton.tsx (compact) and src/components/BackgroundImagePlaceholderDiv.tsx (position) — generic component options added here for this page's toolbar and cards; available to later parts and the rest of the UI.
  • Unresolved airings are split across parts 1 and 2 by design. The IsResolved / SequenceNumber fields, the includeUnresolved read parameter and the page's handling (entries, season cards) are here; the same treatment for the dashboard widget's AiringDetails is in part 2, because that file is created there.
  • src/core/types/api/settings.ts and src/core/react-query/settings/helpers.ts — WebUI_Settings.airingSchedule.* (oldestSeasonYear, recentlyAired, sections, episodeTypes) is this part's; part 2 adds the dashboard widget's own keys to the same files.
  • src/core/router/index.tsx and src/components/Layout/TopNav.tsx — the utility route/menu entry here; part 3 adds its settings route to the same router file.
  • Two different "airing schedule settings" surfaces: the gear modal on this page (view/session preferences, in this part) is not the Settings > Airing Schedule server-configuration page in part 3. The last commit here splits the modal into view and channels sections to keep the two apart.

Testing

  • pnpm lint, pnpm tscheck and pnpm build clean on the tip; pnpm test includes the new suites: season boundaries, the slider strip and browser history/scroll rules, grid columns, the clock offset, channel search. No dependency changes.
  • Against a local core with real airings: every view at 1920px and 1000px, the filters, date-only entries, provider icons, grouped airings and Every Channel, reruns, season sections and counts, the channel filter (channel= on every read) and hide shortcut.

To try: open Utilities > Airing Schedule, step through seasons with the slider, open the browser from the pill and use Back; switch to Week and go back a week or two to see the files icon on episodes you have.

Feedback wanted

  • Season as the default view, and All Kinds as the default kind: the right defaults?
  • Month and week density: day columns of cards rather than a time grid.
  • VITE_MIN_SERVER_VERSION is unchanged; no released server has these routes yet, so which build should it require?
  • Keep the dev-only ?clockOffset= in the shipped code, or drop it before merging?

revam added 10 commits October 7, 2026 20:44
- Added a `selection` option to `LanguagesModal` that hands the picked languages to the caller instead of saving a settings preference, with `multiple: false` for a single pick and `noneLabel` for an explicit "no language" choice
- Added an `exclude` option to leave pseudo-languages out of the list
- Kept every existing call site unchanged
…ion to `BackgroundImagePlaceholderDiv`

- Added a `compact` option to `MultiStateButton` for narrower, lower buttons sized to their labels, for a toolbar
- Added a `position` option to `BackgroundImagePlaceholderDiv` that sets where the image sits in its frame
- Added the Airing Schedule utility, with month, week and agenda views of `GET AiringSchedule/Calendar`, under the utilities menu
- Put the refresh, scope and channel toggles in a toolbar, the view switcher and the period navigation beside it, and the period in the title, sliding in the direction of travel
- Moved the channel filter, airing kind and provider behind a settings gear, kept with the last view for the session
- Kept the month and week grids while they load, with skeletons in each day, and drew only the agenda rows in view
- Refetched the airings on the `airing` feed's sweeps, marked what is on air now and which episodes have local files, and let debug builds shift the schedule's clock
- Added a season view, now the default, listing a season's anime in the server's sections with their next new episode and a countdown, as cards in one to three columns drawn only while in view
- Picked the season from a slider kept in view while scrolling, and browsed every season by year from it, in place of a separate archive view
- Returned to the slider, or to where you were, after picking a season or closing the browser, and kept the slider pinned while the browser opens
- Showed skeleton cards while a season or the browser loads, and refetched the season's anime every few minutes and on the `airing` feed's sweeps
- Added the season view's oldest year to the airing schedule's settings modal, saved to the user's WebUI settings once typing pauses
- Added `SequenceNumber` and `IsResolved` to the airing, and `includeUnresolved` to the airing reads
- Labelled an unresolved airing `Ep N` from its `Number`, with a muted "Not on AniDB yet" hint in the entries, cards and the dashboard widget
- Linked an unresolved airing to its series rather than an episode, and showed no local files for it
- Added a "Show Recently Aired" switch and its hours, 1 to 24, to the airing schedule settings modal, saved in `WebUI_Settings.airingSchedule.recentlyAired`
- Read the current and previous seasons' sections with `at` that many hours back, rounded down to 15 minutes, so a card keeps an episode that aired since
- Kept a season's cards while the next 15 minute step is read, which moves them on
- Showed how long ago a card's episode aired, by the clock, in place of "Just aired"
- Split the settings modal into General and Sections tabs
- Added a draggable section list with an inline editor for the title, anime types, continuing and half length, saved as you edit to `airingSchedule.sections`
- The season view always reads its sections by `POST`, with the saved layout or `null` for the server default, and hides the empty ones
- Dropped the section `ID`; sections are keyed by their place in the layout
- Added an "Episode Types" multi-select to the General tab, saved to `airingSchedule.episodeTypes`, `null` for every type
- Sent the types as `episodeType` on the season sections read and as `type` on the calendar read
- Read `Season` and `Season/ByYear` by POST with the stored layout, which joined their query keys
- Showed the left-out hint only for a custom layout without a rest group

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant