From 8c8ad47581ebee10f6bc6c8e6a1618687451e692 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:31:16 +0100 Subject: [PATCH] feat: watch history --- apps/expo/src/app/(tabs)/index.tsx | 10 ++- .../src/components/item/ItemListSection.tsx | 35 --------- .../src/components/player/VideoPlayer.tsx | 37 ++++++++- apps/expo/src/lib/meta.ts | 23 ++++++ apps/expo/src/stores/settings/index.ts | 75 +++++++++++-------- 5 files changed, 107 insertions(+), 73 deletions(-) diff --git a/apps/expo/src/app/(tabs)/index.tsx b/apps/expo/src/app/(tabs)/index.tsx index afa509e..7e015db 100644 --- a/apps/expo/src/app/(tabs)/index.tsx +++ b/apps/expo/src/app/(tabs)/index.tsx @@ -1,18 +1,22 @@ import React from "react"; import { View } from "tamagui"; -import { ItemListSection, watching } from "~/components/item/ItemListSection"; +import { ItemListSection } from "~/components/item/ItemListSection"; import ScreenLayout from "~/components/layout/ScreenLayout"; -import { useBookmarkStore } from "~/stores/settings"; +import { useBookmarkStore, useWatchHistoryStore } from "~/stores/settings"; export default function HomeScreen() { const { bookmarks } = useBookmarkStore(); + const { watchHistory } = useWatchHistoryStore(); return ( - + x.item)} + /> ); diff --git a/apps/expo/src/components/item/ItemListSection.tsx b/apps/expo/src/components/item/ItemListSection.tsx index f447913..35f48c7 100644 --- a/apps/expo/src/components/item/ItemListSection.tsx +++ b/apps/expo/src/components/item/ItemListSection.tsx @@ -5,41 +5,6 @@ import { ScrollView, Text, View } from "tamagui"; import type { ItemData } 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 screenWidth = Dimensions.get("window").width; const itemWidth = screenWidth / 2.3 - padding; diff --git a/apps/expo/src/components/player/VideoPlayer.tsx b/apps/expo/src/components/player/VideoPlayer.tsx index 3db8f00..0a093ae 100644 --- a/apps/expo/src/components/player/VideoPlayer.tsx +++ b/apps/expo/src/components/player/VideoPlayer.tsx @@ -25,10 +25,17 @@ import { useBrightness } from "~/hooks/player/useBrightness"; import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed"; import { usePlayer } from "~/hooks/player/usePlayer"; 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 { usePlayerStore } from "~/stores/player/store"; -import { usePlayerSettingsStore } from "~/stores/settings"; +import { + usePlayerSettingsStore, + useWatchHistoryStore, +} from "~/stores/settings"; import { CaptionRenderer } from "./CaptionRenderer"; import { ControlsOverlay } from "./ControlsOverlay"; @@ -68,6 +75,7 @@ export const VideoPlayer = () => { const setMeta = usePlayerStore((state) => state.setMeta); const { gestureControls, autoPlay } = usePlayerSettingsStore(); + const { updateWatchHistory, removeFromWatchHistory } = useWatchHistoryStore(); const updateResizeMode = (newMode: ResizeMode) => { setResizeMode(newMode); @@ -195,6 +203,15 @@ export const VideoPlayer = () => { }, 60000); return () => { + if (meta) { + const item = convertMetaToItemData(meta); + const scrapeMedia = convertMetaToScrapeMedia(meta); + updateWatchHistory( + item, + scrapeMedia, + videoRef?.props.positionMillis ?? 0, + ); + } clearTimeout(timeout); void synchronizePlayback(); }; @@ -202,11 +219,14 @@ export const VideoPlayer = () => { asset, dismissFullscreenPlayer, hasStartedPlaying, + meta, router, selectedAudioTrack, setVideoSrc, stream, synchronizePlayback, + updateWatchHistory, + videoRef?.props.positionMillis, ]); const onVideoLoadStart = () => { @@ -218,11 +238,24 @@ export const VideoPlayer = () => { setHasStartedPlaying(true); if (videoRef) { 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) => { setStatus(status); + if (meta && status.isLoaded && status.didJustFinish) { + const item = convertMetaToItemData(meta); + removeFromWatchHistory(item); + } if ( status.isLoaded && status.didJustFinish && diff --git a/apps/expo/src/lib/meta.ts b/apps/expo/src/lib/meta.ts index da1939a..7d5b0a9 100644 --- a/apps/expo/src/lib/meta.ts +++ b/apps/expo/src/lib/meta.ts @@ -1,6 +1,7 @@ import type { ScrapeMedia } from "@movie-web/provider-utils"; import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb"; +import type { ItemData } from "~/components/item/item"; import type { PlayerMeta } from "~/stores/player/slices/video"; export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => { @@ -27,6 +28,28 @@ export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => { 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 ( meta: PlayerMeta, ): Promise => { diff --git a/apps/expo/src/stores/settings/index.ts b/apps/expo/src/stores/settings/index.ts index 6ce201d..6056a07 100644 --- a/apps/expo/src/stores/settings/index.ts +++ b/apps/expo/src/stores/settings/index.ts @@ -4,10 +4,11 @@ import { MMKV } from "react-native-mmkv"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; +import type { ScrapeMedia } from "@movie-web/provider-utils"; + import type { ItemData } from "~/components/item/item"; import type { DownloadItem } from "~/hooks/DownloadManagerContext"; import type { ThemeStoreOption } from "~/stores/theme"; -import type { ScrapeMedia } from "@movie-web/provider-utils"; const storage = new MMKV(); @@ -111,7 +112,7 @@ export const useBookmarkStore = create< persist( (set, get) => ({ bookmarks: [], - setBookmarks: (bookmarks: ItemData[]) => set({ bookmarks }), + setBookmarks: (bookmarks: ItemData[]) => set({ bookmarks }), addBookmark: (item: ItemData) => set((state) => ({ bookmarks: [...state.bookmarks, item], @@ -141,7 +142,11 @@ interface WatchHistoryItem { interface WatchHistoryStoreState { watchHistory: WatchHistoryItem[]; setWatchHistory: (watchHistory: WatchHistoryItem[]) => void; - addToWatchHistory: (item: ItemData, media: ScrapeMedia) => void; + updateWatchHistory: ( + item: ItemData, + media: ScrapeMedia, + positionMillis: number, + ) => void; removeFromWatchHistory: (item: ItemData) => void; } @@ -149,34 +154,38 @@ export const useWatchHistoryStore = create< WatchHistoryStoreState, [["zustand/persist", WatchHistoryStoreState]] >( - persist( - (set) => ({ - watchHistory: [], - setWatchHistory: (watchHistory: WatchHistoryItem[]) => - set({ watchHistory }), - addToWatchHistory: (item: ItemData, media: ScrapeMedia) => - set((state) => ({ - watchHistory: [ - ...state.watchHistory.filter( - (historyItem) => historyItem.item.id !== item.id, - ), - { - item, - media, - positionMillis: 0, - }, - ], - })), - removeFromWatchHistory: (item: ItemData) => - set((state) => ({ - watchHistory: state.watchHistory.filter( - (historyItem) => historyItem.item.id !== item.id, - ), - }), - )}), - { - name: "watch-history", - storage: createJSONStorage(() => zustandStorage), - }, + persist( + (set) => ({ + watchHistory: [], + setWatchHistory: (watchHistory: WatchHistoryItem[]) => + set({ watchHistory }), + updateWatchHistory: ( + item: ItemData, + media: ScrapeMedia, + positionMillis: number, + ) => + set((state) => ({ + watchHistory: [ + ...state.watchHistory.filter( + (historyItem) => historyItem.item.id !== item.id, + ), + { + item, + media, + positionMillis, + }, + ], + })), + removeFromWatchHistory: (item: ItemData) => + set((state) => ({ + watchHistory: state.watchHistory.filter( + (historyItem) => historyItem.item.id !== item.id, + ), + })), + }), + { + name: "watch-history", + storage: createJSONStorage(() => zustandStorage), + }, ), -); \ No newline at end of file +);