Add info popover for media card hover

This commit is contained in:
Pas 2025-03-06 11:20:11 -07:00
parent 787979eb13
commit 34960cc0c2
10 changed files with 465 additions and 52 deletions

View file

@ -612,7 +612,15 @@
"teal": "Teal",
"classic": "Classic"
},
"title": "Appearance"
"title": "Appearance",
"options": {
"discover": "Discover section",
"discoverDescription": "Show the Discover section on the Homepage below your bookmarked media. Enabled by default.",
"discoverLabel": "Discover section",
"hover": "Details popup",
"hoverDescription": "Show a popup with details when hovering over a media item. Disabled by default.",
"hoverLabel": "Details popup"
}
},
"connections": {
"server": {
@ -668,9 +676,6 @@
"sourceOrder": "Reordering sources",
"sourceOrderDescription": "Drag and drop to reorder sources. This will determine the order in which sources are checked for the media you are trying to watch. If a source is greyed out, it means the <bold>extension</bold> is required for that source. <br><br> <strong>(The default order is best for most users)</strong>",
"title": "Preferences",
"discover": "Discover section",
"discoverDescription": "Show the Discover section on the Homepage below your bookmarked media. Enabling this can increase load times. This setting is enabled by default.",
"discoverLabel": "Discover section",
"sourceOrderEnableLabel": "Custom source order"
},
"reset": "Reset",

View file

@ -275,6 +275,18 @@ export function getMediaDetails<
throw new Error("Invalid media type");
}
export function getMediaBackdrop(
backdropPath: string | null,
): string | undefined {
const shouldProxyTmdb = usePreferencesStore.getState().proxyTmdb;
const imgUrl = `https://image.tmdb.org/t/p/original${backdropPath}`;
const proxyUrl = getProxyUrls()[0];
if (proxyUrl && shouldProxyTmdb) {
return `${proxyUrl}/?destination=${imgUrl}`;
}
if (backdropPath) return imgUrl;
}
export function getMediaPoster(posterPath: string | null): string | undefined {
const shouldProxyTmdb = usePreferencesStore.getState().proxyTmdb;
const imgUrl = `https://image.tmdb.org/t/p/w342/${posterPath}`;

View file

@ -0,0 +1,262 @@
import classNames from "classnames";
import { useCallback, useEffect, useRef, useState } from "react";
import { getMediaBackdrop, getMediaDetails } from "@/backend/metadata/tmdb";
import {
TMDBContentTypes,
TMDBMovieData,
TMDBShowData,
} from "@/backend/metadata/types/tmdb";
import { usePreferencesStore } from "@/stores/preferences";
import { MediaItem } from "@/utils/mediaTypes";
interface InfoPopoutProps {
media: MediaItem;
visible: boolean;
}
// Add interface for storing additional details
interface AdditionalDetails {
runtime?: number | null;
genres?: { id: number; name: string }[];
language?: string;
episodes?: number;
seasons?: number;
voteAverage?: number;
voteCount?: number;
}
function InfoSkeleton() {
return (
<div className="animate-pulse">
<div className="relative h-40">
<div
className="absolute inset-0 bg-mediaCard-hoverBackground"
style={{
maskImage:
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 60px)",
WebkitMaskImage:
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 60px)",
}}
/>
</div>
<div className="px-4 pb-4 mt-[-30px]">
<div className="h-7 w-3/4 bg-white/10 rounded mb-2" /> {/* Title */}
<div className="space-y-2 mb-4">
{/* Description */}
<div className="h-4 bg-white/10 rounded w-full" />
<div className="h-4 bg-white/10 rounded w-full" />
<div className="h-4 bg-white/10 rounded w-full" />
<div className="h-4 bg-white/10 rounded w-3/4" />
</div>
{/* Additional details */}
<div className="grid grid-cols-2 gap-2 mb-4">
<div className="h-4 bg-white/10 rounded w-3/4" />
<div className="h-4 bg-white/10 rounded w-3/4" />
<div className="h-4 bg-white/10 rounded w-3/4" />
</div>
{/* Genres */}
<div className="flex flex-wrap gap-1 mt-4">
<div className="h-5 w-16 bg-white/10 rounded-full" />
<div className="h-5 w-20 bg-white/10 rounded-full" />
<div className="h-5 w-14 bg-white/10 rounded-full" />
</div>
</div>
</div>
);
}
export function InfoPopout({ media, visible }: InfoPopoutProps) {
const [description, setDescription] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const [backdrop, setBackdrop] = useState<string | undefined>();
const [dataLoaded, setDataLoaded] = useState(false);
const [shouldShow, setShouldShow] = useState(false);
const [additionalDetails, setAdditionalDetails] = useState<AdditionalDetails>(
{},
);
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
const enablePopDetails = usePreferencesStore((s) => s.enablePopDetails);
const fetchData = useCallback(async () => {
if (!enablePopDetails) return;
if (dataLoaded) return; // Skip if already loaded
setIsLoading(true);
try {
const type =
media.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
const details = await getMediaDetails(media.id, type);
const backdropUrl = getMediaBackdrop(details.backdrop_path);
setDescription(details.overview || undefined);
setBackdrop(backdropUrl);
if (type === TMDBContentTypes.MOVIE) {
const movieDetails = details as TMDBMovieData;
setAdditionalDetails({
runtime: movieDetails.runtime,
genres: movieDetails.genres,
language: movieDetails.original_language,
voteAverage: movieDetails.vote_average,
voteCount: movieDetails.vote_count,
});
} else {
const showDetails = details as TMDBShowData;
setAdditionalDetails({
episodes: showDetails.number_of_episodes,
seasons: showDetails.number_of_seasons,
genres: showDetails.genres,
language: showDetails.original_language,
voteAverage: showDetails.vote_average,
voteCount: showDetails.vote_count,
});
}
setDataLoaded(true);
} catch (err) {
console.error("Failed to fetch media details:", err);
} finally {
setIsLoading(false);
}
}, [enablePopDetails, dataLoaded, media.type, media.id]);
// Start loading data when user hovers
useEffect(() => {
if (visible && !dataLoaded && !isLoading && enablePopDetails) {
fetchData();
}
}, [visible, dataLoaded, isLoading, enablePopDetails, fetchData]);
useEffect(() => {
// Start timer when user hovers
if (visible && !shouldShow) {
hoverTimerRef.current = setTimeout(() => {
setShouldShow(true);
}, 200); // 0.2s
}
if (!visible && shouldShow) {
setShouldShow(false);
}
return () => {
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = null;
}
};
}, [visible, shouldShow]);
const showPopout = visible && shouldShow;
const formatRuntime = (minutes?: number | null) => {
if (!minutes) return null;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const formatVoteCount = (count?: number) => {
if (!count) return "0";
if (count >= 1000) {
return `${Math.floor(count / 1000)}K+`;
}
return count.toString();
};
return (
<div
className={classNames(
"absolute left-[calc(100%+12px)] top-1/2 -translate-y-1/2 ml-1 w-[280px] rounded-xl overflow-hidden transition-all duration-300",
"backdrop-blur-md bg-mediaCard-hoverBackground border border-mediaCard-hoverAccent/40",
"z-[999]",
showPopout
? "opacity-100 translate-x-0"
: "opacity-0 -translate-x-4 pointer-events-none",
)}
onMouseEnter={() => media.onHoverInfoEnter?.()}
onMouseLeave={() => media.onHoverInfoLeave?.()}
>
<div className="p-0">
{isLoading ? (
<InfoSkeleton />
) : (
<div className="relative">
{backdrop && (
<div className="absolute top-0 left-0 right-0 h-full z-0">
<img
src={backdrop}
alt={media.title}
className="w-full h-40 object-cover"
style={{
maskImage:
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 60px)",
WebkitMaskImage:
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 60px)",
}}
/>
</div>
)}
<div className="relative z-10">
<div className="h-40" /> {/* Spacer for backdrop height */}
<div className="px-4 pb-4 mt-[-30px]">
<h3 className="text-lg font-bold text-white mb-2">
{media.title}
</h3>
{description && (
<p className="text-sm text-white/90 mb-4 line-clamp-4">
{description}
</p>
)}
{/* Additional Details Section */}
<div className="grid grid-cols-2 gap-2 text-xs">
{additionalDetails.runtime && (
<div className="flex items-center gap-1 text-white/80">
<span className="font-medium">Runtime:</span>{" "}
{formatRuntime(additionalDetails.runtime)}
</div>
)}
{additionalDetails.language && (
<div className="flex items-center gap-1 text-white/80">
<span className="font-medium">Language:</span>{" "}
{additionalDetails.language.toUpperCase()}
</div>
)}
{additionalDetails.voteAverage !== undefined &&
additionalDetails.voteCount !== undefined &&
additionalDetails.voteCount > 0 && (
<div className="flex items-center gap-1 text-white/80">
<span className="font-medium">Rating:</span>{" "}
{additionalDetails.voteAverage.toFixed(1)}/10
<span className="text-white/60 text-[10px]">
({formatVoteCount(additionalDetails.voteCount)})
</span>
</div>
)}
</div>
{/* Genres */}
{additionalDetails.genres &&
additionalDetails.genres.length > 0 && (
<div className="mt-4 flex flex-wrap gap-1">
{additionalDetails.genres.slice(0, 3).map((genre) => (
<span
key={genre.id}
className="text-[10px] px-1.5 py-0.5 rounded-full bg-white/10 text-white/70"
>
{genre.name}
</span>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -1,5 +1,7 @@
// I'm sorry this is so confusing 😭
import classNames from "classnames";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useCopyToClipboard } from "react-use";
@ -8,12 +10,14 @@ import { mediaItemToId } from "@/backend/metadata/tmdb";
import { DotList } from "@/components/text/DotList";
import { Flare } from "@/components/utils/Flare";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import { usePreferencesStore } from "@/stores/preferences";
import { MediaItem } from "@/utils/mediaTypes";
import { MediaBookmarkButton } from "./MediaBookmark";
import { Button } from "../buttons/Button";
import { IconPatch } from "../buttons/IconPatch";
import { Icon, Icons } from "../Icon";
import { InfoPopout } from "./InfoPopout";
export interface MediaCardProps {
media: MediaItem;
@ -59,12 +63,14 @@ function MediaCardContent({
handleMouseEnter,
handleMouseLeave,
link,
isHoveringCard,
}: MediaCardProps & {
overlayVisible: boolean;
setOverlayVisible: React.Dispatch<React.SetStateAction<boolean>>;
handleMouseEnter: () => void;
handleMouseLeave: () => void;
link: string;
isHoveringCard: boolean;
}) {
const { t } = useTranslation();
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
@ -88,7 +94,7 @@ function MediaCardContent({
setOverlayVisible(false);
}
if (media.year) {
if (isReleased() && media.year) {
dotListContent.push(media.year.toFixed());
}
@ -151,7 +157,7 @@ function MediaCardContent({
/>
<Flare.Child
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
canLink ? "group-hover:scale-95" : "opacity-60"
canLink ? (isHoveringCard ? "scale-95" : "") : "opacity-60"
}`}
>
<div
@ -344,6 +350,7 @@ function MediaCardContent({
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setOverlayVisible(!overlayVisible);
}}
>
@ -363,20 +370,59 @@ function MediaCardContent({
export function MediaCard(props: MediaCardProps) {
const [overlayVisible, setOverlayVisible] = useState(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
const [showHoverInfo, setShowHoverInfo] = useState(false);
const hoverTimer = useRef<NodeJS.Timeout>();
const [isHoveringCard, setIsHoveringCard] = useState(false);
const [isHoveringInfo, setIsHoveringInfo] = useState(false);
const [isBigScreen, setIsBigScreen] = useState(false);
const enablePopDetails = usePreferencesStore((s) => s.enablePopDetails);
useEffect(() => {
const checkScreenSize = () => {
setIsBigScreen(window.innerWidth >= 768); // md breakpoint
};
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
}, []);
const handleMouseEnter = () => {
setIsHoveringCard(true);
if (timeoutId) {
clearTimeout(timeoutId);
setTimeoutId(null);
}
if (hoverTimer.current) {
clearTimeout(hoverTimer.current);
}
if (isBigScreen && !overlayVisible) {
hoverTimer.current = setTimeout(() => {
setShowHoverInfo(true);
}, 200); // 0.2 second delay
}
};
const handleMouseLeave = () => {
setIsHoveringCard(false);
if (hoverTimer.current) {
clearTimeout(hoverTimer.current);
}
if (!isHoveringInfo) {
setShowHoverInfo(false);
}
const id = setTimeout(() => {
setOverlayVisible(false);
}, 2000); // 2 seconds
setTimeoutId(id);
};
const handleMouseEnter = () => {
if (timeoutId) {
clearTimeout(timeoutId);
setTimeoutId(null);
}
};
const shouldShowHoverInfo =
showHoverInfo && !overlayVisible && isBigScreen && enablePopDetails;
const isReleased = useCallback(
() => checkReleased(props.media),
@ -398,6 +444,17 @@ export function MediaCard(props: MediaCardProps) {
}
}
const hoverMedia = {
...props.media,
onHoverInfoEnter: () => setIsHoveringInfo(true),
onHoverInfoLeave: () => {
setIsHoveringInfo(false);
if (!isHoveringCard && !overlayVisible) {
setShowHoverInfo(false);
}
},
};
const content = (
<MediaCardContent
{...props}
@ -406,10 +463,17 @@ export function MediaCard(props: MediaCardProps) {
handleMouseEnter={handleMouseEnter}
handleMouseLeave={handleMouseLeave}
link={link}
isHoveringCard={isHoveringCard}
/>
);
if (!canLink) return <span>{content}</span>;
if (!canLink)
return (
<span className="relative">
{content}{" "}
<InfoPopout media={hoverMedia} visible={shouldShowHoverInfo} />
</span>
);
return (
<div className="relative">
{!overlayVisible ? (
@ -420,6 +484,8 @@ export function MediaCard(props: MediaCardProps) {
"tabbable",
props.closable ? "hover:cursor-default" : "",
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</Link>
@ -430,10 +496,16 @@ export function MediaCard(props: MediaCardProps) {
"tabbable",
props.closable ? "hover:cursor-default" : "",
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</div>
)}
{shouldShowHoverInfo && (
<InfoPopout media={hoverMedia} visible={shouldShowHoverInfo} />
)}
</div>
);
}

View file

@ -54,6 +54,7 @@ export function useSettingsState(
enableThumbnails: boolean,
enableAutoplay: boolean,
enableDiscover: boolean,
enablePopDetails: boolean,
sourceOrder: string[],
enableSourceOrder: boolean,
proxyTmdb: boolean,
@ -108,6 +109,12 @@ export function useSettingsState(
resetEnableDiscover,
enableDiscoverChanged,
] = useDerived(enableDiscover);
const [
enablePopDetailsState,
setEnablePopDetailsState,
resetEnablePopDetails,
enablePopDetailsChanged,
] = useDerived(enablePopDetails);
const [
sourceOrderState,
setSourceOrderState,
@ -136,6 +143,7 @@ export function useSettingsState(
resetEnableThumbnails();
resetEnableAutoplay();
resetEnableDiscover();
resetEnablePopDetails();
resetSourceOrder();
resetEnableSourceOrder();
resetProxyTmdb();
@ -153,6 +161,7 @@ export function useSettingsState(
enableThumbnailsChanged ||
enableAutoplayChanged ||
enableDiscoverChanged ||
enablePopDetailsChanged ||
sourceOrderChanged ||
enableSourceOrderChanged ||
proxyTmdbChanged;
@ -215,6 +224,11 @@ export function useSettingsState(
set: setEnableDiscoverState,
changed: enableDiscoverChanged,
},
enablePopDetails: {
state: enablePopDetailsState,
set: setEnablePopDetailsState,
changed: enablePopDetailsChanged,
},
sourceOrder: {
state: sourceOrderState,
set: setSourceOrderState,

View file

@ -23,12 +23,12 @@ import { useIsMobile } from "@/hooks/useIsMobile";
import { useSettingsState } from "@/hooks/useSettingsState";
import { AccountActionsPart } from "@/pages/parts/settings/AccountActionsPart";
import { AccountEditPart } from "@/pages/parts/settings/AccountEditPart";
import { AppearancePart } from "@/pages/parts/settings/AppearancePart";
import { CaptionsPart } from "@/pages/parts/settings/CaptionsPart";
import { ConnectionsPart } from "@/pages/parts/settings/ConnectionsPart";
import { DeviceListPart } from "@/pages/parts/settings/DeviceListPart";
import { RegisterCalloutPart } from "@/pages/parts/settings/RegisterCalloutPart";
import { SidebarPart } from "@/pages/parts/settings/SidebarPart";
import { ThemePart } from "@/pages/parts/settings/ThemePart";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { useLanguageStore } from "@/stores/language";
@ -146,6 +146,9 @@ export function SettingsPage() {
const enableDiscover = usePreferencesStore((s) => s.enableDiscover);
const setEnableDiscover = usePreferencesStore((s) => s.setEnableDiscover);
const enablePopDetails = usePreferencesStore((s) => s.enablePopDetails);
const setEnablePopDetails = usePreferencesStore((s) => s.setEnablePopDetails);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const setEnableSourceOrder = usePreferencesStore(
(s) => s.setEnableSourceOrder,
@ -179,6 +182,7 @@ export function SettingsPage() {
enableThumbnails,
enableAutoplay,
enableDiscover,
enablePopDetails,
sourceOrder,
enableSourceOrder,
proxyTmdb,
@ -253,6 +257,7 @@ export function SettingsPage() {
setEnableThumbnails(state.enableThumbnails.state);
setEnableAutoplay(state.enableAutoplay.state);
setEnableDiscover(state.enableDiscover.state);
setEnablePopDetails(state.enablePopDetails.state);
setSourceOrder(state.sourceOrder.state);
setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state);
@ -285,6 +290,7 @@ export function SettingsPage() {
state,
setEnableAutoplay,
setEnableDiscover,
setEnablePopDetails,
setSourceOrder,
setAppLanguage,
setTheme,
@ -338,8 +344,6 @@ export function SettingsPage() {
setEnableThumbnails={state.enableThumbnails.set}
enableAutoplay={state.enableAutoplay.state}
setEnableAutoplay={state.enableAutoplay.set}
enableDiscover={state.enableDiscover.state}
setEnableDiscover={state.enableDiscover.set}
sourceOrder={availableSources}
setSourceOrder={state.sourceOrder.set}
enableSourceOrder={state.enableSourceOrder.state}
@ -347,10 +351,14 @@ export function SettingsPage() {
/>
</div>
<div id="settings-appearance" className="mt-28">
<ThemePart
<AppearancePart
active={previewTheme ?? "default"}
inUse={activeTheme ?? "default"}
setTheme={setThemeWithPreview}
enableDiscover={state.enableDiscover.state}
setEnableDiscover={state.enableDiscover.set}
enablePopDetails={state.enablePopDetails.state}
setEnablePopDetails={state.enablePopDetails.set}
/>
</div>
<div id="settings-captions" className="mt-28">

View file

@ -1,6 +1,7 @@
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { Heading1 } from "@/components/utils/Text";
@ -130,27 +131,77 @@ function ThemePreview(props: {
);
}
export function ThemePart(props: {
export function AppearancePart(props: {
active: string;
inUse: string;
setTheme: (theme: string) => void;
enableDiscover: boolean;
setEnableDiscover: (v: boolean) => void;
enablePopDetails: boolean;
setEnablePopDetails: (v: boolean) => void;
}) {
const { t } = useTranslation();
return (
<div>
<div className="space-y-12">
<Heading1 border>{t("settings.appearance.title")}</Heading1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
{availableThemes.map((v) => (
<ThemePreview
selector={v.selector}
active={props.active === v.id}
inUse={props.inUse === v.id}
name={t(v.key)}
key={v.id}
onClick={() => props.setTheme(v.id)}
/>
))}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* First Column - Preferences */}
<div className="space-y-8">
<div>
<p className="text-white font-bold mb-3">
{t("settings.appearance.options.discover")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.appearance.options.discoverDescription")}
</p>
<div
onClick={() => props.setEnableDiscover(!props.enableDiscover)}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.enableDiscover} />
<p className="flex-1 text-white font-bold">
{t("settings.appearance.options.discoverLabel")}
</p>
</div>
</div>
<div>
<p className="text-white font-bold mb-3">
{t("settings.appearance.options.hover")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.appearance.options.hoverDescription")}
</p>
<div
onClick={() => props.setEnablePopDetails(!props.enablePopDetails)}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.enablePopDetails} />
<p className="flex-1 text-white font-bold">
{t("settings.appearance.options.hoverLabel")}
</p>
</div>
</div>
</div>
{/* Second Column - Themes */}
<div className="space-y-8">
<div className="grid grid-cols-2 gap-4 max-w-[600px]">
{availableThemes.map((v) => (
<ThemePreview
selector={v.selector}
active={props.active === v.id}
inUse={props.inUse === v.id}
name={t(v.key)}
key={v.id}
onClick={() => props.setTheme(v.id)}
/>
))}
</div>
</div>
</div>
</div>
);

View file

@ -23,8 +23,6 @@ export function PreferencesPart(props: {
setEnableAutoplay: (v: boolean) => void;
sourceOrder: string[];
setSourceOrder: (v: string[]) => void;
enableDiscover: boolean;
setEnableDiscover: (v: boolean) => void;
enableSourceOrder: boolean;
setenableSourceOrder: (v: boolean) => void;
}) {
@ -125,25 +123,6 @@ export function PreferencesPart(props: {
</p>
</div>
</div>
{/* Show Discover Preference */}
<div>
<p className="text-white font-bold mb-3">
{t("settings.preferences.discover")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.preferences.discoverDescription")}
</p>
<div
onClick={() => props.setEnableDiscover(!props.enableDiscover)}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.enableDiscover} />
<p className="flex-1 text-white font-bold">
{t("settings.preferences.discoverLabel")}
</p>
</div>
</div>
</div>
{/* Column */}

View file

@ -6,6 +6,7 @@ export interface PreferencesStore {
enableThumbnails: boolean;
enableAutoplay: boolean;
enableDiscover: boolean;
enablePopDetails: boolean;
sourceOrder: string[];
enableSourceOrder: boolean;
proxyTmdb: boolean;
@ -13,6 +14,7 @@ export interface PreferencesStore {
setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void;
setEnableDiscover(v: boolean): void;
setEnablePopDetails(v: boolean): void;
setSourceOrder(v: string[]): void;
setEnableSourceOrder(v: boolean): void;
setProxyTmdb(v: boolean): void;
@ -24,6 +26,7 @@ export const usePreferencesStore = create(
enableThumbnails: false,
enableAutoplay: true,
enableDiscover: true,
enablePopDetails: false,
sourceOrder: [],
enableSourceOrder: false,
proxyTmdb: false,
@ -42,6 +45,11 @@ export const usePreferencesStore = create(
s.enableDiscover = v;
});
},
setEnablePopDetails(v) {
set((s) => {
s.enablePopDetails = v;
});
},
setSourceOrder(v) {
set((s) => {
s.sourceOrder = v;

View file

@ -5,4 +5,6 @@ export interface MediaItem {
release_date?: Date;
poster?: string;
type: "show" | "movie";
onHoverInfoEnter?: () => void;
onHoverInfoLeave?: () => void;
}