mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
feat: hide libraries
This commit is contained in:
@@ -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} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
60
app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx
Normal file
60
app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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={() =>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user