Logic to conditionally show continue watching items

Co-authored-by: William Oldham <github@binaryoverload.co.uk>
This commit is contained in:
mrjvs 2023-11-21 20:34:30 +01:00
parent 258b3be687
commit 7a591c82b9
4 changed files with 113 additions and 29 deletions

View File

@ -45,8 +45,8 @@ export interface ProgressResponse {
poster?: string;
type: "show" | "movie";
};
duration: number;
watched: number;
duration: string;
watched: string;
updatedAt: string;
}
@ -81,8 +81,8 @@ export function progressResponsesToEntries(responses: ProgressResponse[]) {
const item = items[v.tmdbId];
if (item.type === "movie") {
item.progress = {
duration: v.duration,
watched: v.watched,
duration: Number(v.duration),
watched: Number(v.watched),
};
}
@ -97,8 +97,8 @@ export function progressResponsesToEntries(responses: ProgressResponse[]) {
number: v.episode.number ?? 0,
title: "",
progress: {
duration: v.duration,
watched: v.watched,
duration: Number(v.duration),
watched: Number(v.watched),
},
seasonId: v.season.id,
updatedAt: new Date(v.updatedAt).getTime(),

View File

@ -1,48 +1,47 @@
import { useMemo } from "react";
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
import { useProgressStore } from "@/stores/progress";
import {
ShowProgressResult,
shouldShowProgress,
} from "@/stores/progress/utils";
import { MediaItem } from "@/utils/mediaTypes";
import { MediaCard } from "./MediaCard";
function formatSeries(series?: ShowProgressResult | null) {
if (!series || !series.episode || !series.season) return undefined;
return {
episode: series.episode?.number,
season: series.season?.number,
episodeId: series.episode?.id,
seasonId: series.season?.id,
};
}
export interface WatchedMediaCardProps {
media: MediaItem;
closable?: boolean;
onClose?: () => void;
}
function formatSeries(obj: ProgressMediaItem | undefined) {
if (!obj) return undefined;
if (obj.type !== "show") return;
const ep = Object.values(obj.episodes).sort(
(a, b) => b.updatedAt - a.updatedAt
)[0];
const season = obj.seasons[ep?.seasonId];
if (!ep || !season) return;
return {
season: season.number,
episode: ep.number,
episodeId: ep.id,
seasonId: ep.seasonId,
progress: ep.progress,
};
}
export function WatchedMediaCard(props: WatchedMediaCardProps) {
const progressItems = useProgressStore((s) => s.items);
const item = useMemo(() => {
return progressItems[props.media.id];
}, [progressItems, props.media]);
const series = useMemo(() => formatSeries(item), [item]);
const progress = item?.progress ?? series?.progress;
const percentage = progress
? (progress.watched / progress.duration) * 100
const itemToDisplay = useMemo(
() => (item ? shouldShowProgress(item) : null),
[item]
);
const percentage = itemToDisplay?.show
? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100
: undefined;
return (
<MediaCard
media={props.media}
series={series}
series={formatSeries(itemToDisplay)}
linkable
percentage={percentage}
onClose={props.onClose}

View File

@ -9,6 +9,7 @@ import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress";
import { shouldShowProgress } from "@/stores/progress/utils";
import { MediaItem } from "@/utils/mediaTypes";
export function WatchingPart() {
@ -22,6 +23,7 @@ export function WatchingPart() {
const sortedProgressItems = useMemo(() => {
let output: MediaItem[] = [];
Object.entries(progressItems)
.filter((entry) => shouldShowProgress(entry[1]).show)
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
.forEach((entry) => {
output.push({

View File

@ -0,0 +1,83 @@
import {
ProgressEpisodeItem,
ProgressItem,
ProgressMediaItem,
ProgressSeasonItem,
} from "@/stores/progress";
export interface ShowProgressResult {
episode?: ProgressEpisodeItem;
season?: ProgressSeasonItem;
progress: ProgressItem;
show: boolean;
}
const defaultProgress = {
duration: 0,
watched: 0,
};
function progressIsCompleted(duration: number, watched: number): boolean {
const timeFromEnd = duration - watched;
// too close to the end, is completed
if (timeFromEnd < 60 * 2) return true;
// satisfies all constraints, not completed
return false;
}
function progressIsNotStarted(duration: number, watched: number): boolean {
// too short watch time
if (watched < 20) return true;
// satisfies all constraints, not completed
return false;
}
function progressIsAcceptableRange(duration: number, watched: number): boolean {
// not started enough yet, not acceptable
if (progressIsNotStarted(duration, watched)) return false;
// is already at the end, not acceptable
if (progressIsCompleted(duration, watched)) return false;
// satisfied all constraints
return true;
}
export function shouldShowProgress(
item: ProgressMediaItem
): ShowProgressResult {
// non shows just hide or show depending on acceptable ranges
if (item.type !== "show") {
return {
show: progressIsAcceptableRange(
item.progress?.duration ?? 0,
item.progress?.watched ?? 0
),
progress: item.progress ?? defaultProgress,
};
}
// shows only hide an item if its too early in episode, it still shows if its near the end.
// Otherwise you would lose episode progress
const ep = Object.values(item.episodes)
.sort((a, b) => b.updatedAt - a.updatedAt)
.filter(
(epi) =>
!progressIsNotStarted(epi.progress.duration, epi.progress.watched)
)[0];
const season = item.seasons[ep?.seasonId];
if (!ep || !season)
return {
show: false,
progress: defaultProgress,
};
return {
season,
episode: ep,
show: true,
progress: ep.progress,
};
}