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, + }, +});