From 30dc3980e335383de118c3f6cfb8dd17e36c5508 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 14 Nov 2025 08:03:00 +0100 Subject: [PATCH] refactor: better settings (#1178) --- app/(auth)/(tabs)/(home)/_layout.tsx | 140 +++++++++++- app/(auth)/(tabs)/(home)/settings.tsx | 91 +++----- .../appearance/hide-libraries/page.tsx | 79 +++++++ .../(home)/settings/appearance/page.tsx | 25 +++ .../(home)/settings/audio-subtitles/page.tsx | 29 +++ .../(tabs)/(home)/settings/intro/page.tsx | 45 ++++ .../(home)/settings/jellyseerr/page.tsx | 16 -- .../(tabs)/(home)/settings/logs/page.tsx | 13 +- .../(home)/settings/marlin-search/page.tsx | 122 ---------- .../settings/playback-controls/page.tsx | 35 +++ .../settings/plugins/jellyseerr/page.tsx | 27 +++ .../settings/plugins/marlin-search/page.tsx | 138 ++++++++++++ .../(tabs)/(home)/settings/plugins/page.tsx | 24 ++ components/settings/AppLanguageSelector.tsx | 10 +- components/settings/AppearanceSettings.tsx | 63 ++++++ components/settings/AudioToggles.tsx | 2 +- components/settings/OtherSettings.tsx | 8 +- .../settings/PlaybackControlsSettings.tsx | 211 ++++++++++++++++++ components/settings/PluginSettings.tsx | 34 ++- components/settings/SubtitleToggles.tsx | 16 +- translations/en.json | 13 ++ 21 files changed, 898 insertions(+), 243 deletions(-) create mode 100644 app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/appearance/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/intro/page.tsx delete mode 100644 app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx delete mode 100644 app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx create mode 100644 app/(auth)/(tabs)/(home)/settings/plugins/page.tsx create mode 100644 components/settings/AppearanceSettings.tsx create mode 100644 components/settings/PlaybackControlsSettings.tsx diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 60d8749b..bfbebed5 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -113,33 +113,144 @@ export default function IndexLayout() { }} /> ( - _router.back()} className='pl-0.5'> + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > ), }} /> ( - _router.back()} className='pl-0.5'> + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > ), }} /> ( - _router.back()} className='pl-0.5'> + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > + + + ), + }} + /> + ( + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > + + + ), + }} + /> + ( + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > + + + ), + }} + /> + ( + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > + + + ), + }} + /> + ( + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > + + + ), + }} + /> + ( + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > ), @@ -148,9 +259,16 @@ export default function IndexLayout() { ( - _router.back()} className='pl-0.5'> + _router.back()} + className='pl-0.5' + style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} + > ), diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 9927b6ae..4bd917d6 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -8,34 +8,16 @@ import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector"; -import { AudioToggles } from "@/components/settings/AudioToggles"; -import { ChromecastSettings } from "@/components/settings/ChromecastSettings"; -import DownloadSettings from "@/components/settings/DownloadSettings"; -import { GestureControls } from "@/components/settings/GestureControls"; -import { MediaProvider } from "@/components/settings/MediaContext"; -import { MediaToggles } from "@/components/settings/MediaToggles"; -import { OtherSettings } from "@/components/settings/OtherSettings"; -import { PluginSettings } from "@/components/settings/PluginSettings"; import { QuickConnect } from "@/components/settings/QuickConnect"; import { StorageSettings } from "@/components/settings/StorageSettings"; -import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { UserInfo } from "@/components/settings/UserInfo"; -import { useHaptic } from "@/hooks/useHaptic"; import { useJellyfin, userAtom } from "@/providers/JellyfinProvider"; -import { clearLogs } from "@/utils/log"; -import { storage } from "@/utils/mmkv"; export default function settings() { const router = useRouter(); const insets = useSafeAreaInsets(); const [_user] = useAtom(userAtom); const { logout } = useJellyfin(); - const successHapticFeedback = useHaptic("success"); - - const onClearLogsClicked = async () => { - clearLogs(); - successHapticFeedback(); - }; const navigation = useNavigation(); useEffect(() => { @@ -63,58 +45,51 @@ export default function settings() { }} > - + + + - - - - - - - - - - {!Platform.isTV && } - - - - - - {!Platform.isTV && } - - - { - router.push("/intro/page"); - }} - title={t("home.settings.intro.show_intro")} - /> - { - storage.set("hasShownIntro", false); - }} - title={t("home.settings.intro.reset_intro")} - /> - + + + - + + router.push("/settings/playback-controls/page")} + showArrow + title={t("home.settings.playback_controls.title")} + /> + router.push("/settings/audio-subtitles/page")} + showArrow + title={t("home.settings.audio_subtitles.title")} + /> + router.push("/settings/appearance/page")} + showArrow + title={t("home.settings.appearance.title")} + /> + router.push("/settings/plugins/page")} + showArrow + title={t("home.settings.plugins.plugins_title")} + /> + router.push("/settings/intro/page")} + showArrow + title={t("home.settings.intro.title")} + /> router.push("/settings/logs/page")} showArrow title={t("home.settings.logs.logs_title")} /> - diff --git a/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx new file mode 100644 index 00000000..a0b3bab9 --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx @@ -0,0 +1,79 @@ +import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; +import { useTranslation } from "react-i18next"; +import { ScrollView, Switch, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "@/components/common/Text"; +import { Loader } from "@/components/Loader"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; + +export default function page() { + const { settings, updateSettings, pluginSettings } = useSettings(); + const user = useAtomValue(userAtom); + const api = useAtomValue(apiAtom); + const insets = useSafeAreaInsets(); + + const { t } = useTranslation(); + + const { data, isLoading } = useQuery({ + queryKey: ["user-views", user?.Id], + queryFn: async () => { + const response = await getUserViewsApi(api!).getUserViews({ + userId: user?.Id, + }); + + return response.data.Items || null; + }, + }); + + if (!settings) return null; + + if (isLoading) + return ( + + + + ); + + return ( + + + + {data?.map((view) => ( + {}}> + { + updateSettings({ + hiddenLibraries: value + ? [...(settings.hiddenLibraries || []), view.Id!] + : settings.hiddenLibraries?.filter( + (id) => id !== view.Id, + ), + }); + }} + /> + + ))} + + + {t("home.settings.other.select_liraries_you_want_to_hide")} + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/appearance/page.tsx b/app/(auth)/(tabs)/(home)/settings/appearance/page.tsx new file mode 100644 index 00000000..6e5b6197 --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/appearance/page.tsx @@ -0,0 +1,25 @@ +import { Platform, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AppearanceSettings } from "@/components/settings/AppearanceSettings"; + +export default function AppearancePage() { + const insets = useSafeAreaInsets(); + + return ( + + + + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx b/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx new file mode 100644 index 00000000..58415127 --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx @@ -0,0 +1,29 @@ +import { Platform, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AudioToggles } from "@/components/settings/AudioToggles"; +import { MediaProvider } from "@/components/settings/MediaContext"; +import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; + +export default function AudioSubtitlesPage() { + const insets = useSafeAreaInsets(); + + return ( + + + + + + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/intro/page.tsx b/app/(auth)/(tabs)/(home)/settings/intro/page.tsx new file mode 100644 index 00000000..4cb43e6d --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/intro/page.tsx @@ -0,0 +1,45 @@ +import { useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; +import { Platform, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import { storage } from "@/utils/mmkv"; + +export default function IntroPage() { + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { t } = useTranslation(); + + return ( + + + + { + router.push("/intro/page"); + }} + title={t("home.settings.intro.show_intro")} + /> + { + storage.set("hasShownIntro", false); + }} + title={t("home.settings.intro.reset_intro")} + /> + + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx deleted file mode 100644 index 86222f93..00000000 --- a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import DisabledSetting from "@/components/settings/DisabledSetting"; -import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; -import { useSettings } from "@/utils/atoms/settings"; - -export default function page() { - const { pluginSettings } = useSettings(); - - return ( - - - - ); -} diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx index d2b9b852..03a8e1ca 100644 --- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ScrollView, TouchableOpacity, View } from "react-native"; import Collapsible from "react-native-collapsible"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { FilterButton } from "@/components/filters/FilterButton"; import { Loader } from "@/components/Loader"; @@ -32,6 +33,7 @@ export default function Page() { const _orderId = useId(); const _levelsId = useId(); + const insets = useSafeAreaInsets(); const filteredLogs = useMemo( () => @@ -66,7 +68,7 @@ export default function Page() { loading ? ( ) : ( - + {t("home.settings.logs.export_logs")} ), @@ -74,7 +76,12 @@ export default function Page() { }, [share, loading]); return ( - <> + - + ); } diff --git a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx deleted file mode 100644 index a9031dd2..00000000 --- a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { useNavigation } from "expo-router"; -import { useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - Linking, - Switch, - TextInput, - TouchableOpacity, - View, -} from "react-native"; -import { toast } from "sonner-native"; -import { Text } from "@/components/common/Text"; -import { ListGroup } from "@/components/list/ListGroup"; -import { ListItem } from "@/components/list/ListItem"; -import DisabledSetting from "@/components/settings/DisabledSetting"; -import { useSettings } from "@/utils/atoms/settings"; - -export default function page() { - const navigation = useNavigation(); - - const { t } = useTranslation(); - - const { settings, updateSettings, pluginSettings } = useSettings(); - const queryClient = useQueryClient(); - - const [value, setValue] = useState(settings?.marlinServerUrl || ""); - - const onSave = (val: string) => { - updateSettings({ - marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1), - }); - toast.success(t("home.settings.plugins.marlin_search.toasts.saved")); - }; - - const handleOpenLink = () => { - Linking.openURL("https://github.com/fredrikburmester/marlin-search"); - }; - - const disabled = useMemo(() => { - return ( - pluginSettings?.searchEngine?.locked === true && - pluginSettings?.marlinServerUrl?.locked === true - ); - }, [pluginSettings]); - - useEffect(() => { - if (!pluginSettings?.marlinServerUrl?.locked) { - navigation.setOptions({ - headerRight: () => ( - onSave(value)}> - - {t("home.settings.plugins.marlin_search.save_button")} - - - ), - }); - } - }, [navigation, value]); - - if (!settings) return null; - - return ( - - - - { - updateSettings({ searchEngine: "Jellyfin" }); - queryClient.invalidateQueries({ queryKey: ["search"] }); - }} - > - { - updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" }); - queryClient.invalidateQueries({ queryKey: ["search"] }); - }} - /> - - - - - - - - {t("home.settings.plugins.marlin_search.url")} - - setValue(text)} - /> - - - - {t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "} - - {t("home.settings.plugins.marlin_search.read_more_about_marlin")} - - - - ); -} diff --git a/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx b/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx new file mode 100644 index 00000000..370247c1 --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx @@ -0,0 +1,35 @@ +import { Platform, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { GestureControls } from "@/components/settings/GestureControls"; +import { MediaProvider } from "@/components/settings/MediaContext"; +import { MediaToggles } from "@/components/settings/MediaToggles"; +import { PlaybackControlsSettings } from "@/components/settings/PlaybackControlsSettings"; +import { ChromecastSettings } from "../../../../../../components/settings/ChromecastSettings"; + +export default function PlaybackControlsPage() { + const insets = useSafeAreaInsets(); + + return ( + + + + + + + + + + {!Platform.isTV && } + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx new file mode 100644 index 00000000..cd1efca5 --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx @@ -0,0 +1,27 @@ +import { ScrollView } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; +import { useSettings } from "@/utils/atoms/settings"; + +export default function page() { + const { pluginSettings } = useSettings(); + const insets = useSafeAreaInsets(); + + return ( + + + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx new file mode 100644 index 00000000..4eb36aef --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx @@ -0,0 +1,138 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigation } from "expo-router"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Linking, + ScrollView, + Switch, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { toast } from "sonner-native"; +import { Text } from "@/components/common/Text"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { useSettings } from "@/utils/atoms/settings"; + +export default function page() { + const navigation = useNavigation(); + + const { t } = useTranslation(); + + const insets = useSafeAreaInsets(); + + const { settings, updateSettings, pluginSettings } = useSettings(); + const queryClient = useQueryClient(); + + const [value, setValue] = useState(settings?.marlinServerUrl || ""); + + const onSave = (val: string) => { + updateSettings({ + marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1), + }); + toast.success(t("home.settings.plugins.marlin_search.toasts.saved")); + }; + + const handleOpenLink = () => { + Linking.openURL("https://github.com/fredrikburmester/marlin-search"); + }; + + const disabled = useMemo(() => { + return ( + pluginSettings?.searchEngine?.locked === true && + pluginSettings?.marlinServerUrl?.locked === true + ); + }, [pluginSettings]); + + useEffect(() => { + if (!pluginSettings?.marlinServerUrl?.locked) { + navigation.setOptions({ + headerRight: () => ( + onSave(value)} className='px-2'> + + {t("home.settings.plugins.marlin_search.save_button")} + + + ), + }); + } + }, [navigation, value]); + + if (!settings) return null; + + return ( + + + + + { + updateSettings({ searchEngine: "Jellyfin" }); + queryClient.invalidateQueries({ queryKey: ["search"] }); + }} + > + { + updateSettings({ + searchEngine: value ? "Marlin" : "Jellyfin", + }); + queryClient.invalidateQueries({ queryKey: ["search"] }); + }} + /> + + + + + + + + {t("home.settings.plugins.marlin_search.url")} + + setValue(text)} + /> + + + + {t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "} + + {t("home.settings.plugins.marlin_search.read_more_about_marlin")} + + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/page.tsx new file mode 100644 index 00000000..a06ab6cf --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/plugins/page.tsx @@ -0,0 +1,24 @@ +import { Platform, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { PluginSettings } from "@/components/settings/PluginSettings"; + +export default function PluginsPage() { + const insets = useSafeAreaInsets(); + + return ( + + + + + + ); +} diff --git a/components/settings/AppLanguageSelector.tsx b/components/settings/AppLanguageSelector.tsx index ac52896c..b0985b9a 100644 --- a/components/settings/AppLanguageSelector.tsx +++ b/components/settings/AppLanguageSelector.tsx @@ -1,3 +1,4 @@ +import Ionicons from "@expo/vector-icons/Ionicons"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Platform, View, type ViewProps } from "react-native"; @@ -50,12 +51,17 @@ export const AppLanguageSelector: React.FC = () => { - + + {APP_LANGUAGES.find( (l) => l.value === settings?.preferedLanguage, )?.label || t("home.settings.languages.system")} + } title={t("home.settings.languages.title")} diff --git a/components/settings/AppearanceSettings.tsx b/components/settings/AppearanceSettings.tsx new file mode 100644 index 00000000..3953c73f --- /dev/null +++ b/components/settings/AppearanceSettings.tsx @@ -0,0 +1,63 @@ +import { useRouter } from "expo-router"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Linking, Switch } from "react-native"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { useSettings } from "@/utils/atoms/settings"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; + +export const AppearanceSettings: React.FC = () => { + const router = useRouter(); + const { settings, updateSettings, pluginSettings } = useSettings(); + const { t } = useTranslation(); + + const disabled = useMemo( + () => + pluginSettings?.showCustomMenuLinks?.locked === true && + pluginSettings?.hiddenLibraries?.locked === true, + [pluginSettings], + ); + + if (!settings) return null; + + return ( + + + + Linking.openURL( + "https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links", + ) + } + > + + updateSettings({ showCustomMenuLinks: value }) + } + /> + + + + updateSettings({ showLargeHomeCarousel: value }) + } + /> + + + router.push("/settings/appearance/hide-libraries/page") + } + title={t("home.settings.other.hide_libraries")} + showArrow + /> + + + ); +}; diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx index 1a1c1457..ef7b42f3 100644 --- a/components/settings/AudioToggles.tsx +++ b/components/settings/AudioToggles.tsx @@ -83,7 +83,7 @@ export const AudioToggles: React.FC = ({ ...props }) => { + {settings?.defaultAudioLanguage?.DisplayName || t("home.settings.audio.none")} diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index a93e219b..452edab0 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -109,7 +109,7 @@ export const OtherSettings: React.FC = () => { + {t( orientationTranslations[ @@ -152,7 +152,7 @@ export const OtherSettings: React.FC = () => { keyExtractor={String} titleExtractor={(item) => t(`home.settings.other.video_players.${VideoPlayer[item]}`)} title={ - + {t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)} @@ -208,7 +208,7 @@ export const OtherSettings: React.FC = () => { + {settings.defaultBitrate?.key} @@ -238,7 +238,7 @@ export const OtherSettings: React.FC = () => { + {t(settings?.maxAutoPlayEpisodeCount.key)} diff --git a/components/settings/PlaybackControlsSettings.tsx b/components/settings/PlaybackControlsSettings.tsx new file mode 100644 index 00000000..c64db5ed --- /dev/null +++ b/components/settings/PlaybackControlsSettings.tsx @@ -0,0 +1,211 @@ +import { Ionicons } from "@expo/vector-icons"; +import { TFunction } from "i18next"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Switch, View } from "react-native"; +import { BITRATES } from "@/components/BitrateSelector"; +import { PlatformDropdown } from "@/components/PlatformDropdown"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; +import { Text } from "../common/Text"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; + +export const PlaybackControlsSettings: React.FC = () => { + const { settings, updateSettings, pluginSettings } = useSettings(); + const { t } = useTranslation(); + + const disabled = useMemo( + () => + pluginSettings?.defaultVideoOrientation?.locked === true && + pluginSettings?.safeAreaInControlsEnabled?.locked === true && + pluginSettings?.disableHapticFeedback?.locked === true, + [pluginSettings], + ); + + const orientations = [ + ScreenOrientation.OrientationLock.DEFAULT, + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ScreenOrientation.OrientationLock.LANDSCAPE_LEFT, + ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, + ]; + + const orientationTranslations = useMemo( + () => ({ + [ScreenOrientation.OrientationLock.DEFAULT]: + "home.settings.other.orientations.DEFAULT", + [ScreenOrientation.OrientationLock.PORTRAIT_UP]: + "home.settings.other.orientations.PORTRAIT_UP", + [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: + "home.settings.other.orientations.LANDSCAPE_LEFT", + [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: + "home.settings.other.orientations.LANDSCAPE_RIGHT", + }), + [], + ); + + const orientationOptions = useMemo( + () => [ + { + options: orientations.map((orientation) => ({ + type: "radio" as const, + label: t(ScreenOrientationEnum[orientation]), + value: String(orientation), + selected: orientation === settings?.defaultVideoOrientation, + onPress: () => + updateSettings({ defaultVideoOrientation: orientation }), + })), + }, + ], + [orientations, settings?.defaultVideoOrientation, t, updateSettings], + ); + + const bitrateOptions = useMemo( + () => [ + { + options: BITRATES.map((bitrate) => ({ + type: "radio" as const, + label: bitrate.key, + value: bitrate.key, + selected: bitrate.key === settings?.defaultBitrate?.key, + onPress: () => updateSettings({ defaultBitrate: bitrate }), + })), + }, + ], + [settings?.defaultBitrate?.key, updateSettings], + ); + + const autoPlayEpisodeOptions = useMemo( + () => [ + { + options: AUTOPLAY_EPISODES_COUNT(t).map((item) => ({ + type: "radio" as const, + label: item.key, + value: item.key, + selected: item.key === settings?.maxAutoPlayEpisodeCount?.key, + onPress: () => updateSettings({ maxAutoPlayEpisodeCount: item }), + })), + }, + ], + [settings?.maxAutoPlayEpisodeCount?.key, t, updateSettings], + ); + + if (!settings) return null; + + return ( + + + + + + {t( + orientationTranslations[ + settings.defaultVideoOrientation as keyof typeof orientationTranslations + ], + ) || "Unknown Orientation"} + + + + } + title={t("home.settings.other.orientation")} + /> + + + + + updateSettings({ safeAreaInControlsEnabled: value }) + } + /> + + + + + + {settings.defaultBitrate?.key} + + + + } + title={t("home.settings.other.default_quality")} + /> + + + + + updateSettings({ disableHapticFeedback }) + } + /> + + + + + + {t(settings?.maxAutoPlayEpisodeCount.key)} + + + + } + title={t("home.settings.other.max_auto_play_episode_count")} + /> + + + + ); +}; + +const AUTOPLAY_EPISODES_COUNT = ( + t: TFunction<"translation", undefined>, +): { + key: string; + value: number; +}[] => [ + { key: t("home.settings.other.disabled"), value: -1 }, + { key: "1", value: 1 }, + { key: "2", value: 2 }, + { key: "3", value: 3 }, + { key: "4", value: 4 }, + { key: "5", value: 5 }, + { key: "6", value: 6 }, + { key: "7", value: 7 }, +]; diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx index 1fb064ef..317136f1 100644 --- a/components/settings/PluginSettings.tsx +++ b/components/settings/PluginSettings.tsx @@ -1,6 +1,5 @@ import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; -import { View } from "react-native"; import { useSettings } from "@/utils/atoms/settings"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -13,23 +12,22 @@ export const PluginSettings = () => { const { t } = useTranslation(); if (!settings) return null; + return ( - - - router.push("/settings/jellyseerr/page")} - title={"Jellyseerr"} - showArrow - /> - router.push("/settings/marlin-search/page")} - title='Marlin Search' - showArrow - /> - - + + router.push("/settings/plugins/jellyseerr/page")} + title={"Jellyseerr"} + showArrow + /> + router.push("/settings/plugins/marlin-search/page")} + title='Marlin Search' + showArrow + /> + ); }; diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 5389d15e..5a6bfee3 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -187,7 +187,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {settings?.defaultSubtitleLanguage?.DisplayName || t("home.settings.subtitles.none")} @@ -210,7 +210,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t(subtitleModeKeys[settings?.subtitleMode]) || t("home.settings.subtitles.loading")} @@ -256,7 +256,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t( `home.settings.subtitles.colors.${settings?.vlcTextColor || "White"}`, @@ -276,7 +276,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t( `home.settings.subtitles.colors.${settings?.vlcBackgroundColor || "Black"}`, @@ -296,7 +296,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t( `home.settings.subtitles.colors.${settings?.vlcOutlineColor || "Black"}`, @@ -316,7 +316,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t( `home.settings.subtitles.thickness.${settings?.vlcOutlineThickness || "Normal"}`, @@ -336,7 +336,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {`${Math.round(((settings?.vlcBackgroundOpacity ?? 128) / 255) * 100)}%`} = ({ ...props }) => { + {`${Math.round(((settings?.vlcOutlineOpacity ?? 255) / 255) * 100)}%`}