mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-26 03:06:22 +00:00
wip: remove zeego + expo ui
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import { type OptionGroup, PlatformOptionsMenu } from "../PlatformOptionsMenu";
|
||||
|
||||
interface Props<T> {
|
||||
data: T[];
|
||||
@@ -35,7 +35,7 @@ const Dropdown = <T,>({
|
||||
...props
|
||||
}: PropsWithChildren<Props<T> & ViewProps>) => {
|
||||
const isTv = Platform.isTV;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<T[]>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,80 +44,97 @@ const Dropdown = <T,>({
|
||||
}
|
||||
}, [selected, onSelected]);
|
||||
|
||||
const optionGroups: OptionGroup[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "dropdown-items",
|
||||
title: label,
|
||||
options: data.map((item) => {
|
||||
const key = keyExtractor(item);
|
||||
const isSelected =
|
||||
selected?.some((s) => keyExtractor(s) === key) || false;
|
||||
|
||||
return {
|
||||
id: key,
|
||||
type: multiple ? "checkbox" : ("radio" as const),
|
||||
groupId: "dropdown-items",
|
||||
label: titleExtractor(item) || key,
|
||||
...(multiple ? { checked: isSelected } : { selected: isSelected }),
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
[data, selected, multiple, keyExtractor, titleExtractor, label],
|
||||
);
|
||||
|
||||
const handleOptionSelect = (optionId: string, value?: any) => {
|
||||
const selectedItem = data.find((item) => keyExtractor(item) === optionId);
|
||||
if (!selectedItem) return;
|
||||
|
||||
if (multiple) {
|
||||
setSelected((prev) => {
|
||||
const prevItems = prev || [];
|
||||
if (value) {
|
||||
// Add item if not already selected
|
||||
if (!prevItems.some((s) => keyExtractor(s) === optionId)) {
|
||||
return [...prevItems, selectedItem];
|
||||
}
|
||||
return prevItems;
|
||||
} else {
|
||||
// Remove item
|
||||
return prevItems.filter((s) => keyExtractor(s) !== optionId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setSelected([selectedItem]);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getDisplayValue = () => {
|
||||
if (selected?.length !== undefined && selected.length > 0) {
|
||||
return selected.map(titleExtractor).join(",");
|
||||
}
|
||||
return placeholderText || "";
|
||||
};
|
||||
|
||||
const trigger =
|
||||
typeof title === "string" ? (
|
||||
<View className='flex flex-col'>
|
||||
<Text className='opacity-50 mb-1 text-xs'>{title}</Text>
|
||||
<TouchableOpacity
|
||||
className='bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between'
|
||||
onPress={() => setOpen(true)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text numberOfLines={1}>{getDisplayValue()}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity onPress={() => setOpen(true)} disabled={disabled}>
|
||||
{title}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (isTv) return null;
|
||||
|
||||
return (
|
||||
<DisabledSetting disabled={disabled === true} showText={false} {...props}>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{typeof title === "string" ? (
|
||||
<View className='flex flex-col'>
|
||||
<Text className='opacity-50 mb-1 text-xs'>{title}</Text>
|
||||
<TouchableOpacity className='bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between'>
|
||||
<Text style={{}} className='' numberOfLines={1}>
|
||||
{selected?.length !== undefined
|
||||
? selected.map(titleExtractor).join(",")
|
||||
: placeholderText}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
loop={false}
|
||||
side='bottom'
|
||||
align='center'
|
||||
alignOffset={0}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={0}
|
||||
sideOffset={0}
|
||||
>
|
||||
<DropdownMenu.Label>{label}</DropdownMenu.Label>
|
||||
{data.map((item, _idx) =>
|
||||
multiple ? (
|
||||
<DropdownMenu.CheckboxItem
|
||||
value={
|
||||
selected?.some((s) => keyExtractor(s) === keyExtractor(item))
|
||||
? "on"
|
||||
: "off"
|
||||
}
|
||||
key={keyExtractor(item)}
|
||||
onValueChange={(
|
||||
next: "on" | "off",
|
||||
_previous: "on" | "off",
|
||||
) => {
|
||||
setSelected((p) => {
|
||||
const prev = p || [];
|
||||
if (next === "on") {
|
||||
return [...prev, item];
|
||||
}
|
||||
return [
|
||||
...prev.filter(
|
||||
(p) => keyExtractor(p) !== keyExtractor(item),
|
||||
),
|
||||
];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>
|
||||
{titleExtractor(item)}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
) : (
|
||||
<DropdownMenu.Item
|
||||
key={keyExtractor(item)}
|
||||
onSelect={() => setSelected([item])}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>
|
||||
{titleExtractor(item)}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
),
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<PlatformOptionsMenu
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={label}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
bottomSheetConfig={{
|
||||
enableDynamicSizing: true,
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
</DisabledSetting>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import type React from "react";
|
||||
import { type PropsWithChildren, useCallback, useMemo } from "react";
|
||||
import { type PropsWithChildren } from "react";
|
||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||
import * as ContextMenu from "zeego/context-menu";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import {
|
||||
hasPermission,
|
||||
Permission,
|
||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
||||
import type {
|
||||
@@ -38,90 +32,33 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const segments = useSegments();
|
||||
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
|
||||
|
||||
const from = (segments as string[])[2] || "(home)";
|
||||
|
||||
const autoApprove = useMemo(() => {
|
||||
return (
|
||||
jellyseerrUser &&
|
||||
hasPermission(Permission.AUTO_APPROVE, jellyseerrUser.permissions, {
|
||||
type: "or",
|
||||
})
|
||||
);
|
||||
}, [jellyseerrApi, jellyseerrUser]);
|
||||
|
||||
const request = useCallback(() => {
|
||||
if (!result) return;
|
||||
requestMedia(mediaTitle, {
|
||||
mediaId: result.id,
|
||||
mediaType,
|
||||
});
|
||||
}, [jellyseerrApi, result]);
|
||||
|
||||
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (!result) return;
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (!result) return;
|
||||
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
|
||||
// @ts-expect-error
|
||||
params: {
|
||||
...result,
|
||||
mediaTitle,
|
||||
releaseYear,
|
||||
canRequest: canRequest.toString(),
|
||||
posterSrc,
|
||||
mediaType,
|
||||
},
|
||||
});
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content
|
||||
avoidCollisions
|
||||
alignOffset={0}
|
||||
collisionPadding={0}
|
||||
loop={false}
|
||||
key={"content"}
|
||||
>
|
||||
<ContextMenu.Label key='label-1'>Actions</ContextMenu.Label>
|
||||
{canRequest && mediaType === MediaType.MOVIE && (
|
||||
<ContextMenu.Item
|
||||
key='item-1'
|
||||
onSelect={() => {
|
||||
if (autoApprove) {
|
||||
request();
|
||||
}
|
||||
}}
|
||||
shouldDismissMenuOnSelect
|
||||
>
|
||||
<ContextMenu.ItemTitle key='item-1-title'>
|
||||
Request
|
||||
</ContextMenu.ItemTitle>
|
||||
<ContextMenu.ItemIcon
|
||||
ios={{
|
||||
name: "arrow.down.to.line",
|
||||
pointSize: 18,
|
||||
weight: "semibold",
|
||||
scale: "medium",
|
||||
hierarchicalColor: {
|
||||
dark: "purple",
|
||||
light: "purple",
|
||||
},
|
||||
}}
|
||||
androidIconName='download'
|
||||
/>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
|
||||
// @ts-expect-error
|
||||
params: {
|
||||
...result,
|
||||
mediaTitle,
|
||||
releaseYear,
|
||||
canRequest: canRequest.toString(),
|
||||
posterSrc,
|
||||
mediaType,
|
||||
},
|
||||
});
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user