mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 18:13:25 +00:00
feat: watch history
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 &&
|
||||||
|
@@ -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> => {
|
||||||
|
@@ -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();
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +159,11 @@ export const useWatchHistoryStore = create<
|
|||||||
watchHistory: [],
|
watchHistory: [],
|
||||||
setWatchHistory: (watchHistory: WatchHistoryItem[]) =>
|
setWatchHistory: (watchHistory: WatchHistoryItem[]) =>
|
||||||
set({ watchHistory }),
|
set({ watchHistory }),
|
||||||
addToWatchHistory: (item: ItemData, media: ScrapeMedia) =>
|
updateWatchHistory: (
|
||||||
|
item: ItemData,
|
||||||
|
media: ScrapeMedia,
|
||||||
|
positionMillis: number,
|
||||||
|
) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
watchHistory: [
|
watchHistory: [
|
||||||
...state.watchHistory.filter(
|
...state.watchHistory.filter(
|
||||||
@@ -163,7 +172,7 @@ export const useWatchHistoryStore = create<
|
|||||||
{
|
{
|
||||||
item,
|
item,
|
||||||
media,
|
media,
|
||||||
positionMillis: 0,
|
positionMillis,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
@@ -172,8 +181,8 @@ export const useWatchHistoryStore = create<
|
|||||||
watchHistory: state.watchHistory.filter(
|
watchHistory: state.watchHistory.filter(
|
||||||
(historyItem) => historyItem.item.id !== item.id,
|
(historyItem) => historyItem.item.id !== item.id,
|
||||||
),
|
),
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
)}),
|
|
||||||
{
|
{
|
||||||
name: "watch-history",
|
name: "watch-history",
|
||||||
storage: createJSONStorage(() => zustandStorage),
|
storage: createJSONStorage(() => zustandStorage),
|
||||||
|
Reference in New Issue
Block a user