mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-16 01:13:27 +01:00
Compare commits
4 Commits
fix/nav-do
...
fix/settin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee8323110 | ||
|
|
2e79815d78 | ||
|
|
43ce50fe70 | ||
|
|
bd0eef0407 |
@@ -2,8 +2,9 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ScrollView, Switch, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
@@ -50,12 +51,12 @@ export default function AppearanceHideLibrariesPage() {
|
|||||||
>
|
>
|
||||||
<DisabledSetting
|
<DisabledSetting
|
||||||
disabled={pluginSettings?.hiddenLibraries?.locked === true}
|
disabled={pluginSettings?.hiddenLibraries?.locked === true}
|
||||||
className='px-4'
|
className='px-4 pt-4'
|
||||||
>
|
>
|
||||||
<ListGroup title={t("home.settings.other.hide_libraries")}>
|
<ListGroup title={t("home.settings.other.hide_libraries")}>
|
||||||
{data?.map((view) => (
|
{data?.map((view) => (
|
||||||
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -71,7 +72,7 @@ export default function AppearanceHideLibrariesPage() {
|
|||||||
))}
|
))}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||||
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
{t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||||
</Text>
|
</Text>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Switch, View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
@@ -46,7 +47,7 @@ export default function HideLibrariesPage() {
|
|||||||
<ListGroup>
|
<ListGroup>
|
||||||
{data?.map((view) => (
|
{data?.map((view) => (
|
||||||
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -60,7 +61,7 @@ export default function HideLibrariesPage() {
|
|||||||
))}
|
))}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||||
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
{t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||||
</Text>
|
</Text>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, ScrollView, View } from "react-native";
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
import { Switch } from "react-native-gesture-handler";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
@@ -136,7 +136,7 @@ export default function MusicSettingsPage() {
|
|||||||
title={t("home.settings.music.prefer_downloaded")}
|
title={t("home.settings.music.prefer_downloaded")}
|
||||||
disabled={pluginSettings?.preferLocalAudio?.locked}
|
disabled={pluginSettings?.preferLocalAudio?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.preferLocalAudio}
|
value={settings.preferLocalAudio}
|
||||||
disabled={pluginSettings?.preferLocalAudio?.locked}
|
disabled={pluginSettings?.preferLocalAudio?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -159,7 +159,7 @@ export default function MusicSettingsPage() {
|
|||||||
title={t("home.settings.music.lookahead_enabled")}
|
title={t("home.settings.music.lookahead_enabled")}
|
||||||
disabled={pluginSettings?.audioLookaheadEnabled?.locked}
|
disabled={pluginSettings?.audioLookaheadEnabled?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.audioLookaheadEnabled}
|
value={settings.audioLookaheadEnabled}
|
||||||
disabled={pluginSettings?.audioLookaheadEnabled?.locked}
|
disabled={pluginSettings?.audioLookaheadEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -233,7 +233,7 @@ export default function MusicSettingsPage() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
<ListGroup>
|
<ListGroup className='mt-4'>
|
||||||
<ListItem
|
<ListItem
|
||||||
textColor='red'
|
textColor='red'
|
||||||
onPress={onDeleteDownloadedSongsClicked}
|
onPress={onDeleteDownloadedSongsClicked}
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ export default function PlaybackControlsPage() {
|
|||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingLeft: insets.left,
|
paddingLeft: insets.left,
|
||||||
paddingRight: insets.right,
|
paddingRight: insets.right,
|
||||||
|
paddingBottom: insets.bottom,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className='p-4 flex flex-col'
|
className='p-4 flex flex-col'
|
||||||
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
>
|
>
|
||||||
<View className='mb-4'>
|
<View>
|
||||||
<MediaProvider>
|
<MediaProvider>
|
||||||
<MediaToggles className='mb-4' />
|
<MediaToggles className='mb-4' />
|
||||||
<GestureControls className='mb-4' />
|
<GestureControls className='mb-4' />
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Switch,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
@@ -72,7 +72,7 @@ export default function MarlinSearchPage() {
|
|||||||
paddingRight: insets.right,
|
paddingRight: insets.right,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DisabledSetting disabled={disabled} className='px-4'>
|
<DisabledSetting disabled={disabled} className='px-4 pt-4'>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<DisabledSetting
|
<DisabledSetting
|
||||||
disabled={
|
disabled={
|
||||||
@@ -90,7 +90,7 @@ export default function MarlinSearchPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.searchEngine === "Marlin"}
|
value={settings.searchEngine === "Marlin"}
|
||||||
disabled={!!pluginSettings?.streamyStatsServerUrl?.value}
|
disabled={!!pluginSettings?.streamyStatsServerUrl?.value}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Switch,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
@@ -134,7 +134,7 @@ export default function StreamystatsPage() {
|
|||||||
paddingRight: insets.right,
|
paddingRight: insets.right,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className='px-4'>
|
<View className='px-4 pt-4'>
|
||||||
<ListGroup className='flex-1'>
|
<ListGroup className='flex-1'>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.plugins.streamystats.url")}
|
title={t("home.settings.plugins.streamystats.url")}
|
||||||
@@ -173,7 +173,7 @@ export default function StreamystatsPage() {
|
|||||||
title={t("home.settings.plugins.streamystats.enable_search")}
|
title={t("home.settings.plugins.streamystats.enable_search")}
|
||||||
disabledByAdmin={pluginSettings?.searchEngine?.locked === true}
|
disabledByAdmin={pluginSettings?.searchEngine?.locked === true}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={useForSearch}
|
value={useForSearch}
|
||||||
disabled={!isStreamystatsEnabled}
|
disabled={!isStreamystatsEnabled}
|
||||||
onValueChange={setUseForSearch}
|
onValueChange={setUseForSearch}
|
||||||
@@ -187,7 +187,7 @@ export default function StreamystatsPage() {
|
|||||||
pluginSettings?.streamyStatsMovieRecommendations?.locked === true
|
pluginSettings?.streamyStatsMovieRecommendations?.locked === true
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={movieRecs}
|
value={movieRecs}
|
||||||
onValueChange={setMovieRecs}
|
onValueChange={setMovieRecs}
|
||||||
disabled={!isStreamystatsEnabled}
|
disabled={!isStreamystatsEnabled}
|
||||||
@@ -201,7 +201,7 @@ export default function StreamystatsPage() {
|
|||||||
pluginSettings?.streamyStatsSeriesRecommendations?.locked === true
|
pluginSettings?.streamyStatsSeriesRecommendations?.locked === true
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={seriesRecs}
|
value={seriesRecs}
|
||||||
onValueChange={setSeriesRecs}
|
onValueChange={setSeriesRecs}
|
||||||
disabled={!isStreamystatsEnabled}
|
disabled={!isStreamystatsEnabled}
|
||||||
@@ -215,7 +215,7 @@ export default function StreamystatsPage() {
|
|||||||
pluginSettings?.streamyStatsPromotedWatchlists?.locked === true
|
pluginSettings?.streamyStatsPromotedWatchlists?.locked === true
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={promotedWatchlists}
|
value={promotedWatchlists}
|
||||||
onValueChange={setPromotedWatchlists}
|
onValueChange={setPromotedWatchlists}
|
||||||
disabled={!isStreamystatsEnabled}
|
disabled={!isStreamystatsEnabled}
|
||||||
@@ -225,7 +225,7 @@ export default function StreamystatsPage() {
|
|||||||
title={t("home.settings.plugins.streamystats.hide_watchlists_tab")}
|
title={t("home.settings.plugins.streamystats.hide_watchlists_tab")}
|
||||||
disabledByAdmin={pluginSettings?.hideWatchlistsTab?.locked === true}
|
disabledByAdmin={pluginSettings?.hideWatchlistsTab?.locked === true}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={hideWatchlistsTab}
|
value={hideWatchlistsTab}
|
||||||
onValueChange={setHideWatchlistsTab}
|
onValueChange={setHideWatchlistsTab}
|
||||||
disabled={!isStreamystatsEnabled}
|
disabled={!isStreamystatsEnabled}
|
||||||
|
|||||||
@@ -1318,7 +1318,7 @@ export default function DirectPlayerPage() {
|
|||||||
console.error("Video Error:", e.nativeEvent);
|
console.error("Video Error:", e.nativeEvent);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("player.error"),
|
t("player.error"),
|
||||||
t("player.an_error_occured_while_playing_the_video"),
|
t("player.an_error_occurred_while_playing_the_video"),
|
||||||
);
|
);
|
||||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ configureReanimatedLogger({
|
|||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
Notifications.setNotificationHandler({
|
Notifications.setNotificationHandler({
|
||||||
handleNotification: async () => ({
|
handleNotification: async () => ({
|
||||||
shouldShowAlert: true,
|
shouldShowBanner: true,
|
||||||
|
shouldShowList: true,
|
||||||
shouldPlaySound: true,
|
shouldPlaySound: true,
|
||||||
shouldSetBadge: false,
|
shouldSetBadge: false,
|
||||||
}),
|
}),
|
||||||
@@ -350,9 +351,12 @@ function Layout() {
|
|||||||
notificationListener.current =
|
notificationListener.current =
|
||||||
Notifications?.addNotificationReceivedListener(
|
Notifications?.addNotificationReceivedListener(
|
||||||
(notification: Notification) => {
|
(notification: Notification) => {
|
||||||
|
// Log only the title — serializing the whole notification touches
|
||||||
|
// the deprecated dataString getter (deprecation warning) and dumps
|
||||||
|
// noisy payloads into the console.
|
||||||
console.log(
|
console.log(
|
||||||
"Notification received while app running",
|
"Notification received while app running:",
|
||||||
notification,
|
notification.request.content.title,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BlurView, type BlurViewProps } from "expo-blur";
|
import { BlurView, type BlurViewProps } from "expo-blur";
|
||||||
import { Keyboard, Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { Pressable, type PressableProps } from "react-native-gesture-handler";
|
import { Pressable, type PressableProps } from "react-native-gesture-handler";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
|
||||||
@@ -16,17 +16,10 @@ export const HeaderBackButton: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Dismiss the keyboard before navigating — otherwise it lingers over the
|
|
||||||
// previous screen (e.g. leaving the Jellyseerr login while typing).
|
|
||||||
const handleBack = () => {
|
|
||||||
Keyboard.dismiss();
|
|
||||||
router.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Platform.OS === "ios") {
|
if (Platform.OS === "ios") {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleBack}
|
onPress={() => router.back()}
|
||||||
className='flex items-center justify-center w-9 h-9'
|
className='flex items-center justify-center w-9 h-9'
|
||||||
{...pressableProps}
|
{...pressableProps}
|
||||||
>
|
>
|
||||||
@@ -37,7 +30,7 @@ export const HeaderBackButton: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (background === "transparent" && Platform.OS !== "android")
|
if (background === "transparent" && Platform.OS !== "android")
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={handleBack} {...pressableProps}>
|
<Pressable onPress={() => router.back()} {...pressableProps}>
|
||||||
<BlurView
|
<BlurView
|
||||||
{...props}
|
{...props}
|
||||||
intensity={100}
|
intensity={100}
|
||||||
@@ -55,7 +48,7 @@ export const HeaderBackButton: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleBack}
|
onPress={() => router.back()}
|
||||||
className=' rounded-full p-2'
|
className=' rounded-full p-2'
|
||||||
{...pressableProps}
|
{...pressableProps}
|
||||||
>
|
>
|
||||||
|
|||||||
40
components/common/SettingSwitch.tsx
Normal file
40
components/common/SettingSwitch.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { Platform, Switch, type SwitchProps, View } from "react-native";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings toggle. Android's native Switch lays out ~40px tall / ~56px wide and
|
||||||
|
* inflates list rows (iOS renders it ~31px). A plain `transform: scale` is
|
||||||
|
* visual-only and does NOT shrink the layout box, so we pin the Switch inside a
|
||||||
|
* FIXED-SIZE box (overflow hidden) and center it:
|
||||||
|
* - the fixed height caps the row height (compact, uniform rows),
|
||||||
|
* - the fixed width + centering keep the switch in the exact same spot in the
|
||||||
|
* on/off states (a non-fixed wrapper let its width fluctuate between states,
|
||||||
|
* which shifted the switch sideways on toggle).
|
||||||
|
* iOS renders the switch untouched.
|
||||||
|
*
|
||||||
|
* Tunables: BOX_H drives the row height; SCALE shrinks the visual to fit the
|
||||||
|
* box; keep BOX_W >= scaled visual width to avoid clipping the switch sideways.
|
||||||
|
*/
|
||||||
|
const BOX_W = 40;
|
||||||
|
const BOX_H = 30;
|
||||||
|
const SCALE = 0.9;
|
||||||
|
|
||||||
|
export const SettingSwitch: React.FC<SwitchProps> = (props) => {
|
||||||
|
if (Platform.OS !== "android") return <Switch {...props} />;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: BOX_W,
|
||||||
|
height: BOX_H,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
{...props}
|
||||||
|
style={[props.style, { transform: [{ scale: SCALE }] }]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,9 +16,12 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
|
|||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Keyed on SeriesId so recycled FlashList cells re-read the correct poster
|
||||||
|
// instead of freezing the first-rendered series' image (empty deps bug).
|
||||||
const base64Image = useMemo(() => {
|
const base64Image = useMemo(() => {
|
||||||
return storage.getString(items[0].SeriesId!);
|
const seriesId = items[0]?.SeriesId;
|
||||||
}, []);
|
return seriesId ? storage.getString(seriesId) : undefined;
|
||||||
|
}, [items[0]?.SeriesId]);
|
||||||
|
|
||||||
const deleteSeries = useCallback(
|
const deleteSeries = useCallback(
|
||||||
async () =>
|
async () =>
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className='ml-4 mb-1 uppercase text-[#8E8D91] text-xs'>
|
{title ? (
|
||||||
{title}
|
<Text className='ml-4 mb-1 uppercase text-[#8E8D91] text-xs'>
|
||||||
</Text>
|
{title}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
<View
|
<View
|
||||||
style={[]}
|
style={[]}
|
||||||
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
|
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { PropsWithChildren, ReactNode } from "react";
|
import type { PropsWithChildren, ReactNode } from "react";
|
||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
@@ -34,12 +34,17 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const effectiveSubtitle = disabledByAdmin ? "Disabled by admin" : subtitle;
|
const effectiveSubtitle = disabledByAdmin ? "Disabled by admin" : subtitle;
|
||||||
const isDisabled = disabled || disabledByAdmin;
|
const isDisabled = disabled || disabledByAdmin;
|
||||||
|
// Keep the row floor uniform; Android trims padding slightly (its native
|
||||||
|
// controls sit taller). Switch height is capped via SettingSwitch so toggle
|
||||||
|
// rows match non-toggle rows.
|
||||||
|
const rowSizing =
|
||||||
|
Platform.OS === "android" ? "min-h-[42px] py-1.5" : "min-h-[42px] py-2";
|
||||||
if (onPress)
|
if (onPress)
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
className={`flex flex-row items-center justify-between bg-neutral-900 ${rowSizing} pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
||||||
{...(viewProps as any)}
|
{...(viewProps as any)}
|
||||||
>
|
>
|
||||||
<ListItemContent
|
<ListItemContent
|
||||||
@@ -58,7 +63,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
className={`flex flex-row items-center justify-between bg-neutral-900 ${rowSizing} pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
||||||
{...viewProps}
|
{...viewProps}
|
||||||
>
|
>
|
||||||
<ListItemContent
|
<ListItemContent
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const Login: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("login.connection_failed"),
|
t("login.connection_failed"),
|
||||||
t("login.an_unexpected_error_occured"),
|
t("login.an_unexpected_error_occurred"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ export const TVLogin: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("login.connection_failed"),
|
t("login.connection_failed"),
|
||||||
t("login.an_unexpected_error_occured"),
|
t("login.an_unexpected_error_occurred"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -499,7 +499,7 @@ export const TVLogin: React.FC = () => {
|
|||||||
const message =
|
const message =
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: t("login.an_unexpected_error_occured");
|
: t("login.an_unexpected_error_occurred");
|
||||||
Alert.alert(t("login.connection_failed"), message);
|
Alert.alert(t("login.connection_failed"), message);
|
||||||
goToQRScreen();
|
goToQRScreen();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -523,7 +523,7 @@ export const TVLogin: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("login.connection_failed"),
|
t("login.connection_failed"),
|
||||||
t("login.an_unexpected_error_occured"),
|
t("login.an_unexpected_error_occurred"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -768,7 +768,7 @@ export const TVLogin: React.FC = () => {
|
|||||||
const message =
|
const message =
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: t("login.an_unexpected_error_occured");
|
: t("login.an_unexpected_error_occurred");
|
||||||
Alert.alert(t("login.connection_failed"), message);
|
Alert.alert(t("login.connection_failed"), message);
|
||||||
goToQRScreen();
|
goToQRScreen();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Linking, Switch } from "react-native";
|
import { Linking } from "react-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
@@ -34,7 +35,7 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.showCustomMenuLinks}
|
value={settings.showCustomMenuLinks}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -45,13 +46,23 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.appearance.merge_next_up_continue_watching")}
|
title={t("home.settings.appearance.merge_next_up_continue_watching")}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.mergeNextUpAndContinueWatching}
|
value={settings.mergeNextUpAndContinueWatching}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ mergeNextUpAndContinueWatching: value })
|
updateSettings({ mergeNextUpAndContinueWatching: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.appearance.hide_remote_session_button")}
|
||||||
|
>
|
||||||
|
<SettingSwitch
|
||||||
|
value={settings.hideRemoteSessionButton}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ hideRemoteSessionButton: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
router.push("/settings/appearance/hide-libraries/page")
|
router.push("/settings/appearance/hide-libraries/page")
|
||||||
@@ -59,16 +70,6 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.hide_libraries")}
|
title={t("home.settings.other.hide_libraries")}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.appearance.hide_remote_session_button")}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
value={settings.hideRemoteSessionButton}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateSettings({ hideRemoteSessionButton: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View, type ViewProps } from "react-native";
|
import { Platform, View, type ViewProps } from "react-native";
|
||||||
import { Switch } from "react-native-gesture-handler";
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { AudioTranscodeMode, useSettings } from "@/utils/atoms/settings";
|
import { AudioTranscodeMode, useSettings } from "@/utils/atoms/settings";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
@@ -135,7 +135,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
title={t("home.settings.audio.set_audio_track")}
|
title={t("home.settings.audio.set_audio_track")}
|
||||||
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.rememberAudioSelections}
|
value={settings.rememberAudioSelections}
|
||||||
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Switch, View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
@@ -9,7 +10,7 @@ export const ChromecastSettings: React.FC = ({ ...props }) => {
|
|||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={"Chromecast"}>
|
<ListGroup title={"Chromecast"}>
|
||||||
<ListItem title={"Enable H265 for Chromecast"}>
|
<ListItem title={"Enable H265 for Chromecast"}>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.enableH265ForChromecast}
|
value={settings.enableH265ForChromecast}
|
||||||
onValueChange={(enableH265ForChromecast) =>
|
onValueChange={(enableH265ForChromecast) =>
|
||||||
updateSettings({ enableH265ForChromecast })
|
updateSettings({ enableH265ForChromecast })
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type React from "react";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ViewProps } from "react-native";
|
import type { ViewProps } from "react-native";
|
||||||
import { Switch } from "react-native";
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
@@ -39,7 +39,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.enableHorizontalSwipeSkip}
|
value={settings.enableHorizontalSwipeSkip}
|
||||||
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
||||||
onValueChange={(enableHorizontalSwipeSkip) =>
|
onValueChange={(enableHorizontalSwipeSkip) =>
|
||||||
@@ -55,7 +55,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.enableLeftSideBrightnessSwipe}
|
value={settings.enableLeftSideBrightnessSwipe}
|
||||||
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
||||||
onValueChange={(enableLeftSideBrightnessSwipe) =>
|
onValueChange={(enableLeftSideBrightnessSwipe) =>
|
||||||
@@ -71,7 +71,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.enableRightSideVolumeSwipe}
|
value={settings.enableRightSideVolumeSwipe}
|
||||||
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
||||||
onValueChange={(enableRightSideVolumeSwipe) =>
|
onValueChange={(enableRightSideVolumeSwipe) =>
|
||||||
@@ -87,7 +87,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.hideVolumeSlider}
|
value={settings.hideVolumeSlider}
|
||||||
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
||||||
onValueChange={(hideVolumeSlider) =>
|
onValueChange={(hideVolumeSlider) =>
|
||||||
@@ -103,7 +103,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.hideBrightnessSlider}
|
value={settings.hideBrightnessSlider}
|
||||||
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||||
onValueChange={(hideBrightnessSlider) =>
|
onValueChange={(hideBrightnessSlider) =>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Switch, Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
export const KefinTweaksSettings = () => {
|
export const KefinTweaksSettings = () => {
|
||||||
@@ -20,7 +21,7 @@ export const KefinTweaksSettings = () => {
|
|||||||
{isEnabled ? t("Watchlist On") : t("Watchlist Off")}
|
{isEnabled ? t("Watchlist On") : t("Watchlist Off")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={isEnabled}
|
value={isEnabled}
|
||||||
onValueChange={(value) => updateSettings({ useKefinTweaks: value })}
|
onValueChange={(value) => updateSettings({ useKefinTweaks: value })}
|
||||||
trackColor={{ false: "#555", true: "purple" }}
|
trackColor={{ false: "#555", true: "purple" }}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Switch, TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { useWifiSSID } from "@/hooks/useWifiSSID";
|
import { useWifiSSID } from "@/hooks/useWifiSSID";
|
||||||
import { useServerUrl } from "@/providers/ServerUrlProvider";
|
import { useServerUrl } from "@/providers/ServerUrlProvider";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
@@ -147,7 +148,10 @@ export function LocalNetworkSettings(): React.ReactElement | null {
|
|||||||
title={t("home.settings.network.auto_switch_enabled")}
|
title={t("home.settings.network.auto_switch_enabled")}
|
||||||
subtitle={t("home.settings.network.auto_switch_description")}
|
subtitle={t("home.settings.network.auto_switch_description")}
|
||||||
>
|
>
|
||||||
<Switch value={config.enabled} onValueChange={handleToggleEnabled} />
|
<SettingSwitch
|
||||||
|
value={config.enabled}
|
||||||
|
onValueChange={handleToggleEnabled}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Platform, Switch, View, type ViewProps } from "react-native";
|
import { Platform, View, type ViewProps } from "react-native";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Stepper } from "@/components/inputs/Stepper";
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
@@ -122,7 +123,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ListItem title='Opaque Background'>
|
<ListItem title='Opaque Background'>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ mpvSubtitleBackgroundEnabled: value })
|
updateSettings({ mpvSubtitleBackgroundEnabled: value })
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { TFunction } from "i18next";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Linking, Switch, View } from "react-native";
|
import { Linking, View } from "react-native";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
@@ -132,7 +133,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.safe_area_in_controls")}
|
title={t("home.settings.other.safe_area_in_controls")}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.safeAreaInControlsEnabled}
|
value={settings.safeAreaInControlsEnabled}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -150,7 +151,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.showCustomMenuLinks}
|
value={settings.showCustomMenuLinks}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -188,7 +189,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.disable_haptic_feedback")}
|
title={t("home.settings.other.disable_haptic_feedback")}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.disableHapticFeedback}
|
value={settings.disableHapticFeedback}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
onValueChange={(disableHapticFeedback) =>
|
onValueChange={(disableHapticFeedback) =>
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { TFunction } from "i18next";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Switch, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
@@ -115,7 +116,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={disabled}>
|
<DisabledSetting disabled={disabled}>
|
||||||
<ListGroup title={t("home.settings.other.other_title")} className=''>
|
<ListGroup title={t("home.settings.other.other_title")} className='mb-4'>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.video_orientation")}
|
title={t("home.settings.other.video_orientation")}
|
||||||
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
||||||
@@ -146,7 +147,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.safe_area_in_controls")}
|
title={t("home.settings.other.safe_area_in_controls")}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.safeAreaInControlsEnabled}
|
value={settings.safeAreaInControlsEnabled}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -205,7 +206,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.disable_haptic_feedback")}
|
title={t("home.settings.other.disable_haptic_feedback")}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.disableHapticFeedback}
|
value={settings.disableHapticFeedback}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
onValueChange={(disableHapticFeedback) =>
|
onValueChange={(disableHapticFeedback) =>
|
||||||
@@ -218,7 +219,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.auto_play_next_episode")}
|
title={t("home.settings.other.auto_play_next_episode")}
|
||||||
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.autoPlayNextEpisode}
|
value={settings.autoPlayNextEpisode}
|
||||||
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||||
onValueChange={(autoPlayNextEpisode) =>
|
onValueChange={(autoPlayNextEpisode) =>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
successHapticFeedback();
|
successHapticFeedback();
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("home.settings.quick_connect.success"),
|
t("home.settings.quick_connect.success"),
|
||||||
t("home.settings.quick_connect.quick_connect_autorized"),
|
t("home.settings.quick_connect.quick_connect_authorized"),
|
||||||
);
|
);
|
||||||
setQuickConnectCode(undefined);
|
setQuickConnectCode(undefined);
|
||||||
bottomSheetModalRef?.current?.close();
|
bottomSheetModalRef?.current?.close();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Alert, Platform, View } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
@@ -12,6 +12,7 @@ import { ListItem } from "../list/ListItem";
|
|||||||
export const StorageSettings = () => {
|
export const StorageSettings = () => {
|
||||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
const errorHapticFeedback = useHaptic("error");
|
const errorHapticFeedback = useHaptic("error");
|
||||||
|
|
||||||
@@ -27,16 +28,38 @@ export const StorageSettings = () => {
|
|||||||
used: (app.total - app.remaining) / app.total,
|
used: (app.total - app.remaining) / app.total,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// Keep the bar moving while a download is writing to disk.
|
||||||
|
refetchInterval: 10 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDeleteClicked = async () => {
|
const onDeleteClicked = () => {
|
||||||
try {
|
Alert.alert(
|
||||||
await deleteAllFiles();
|
t("home.settings.storage.delete_all_downloaded_files_confirm"),
|
||||||
successHapticFeedback();
|
t("home.settings.storage.delete_all_downloaded_files_confirm_desc"),
|
||||||
} catch (_e) {
|
[
|
||||||
errorHapticFeedback();
|
{
|
||||||
toast.error(t("home.settings.toasts.error_deleting_files"));
|
text: t("common.cancel"),
|
||||||
}
|
style: "cancel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("common.ok"),
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
await deleteAllFiles();
|
||||||
|
successHapticFeedback();
|
||||||
|
} catch (_e) {
|
||||||
|
errorHapticFeedback();
|
||||||
|
toast.error(t("home.settings.toasts.error_deleting_files"));
|
||||||
|
} finally {
|
||||||
|
// Reflect the freed space immediately instead of waiting for
|
||||||
|
// the next poll.
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["appSize"] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculatePercentage = (value: number, total: number) => {
|
const calculatePercentage = (value: number, total: number) => {
|
||||||
@@ -102,7 +125,7 @@ export const StorageSettings = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<ListGroup>
|
<ListGroup className={Platform.OS === "android" ? "mt-4" : undefined}>
|
||||||
<ListItem
|
<ListItem
|
||||||
textColor='red'
|
textColor='red'
|
||||||
onPress={onDeleteClicked}
|
onPress={onDeleteClicked}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View, type ViewProps } from "react-native";
|
import { Platform, View, type ViewProps } from "react-native";
|
||||||
import { Switch } from "react-native-gesture-handler";
|
|
||||||
import { Input } from "@/components/common/Input";
|
import { Input } from "@/components/common/Input";
|
||||||
|
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||||
import { Stepper } from "@/components/inputs/Stepper";
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -98,6 +98,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup
|
<ListGroup
|
||||||
|
className='mb-4'
|
||||||
title={t("home.settings.subtitles.subtitle_title")}
|
title={t("home.settings.subtitles.subtitle_title")}
|
||||||
description={
|
description={
|
||||||
<Text className='text-[#8E8D91] text-xs'>
|
<Text className='text-[#8E8D91] text-xs'>
|
||||||
@@ -152,7 +153,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
title={t("home.settings.subtitles.set_subtitle_track")}
|
title={t("home.settings.subtitles.set_subtitle_track")}
|
||||||
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<SettingSwitch
|
||||||
value={settings.rememberSubtitleSelections}
|
value={settings.rememberSubtitleSelections}
|
||||||
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
// Imported from expo-router's bundled copy, NOT "@react-navigation/*": as of
|
|
||||||
// SDK 56 expo-router's Metro check rejects direct @react-navigation imports.
|
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { NavigationContext } from "expo-router/react-navigation";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
||||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop-in replacement for expo-router's useRouter that automatically
|
* Drop-in replacement for expo-router's useRouter that automatically
|
||||||
* preserves offline state across navigation and guards against duplicate
|
* preserves offline state across navigation.
|
||||||
* screens from rapid taps.
|
|
||||||
*
|
*
|
||||||
* - For object-form navigation, automatically adds offline=true when in offline context
|
* - For object-form navigation, automatically adds offline=true when in offline context
|
||||||
* - For string URLs, passes through unchanged (caller handles offline param)
|
* - For string URLs, passes through unchanged (caller handles offline param)
|
||||||
* - push() is a no-op while the source screen is not focused, so taps fired
|
|
||||||
* before the pushed screen has rendered (slow devices) can't stack duplicates
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* import useRouter from "@/hooks/useAppRouter";
|
* import useRouter from "@/hooks/useAppRouter";
|
||||||
@@ -25,36 +19,8 @@ export function useAppRouter() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isOffline = useOfflineMode();
|
const isOffline = useOfflineMode();
|
||||||
|
|
||||||
// Optional: undefined when used outside a navigator (root layout, providers).
|
|
||||||
// When present it reflects the focus state of the screen this hook lives in.
|
|
||||||
const navigation = useContext(NavigationContext);
|
|
||||||
|
|
||||||
// Synchronous re-entry guard for TV: a single remote "select" on Android TV
|
|
||||||
// can fire onPress more than once within the same JS batch
|
|
||||||
// (react-native-tvos#110/#138), BEFORE react-navigation commits the pushed
|
|
||||||
// route — so isFocused() still reads true for the duplicate. The ref flips
|
|
||||||
// synchronously on the first push and resets when the screen regains focus.
|
|
||||||
const pushInFlightRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!navigation) return;
|
|
||||||
return navigation.addListener("focus", () => {
|
|
||||||
pushInFlightRef.current = false;
|
|
||||||
});
|
|
||||||
}, [navigation]);
|
|
||||||
|
|
||||||
const push = useCallback(
|
const push = useCallback(
|
||||||
(href: Parameters<typeof router.push>[0]) => {
|
(href: Parameters<typeof router.push>[0]) => {
|
||||||
// Rapid-push guard: a push blurs the source screen synchronously in the
|
|
||||||
// navigation state (only the native render is slow). Any further push from
|
|
||||||
// this screen — duplicate or not — is dropped until focus returns, so taps
|
|
||||||
// fired before the pushed screen renders can't stack screens.
|
|
||||||
// No navigation context => nothing to guard (deep-link pushes from root).
|
|
||||||
if (navigation) {
|
|
||||||
if (navigation.isFocused?.() === false || pushInFlightRef.current)
|
|
||||||
return;
|
|
||||||
pushInFlightRef.current = true;
|
|
||||||
}
|
|
||||||
if (typeof href === "string") {
|
if (typeof href === "string") {
|
||||||
router.push(href as any);
|
router.push(href as any);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +36,7 @@ export function useAppRouter() {
|
|||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[router, isOffline, navigation],
|
[router, isOffline],
|
||||||
);
|
);
|
||||||
|
|
||||||
const replace = useCallback(
|
const replace = useCallback(
|
||||||
|
|||||||
@@ -96,5 +96,24 @@ export function getDownloadedItemSize(id: string): number {
|
|||||||
*/
|
*/
|
||||||
export function calculateTotalDownloadedSize(): number {
|
export function calculateTotalDownloadedSize(): number {
|
||||||
const items = getAllDownloadedItems();
|
const items = getAllDownloadedItems();
|
||||||
return items.reduce((sum, item) => sum + (item.videoFileSize || 0), 0);
|
return items.reduce((sum, item) => {
|
||||||
|
// Trickplay bytes count too — getDownloadedItemSize models per-item size
|
||||||
|
// as video + trickplay, the total must match.
|
||||||
|
const trickplaySize = item.trickPlayData?.size ?? 0;
|
||||||
|
// Read the live file size on disk so the total reflects actual usage and
|
||||||
|
// self-heals items whose stored videoFileSize is 0 (old schema, or
|
||||||
|
// `fileInfo.size` was undefined at download time). Fall back to the stored
|
||||||
|
// value if the file can't be stat'd.
|
||||||
|
if (item.videoFilePath) {
|
||||||
|
try {
|
||||||
|
const file = new File(filePathToUri(item.videoFilePath));
|
||||||
|
if (file.exists) {
|
||||||
|
return sum + (file.size ?? item.videoFileSize ?? 0) + trickplaySize;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to stat downloaded file for size:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum + (item.videoFileSize ?? 0) + trickplaySize;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,7 +289,24 @@ export function useDownloadOperations({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const appSizeUsage = useCallback(async () => {
|
const appSizeUsage = useCallback(async () => {
|
||||||
const totalSize = calculateTotalDownloadedSize();
|
let totalSize = calculateTotalDownloadedSize();
|
||||||
|
|
||||||
|
// Also count in-progress downloads (they write straight to their final
|
||||||
|
// path) so the growing file shows up as app usage instead of drifting
|
||||||
|
// into the generic device share until completion.
|
||||||
|
for (const process of processes) {
|
||||||
|
try {
|
||||||
|
const file = new File(
|
||||||
|
Paths.document,
|
||||||
|
`${generateFilename(process.item)}.mp4`,
|
||||||
|
);
|
||||||
|
if (file.exists) {
|
||||||
|
totalSize += file.size ?? 0;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// File not created yet — ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [freeDiskStorage, totalDiskCapacity] = await Promise.all([
|
const [freeDiskStorage, totalDiskCapacity] = await Promise.all([
|
||||||
@@ -310,7 +327,7 @@ export function useDownloadOperations({
|
|||||||
appSize: totalSize,
|
appSize: totalSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, []);
|
}, [processes]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startBackgroundDownload,
|
startBackgroundDownload,
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
t(
|
t(
|
||||||
"login.an_unexpected_error_occured_did_you_enter_the_correct_url",
|
"login.an_unexpected_error_occurred_did_you_enter_the_correct_url",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
"got_it": "Got it",
|
"got_it": "Got it",
|
||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Connection failed",
|
||||||
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
||||||
"an_unexpected_error_occured": "An unexpected error occurred",
|
"an_unexpected_error_occurred": "An unexpected error occurred",
|
||||||
"change_server": "Change server",
|
"change_server": "Change server",
|
||||||
"invalid_username_or_password": "Invalid username or password",
|
"invalid_username_or_password": "Invalid username or password",
|
||||||
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
|
"server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
|
||||||
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
||||||
"there_is_a_server_error": "There is a server error",
|
"there_is_a_server_error": "There is a server error",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
||||||
"too_old_server_text": "Unsupported Jellyfin server discovered",
|
"too_old_server_text": "Unsupported Jellyfin server discovered",
|
||||||
"too_old_server_description": "Please update Jellyfin to the latest version"
|
"too_old_server_description": "Please update Jellyfin to the latest version"
|
||||||
},
|
},
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
"authorize_button": "Authorize Quick Connect",
|
"authorize_button": "Authorize Quick Connect",
|
||||||
"enter_the_quick_connect_code": "Enter the Quick Connect code...",
|
"enter_the_quick_connect_code": "Enter the Quick Connect code...",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"quick_connect_autorized": "Quick Connect authorized",
|
"quick_connect_authorized": "Quick Connect authorized",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"invalid_code": "Invalid code",
|
"invalid_code": "Invalid code",
|
||||||
"authorize": "Authorize"
|
"authorize": "Authorize"
|
||||||
@@ -298,7 +298,7 @@
|
|||||||
"show_custom_menu_links": "Show custom menu links",
|
"show_custom_menu_links": "Show custom menu links",
|
||||||
"show_large_home_carousel": "Show large home carousel (beta)",
|
"show_large_home_carousel": "Show large home carousel (beta)",
|
||||||
"hide_libraries": "Hide libraries",
|
"hide_libraries": "Hide libraries",
|
||||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_libraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
"disable_haptic_feedback": "Disable haptic feedback",
|
"disable_haptic_feedback": "Disable haptic feedback",
|
||||||
"default_quality": "Default quality",
|
"default_quality": "Default quality",
|
||||||
"default_playback_speed": "Default playback speed",
|
"default_playback_speed": "Default playback speed",
|
||||||
@@ -384,6 +384,8 @@
|
|||||||
"device_usage": "Device {{availableSpace}}%",
|
"device_usage": "Device {{availableSpace}}%",
|
||||||
"size_used": "{{used}} of {{total}} used",
|
"size_used": "{{used}} of {{total}} used",
|
||||||
"delete_all_downloaded_files": "Delete all downloaded files",
|
"delete_all_downloaded_files": "Delete all downloaded files",
|
||||||
|
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
|
||||||
|
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
|
||||||
"music_cache_title": "Music cache",
|
"music_cache_title": "Music cache",
|
||||||
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
|
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
|
||||||
"clear_music_cache": "Clear music cache",
|
"clear_music_cache": "Clear music cache",
|
||||||
@@ -598,7 +600,7 @@
|
|||||||
"mpv_player_title": "MPV player",
|
"mpv_player_title": "MPV player",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
"an_error_occurred_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
"client_error": "Client error",
|
"client_error": "Client error",
|
||||||
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
|
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
|
||||||
"message_from_server": "Message from server: {{message}}",
|
"message_from_server": "Message from server: {{message}}",
|
||||||
|
|||||||
Reference in New Issue
Block a user