Add Episodes panel: search a series, browse a season's episode list

New src-tauri/src/episodes.rs exposes browse_episodes, reusing the
existing AnyTvClient::fetch_season machinery Fetch Data already built
(via a build_tv_client helper factored out of metadata.rs) so this
panel needs no credentials of its own - it reads whatever TV source
(TMDB/TVmaze/OMDb) is already configured in Fetch Data Settings.

New EpisodesPanel.tsx/.css: series name + season stepper, an episode
list (number/title/air date), and a Copy list button that copies the
season as plain text. Wired into App.tsx's sidebar routing as the
second stub panel (after Subtitles) to become a real screen.

145/145 Rust tests passing (+1), npm run build clean, 15/15 Playwright
checks against npm run dev (mocked Tauri bridge) covering search,
the empty-season case, a not-found series, the season stepper, Copy
list, and the blank-query guard.
This commit is contained in:
Renamr Scaffold
2026-08-29 15:35:27 +00:00
parent b47d28375b
commit ed26391651
8 changed files with 462 additions and 22 deletions
+51 -8
View File
@@ -42,7 +42,10 @@ below. A new Subtitles panel searches and downloads subtitle files from
[OpenSubtitles](https://www.opensubtitles.com/) — load a video, its title/season/episode [OpenSubtitles](https://www.opensubtitles.com/) — load a video, its title/season/episode
(or movie title/year) auto-fills from the same local matching "Match" uses, pick a (or movie title/year) auto-fills from the same local matching "Match" uses, pick a
result, and the subtitle is saved right next to the video ready for any player to pick result, and the subtitle is saved right next to the video ready for any player to pick
up automatically; see "Subtitles" below. See "Porting roadmap" below. up automatically; see "Subtitles" below. A new Episodes panel searches a series by
name and browses a full season's episode list — episode number, title, air date — using
whichever TV source (TMDB/TVmaze/OMDb) is already configured in Fetch Data Settings, no
separate credentials needed; see "Episodes" below. See "Porting roadmap" below.
The naming scheme is fully user-editable from the app (the "Naming Scheme" button next The naming scheme is fully user-editable from the app (the "Naming Scheme" button next
to the New Names list) — the default is `{show name} - {season number}x{episode to the New Names list) — the default is `{show name} - {season number}x{episode
@@ -153,7 +156,12 @@ crates/aurora-curator-core/ Pure Rust core: naming-expression engine (ported),
download against OpenSubtitles' REST API; search only needs an download against OpenSubtitles' REST API; search only needs an
app-level API key, download additionally needs the end user's own app-level API key, download additionally needs the end user's own
account login, since downloads are quota-limited per account — account login, since downloads are quota-limited per account —
see "Subtitles" below), the on-disk see "Subtitles" below). The Episodes panel adds no new core-crate
code of its own — it reuses `metadata::AnyTvClient::fetch_season`
(the same season-resolving call `AnyTvClient` already made available
to "Fetch Data" and the CLI) from the Tauri side, so `web/tmdb.rs`/
`tvmaze.rs`/`omdb.rs`'s existing season-episode-list methods are
exactly what it browses. The on-disk
rename engine (`rename/` — Move/Copy/Symlink actions, rename engine (`rename/` — Move/Copy/Symlink actions,
Skip/Auto-number/Override conflict policies, can also organize Skip/Auto-number/Override conflict policies, can also organize
into (auto-created) subfolders when into (auto-created) subfolders when
@@ -180,7 +188,11 @@ src-tauri/ Tauri app shell: exposes aurora-curator-core as commands
returns to the frontend, only whether one is saved), returns to the frontend, only whether one is saved),
`subtitles.rs` (search/download commands wrapping `subtitles.rs` (search/download commands wrapping
`aurora-curator-core::web::opensubtitles`, saving a downloaded `aurora-curator-core::web::opensubtitles`, saving a downloaded
subtitle right next to its video file), subtitle right next to its video file), `episodes.rs` (the
`browse_episodes` command backing the Episodes panel — series
search + one season's episode list, via a `build_tv_client` helper
now shared with `metadata.rs`'s `fetch_metadata`, so both read the
same stored TV-source setting and credential),
`history.rs` (rename history/undo `history.rs` (rename history/undo
log), `presets.rs` (up to 9 saved naming schemes) — all three log), `presets.rs` (up to 9 saved naming schemes) — all three
stored as JSON in the OS app-config directory — `cli.rs` (the stored as JSON in the OS app-config directory — `cli.rs` (the
@@ -203,6 +215,10 @@ src/ React + TypeScript UI (Vite). Faithful recreation of the
panel (its own Subtitle Settings modal, its own credential state) panel (its own Subtitle Settings modal, its own credential state)
reached from the sidebar's "Subtitles" entry — the first sidebar reached from the sidebar's "Subtitles" entry — the first sidebar
entry besides Rename to be a real screen rather than a stub. entry besides Rename to be a real screen rather than a stub.
`EpisodesPanel.tsx` is the second: a series-search + season-browse
screen with no settings modal of its own at all — it deliberately
has no separate credentials UI, reading whatever TV source/token
Fetch Data Settings (in the Rename panel) already has saved.
scripts/gen_icons.py Regenerates src-tauri/icons/ from the real logo mark scripts/gen_icons.py Regenerates src-tauri/icons/ from the real logo mark
(`scripts/logo/icon-source.png`, cropped from the supplied (`scripts/logo/icon-source.png`, cropped from the supplied
lockup) — replace that source image to change the look. lockup) — replace that source image to change the look.
@@ -227,7 +243,7 @@ npm run tauri dev
npm run tauri build npm run tauri build
``` ```
Verified in the environment this was built in: `cargo test --workspace` (all 144 Verified in the environment this was built in: `cargo test --workspace` (all 145
passing, including the TMDB, TVmaze, OMDb, and OpenSubtitles client tests — those run passing, including the TMDB, TVmaze, OMDb, and OpenSubtitles client tests — those run
against a local mock HTTP server via `mockito`, no live network or API key needed to against a local mock HTTP server via `mockito`, no live network or API key needed to
build/test), `cargo build/test), `cargo
@@ -315,6 +331,28 @@ tree (see "Organizing into subfolders" above) — a naming scheme's own `/` alre
organizing into a subfolder, and relaxing that invariant just for `--output` wasn't organizing into a subfolder, and relaxing that invariant just for `--output` wasn't
judged worth it. judged worth it.
## Episodes
The Episodes sidebar panel searches for a TV series by name and browses one season's
full episode list — episode number, title, and air date — as a standalone reference,
independent of whatever files (if any) are loaded in the Rename panel.
Type a series name, pick a season (the +/− stepper re-queries automatically once you've
searched at least once), and click **Browse**. A season with a real series match but no
episodes of its own (e.g. asking for season 99 of a 5-season show) comes back as a clear
"no episodes found for season N" message rather than an error; a series name that
doesn't match anything at all surfaces the underlying source's own not-found message. A
**Copy list** button copies every row as plain text (`Series Name - SxEE - Episode
Title`, one per line) to the clipboard — handy for pasting into a naming reference or
anywhere outside the app.
Unlike Subtitles, this panel has **no settings or credentials of its own**: it reuses
whichever TV source — TMDB, TVmaze, or OMDb — is already configured in the Rename
panel's Fetch Data Settings, and the same season-lookup call ("Fetch Data" and the
`-rename --db` CLI both already made) is exactly what it browses. If nothing is
configured yet (only relevant for TMDB/OMDb — TVmaze needs no credential), Browse
reports the same "open Settings to add one" message Fetch Data itself would.
## Subtitles ## Subtitles
The Subtitles sidebar panel searches and downloads subtitle files from The Subtitles sidebar panel searches and downloads subtitle files from
@@ -411,7 +449,10 @@ ported from, for anyone picking this up:
items resolve against respectively (TMDB stays the default for both; a batch can mix items resolve against respectively (TMDB stays the default for both; a batch can mix
sources across its TV and movie halves) — disambiguating multi-result searches via sources across its TV and movie halves) — disambiguating multi-result searches via
the similarity-scoring pass described in item 3 above rather than blindly trusting the similarity-scoring pass described in item 3 above rather than blindly trusting
any source's top-ranked hit. **OpenSubtitles is also done, but as a subtitle source any source's top-ranked hit. The same per-season episode-list methods these three
clients already exposed for Fetch Data are also what the new Episodes panel browses
directly (see "Episodes" above) — no new client code, just a new UI on top of what
was already there. **OpenSubtitles is also done, but as a subtitle source
rather than a fourth title-metadata source** — `crates/aurora-curator-core/src/web/opensubtitles.rs`, rather than a fourth title-metadata source** — `crates/aurora-curator-core/src/web/opensubtitles.rs`,
wired to the new Subtitles panel (see "Subtitles" above) rather than to Fetch Data's wired to the new Subtitles panel (see "Subtitles" above) rather than to Fetch Data's
TV/movie pickers, since it fetches subtitle files, not episode/movie titles. All TV/movie pickers, since it fetches subtitle files, not episode/movie titles. All
@@ -449,9 +490,11 @@ ported from, for anyone picking this up:
7. **The rest of the UI** — the Rename panel (file loading, drag-drop, live preview, 7. **The rest of the UI** — the Rename panel (file loading, drag-drop, live preview,
local TV/movie matching, confirmed rename with per-item exclude, rename history/undo, local TV/movie matching, confirmed rename with per-item exclude, rename history/undo,
Align Up/Down reordering, naming-scheme Presets with `1`-`9` hotkeys) is built, and so Align Up/Down reordering, naming-scheme Presets with `1`-`9` hotkeys) is built, and so
is the Subtitles panel (search + download via OpenSubtitles, see "Subtitles" above) — are the Subtitles panel (search + download via OpenSubtitles, see "Subtitles" above)
the first of the other five sidebar panels to become a real screen. The remaining and the Episodes panel (series search + season browse with a Copy list export, no
four (Episodes/SFV/Filter/List) are still stub screens. Align Up/Down is button-based credentials of its own, see "Episodes" above) — the first two of the other five
sidebar panels to become real screens. The remaining three (SFV/Filter/List) are
still stub screens. Align Up/Down is button-based
(swaps the selected file with its neighbor), not the original's (swaps the selected file with its neighbor), not the original's
drag-and-drop-within-a-list interaction. drag-and-drop-within-a-list interaction.
8. **Undo/history for renames** — done, as a flat JSON-backed undo stack rather than a 8. **Undo/history for renames** — done, as a flat JSON-backed undo stack rather than a
+93
View File
@@ -0,0 +1,93 @@
//! The Tauri-facing side of the Episodes panel: search a series by name and browse one
//! season's full episode list, independent of any loaded local files. Unlike
//! Subtitles (`subtitles.rs`), this needs no credentials of its own - it reuses the
//! exact same `AnyTvClient`/`fetch_season` machinery "Fetch Data" already built (see
//! `metadata.rs`), via the shared `metadata::build_tv_client` helper, so this panel
//! automatically respects whichever TV source (TMDB/TVmaze/OMDb) is already configured
//! in Settings rather than needing its own.
use serde::Serialize;
use tauri::AppHandle;
use crate::metadata::build_tv_client;
/// One episode row, as shown in the Episodes panel's list.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EpisodeRow {
pub episode_number: i32,
pub title: String,
pub air_date: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum EpisodeListOutcome {
Found {
series_name: String,
episodes: Vec<EpisodeRow>,
},
Failed {
error: String,
},
}
/// Searches for `series_query` against the currently configured TV source and returns
/// every episode of `season`. A season with a valid series match but zero episodes
/// (e.g. a season number past the end of the show) still comes back `Found` with an
/// empty list - it's a normal "nothing here" result, not a failure - so the frontend
/// can tell "found the show, this season is just empty" apart from "couldn't find the
/// show at all" (which `fetch_season` reports as an `Err` and this maps to `Failed`).
#[tauri::command]
pub async fn browse_episodes(app: AppHandle, series_query: String, season: i32) -> EpisodeListOutcome {
let trimmed = series_query.trim();
if trimmed.is_empty() {
return EpisodeListOutcome::Failed {
error: "Enter a series name to search for.".to_string(),
};
}
let client = match build_tv_client(&app) {
Ok(client) => client,
Err(error) => return EpisodeListOutcome::Failed { error },
};
match client.fetch_season(trimmed, season).await {
Ok((series_name, episodes)) => EpisodeListOutcome::Found {
series_name,
episodes: episodes
.into_iter()
.map(|e| EpisodeRow {
episode_number: e.episode_number,
title: e.title,
air_date: e.air_date,
})
.collect(),
},
Err(error) => EpisodeListOutcome::Failed { error },
}
}
#[cfg(test)]
mod tests {
use super::*;
// `browse_episodes` itself needs a running `AppHandle` (for `build_tv_client` to
// read settings), which unit tests here have no way to construct - same reason
// `subtitles.rs`'s `search_subtitles`/`download_subtitle` commands aren't
// unit-tested directly either. It's covered end-to-end (real query text, mocked
// Tauri backend) by the Playwright pass instead; what's unit-testable here is the
// plain data mapping below.
#[test]
fn episode_row_maps_fields_straight_across() {
let row = EpisodeRow {
episode_number: 5,
title: "Ozymandias".to_string(),
air_date: Some("2013-09-15".to_string()),
};
assert_eq!(row.episode_number, 5);
assert_eq!(row.title, "Ozymandias");
assert_eq!(row.air_date.as_deref(), Some("2013-09-15"));
}
}
+2
View File
@@ -1,4 +1,5 @@
mod cli; mod cli;
mod episodes;
mod files; mod files;
mod history; mod history;
mod metadata; mod metadata;
@@ -115,6 +116,7 @@ pub fn run() {
settings::save_opensubtitles_login, settings::save_opensubtitles_login,
subtitles::search_subtitles, subtitles::search_subtitles,
subtitles::download_subtitle, subtitles::download_subtitle,
episodes::browse_episodes,
history::list_rename_history, history::list_rename_history,
history::undo_rename_batch, history::undo_rename_batch,
presets::list_presets, presets::list_presets,
+24 -14
View File
@@ -210,6 +210,27 @@ fn failed_all(items: Vec<FetchQuery>, error: &str) -> Vec<FetchResult> {
.collect() .collect()
} }
/// Builds whichever TV metadata client `settings::tv_metadata_source` currently has
/// picked, reading that source's saved credential (if it needs one). Shared by
/// `fetch_metadata` below and, from the separate Episodes-browsing panel,
/// `episodes::browse_episodes` - both need "the currently configured TV client" and
/// neither should duplicate this match. `pub(crate)`: see `LocalEpisode`'s doc comment.
pub(crate) fn build_tv_client(app: &AppHandle) -> Result<AnyTvClient, String> {
match settings::tv_metadata_source(app) {
TvMetadataSource::Tmdb => {
let token = settings::tmdb_token(app)
.ok_or_else(|| "No TMDB API token configured — open Settings to add one.".to_string())?;
Ok(AnyTvClient::Tmdb(TmdbClient::new(token)))
}
TvMetadataSource::Tvmaze => Ok(AnyTvClient::Tvmaze(TvmazeClient::new())),
TvMetadataSource::Omdb => {
let key = settings::omdb_api_key(app)
.ok_or_else(|| "No OMDb API key configured — open Settings to add one.".to_string())?;
Ok(AnyTvClient::Omdb(OmdbClient::new(key)))
}
}
}
/// Resolves a batch of local guesses against real TV metadata. Runs off the main /// Resolves a batch of local guesses against real TV metadata. Runs off the main
/// thread since it's doing real network I/O; not `blocking_*`-style Tauri API, so no /// thread since it's doing real network I/O; not `blocking_*`-style Tauri API, so no
/// deadlock risk like the dialog commands had - `spawn` here is just to keep it /// deadlock risk like the dialog commands had - `spawn` here is just to keep it
@@ -221,20 +242,9 @@ fn failed_all(items: Vec<FetchQuery>, error: &str) -> Vec<FetchResult> {
/// using, elsewhere in this file). /// using, elsewhere in this file).
#[tauri::command] #[tauri::command]
pub async fn fetch_metadata(app: AppHandle, items: Vec<FetchQuery>) -> Vec<FetchResult> { pub async fn fetch_metadata(app: AppHandle, items: Vec<FetchQuery>) -> Vec<FetchResult> {
let client = match settings::tv_metadata_source(&app) { let client = match build_tv_client(&app) {
TvMetadataSource::Tmdb => { Ok(client) => client,
let Some(token) = settings::tmdb_token(&app) else { Err(error) => return failed_all(items, &error),
return failed_all(items, "No TMDB API token configured — open Settings to add one.");
};
AnyTvClient::Tmdb(TmdbClient::new(token))
}
TvMetadataSource::Tvmaze => AnyTvClient::Tvmaze(TvmazeClient::new()),
TvMetadataSource::Omdb => {
let Some(key) = settings::omdb_api_key(&app) else {
return failed_all(items, "No OMDb API key configured — open Settings to add one.");
};
AnyTvClient::Omdb(OmdbClient::new(key))
}
}; };
tauri::async_runtime::spawn(run_fetch(client, items)) tauri::async_runtime::spawn(run_fetch(client, items))
+3
View File
@@ -2,6 +2,7 @@ import { useState } from "react";
import Sidebar, { PANELS } from "./components/Sidebar"; import Sidebar, { PANELS } from "./components/Sidebar";
import Header from "./components/Header"; import Header from "./components/Header";
import RenamePanel from "./components/RenamePanel"; import RenamePanel from "./components/RenamePanel";
import EpisodesPanel from "./components/EpisodesPanel";
import SubtitlesPanel from "./components/SubtitlesPanel"; import SubtitlesPanel from "./components/SubtitlesPanel";
import PanelStub from "./components/PanelStub"; import PanelStub from "./components/PanelStub";
@@ -17,6 +18,8 @@ export default function App() {
<div className="main-content"> <div className="main-content">
{selected === "rename" ? ( {selected === "rename" ? (
<RenamePanel /> <RenamePanel />
) : selected === "episodes" ? (
<EpisodesPanel />
) : selected === "subtitles" ? ( ) : selected === "subtitles" ? (
<SubtitlesPanel /> <SubtitlesPanel />
) : ( ) : (
+134
View File
@@ -0,0 +1,134 @@
.episodes-panel {
height: 100%;
min-width: 0;
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
overflow-y: auto;
}
.episodes-search-form {
display: flex;
flex-direction: column;
gap: 8px;
background: var(--card);
border: 1px solid var(--surface-deep);
border-radius: 12px;
padding: 14px;
}
.episodes-search-row-inline {
display: flex;
align-items: flex-end;
gap: 12px;
}
.episodes-search-row-inline > label {
font-size: 11.5px;
color: var(--muted-text);
}
.episodes-series-field,
.episodes-season-field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.episodes-series-field {
flex: 1;
min-width: 140px;
}
.episodes-series-field label,
.episodes-season-field label {
font-size: 11.5px;
color: var(--muted-text);
}
.episodes-season-field {
flex: 0 0 auto;
}
.episodes-season-stepper {
display: flex;
align-items: center;
gap: 6px;
}
.episodes-season-input {
width: 56px;
text-align: center;
}
.episodes-hint {
font-size: 11.5px;
color: var(--muted-text);
margin: 0;
}
.episodes-results {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.episodes-results-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 2px 2px 4px;
}
.episodes-series-name {
font-size: 13.5px;
font-weight: 600;
}
.episodes-row {
display: flex;
align-items: center;
gap: 12px;
background: var(--card);
border: 1px solid var(--surface-deep);
border-radius: 10px;
padding: 9px 12px;
}
.episodes-row-number {
flex: 0 0 28px;
font-size: 12px;
font-weight: 700;
color: var(--aurora-cyan);
}
.episodes-row-title {
flex: 1;
min-width: 0;
font-size: 12.5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.episodes-row-date {
flex: 0 0 auto;
font-size: 11.5px;
color: var(--muted-text);
}
.episodes-empty-note {
color: var(--muted-text);
font-size: 12.5px;
padding: 8px 2px;
}
.episodes-status {
font-size: 12px;
color: var(--aurora-cyan);
}
+154
View File
@@ -0,0 +1,154 @@
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
interface EpisodeRow {
episodeNumber: number;
title: string;
airDate: string | null;
}
type EpisodeListOutcome =
| { status: "found"; seriesName: string; episodes: EpisodeRow[] }
| { status: "failed"; error: string };
export default function EpisodesPanel() {
const [seriesQuery, setSeriesQuery] = useState("");
const [season, setSeason] = useState(1);
const [searching, setSearching] = useState(false);
const [seriesName, setSeriesName] = useState<string | null>(null);
const [episodes, setEpisodes] = useState<EpisodeRow[]>([]);
const [status, setStatus] = useState<string | null>(null);
const [hasSearched, setHasSearched] = useState(false);
async function runBrowse(seasonToUse: number) {
const trimmed = seriesQuery.trim();
if (!trimmed) {
setStatus("Enter a series name to search for.");
return;
}
setSearching(true);
setStatus(null);
try {
const outcome = await invoke<EpisodeListOutcome>("browse_episodes", {
seriesQuery: trimmed,
season: seasonToUse,
});
setHasSearched(true);
if (outcome.status === "found") {
setSeriesName(outcome.seriesName);
setEpisodes(outcome.episodes);
if (outcome.episodes.length === 0) {
setStatus(`${outcome.seriesName} — no episodes found for season ${seasonToUse}.`);
}
} else {
setSeriesName(null);
setEpisodes([]);
setStatus(outcome.error);
}
} catch (err) {
setSeriesName(null);
setEpisodes([]);
setStatus(String(err));
} finally {
setSearching(false);
}
}
function changeSeason(delta: number) {
const next = Math.max(1, season + delta);
setSeason(next);
if (hasSearched) {
runBrowse(next);
}
}
function copyList() {
if (!seriesName || episodes.length === 0) return;
const lines = episodes.map(
(e) => `${seriesName} - ${season}x${String(e.episodeNumber).padStart(2, "0")} - ${e.title}`
);
navigator.clipboard
.writeText(lines.join("\n"))
.then(() => setStatus(`Copied ${lines.length} episode names to the clipboard.`))
.catch(() => setStatus("Couldn't copy to the clipboard."));
}
return (
<div className="episodes-panel">
<div className="episodes-search-form">
<div className="episodes-search-row-inline">
<div className="episodes-series-field">
<label htmlFor="episodes-series">Series name</label>
<input
id="episodes-series"
className="modal-text-input"
type="text"
placeholder="e.g. Breaking Bad"
value={seriesQuery}
onChange={(e) => setSeriesQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") runBrowse(season);
}}
/>
</div>
<div className="episodes-season-field">
<label htmlFor="episodes-season">Season</label>
<div className="episodes-season-stepper">
<button className="action-button" onClick={() => changeSeason(-1)} disabled={searching || season <= 1}>
−
</button>
<input
id="episodes-season"
className="modal-text-input episodes-season-input"
type="number"
min={1}
value={season}
onChange={(e) => setSeason(Math.max(1, Number(e.target.value) || 1))}
/>
<button className="action-button" onClick={() => changeSeason(1)} disabled={searching}>
+
</button>
</div>
</div>
<button className="action-button primary" onClick={() => runBrowse(season)} disabled={searching}>
{searching ? "Searching…" : "Browse"}
</button>
</div>
<p className="episodes-hint">
Searches whichever TV source (TMDB, TVmaze, or OMDb) is set in the Rename panel's Fetch Data Settings — no
separate credentials needed here.
</p>
</div>
<div className="episodes-results">
{seriesName && episodes.length > 0 && (
<div className="episodes-results-header">
<span className="episodes-series-name">
{seriesName} — Season {season}
</span>
<button className="action-button" onClick={copyList}>
Copy list
</button>
</div>
)}
{episodes.length === 0 && !status && !searching && (
<div className="episodes-empty-note">
{hasSearched ? "No episodes to show." : "Search a series and season to browse its episode list."}
</div>
)}
{episodes.map((ep) => (
<div className="episodes-row" key={ep.episodeNumber}>
<span className="episodes-row-number">{String(ep.episodeNumber).padStart(2, "0")}</span>
<span className="episodes-row-title">{ep.title}</span>
{ep.airDate && <span className="episodes-row-date">{ep.airDate}</span>}
</div>
))}
</div>
{status && <div className="episodes-status">{status}</div>}
</div>
);
}
+1
View File
@@ -6,6 +6,7 @@ import "./App.css";
import "./components/Sidebar.css"; import "./components/Sidebar.css";
import "./components/Header.css"; import "./components/Header.css";
import "./components/RenamePanel.css"; import "./components/RenamePanel.css";
import "./components/EpisodesPanel.css";
import "./components/SubtitlesPanel.css"; import "./components/SubtitlesPanel.css";
import "./components/PanelStub.css"; import "./components/PanelStub.css";