diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 2e3e2ebb..05a78ead 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -2,18 +2,18 @@ import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { ListItem } from "@/components/ListItem"; import { SettingToggles } from "@/components/settings/SettingToggles"; -import {bytesToReadable, useDownload} from "@/providers/DownloadProvider"; +import { bytesToReadable, useDownload } from "@/providers/DownloadProvider"; import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; -import {clearLogs, useLog} from "@/utils/log"; +import { clearLogs, useLog } from "@/utils/log"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; +import * as FileSystem from "expo-file-system"; import * as Haptics from "expo-haptics"; import { useAtom } from "jotai"; -import {Alert, ScrollView, View} from "react-native"; +import { Alert, ScrollView, View } from "react-native"; +import * as Progress from "react-native-progress"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { toast } from "sonner-native"; -import * as Progress from 'react-native-progress'; -import * as FileSystem from "expo-file-system"; export default function settings() { const { logout } = useJellyfin(); @@ -25,17 +25,17 @@ export default function settings() { const insets = useSafeAreaInsets(); - const {data: size , isLoading: appSizeLoading } = useQuery({ + const { data: size, isLoading: appSizeLoading } = useQuery({ queryKey: ["appSize", appSizeUsage], queryFn: async () => { - const app = await appSizeUsage + const app = await appSizeUsage; - const remaining = await FileSystem.getFreeDiskStorageAsync() - const total = await FileSystem.getTotalDiskCapacityAsync() + const remaining = await FileSystem.getFreeDiskStorageAsync(); + const total = await FileSystem.getTotalDiskCapacityAsync(); - return {app, remaining, total, used: (total - remaining) / total} - } - }) + return { app, remaining, total, used: (total - remaining) / total }; + }, + }); const openQuickConnectAuthCodeInput = () => { Alert.prompt( @@ -69,22 +69,16 @@ export default function settings() { const onDeleteClicked = async () => { try { await deleteAllFiles(); - Haptics.notificationAsync( - Haptics.NotificationFeedbackType.Success - ); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } catch (e) { - Haptics.notificationAsync( - Haptics.NotificationFeedbackType.Error - ); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); toast.error("Error deleting files"); } - } + }; const onClearLogsClicked = async () => { clearLogs(); - Haptics.notificationAsync( - Haptics.NotificationFeedbackType.Success - ); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }; return ( @@ -140,19 +134,16 @@ export default function settings() { progress={size?.used} /> {size && ( - Available: {bytesToReadable(size.remaining)}, Total: {bytesToReadable(size.total)} + + Available: {bytesToReadable(size.remaining)}, Total:{" "} + {bytesToReadable(size.total)} + )} - - diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index d169d347..15890a9f 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -1,10 +1,6 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; -import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; +import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import * as Haptics from "expo-haptics"; -import { useAtom } from "jotai"; import React from "react"; import { View, ViewProps } from "react-native"; import { RoundButton } from "./RoundButton"; @@ -14,9 +10,6 @@ interface Props extends ViewProps { } export const PlayedStatus: React.FC = ({ item, ...props }) => { - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - const queryClient = useQueryClient(); const invalidateQueries = () => { @@ -46,68 +39,14 @@ export const PlayedStatus: React.FC = ({ item, ...props }) => { }); }; - const handlePress = async (played: boolean) => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - - // Optimistic update - queryClient.setQueryData( - ["item", item.Id], - (oldData: BaseItemDto | undefined) => { - if (oldData) { - return { - ...oldData, - UserData: { - ...oldData.UserData, - Played: !played, - }, - }; - } - return oldData; - } - ); - - try { - if (played) { - await markAsNotPlayed({ - api: api, - itemId: item?.Id, - userId: user?.Id, - }); - } else { - await markAsPlayed({ - api: api, - item: item, - userId: user?.Id, - }); - } - invalidateQueries(); - } catch (error) { - // Revert optimistic update on error - queryClient.setQueryData( - ["item", item.Id], - (oldData: BaseItemDto | undefined) => { - if (oldData) { - return { - ...oldData, - UserData: { - ...oldData.UserData, - Played: played, - }, - }; - } - return oldData; - } - ); - console.error("Error updating played status:", error); - } - }; + const markAsPlayedStatus = useMarkAsPlayed(item); return ( handlePress(item.UserData?.Played || false)} + onPress={() => markAsPlayedStatus(item.UserData?.Played || false)} size="large" /> diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 012739e1..cd5fce45 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,8 +1,10 @@ +import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import * as Haptics from "expo-haptics"; import { useRouter, useSegments } from "expo-router"; import { PropsWithChildren } from "react"; import { TouchableOpacity, TouchableOpacityProps } from "react-native"; +import * as ContextMenu from "zeego/context-menu"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -62,17 +64,82 @@ export const TouchableItemRouter: React.FC> = ({ const from = segments[2]; + const markAsPlayedStatus = useMarkAsPlayed(item); + if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( - { - const url = itemRouter(item, from); - // @ts-ignore - router.push(url); - }} - {...props} - > - {children} - + + + { + const url = itemRouter(item, from); + // @ts-ignore + router.push(url); + }} + {...props} + > + {children} + + + + Actions + { + markAsPlayedStatus(true); + }} + shouldDismissMenuOnSelect + > + + Mark as watched + + + + { + markAsPlayedStatus(true); + }} + shouldDismissMenuOnSelect + destructive + > + + Mark as not watched + + + + + ); }; diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index e01309d3..2ad00ad8 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -258,6 +258,21 @@ export const SettingToggles: React.FC = ({ ...props }) => { + + + Safe area in controls + + Enable safe area in video player controls + + + + updateSettings({ safeAreaInControlsEnabled: value }) + } + /> + + diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 7fa33e2e..2fbc3d05 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -513,8 +513,8 @@ export const Controls: React.FC = ({ style={[ { position: "absolute", - top: insets.top, - left: insets.left, + top: settings?.safeAreaInControlsEnabled ? insets.top : 0, + left: settings?.safeAreaInControlsEnabled ? insets.left : 0, opacity: showControls ? 1 : 0, zIndex: 1000, }, @@ -543,8 +543,8 @@ export const Controls: React.FC = ({ style={[ { position: "absolute", - top: insets.top, - right: insets.right, + top: settings?.safeAreaInControlsEnabled ? insets.top : 0, + right: settings?.safeAreaInControlsEnabled ? insets.right : 0, opacity: showControls ? 1 : 0, }, ]} @@ -606,8 +606,8 @@ export const Controls: React.FC = ({ style={{ position: "absolute", top: "50%", // Center vertically - left: insets.left, - right: insets.right, + left: settings?.safeAreaInControlsEnabled ? insets.left : 0, + right: settings?.safeAreaInControlsEnabled ? insets.right : 0, flexDirection: "row", justifyContent: "space-between", alignItems: "center", @@ -720,9 +720,9 @@ export const Controls: React.FC = ({ style={[ { position: "absolute", - right: insets.right, - left: insets.left, - bottom: insets.bottom, + right: settings?.safeAreaInControlsEnabled ? insets.right : 0, + left: settings?.safeAreaInControlsEnabled ? insets.left : 0, + bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0, }, ]} className={`flex flex-col p-4`} diff --git a/hooks/useMarkAsPlayed.ts b/hooks/useMarkAsPlayed.ts new file mode 100644 index 00000000..ff039cc8 --- /dev/null +++ b/hooks/useMarkAsPlayed.ts @@ -0,0 +1,88 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; +import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useQueryClient } from "@tanstack/react-query"; +import * as Haptics from "expo-haptics"; +import { useAtom } from "jotai"; + +export const useMarkAsPlayed = (item: BaseItemDto) => { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const queryClient = useQueryClient(); + + const invalidateQueries = () => { + const queriesToInvalidate = [ + ["item", item.Id], + ["resumeItems"], + ["continueWatching"], + ["nextUp-all"], + ["nextUp"], + ["episodes"], + ["seasons"], + ["home"], + ]; + + queriesToInvalidate.forEach((queryKey) => { + queryClient.invalidateQueries({ queryKey }); + }); + }; + + const markAsPlayedStatus = async (played: boolean) => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + + // Optimistic update + queryClient.setQueryData( + ["item", item.Id], + (oldData: BaseItemDto | undefined) => { + if (oldData) { + return { + ...oldData, + UserData: { + ...oldData.UserData, + Played: !played, + }, + }; + } + return oldData; + } + ); + + try { + if (played) { + await markAsNotPlayed({ + api: api, + itemId: item?.Id, + userId: user?.Id, + }); + } else { + await markAsPlayed({ + api: api, + item: item, + userId: user?.Id, + }); + } + invalidateQueries(); + } catch (error) { + // Revert optimistic update on error + queryClient.setQueryData( + ["item", item.Id], + (oldData: BaseItemDto | undefined) => { + if (oldData) { + return { + ...oldData, + UserData: { + ...oldData.UserData, + Played: played, + }, + }; + } + return oldData; + } + ); + console.error("Error updating played status:", error); + } + }; + + return markAsPlayedStatus; +}; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 66cece20..f5f22046 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -85,7 +85,8 @@ export type Settings = { autoDownload: boolean; showCustomMenuLinks: boolean; subtitleSize: number; - remuxConcurrentLimit: 1 | 2 | 3 | 4; // TODO: Maybe let people choose their own limit? 4 seems like a safe max? + remuxConcurrentLimit: 1 | 2 | 3 | 4; + safeAreaInControlsEnabled: boolean; }; const loadSettings = (): Settings => { @@ -122,6 +123,7 @@ const loadSettings = (): Settings => { showCustomMenuLinks: false, subtitleSize: Platform.OS === "ios" ? 60 : 100, remuxConcurrentLimit: 1, + safeAreaInControlsEnabled: true, }; try {