feat: watch history

This commit is contained in:
Adrian Castro
2024-03-27 12:31:16 +01:00
parent fa2425c183
commit 8c8ad47581
5 changed files with 107 additions and 73 deletions

View File

@@ -1,18 +1,22 @@
import React from "react"; import React from "react";
import { View } from "tamagui"; import { View } from "tamagui";
import { ItemListSection, watching } from "~/components/item/ItemListSection"; import { ItemListSection } from "~/components/item/ItemListSection";
import ScreenLayout from "~/components/layout/ScreenLayout"; import ScreenLayout from "~/components/layout/ScreenLayout";
import { useBookmarkStore } from "~/stores/settings"; import { useBookmarkStore, useWatchHistoryStore } from "~/stores/settings";
export default function HomeScreen() { export default function HomeScreen() {
const { bookmarks } = useBookmarkStore(); const { bookmarks } = useBookmarkStore();
const { watchHistory } = useWatchHistoryStore();
return ( return (
<View style={{ flex: 1 }} flex={1}> <View style={{ flex: 1 }} flex={1}>
<ScreenLayout> <ScreenLayout>
<ItemListSection title="Bookmarks" items={bookmarks} /> <ItemListSection title="Bookmarks" items={bookmarks} />
<ItemListSection title="Continue Watching" items={watching} /> <ItemListSection
title="Continue Watching"
items={watchHistory.map((x) => x.item)}
/>
</ScreenLayout> </ScreenLayout>
</View> </View>
); );

View File

@@ -5,41 +5,6 @@ import { ScrollView, Text, View } from "tamagui";
import type { ItemData } from "~/components/item/item"; import type { ItemData } from "~/components/item/item";
import Item from "~/components/item/item"; import Item from "~/components/item/item";
export const watching: ItemData[] = [
{
id: "219651",
title: "Welcome to Samdal-ri",
posterUrl:
"https://www.themoviedb.org/t/p/w500/98IvA2i0PsTY8CThoHByCKOEAjz.jpg",
type: "tv",
year: 2023,
},
{
id: "194797",
title: "Doona!",
posterUrl:
"https://www.themoviedb.org/t/p/w500/bQhiOkU3lCu5pwCqPdNVG5GBLlj.jpg",
type: "tv",
year: 2023,
},
{
id: "113268",
title: "The Uncanny Counter",
posterUrl:
"https://www.themoviedb.org/t/p/w500/tKU34QiJUfVipcuhAs5S3TdCpAF.jpg",
type: "tv",
year: 2020,
},
{
id: "203508",
title: "Earth Arcade",
posterUrl:
"https://www.themoviedb.org/t/p/w500/vBJ0uF0WlFcjr9obZZqE6GSsKoL.jpg",
type: "tv",
year: 2022,
},
];
const padding = 20; const padding = 20;
const screenWidth = Dimensions.get("window").width; const screenWidth = Dimensions.get("window").width;
const itemWidth = screenWidth / 2.3 - padding; const itemWidth = screenWidth / 2.3 - padding;

View File

@@ -25,10 +25,17 @@ import { useBrightness } from "~/hooks/player/useBrightness";
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed"; import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
import { usePlayer } from "~/hooks/player/usePlayer"; import { usePlayer } from "~/hooks/player/usePlayer";
import { useVolume } from "~/hooks/player/useVolume"; import { useVolume } from "~/hooks/player/useVolume";
import { convertMetaToScrapeMedia, getNextEpisode } from "~/lib/meta"; import {
convertMetaToItemData,
convertMetaToScrapeMedia,
getNextEpisode,
} from "~/lib/meta";
import { useAudioTrackStore } from "~/stores/audio"; import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { usePlayerSettingsStore } from "~/stores/settings"; import {
usePlayerSettingsStore,
useWatchHistoryStore,
} from "~/stores/settings";
import { CaptionRenderer } from "./CaptionRenderer"; import { CaptionRenderer } from "./CaptionRenderer";
import { ControlsOverlay } from "./ControlsOverlay"; import { ControlsOverlay } from "./ControlsOverlay";
@@ -68,6 +75,7 @@ export const VideoPlayer = () => {
const setMeta = usePlayerStore((state) => state.setMeta); const setMeta = usePlayerStore((state) => state.setMeta);
const { gestureControls, autoPlay } = usePlayerSettingsStore(); const { gestureControls, autoPlay } = usePlayerSettingsStore();
const { updateWatchHistory, removeFromWatchHistory } = useWatchHistoryStore();
const updateResizeMode = (newMode: ResizeMode) => { const updateResizeMode = (newMode: ResizeMode) => {
setResizeMode(newMode); setResizeMode(newMode);
@@ -195,6 +203,15 @@ export const VideoPlayer = () => {
}, 60000); }, 60000);
return () => { return () => {
if (meta) {
const item = convertMetaToItemData(meta);
const scrapeMedia = convertMetaToScrapeMedia(meta);
updateWatchHistory(
item,
scrapeMedia,
videoRef?.props.positionMillis ?? 0,
);
}
clearTimeout(timeout); clearTimeout(timeout);
void synchronizePlayback(); void synchronizePlayback();
}; };
@@ -202,11 +219,14 @@ export const VideoPlayer = () => {
asset, asset,
dismissFullscreenPlayer, dismissFullscreenPlayer,
hasStartedPlaying, hasStartedPlaying,
meta,
router, router,
selectedAudioTrack, selectedAudioTrack,
setVideoSrc, setVideoSrc,
stream, stream,
synchronizePlayback, synchronizePlayback,
updateWatchHistory,
videoRef?.props.positionMillis,
]); ]);
const onVideoLoadStart = () => { const onVideoLoadStart = () => {
@@ -218,11 +238,24 @@ export const VideoPlayer = () => {
setHasStartedPlaying(true); setHasStartedPlaying(true);
if (videoRef) { if (videoRef) {
void videoRef.setRateAsync(currentSpeed, true); void videoRef.setRateAsync(currentSpeed, true);
if (meta) {
const item = convertMetaToItemData(meta);
const scrapeMedia = convertMetaToScrapeMedia(meta);
updateWatchHistory(
item,
scrapeMedia,
videoRef.props.positionMillis ?? 0,
);
}
} }
}; };
const onPlaybackStatusUpdate = async (status: AVPlaybackStatus) => { const onPlaybackStatusUpdate = async (status: AVPlaybackStatus) => {
setStatus(status); setStatus(status);
if (meta && status.isLoaded && status.didJustFinish) {
const item = convertMetaToItemData(meta);
removeFromWatchHistory(item);
}
if ( if (
status.isLoaded && status.isLoaded &&
status.didJustFinish && status.didJustFinish &&

View File

@@ -1,6 +1,7 @@
import type { ScrapeMedia } from "@movie-web/provider-utils"; import type { ScrapeMedia } from "@movie-web/provider-utils";
import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb"; import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb";
import type { ItemData } from "~/components/item/item";
import type { PlayerMeta } from "~/stores/player/slices/video"; import type { PlayerMeta } from "~/stores/player/slices/video";
export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => { export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
@@ -27,6 +28,28 @@ export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
throw new Error("Invalid meta type"); throw new Error("Invalid meta type");
}; };
export const convertMetaToItemData = (meta: PlayerMeta): ItemData => {
if (meta.type === "movie") {
return {
id: meta.tmdbId,
title: meta.title,
year: meta.releaseYear,
type: meta.type,
posterUrl: meta.poster ?? "",
};
}
if (meta.type === "show") {
return {
id: meta.tmdbId,
title: meta.title,
year: meta.releaseYear,
type: "tv",
posterUrl: meta.poster ?? "",
};
}
throw new Error("Invalid media type");
};
export const getNextEpisode = async ( export const getNextEpisode = async (
meta: PlayerMeta, meta: PlayerMeta,
): Promise<PlayerMeta | undefined> => { ): Promise<PlayerMeta | undefined> => {

View File

@@ -4,10 +4,11 @@ import { MMKV } from "react-native-mmkv";
import { create } from "zustand"; import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware"; import { createJSONStorage, persist } from "zustand/middleware";
import type { ScrapeMedia } from "@movie-web/provider-utils";
import type { ItemData } from "~/components/item/item"; import type { ItemData } from "~/components/item/item";
import type { DownloadItem } from "~/hooks/DownloadManagerContext"; import type { DownloadItem } from "~/hooks/DownloadManagerContext";
import type { ThemeStoreOption } from "~/stores/theme"; import type { ThemeStoreOption } from "~/stores/theme";
import type { ScrapeMedia } from "@movie-web/provider-utils";
const storage = new MMKV(); const storage = new MMKV();
@@ -111,7 +112,7 @@ export const useBookmarkStore = create<
persist( persist(
(set, get) => ({ (set, get) => ({
bookmarks: [], bookmarks: [],
setBookmarks: (bookmarks: ItemData[]) => set({ bookmarks }), setBookmarks: (bookmarks: ItemData[]) => set({ bookmarks }),
addBookmark: (item: ItemData) => addBookmark: (item: ItemData) =>
set((state) => ({ set((state) => ({
bookmarks: [...state.bookmarks, item], bookmarks: [...state.bookmarks, item],
@@ -141,7 +142,11 @@ interface WatchHistoryItem {
interface WatchHistoryStoreState { interface WatchHistoryStoreState {
watchHistory: WatchHistoryItem[]; watchHistory: WatchHistoryItem[];
setWatchHistory: (watchHistory: WatchHistoryItem[]) => void; setWatchHistory: (watchHistory: WatchHistoryItem[]) => void;
addToWatchHistory: (item: ItemData, media: ScrapeMedia) => void; updateWatchHistory: (
item: ItemData,
media: ScrapeMedia,
positionMillis: number,
) => void;
removeFromWatchHistory: (item: ItemData) => void; removeFromWatchHistory: (item: ItemData) => void;
} }
@@ -149,34 +154,38 @@ export const useWatchHistoryStore = create<
WatchHistoryStoreState, WatchHistoryStoreState,
[["zustand/persist", WatchHistoryStoreState]] [["zustand/persist", WatchHistoryStoreState]]
>( >(
persist( persist(
(set) => ({ (set) => ({
watchHistory: [], watchHistory: [],
setWatchHistory: (watchHistory: WatchHistoryItem[]) => setWatchHistory: (watchHistory: WatchHistoryItem[]) =>
set({ watchHistory }), set({ watchHistory }),
addToWatchHistory: (item: ItemData, media: ScrapeMedia) => updateWatchHistory: (
set((state) => ({ item: ItemData,
watchHistory: [ media: ScrapeMedia,
...state.watchHistory.filter( positionMillis: number,
(historyItem) => historyItem.item.id !== item.id, ) =>
), set((state) => ({
{ watchHistory: [
item, ...state.watchHistory.filter(
media, (historyItem) => historyItem.item.id !== item.id,
positionMillis: 0, ),
}, {
], item,
})), media,
removeFromWatchHistory: (item: ItemData) => positionMillis,
set((state) => ({ },
watchHistory: state.watchHistory.filter( ],
(historyItem) => historyItem.item.id !== item.id, })),
), removeFromWatchHistory: (item: ItemData) =>
}), set((state) => ({
)}), watchHistory: state.watchHistory.filter(
{ (historyItem) => historyItem.item.id !== item.id,
name: "watch-history", ),
storage: createJSONStorage(() => zustandStorage), })),
}, }),
{
name: "watch-history",
storage: createJSONStorage(() => zustandStorage),
},
), ),
); );