mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
Compare commits
3 Commits
feat/hide-
...
as-any-rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379e93edf9 | ||
|
|
75d6948a81 | ||
|
|
c05cef295e |
@@ -1,6 +1,5 @@
|
||||
import { Platform, ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ControlsSettings } from "@/components/settings/ControlsSettings";
|
||||
import { GestureControls } from "@/components/settings/GestureControls";
|
||||
import { MediaProvider } from "@/components/settings/MediaContext";
|
||||
import { MediaToggles } from "@/components/settings/MediaToggles";
|
||||
@@ -26,7 +25,6 @@ export default function PlaybackControlsPage() {
|
||||
<MediaProvider>
|
||||
<MediaToggles className='mb-4' />
|
||||
<GestureControls className='mb-4' />
|
||||
<ControlsSettings className='mb-4' />
|
||||
<PlaybackControlsSettings />
|
||||
</MediaProvider>
|
||||
</View>
|
||||
|
||||
@@ -27,8 +27,8 @@ const Page: React.FC = () => {
|
||||
ItemFields.MediaStreams,
|
||||
]);
|
||||
|
||||
// preload media sources in background
|
||||
useItemQuery(id, false, undefined, []);
|
||||
// preload media sources
|
||||
const { data: itemWithSources } = useItemQuery(id, false, undefined, []);
|
||||
|
||||
const opacity = useSharedValue(1);
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
@@ -98,7 +98,13 @@ const Page: React.FC = () => {
|
||||
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
|
||||
</Animated.View>
|
||||
{item && <ItemContent item={item} isOffline={isOffline} />}
|
||||
{item && (
|
||||
<ItemContent
|
||||
item={item}
|
||||
isOffline={isOffline}
|
||||
itemWithSources={itemWithSources}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MMKV } from "react-native-mmkv";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
|
||||
declare module "react-native-mmkv" {
|
||||
@@ -7,9 +8,9 @@ declare module "react-native-mmkv" {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the augmentation methods directly to the MMKV prototype
|
||||
// This follows the recommended pattern while adding the helper methods your app uses
|
||||
(storage as any).get = function <T>(key: string): T | undefined {
|
||||
// Add the augmentation methods directly to the MMKV instance
|
||||
// We need to bind these methods to preserve the 'this' context
|
||||
storage.get = function <T>(this: MMKV, key: string): T | undefined {
|
||||
try {
|
||||
const serializedItem = this.getString(key);
|
||||
if (!serializedItem) return undefined;
|
||||
@@ -20,7 +21,11 @@ declare module "react-native-mmkv" {
|
||||
}
|
||||
};
|
||||
|
||||
(storage as any).setAny = function (key: string, value: any | undefined): void {
|
||||
storage.setAny = function (
|
||||
this: MMKV,
|
||||
key: string,
|
||||
value: any | undefined,
|
||||
): void {
|
||||
try {
|
||||
if (value === undefined) {
|
||||
this.remove(key);
|
||||
|
||||
@@ -108,10 +108,10 @@ export const BitrateSheet: React.FC<Props> = ({
|
||||
values={selected ? [selected] : []}
|
||||
multiple={false}
|
||||
searchFilter={(item, query) => {
|
||||
const label = (item as any).key || "";
|
||||
const label = item.key || "";
|
||||
return label.toLowerCase().includes(query.toLowerCase());
|
||||
}}
|
||||
renderItemLabel={(item) => <Text>{(item as any).key || ""}</Text>}
|
||||
renderItemLabel={(item) => <Text>{item.key || ""}</Text>}
|
||||
set={(vals) => {
|
||||
const chosen = vals[0] as Bitrate | undefined;
|
||||
if (chosen) onChange(chosen);
|
||||
|
||||
@@ -46,10 +46,11 @@ export type SelectedOptions = {
|
||||
interface ItemContentProps {
|
||||
item: BaseItemDto;
|
||||
isOffline: boolean;
|
||||
itemWithSources?: BaseItemDto | null;
|
||||
}
|
||||
|
||||
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
({ item, isOffline }) => {
|
||||
({ item, isOffline, itemWithSources }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const { settings } = useSettings();
|
||||
const { orientation } = useOrientation();
|
||||
@@ -98,7 +99,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Platform.isTV) {
|
||||
if (!Platform.isTV && itemWithSources) {
|
||||
navigation.setOptions({
|
||||
headerRight: () =>
|
||||
item &&
|
||||
@@ -108,7 +109,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
{item.Type !== "Program" && (
|
||||
<View className='flex flex-row items-center'>
|
||||
{!Platform.isTV && (
|
||||
<DownloadSingleItem item={item} size='large' />
|
||||
<DownloadSingleItem item={itemWithSources} size='large' />
|
||||
)}
|
||||
{user?.Policy?.IsAdministrator && (
|
||||
<PlayInRemoteSessionButton item={item} size='large' />
|
||||
@@ -125,7 +126,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
{item.Type !== "Program" && (
|
||||
<View className='flex flex-row items-center space-x-2'>
|
||||
{!Platform.isTV && (
|
||||
<DownloadSingleItem item={item} size='large' />
|
||||
<DownloadSingleItem item={itemWithSources} size='large' />
|
||||
)}
|
||||
{user?.Policy?.IsAdministrator && (
|
||||
<PlayInRemoteSessionButton item={item} size='large' />
|
||||
@@ -139,7 +140,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
)),
|
||||
});
|
||||
}
|
||||
}, [item, navigation, user]);
|
||||
}, [item, navigation, user, itemWithSources]);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
@@ -212,7 +213,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
<MediaSourceButton
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
item={item}
|
||||
item={itemWithSources}
|
||||
colors={itemColors}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -7,13 +7,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||
import { useItemQuery } from "@/hooks/useItemQuery";
|
||||
import { BITRATES } from "./BitRateSheet";
|
||||
import type { SelectedOptions } from "./ItemContent";
|
||||
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
|
||||
|
||||
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
|
||||
item: BaseItemDto;
|
||||
item?: BaseItemDto | null;
|
||||
selectedOptions: SelectedOptions;
|
||||
setSelectedOptions: React.Dispatch<
|
||||
React.SetStateAction<SelectedOptions | undefined>
|
||||
@@ -29,12 +28,6 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: itemWithSources, isLoading } = useItemQuery(
|
||||
item.Id,
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
);
|
||||
|
||||
const effectiveColors = colors || {
|
||||
primary: "#7c3aed",
|
||||
@@ -42,7 +35,7 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const firstMediaSource = itemWithSources?.MediaSources?.[0];
|
||||
const firstMediaSource = item?.MediaSources?.[0];
|
||||
if (!firstMediaSource) return;
|
||||
setSelectedOptions((prev) => {
|
||||
if (!prev) return prev;
|
||||
@@ -51,7 +44,7 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
mediaSource: firstMediaSource,
|
||||
};
|
||||
});
|
||||
}, [itemWithSources, setSelectedOptions]);
|
||||
}, [item, setSelectedOptions]);
|
||||
|
||||
const getMediaSourceDisplayName = useCallback((source: MediaSourceInfo) => {
|
||||
const videoStream = source.MediaStreams?.find((x) => x.Type === "Video");
|
||||
@@ -93,13 +86,10 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
});
|
||||
|
||||
// Media Source group (only if multiple sources)
|
||||
if (
|
||||
itemWithSources?.MediaSources &&
|
||||
itemWithSources.MediaSources.length > 1
|
||||
) {
|
||||
if (item?.MediaSources && item.MediaSources.length > 1) {
|
||||
groups.push({
|
||||
title: t("item_card.video"),
|
||||
options: itemWithSources.MediaSources.map((source) => ({
|
||||
options: item.MediaSources.map((source) => ({
|
||||
type: "radio" as const,
|
||||
label: getMediaSourceDisplayName(source),
|
||||
value: source,
|
||||
@@ -159,7 +149,7 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
itemWithSources,
|
||||
item,
|
||||
selectedOptions,
|
||||
audioStreams,
|
||||
subtitleStreams,
|
||||
@@ -170,7 +160,7 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
|
||||
const trigger = (
|
||||
<TouchableOpacity
|
||||
disabled={!item || isLoading}
|
||||
disabled={!item}
|
||||
onPress={() => setOpen(true)}
|
||||
className='relative'
|
||||
>
|
||||
@@ -179,7 +169,7 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
className='absolute w-12 h-12 rounded-full'
|
||||
/>
|
||||
<View className='w-12 h-12 rounded-full z-10 items-center justify-center'>
|
||||
{isLoading ? (
|
||||
{!item ? (
|
||||
<ActivityIndicator size='small' color={effectiveColors.text} />
|
||||
) : (
|
||||
<Ionicons name='list' size={24} color={effectiveColors.text} />
|
||||
|
||||
@@ -4,7 +4,19 @@ import type { PropsWithChildren } from "react";
|
||||
import { Platform, TouchableOpacity, type ViewProps } from "react-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
interface Props
|
||||
extends Omit<
|
||||
ViewProps,
|
||||
| "children"
|
||||
| "onPressIn"
|
||||
| "onPressOut"
|
||||
| "onPress"
|
||||
| "nextFocusDown"
|
||||
| "nextFocusForward"
|
||||
| "nextFocusLeft"
|
||||
| "nextFocusRight"
|
||||
| "nextFocusUp"
|
||||
> {
|
||||
onPress?: () => void;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
background?: boolean;
|
||||
@@ -41,7 +53,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -60,7 +72,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -78,7 +90,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -98,7 +110,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${
|
||||
fillColor ? fillColorClass : "bg-transparent"
|
||||
}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -112,11 +124,11 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} {...(viewProps as any)}>
|
||||
<TouchableOpacity onPress={handlePress} {...viewProps}>
|
||||
<BlurView
|
||||
intensity={90}
|
||||
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
|
||||
@@ -81,14 +81,12 @@ export const TrackSheet: React.FC<Props> = ({
|
||||
}
|
||||
multiple={false}
|
||||
searchFilter={(item, query) => {
|
||||
const label = (item as any).DisplayTitle || "";
|
||||
const label = item.DisplayTitle || "";
|
||||
return label.toLowerCase().includes(query.toLowerCase());
|
||||
}}
|
||||
renderItemLabel={(item) => (
|
||||
<Text>{(item as any).DisplayTitle || ""}</Text>
|
||||
)}
|
||||
renderItemLabel={(item) => <Text>{item.DisplayTitle || ""}</Text>}
|
||||
set={(vals) => {
|
||||
const chosen = vals[0] as any;
|
||||
const chosen = vals[0];
|
||||
if (chosen && chosen.Index !== null && chosen.Index !== undefined) {
|
||||
onChange(chosen.Index);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useRouter } from "expo-router";
|
||||
import { type Href, useRouter } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
@@ -340,7 +340,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
const navigateToItem = useCallback(
|
||||
(item: BaseItemDto) => {
|
||||
const navigation = getItemNavigation(item, "(home)");
|
||||
router.push(navigation as any);
|
||||
router.push(navigation as Href);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import { type Href, useRouter, useSegments } from "expo-router";
|
||||
import { type PropsWithChildren, useCallback } from "react";
|
||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||
import { useFavorite } from "@/hooks/useFavorite";
|
||||
@@ -146,12 +146,12 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
if (isOffline) {
|
||||
// For offline mode, we still need to use query params
|
||||
const url = `${itemRouter(item, from)}&offline=true`;
|
||||
router.push(url as any);
|
||||
router.push(url as Href);
|
||||
return;
|
||||
}
|
||||
|
||||
const navigation = getItemNavigation(item, from);
|
||||
router.push(navigation as any);
|
||||
router.push(navigation as Href);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import { type Href, useRouter, useSegments } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Dimensions, View, type ViewProps } from "react-native";
|
||||
@@ -156,7 +156,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
if (!from) return;
|
||||
lightHapticFeedback();
|
||||
const navigation = getItemNavigation(item, from);
|
||||
router.push(navigation as any);
|
||||
router.push(navigation as Href);
|
||||
}, [item, from]);
|
||||
|
||||
const tap = Gesture.Tap()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { router, useSegments } from "expo-router";
|
||||
import { type Href, router, useSegments } from "expo-router";
|
||||
import type React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
||||
@@ -21,10 +21,10 @@ const CompanySlide: React.FC<
|
||||
const navigate = useCallback(
|
||||
({ id, image, name }: Network | Studio) =>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}` as any,
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
|
||||
params: { id, image, name, type: slide.type },
|
||||
}),
|
||||
[slide],
|
||||
} as Href),
|
||||
[slide, from],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { router, useSegments } from "expo-router";
|
||||
import { type Href, router, useSegments } from "expo-router";
|
||||
import type React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
||||
@@ -18,10 +18,10 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const navigate = useCallback(
|
||||
(genre: GenreSliderItem) =>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}` as any,
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
|
||||
params: { type: slide.type, name: genre.name },
|
||||
}),
|
||||
[slide],
|
||||
} as Href),
|
||||
[slide, from],
|
||||
);
|
||||
|
||||
const { data } = useQuery({
|
||||
|
||||
@@ -31,15 +31,16 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
|
||||
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
|
||||
>
|
||||
{Children.map(childrenArray, (child, index) => {
|
||||
if (isValidElement<{ style?: ViewStyle }>(child)) {
|
||||
return cloneElement(child as any, {
|
||||
style: StyleSheet.compose(
|
||||
child.props.style,
|
||||
index < childrenArray.length - 1
|
||||
? styles.borderBottom
|
||||
: undefined,
|
||||
),
|
||||
});
|
||||
if (isValidElement(child)) {
|
||||
const style = StyleSheet.compose(
|
||||
(child.props as { style?: ViewStyle }).style,
|
||||
index < childrenArray.length - 1
|
||||
? styles.borderBottom
|
||||
: undefined,
|
||||
);
|
||||
return cloneElement(child, { style } as Partial<
|
||||
typeof child.props
|
||||
>);
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
|
||||
@@ -3,7 +3,18 @@ import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||
import { Text } from "../common/Text";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
interface Props
|
||||
extends Omit<
|
||||
ViewProps,
|
||||
| "children"
|
||||
| "onPressIn"
|
||||
| "onPressOut"
|
||||
| "nextFocusDown"
|
||||
| "nextFocusForward"
|
||||
| "nextFocusLeft"
|
||||
| "nextFocusRight"
|
||||
| "nextFocusUp"
|
||||
> {
|
||||
title?: string | null | undefined;
|
||||
subtitle?: string | null | undefined;
|
||||
value?: string | null | undefined;
|
||||
@@ -37,7 +48,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
||||
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${
|
||||
disabled ? "opacity-50" : ""
|
||||
}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
<ListItemContent
|
||||
title={title}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ViewProps } from "react-native";
|
||||
import { Switch } from "react-native";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import DisabledSetting from "./DisabledSetting";
|
||||
|
||||
interface Props extends ViewProps {}
|
||||
|
||||
export const ControlsSettings: React.FC<Props> = ({ ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||
|
||||
const disabled = useMemo(
|
||||
() =>
|
||||
pluginSettings?.showVolumeSlider?.locked === true &&
|
||||
pluginSettings?.showBrightnessSlider?.locked === true &&
|
||||
pluginSettings?.showSeekButtons?.locked === true,
|
||||
[pluginSettings],
|
||||
);
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
return (
|
||||
<DisabledSetting disabled={disabled} {...props}>
|
||||
<ListGroup title={t("home.settings.controls.controls_title")}>
|
||||
<ListItem
|
||||
title={t("home.settings.controls.show_volume_slider")}
|
||||
subtitle={t("home.settings.controls.show_volume_slider_description")}
|
||||
disabled={pluginSettings?.showVolumeSlider?.locked}
|
||||
>
|
||||
<Switch
|
||||
value={settings.showVolumeSlider}
|
||||
disabled={pluginSettings?.showVolumeSlider?.locked}
|
||||
onValueChange={(showVolumeSlider) =>
|
||||
updateSettings({ showVolumeSlider })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={t("home.settings.controls.show_brightness_slider")}
|
||||
subtitle={t(
|
||||
"home.settings.controls.show_brightness_slider_description",
|
||||
)}
|
||||
disabled={pluginSettings?.showBrightnessSlider?.locked}
|
||||
>
|
||||
<Switch
|
||||
value={settings.showBrightnessSlider}
|
||||
disabled={pluginSettings?.showBrightnessSlider?.locked}
|
||||
onValueChange={(showBrightnessSlider) =>
|
||||
updateSettings({ showBrightnessSlider })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={t("home.settings.controls.show_seek_buttons")}
|
||||
subtitle={t("home.settings.controls.show_seek_buttons_description")}
|
||||
disabled={pluginSettings?.showSeekButtons?.locked}
|
||||
>
|
||||
<Switch
|
||||
value={settings.showSeekButtons}
|
||||
disabled={pluginSettings?.showSeekButtons?.locked}
|
||||
onValueChange={(showSeekButtons) =>
|
||||
updateSettings({ showSeekButtons })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
</DisabledSetting>
|
||||
);
|
||||
};
|
||||
@@ -48,57 +48,49 @@ export const CenterControls: FC<CenterControlsProps> = ({
|
||||
}}
|
||||
pointerEvents={showControls ? "box-none" : "none"}
|
||||
>
|
||||
{settings?.showBrightnessSlider && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignItems: "center",
|
||||
transform: [{ rotate: "270deg" }],
|
||||
left: 0,
|
||||
bottom: 30,
|
||||
}}
|
||||
>
|
||||
<BrightnessSlider />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignItems: "center",
|
||||
transform: [{ rotate: "270deg" }],
|
||||
left: 0,
|
||||
bottom: 30,
|
||||
}}
|
||||
>
|
||||
<BrightnessSlider />
|
||||
</View>
|
||||
|
||||
{!Platform.isTV ? (
|
||||
settings?.showSeekButtons ? (
|
||||
<TouchableOpacity onPress={handleSkipBackward}>
|
||||
<View
|
||||
{!Platform.isTV && (
|
||||
<TouchableOpacity onPress={handleSkipBackward}>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name='refresh-outline'
|
||||
size={ICON_SIZES.CENTER}
|
||||
color='white'
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
transform: [{ scaleY: -1 }, { rotate: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name='refresh-outline'
|
||||
size={ICON_SIZES.CENTER}
|
||||
color='white'
|
||||
style={{
|
||||
transform: [{ scaleY: -1 }, { rotate: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
{settings?.rewindSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View
|
||||
style={{ width: ICON_SIZES.CENTER, height: ICON_SIZES.CENTER }}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
{settings?.rewindSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}>
|
||||
<TouchableOpacity onPress={togglePlay}>
|
||||
@@ -114,55 +106,47 @@ export const CenterControls: FC<CenterControlsProps> = ({
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{!Platform.isTV ? (
|
||||
settings?.showSeekButtons ? (
|
||||
<TouchableOpacity onPress={handleSkipForward}>
|
||||
<View
|
||||
{!Platform.isTV && (
|
||||
<TouchableOpacity onPress={handleSkipForward}>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name='refresh-outline'
|
||||
size={ICON_SIZES.CENTER}
|
||||
color='white'
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name='refresh-outline'
|
||||
size={ICON_SIZES.CENTER}
|
||||
color='white'
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
{settings?.forwardSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View
|
||||
style={{ width: ICON_SIZES.CENTER, height: ICON_SIZES.CENTER }}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{settings?.showVolumeSlider && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignItems: "center",
|
||||
transform: [{ rotate: "270deg" }],
|
||||
bottom: 30,
|
||||
right: 0,
|
||||
opacity: showAudioSlider || showControls ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<AudioSlider setVisibility={setShowAudioSlider} />
|
||||
</View>
|
||||
{settings?.forwardSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignItems: "center",
|
||||
transform: [{ rotate: "270deg" }],
|
||||
bottom: 30,
|
||||
right: 0,
|
||||
opacity: showAudioSlider || showControls ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<AudioSlider setVisibility={setShowAudioSlider} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { type Href, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import {
|
||||
type Dispatch,
|
||||
type FC,
|
||||
@@ -379,7 +379,7 @@ export const Controls: FC<Props> = ({
|
||||
|
||||
console.log("queryParams", queryParams);
|
||||
|
||||
router.replace(`player/direct-player?${queryParams}` as any);
|
||||
router.replace(`player/direct-player?${queryParams}` as Href);
|
||||
},
|
||||
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
|
||||
interface FeedbackState {
|
||||
visible: boolean;
|
||||
icon: string;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
text: string;
|
||||
side?: "left" | "right";
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export const GestureOverlay = ({
|
||||
|
||||
const [feedback, setFeedback] = useState<FeedbackState>({
|
||||
visible: false,
|
||||
icon: "",
|
||||
icon: "play",
|
||||
text: "",
|
||||
});
|
||||
const [fadeAnim] = useState(new Animated.Value(0));
|
||||
@@ -46,7 +46,7 @@ export const GestureOverlay = ({
|
||||
|
||||
const showFeedback = useCallback(
|
||||
(
|
||||
icon: string,
|
||||
icon: keyof typeof Ionicons.glyphMap,
|
||||
text: string,
|
||||
side?: "left" | "right",
|
||||
isDuringDrag = false,
|
||||
@@ -320,7 +320,7 @@ export const GestureOverlay = ({
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={feedback.icon as any}
|
||||
name={feedback.icon}
|
||||
size={24}
|
||||
color='white'
|
||||
style={{ marginRight: 8 }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { type Href, router, useLocalSearchParams } from "expo-router";
|
||||
import type React from "react";
|
||||
import {
|
||||
createContext,
|
||||
@@ -95,7 +95,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
playbackPosition: playbackPosition,
|
||||
}).toString();
|
||||
|
||||
router.replace(`player/direct-player?${queryParams}` as any);
|
||||
router.replace(`player/direct-player?${queryParams}` as Href);
|
||||
};
|
||||
|
||||
const setTrackParams = (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { type Href, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
@@ -53,7 +53,7 @@ const DropdownView = () => {
|
||||
bitrateValue: bitrate.toString(),
|
||||
playbackPosition: playbackPositionRef.current,
|
||||
}).toString();
|
||||
router.replace(`player/direct-player?${queryParams}` as any);
|
||||
router.replace(`player/direct-player?${queryParams}` as Href);
|
||||
},
|
||||
[audioIndex, subtitleIndex, router],
|
||||
);
|
||||
|
||||
@@ -113,15 +113,6 @@
|
||||
"right_side_volume": "Right Side Volume Control",
|
||||
"right_side_volume_description": "Swipe up/down on right side to adjust volume"
|
||||
},
|
||||
"controls": {
|
||||
"controls_title": "Controls",
|
||||
"show_volume_slider": "Show Volume Slider",
|
||||
"show_volume_slider_description": "Display volume slider on the right side of video controls",
|
||||
"show_brightness_slider": "Show Brightness Slider",
|
||||
"show_brightness_slider_description": "Display brightness slider on the left side of video controls",
|
||||
"show_seek_buttons": "Show Seek Buttons",
|
||||
"show_seek_buttons_description": "Display forward/rewind buttons next to the play button"
|
||||
},
|
||||
"audio": {
|
||||
"audio_title": "Audio",
|
||||
"set_audio_track": "Set Audio Track From Previous Item",
|
||||
|
||||
@@ -180,10 +180,6 @@ export type Settings = {
|
||||
enableRightSideVolumeSwipe: boolean;
|
||||
usePopularPlugin: boolean;
|
||||
showLargeHomeCarousel: boolean;
|
||||
// Controls
|
||||
showVolumeSlider: boolean;
|
||||
showBrightnessSlider: boolean;
|
||||
showSeekButtons: boolean;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -248,10 +244,6 @@ export const defaultValues: Settings = {
|
||||
enableRightSideVolumeSwipe: true,
|
||||
usePopularPlugin: true,
|
||||
showLargeHomeCarousel: false,
|
||||
// Controls
|
||||
showVolumeSlider: true,
|
||||
showBrightnessSlider: true,
|
||||
showSeekButtons: true,
|
||||
};
|
||||
|
||||
const loadSettings = (): Partial<Settings> => {
|
||||
@@ -370,10 +362,11 @@ export const useSettings = () => {
|
||||
value !== undefined &&
|
||||
_settings?.[settingsKey] !== value
|
||||
) {
|
||||
(unlockedPluginDefaults as any)[settingsKey] = value;
|
||||
(unlockedPluginDefaults as Record<string, unknown>)[settingsKey] =
|
||||
value;
|
||||
}
|
||||
|
||||
(acc as any)[settingsKey] = locked
|
||||
(acc as Record<string, unknown>)[settingsKey] = locked
|
||||
? value
|
||||
: (_settings?.[settingsKey] ?? value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user