feat: hide libraries

This commit is contained in:
Fredrik Burmester
2025-01-05 15:46:44 +01:00
parent 49d157a95a
commit cf2beb8299
6 changed files with 125 additions and 89 deletions

View File

@@ -77,6 +77,12 @@ export default function IndexLayout() {
title: "", title: "",
}} }}
/> />
<Stack.Screen
name="settings/hide-libraries/page"
options={{
title: "",
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => ( {Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} /> <Stack.Screen key={name} name={name} options={options} />
))} ))}

View File

@@ -0,0 +1,60 @@
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { Loader } from "@/components/Loader";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { Switch, View } from "react-native";
export default function page() {
const [settings, updateSettings] = useSettings();
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
const { data, isLoading: isLoading } = useQuery({
queryKey: ["user-views", user?.Id],
queryFn: async () => {
const response = await getUserViewsApi(api!).getUserViews({
userId: user?.Id,
});
return response.data.Items || null;
},
});
if (!settings) return null;
if (isLoading)
return (
<View className="mt-4">
<Loader />
</View>
);
return (
<View className="px-4">
<ListGroup>
{data?.map((view) => (
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
<Switch
value={settings.hiddenLibraries?.includes(view.Id!) || false}
onValueChange={(value) => {
updateSettings({
hiddenLibraries: value
? [...(settings.hiddenLibraries || []), view.Id!]
: settings.hiddenLibraries?.filter((id) => id !== view.Id),
});
}}
/>
</ListItem>
))}
</ListGroup>
<Text className="px-4 text-xs text-neutral-500 mt-1">
Select the libraries you want to hide from the Library tab.
</Text>
</View>
);
}

View File

@@ -10,7 +10,7 @@ import {
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useEffect } from "react"; import { useEffect, useMemo } from "react";
import { StyleSheet, View } from "react-native"; import { StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -23,20 +23,20 @@ export default function index() {
const { data, isLoading: isLoading } = useQuery({ const { data, isLoading: isLoading } = useQuery({
queryKey: ["user-views", user?.Id], queryKey: ["user-views", user?.Id],
queryFn: async () => { queryFn: async () => {
if (!api || !user?.Id) { const response = await getUserViewsApi(api!).getUserViews({
return null; userId: user?.Id,
}
const response = await getUserViewsApi(api).getUserViews({
userId: user.Id,
}); });
return response.data.Items || null; return response.data.Items || null;
}, },
enabled: !!api && !!user?.Id, staleTime: 60,
staleTime: 60 * 1000 * 60,
}); });
const libraries = useMemo(
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
[data, settings?.hiddenLibraries]
);
useEffect(() => { useEffect(() => {
for (const item of data || []) { for (const item of data || []) {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
@@ -63,7 +63,7 @@ export default function index() {
</View> </View>
); );
if (!data) if (!libraries)
return ( return (
<View className="h-full w-full flex justify-center items-center"> <View className="h-full w-full flex justify-center items-center">
<Text className="text-lg text-neutral-500">No libraries found</Text> <Text className="text-lg text-neutral-500">No libraries found</Text>
@@ -81,7 +81,7 @@ export default function index() {
paddingLeft: insets.left, paddingLeft: insets.left,
paddingRight: insets.right, paddingRight: insets.right,
}} }}
data={data} data={libraries}
renderItem={({ item }) => <LibraryItemCard library={item} />} renderItem={({ item }) => <LibraryItemCard library={item} />}
keyExtractor={(item) => item.Id || ""} keyExtractor={(item) => item.Id || ""}
ItemSeparatorComponent={() => ItemSeparatorComponent={() =>

View File

@@ -4,9 +4,11 @@ import {
BaseItemPerson, BaseItemPerson,
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import { useRouter, useSegments } from "expo-router"; import { useRouter, useSegments } from "expo-router";
import { PropsWithChildren } from "react"; import { PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, TouchableOpacityProps } from "react-native"; import { TouchableOpacity, TouchableOpacityProps } from "react-native";
import * as ContextMenu from "zeego/context-menu"; import * as ContextMenu from "zeego/context-menu";
import { useActionSheet } from "@expo/react-native-action-sheet";
import * as Haptics from "expo-haptics";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
item: BaseItemDto; item: BaseItemDto;
@@ -16,8 +18,6 @@ export const itemRouter = (
item: BaseItemDto | BaseItemPerson, item: BaseItemDto | BaseItemPerson,
from: string from: string
) => { ) => {
console.log(item.Type, item?.CollectionType);
if ("CollectionType" in item && item.CollectionType === "livetv") { if ("CollectionType" in item && item.CollectionType === "livetv") {
return `/(auth)/(tabs)/${from}/livetv`; return `/(auth)/(tabs)/${from}/livetv`;
} }
@@ -68,10 +68,33 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
}) => { }) => {
const router = useRouter(); const router = useRouter();
const segments = useSegments(); const segments = useSegments();
const { showActionSheetWithOptions } = useActionSheet();
const markAsPlayedStatus = useMarkAsPlayed(item);
const from = segments[2]; const from = segments[2];
const markAsPlayedStatus = useMarkAsPlayed(item); const showActionSheet = useCallback(() => {
if (!(item.Type === "Movie" || item.Type === "Episode")) return;
const options = ["Mark as Played", "Mark as Not Played", "Cancel"];
const cancelButtonIndex = 2;
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
},
async (selectedIndex) => {
if (selectedIndex === 0) {
await markAsPlayedStatus(true);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else if (selectedIndex === 1) {
await markAsPlayedStatus(false);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
}
);
}, [showActionSheetWithOptions, markAsPlayedStatus]);
if ( if (
from === "(home)" || from === "(home)" ||
@@ -80,78 +103,16 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
from === "(favorites)" from === "(favorites)"
) )
return ( return (
<ContextMenu.Root> <TouchableOpacity
<ContextMenu.Trigger> onLongPress={showActionSheet}
<TouchableOpacity onPress={() => {
onPress={() => { const url = itemRouter(item, from);
const url = itemRouter(item, from); // @ts-expect-error
// @ts-ignore router.push(url);
router.push(url); }}
}} {...props}
{...props} >
> {children}
{children} </TouchableOpacity>
</TouchableOpacity>
</ContextMenu.Trigger>
<ContextMenu.Content
avoidCollisions
alignOffset={0}
collisionPadding={0}
loop={false}
key={"content"}
>
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
<ContextMenu.Item
key="item-1"
onSelect={() => {
markAsPlayedStatus(true);
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key="item-1-title">
Mark as watched
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "checkmark.circle", // Changed to "checkmark.circle" which represents "watched"
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "green", // Changed to green for "watched"
light: "green",
},
}}
androidIconName="checkmark-circle"
></ContextMenu.ItemIcon>
</ContextMenu.Item>
<ContextMenu.Item
key="item-2"
onSelect={() => {
markAsPlayedStatus(false);
}}
shouldDismissMenuOnSelect
destructive
>
<ContextMenu.ItemTitle key="item-2-title">
Mark as not watched
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "eye.slash", // Changed to "eye.slash" which represents "not watched"
pointSize: 18, // Adjusted for better visibility
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "red", // Changed to red for "not watched"
light: "red",
},
// Removed paletteColors as it's not necessary in this case
}}
androidIconName="eye-slash"
></ContextMenu.ItemIcon>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
); );
}; };

View File

@@ -15,10 +15,12 @@ import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
import { useRouter } from "expo-router";
interface Props extends ViewProps {} interface Props extends ViewProps {}
export const OtherSettings: React.FC = () => { export const OtherSettings: React.FC = () => {
const router = useRouter();
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();
/******************** /********************
@@ -54,7 +56,7 @@ export const OtherSettings: React.FC = () => {
if (!settings) return null; if (!settings) return null;
return ( return (
<ListGroup title="Other" className="mb-4"> <ListGroup title="Other" className="">
<ListItem title="Auto rotate"> <ListItem title="Auto rotate">
<Switch <Switch
value={settings.autoRotate} value={settings.autoRotate}
@@ -178,6 +180,11 @@ export const OtherSettings: React.FC = () => {
} }
/> />
</ListItem> </ListItem>
<ListItem
onPress={() => router.push("/settings/hide-libraries/page")}
title="Hide Libraries"
showArrow
/>
</ListGroup> </ListGroup>
); );
}; };

View File

@@ -88,6 +88,7 @@ export type Settings = {
remuxConcurrentLimit: 1 | 2 | 3 | 4; remuxConcurrentLimit: 1 | 2 | 3 | 4;
safeAreaInControlsEnabled: boolean; safeAreaInControlsEnabled: boolean;
jellyseerrServerUrl?: string; jellyseerrServerUrl?: string;
hiddenLibraries?: string[];
}; };
const loadSettings = (): Settings => { const loadSettings = (): Settings => {
@@ -126,6 +127,7 @@ const loadSettings = (): Settings => {
remuxConcurrentLimit: 1, remuxConcurrentLimit: 1,
safeAreaInControlsEnabled: true, safeAreaInControlsEnabled: true,
jellyseerrServerUrl: undefined, jellyseerrServerUrl: undefined,
hiddenLibraries: [],
}; };
try { try {