diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index d0c14078..b38030b3 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -1,129 +1,61 @@ import { Icon, Icons } from "components/Icon"; -import React, { - MouseEventHandler, - SyntheticEvent, - useEffect, - useState, -} from "react"; +import React, { Fragment } from "react"; -import { Backdrop, useBackdrop } from "components/layout/Backdrop"; -import { ButtonControl } from "./buttons/ButtonControl"; +import { Listbox, Transition } from "@headlessui/react"; export interface OptionItem { - id: string; + id: number; name: string; } interface DropdownProps { - open: boolean; - setOpen: React.Dispatch>; - selectedItem: string; - setSelectedItem: (value: string) => void; + selectedItem: OptionItem; + setSelectedItem: (value: OptionItem) => void; options: Array; } -export interface OptionProps { - option: OptionItem; - onClick: MouseEventHandler; - tabIndex?: number; -} - -function Option({ option, onClick, tabIndex }: OptionProps) { - return ( -
- - -
- ); -} - export const Dropdown = React.forwardRef( - (props: DropdownProps, ref) => { - const [setBackdrop, backdropProps, highlightedProps] = useBackdrop(); - const [delayedSelectedId, setDelayedSelectedId] = useState( - props.selectedItem - ); - - useEffect(() => { - let id: NodeJS.Timeout; - - if (props.open) { - setDelayedSelectedId(props.selectedItem); - } else { - id = setTimeout(() => { - setDelayedSelectedId(props.selectedItem); - }, 200); - } - return () => { - if (id) clearTimeout(id); - }; - /* eslint-disable-next-line */ - }, [props.open]); - - const selectedItem: OptionItem = - props.options.find((opt) => opt.id === props.selectedItem) || - props.options[0]; - - useEffect(() => { - setBackdrop(props.open); - /* eslint-disable-next-line */ - }, [props.open]); - - const onOptionClick = (e: SyntheticEvent, option: OptionItem) => { - e.stopPropagation(); - props.setSelectedItem(option.id); - props.setOpen(false); - }; - - return ( -
props.setOpen((open) => !open)} - > -
- - {selectedItem.name} - - -
- {props.options - .filter((opt) => opt.id !== delayedSelectedId) - .map((opt) => ( -
- props.setOpen(false)} {...backdropProps} /> -
- ); - } + + + + + {props.options.map((opt) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? "bg-denim-400 text-bink-700" : "text-white" + }` + } + key={opt.id} + value={opt} + > + {opt.name} + + ))} + + + + )} + +
+ ) ); diff --git a/src/components/layout/Seasons.tsx b/src/components/layout/Seasons.tsx index 1a4f835d..3e324ad7 100644 --- a/src/components/layout/Seasons.tsx +++ b/src/components/layout/Seasons.tsx @@ -25,8 +25,6 @@ export function Seasons(props: SeasonsProps) { const seasonSelected = props.media.season as number; const episodeSelected = props.media.episode as number; - const [dropdownOpen, setDropdownOpen] = useState(false); - useEffect(() => { (async () => { const seasonData = await searchSeasons(props.media); @@ -45,6 +43,16 @@ export function Seasons(props: SeasonsProps) { ); } + const options = seasons.seasons.map((season) => ({ + id: season.seasonNumber, + name: `Season ${season.seasonNumber}`, + })); + + const selectedItem = { + id: seasonSelected, + name: `Season ${seasonSelected}`, + }; + return ( <> {loading ?

Loading...

: null} @@ -52,17 +60,12 @@ export function Seasons(props: SeasonsProps) { {success && seasons.seasons.length ? ( <> ({ - id: `${season.seasonNumber}`, - name: `Season ${season.seasonNumber}`, - }))} - setSelectedItem={(id) => + selectedItem={selectedItem} + options={options} + setSelectedItem={(seasonItem) => navigateToSeasonAndEpisode( - +id, - seasons.seasons[+id]?.episodes[0].episodeNumber + seasonItem.id, + seasons.seasons[seasonItem.id]?.episodes[0].episodeNumber ) } /> diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 18e7d36f..112003ea 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -13,12 +13,14 @@ export interface MediaCardProps { media: MWMediaMeta; watchedPercentage: number; linkable?: boolean; + series?: boolean; } function MediaCardContent({ media, linkable, watchedPercentage, + series, }: MediaCardProps) { const provider = getProviderFromId(media.providerId); @@ -49,7 +51,14 @@ function MediaCardContent({
{/* card content */}
-

{media.title}

+

+ {media.title} + {series ? ( + + S{media.season} E{media.episode} + + ) : null} +

); diff --git a/src/providers/README.md b/src/providers/README.md index 19f3dc6d..a32dcc4f 100644 --- a/src/providers/README.md +++ b/src/providers/README.md @@ -21,3 +21,11 @@ All these rules are because `PortableMedia` objects need to stay functional. bec - It's used for routing, links would stop working - It's used for storage, continue watching and bookmarks would stop working + +# The list of providers and their quirks + +Some providers have quirks, stuff they do differently than other providers + +## TheFlix + +- for series, the latest episode released will be one playing at first when you select it from search results diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 07380985..e2689375 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,4 +1,4 @@ -import { MWMediaMeta, getProviderMetadata } from "providers"; +import { MWMediaMeta, getProviderMetadata, MWMediaType } from "providers"; import React, { createContext, ReactNode, @@ -98,9 +98,43 @@ export function WatchedContextProvider(props: { children: ReactNode }) { }); }, getFilteredWatched() { - return watched.items.filter( + // remove disabled providers + let filtered = watched.items.filter( (item) => getProviderMetadata(item.providerId)?.enabled ); + + // get highest episode number for every anime/season + const highestEpisode: Record = {}; + const highestWatchedItem: Record = {}; + filtered = filtered.filter((item) => { + if ( + [MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType) + ) { + const key = `${item.mediaType}-${item.mediaId}`; + const current: [number, number] = [ + item.season ?? -1, + item.episode ?? -1, + ]; + let existing = highestEpisode[key]; + if (!existing) { + existing = current; + highestEpisode[key] = current; + highestWatchedItem[key] = item; + } + + if ( + current[0] > existing[0] || + (current[0] === existing[0] && current[1] > existing[1]) + ) { + highestEpisode[key] = current; + highestWatchedItem[key] = item; + } + return false; + } + return true; + }); + + return [...filtered, ...Object.values(highestWatchedItem)]; }, watched, }), diff --git a/src/views/SearchView.tsx b/src/views/SearchView.tsx index dd52418d..3054a8f9 100644 --- a/src/views/SearchView.tsx +++ b/src/views/SearchView.tsx @@ -153,6 +153,7 @@ function ExtraItems() { ))}