diff --git a/app/series/[id]/index.tsx b/app/series/[id]/index.tsx
index 8f0df30..efb7592 100644
--- a/app/series/[id]/index.tsx
+++ b/app/series/[id]/index.tsx
@@ -26,6 +26,7 @@ import {
type MarkEvent,
} from "@/components/ProgressMarkBanner";
import { SeasonCoverage } from "@/components/SeasonCoverage";
+import { SeriesMovies } from "@/components/SeriesMovies";
import { VolumesGrid } from "@/components/VolumesGrid";
import { MOBILE_WIDTH_BREAKPOINT } from "@/components/CoverCarousel";
import { Footer } from "@/components/Footer";
@@ -197,11 +198,13 @@ export default function SeriesDetail() {
subParts.push(`${totalChapters ?? "?"} ch`);
subParts.push(`${totalVolumes ?? "?"} vol`);
}
+ const movies = curatedMapping?.movies ?? [];
if (showAnimeStats) {
subParts.push(`${totalEpisodes ?? "?"} eps`);
- const totalMovies = curatedMapping?.movies?.length ?? 0;
- if (totalMovies > 0) {
- subParts.push(`${totalMovies} ${totalMovies === 1 ? "movie" : "movies"}`);
+ if (movies.length > 0) {
+ subParts.push(
+ `${movies.length} ${movies.length === 1 ? "movie" : "movies"}`,
+ );
}
}
if (primary.format) subParts.push(primary.format);
@@ -342,6 +345,7 @@ export default function SeriesDetail() {
/>
)}
{curatedMapping ? : null}
+ {movies.length > 0 ? : null}
) : badge === "anime-only" ? null : (
diff --git a/src/components/EpisodeChapterRail.tsx b/src/components/EpisodeChapterRail.tsx
index 3f20b83..ffc1044 100644
--- a/src/components/EpisodeChapterRail.tsx
+++ b/src/components/EpisodeChapterRail.tsx
@@ -1,6 +1,6 @@
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useRouter } from "expo-router";
-import type { PressableState, SeriesMapping } from "@/types";
+import type { MovieEntry, PressableState, SeriesMapping } from "@/types";
import { FONT } from "@/theme";
import {
useProgress,
@@ -22,6 +22,7 @@ const COLORS = [
const BAR_HEIGHT = 44;
const LONG_PRESS_MS = 320;
+const MOVIE_COLOR = "#5cdfff";
export function EpisodeChapterRail({
mapping,
@@ -70,6 +71,11 @@ export function EpisodeChapterRail({
);
const mangaTotal = showTail ? totalChapters! : maxCoveredChapter;
+ const movieMarkers = (mapping.movies ?? []).filter(
+ (movie): movie is MovieEntry & { afterEpisode: number } =>
+ typeof movie.afterEpisode === "number",
+ );
+
const animeFrac =
progress?.anime && animeTotal > 0
? Math.min(progress.anime.position, animeTotal) / animeTotal
@@ -117,6 +123,39 @@ export function EpisodeChapterRail({
})}
{animeFrac !== null && }
+ {movieMarkers.length > 0 && animeTotal > 0 && (
+
+ {movieMarkers.map((movie, idx) => {
+ const label = `${movie.title} (${movie.year})${
+ movie.chapters
+ ? ` · ch ${movie.chapters[0]}–${movie.chapters[1]}`
+ : ""
+ }`;
+ return (
+
+
+ moveTo({ label, color: MOVIE_COLOR, textColor: "#000" }, e)
+ }
+ style={styles.movieMarker}
+ >
+ ◆
+
+
+ );
+ })}
+
+ )}
Manga chapters →
@@ -165,7 +204,10 @@ export function EpisodeChapterRail({
{mangaFrac !== null && }
- Tap to mark · Long-press to open arc
+
+ Tap to mark · Long-press to open arc
+ {movieMarkers.length > 0 ? " · ◆ movie premiere" : ""}
+
@@ -230,6 +272,29 @@ const styles = StyleSheet.create({
letterSpacing: -0.2,
fontFamily: FONT.bold,
},
+ movieLane: {
+ height: 18,
+ width: "100%",
+ position: "relative",
+ },
+ movieAnchor: {
+ position: "absolute",
+ top: 0,
+ bottom: 0,
+ width: 0,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ movieMarker: {
+ paddingHorizontal: 8,
+ justifyContent: "center",
+ },
+ movieMarkerText: {
+ color: MOVIE_COLOR,
+ fontSize: 11,
+ lineHeight: 18,
+ fontFamily: FONT.bold,
+ },
unadaptedBarText: {
color: "#9aa0a6",
fontSize: 13,
diff --git a/src/components/SeriesMovies.tsx b/src/components/SeriesMovies.tsx
new file mode 100644
index 0000000..558b1bf
--- /dev/null
+++ b/src/components/SeriesMovies.tsx
@@ -0,0 +1,87 @@
+import { StyleSheet, Text, View } from "react-native";
+import type { MovieEntry } from "@/types";
+import { FONT } from "@/theme";
+
+const MOVIE_COLOR = "#5cdfff";
+
+export function SeriesMovies({ movies }: { movies: MovieEntry[] }) {
+ const ordered = [...movies].sort(
+ (a, b) =>
+ (a.afterEpisode ?? Number.MAX_SAFE_INTEGER) -
+ (b.afterEpisode ?? Number.MAX_SAFE_INTEGER) || a.year - b.year,
+ );
+
+ return (
+
+ Movies
+
+ {ordered.map((movie, idx) => (
+
+
+
+ {typeof movie.afterEpisode === "number"
+ ? `◆ AFTER EP ${movie.afterEpisode}`
+ : "◆ MOVIE"}
+
+
+ {movie.year}
+ {movie.chapters
+ ? ` · ch ${movie.chapters[0]}–${movie.chapters[1]}`
+ : ""}
+
+
+ {movie.title}
+ {movie.note ? {movie.note} : null}
+
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ sectionTitle: {
+ color: "#f5f5f5",
+ fontSize: 20,
+ letterSpacing: -0.4,
+ fontFamily: FONT.bold,
+ },
+ list: { gap: 8 },
+ card: {
+ padding: 12,
+ backgroundColor: "#17181b",
+ borderLeftWidth: 2,
+ borderLeftColor: MOVIE_COLOR,
+ gap: 4,
+ },
+ cardTop: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "baseline",
+ gap: 12,
+ flexWrap: "wrap",
+ },
+ position: {
+ color: MOVIE_COLOR,
+ fontSize: 11,
+ letterSpacing: 1.4,
+ fontFamily: FONT.bold,
+ },
+ meta: {
+ color: "#9aa0a6",
+ fontSize: 12,
+ fontFamily: FONT.regular,
+ },
+ movieTitle: {
+ color: "#f5f5f5",
+ fontSize: 14,
+ fontFamily: FONT.semibold,
+ },
+ note: {
+ color: "#cfd2d6",
+ fontSize: 12,
+ lineHeight: 17,
+ fontFamily: FONT.regular,
+ },
+});