From 2ac664de5a8320f641b105c6103c5c6ae69e55ee Mon Sep 17 00:00:00 2001 From: Gauvain Date: Wed, 24 Jun 2026 22:21:06 +0200 Subject: [PATCH] fix(settings): tidy spacing, uniform Android row heights & toggle layout - spacing: top padding + inter-group gaps so section titles, descriptions and buttons no longer touch (playback buffer, audio/subtitles, music, marlin, kefin, streamystats, storage, hide-libraries) - ListGroup: skip empty section titles - SettingSwitch: cap the native Android switch in a fixed centered box so toggle rows match the other rows and stay put when toggled; iOS unchanged - Appearance: move "Hide libraries" to the end of the list --- .../appearance/hide-libraries/page.tsx | 7 ++-- .../(home)/settings/hide-libraries/page.tsx | 5 ++- .../(tabs)/(home)/settings/music/page.tsx | 8 ++-- .../settings/playback-controls/page.tsx | 3 +- .../settings/plugins/kefinTweaks/page.tsx | 2 +- .../settings/plugins/marlin-search/page.tsx | 6 +-- .../settings/plugins/streamystats/page.tsx | 14 +++---- components/common/SettingSwitch.tsx | 40 +++++++++++++++++++ components/list/ListGroup.tsx | 8 ++-- components/list/ListItem.tsx | 11 +++-- components/settings/AppearanceSettings.tsx | 27 +++++++------ components/settings/AudioToggles.tsx | 4 +- components/settings/ChromecastSettings.tsx | 5 ++- components/settings/GestureControls.tsx | 12 +++--- components/settings/KefinTweaks.tsx | 4 +- components/settings/LocalNetworkSettings.tsx | 8 +++- components/settings/MpvSubtitleSettings.tsx | 5 ++- components/settings/OtherSettings.tsx | 9 +++-- .../settings/PlaybackControlsSettings.tsx | 11 ++--- components/settings/StorageSettings.tsx | 2 +- components/settings/SubtitleToggles.tsx | 5 ++- 21 files changed, 128 insertions(+), 68 deletions(-) create mode 100644 components/common/SettingSwitch.tsx diff --git a/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx index d0109fb7..e4a7e3b9 100644 --- a/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx @@ -2,8 +2,9 @@ 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 { ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; import { ListGroup } from "@/components/list/ListGroup"; @@ -50,12 +51,12 @@ export default function AppearanceHideLibrariesPage() { > {data?.map((view) => ( {}}> - { updateSettings({ diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index fe777462..5aba233a 100644 --- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx @@ -2,7 +2,8 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; 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 { Loader } from "@/components/Loader"; import { ListGroup } from "@/components/list/ListGroup"; @@ -46,7 +47,7 @@ export default function HideLibrariesPage() { {data?.map((view) => ( {}}> - { updateSettings({ diff --git a/app/(auth)/(tabs)/(home)/settings/music/page.tsx b/app/(auth)/(tabs)/(home)/settings/music/page.tsx index 70467b4d..7dc5e22e 100644 --- a/app/(auth)/(tabs)/(home)/settings/music/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/music/page.tsx @@ -3,9 +3,9 @@ import { useQuery } from "@tanstack/react-query"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Platform, ScrollView, View } from "react-native"; -import { Switch } from "react-native-gesture-handler"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { toast } from "sonner-native"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; @@ -136,7 +136,7 @@ export default function MusicSettingsPage() { title={t("home.settings.music.prefer_downloaded")} disabled={pluginSettings?.preferLocalAudio?.locked} > - @@ -159,7 +159,7 @@ export default function MusicSettingsPage() { title={t("home.settings.music.lookahead_enabled")} disabled={pluginSettings?.audioLookaheadEnabled?.locked} > - @@ -233,7 +233,7 @@ export default function MusicSettingsPage() { })} /> - + - + diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx index b6fd8c2e..059afbf0 100644 --- a/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx @@ -13,7 +13,7 @@ export default function KefinTweaksPage() { paddingRight: insets.right, }} > - + diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx index d9e737ec..b36a493b 100644 --- a/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx @@ -4,13 +4,13 @@ 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 { SettingSwitch } from "@/components/common/SettingSwitch"; import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; @@ -65,7 +65,7 @@ export default function MarlinSearchPage() { paddingRight: insets.right, }} > - + {/* disabledByAdmin renders the "Disabled by admin" notice as the row's subtitle (same pattern as the Streamystats settings) — no clipping. */} @@ -79,7 +79,7 @@ export default function MarlinSearchPage() { queryClient.invalidateQueries({ queryKey: ["search"] }); }} > - { diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx index a5a45519..7774a925 100644 --- a/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx @@ -4,13 +4,13 @@ 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 { SettingSwitch } from "@/components/common/SettingSwitch"; import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; @@ -132,7 +132,7 @@ export default function StreamystatsPage() { paddingRight: insets.right, }} > - + - - - - = 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 = (props) => { + if (Platform.OS !== "android") return ; + return ( + + + + ); +}; diff --git a/components/list/ListGroup.tsx b/components/list/ListGroup.tsx index b9752bac..d3f955f0 100644 --- a/components/list/ListGroup.tsx +++ b/components/list/ListGroup.tsx @@ -23,9 +23,11 @@ export const ListGroup: React.FC> = ({ return ( - - {title} - + {title ? ( + + {title} + + ) : null} > = ({ }) => { const effectiveSubtitle = disabledByAdmin ? "Disabled by admin" : subtitle; 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) return ( > = ({ ); return ( { ) } > - @@ -45,13 +46,23 @@ export const AppearanceSettings: React.FC = () => { - updateSettings({ mergeNextUpAndContinueWatching: value }) } /> + + + updateSettings({ hideRemoteSessionButton: value }) + } + /> + router.push("/settings/appearance/hide-libraries/page") @@ -59,16 +70,6 @@ export const AppearanceSettings: React.FC = () => { title={t("home.settings.other.hide_libraries")} showArrow /> - - - updateSettings({ hideRemoteSessionButton: value }) - } - /> - ); diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx index 93a267bd..e34d5a98 100644 --- a/components/settings/AudioToggles.tsx +++ b/components/settings/AudioToggles.tsx @@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; 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 { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; @@ -135,7 +135,7 @@ export const AudioToggles: React.FC = ({ ...props }) => { title={t("home.settings.audio.set_audio_track")} disabled={pluginSettings?.rememberAudioSelections?.locked} > - diff --git a/components/settings/ChromecastSettings.tsx b/components/settings/ChromecastSettings.tsx index 4da06332..6842528e 100644 --- a/components/settings/ChromecastSettings.tsx +++ b/components/settings/ChromecastSettings.tsx @@ -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 { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -9,7 +10,7 @@ export const ChromecastSettings: React.FC = ({ ...props }) => { - updateSettings({ enableH265ForChromecast }) diff --git a/components/settings/GestureControls.tsx b/components/settings/GestureControls.tsx index b9c39ef4..8d30a157 100644 --- a/components/settings/GestureControls.tsx +++ b/components/settings/GestureControls.tsx @@ -2,7 +2,7 @@ import type React from "react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { ViewProps } from "react-native"; -import { Switch } from "react-native"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import DisabledSetting from "@/components/settings/DisabledSetting"; import { useSettings } from "@/utils/atoms/settings"; import { ListGroup } from "../list/ListGroup"; @@ -39,7 +39,7 @@ export const GestureControls: React.FC = ({ ...props }) => { )} disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked} > - @@ -55,7 +55,7 @@ export const GestureControls: React.FC = ({ ...props }) => { )} disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked} > - @@ -71,7 +71,7 @@ export const GestureControls: React.FC = ({ ...props }) => { )} disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked} > - @@ -87,7 +87,7 @@ export const GestureControls: React.FC = ({ ...props }) => { )} disabled={pluginSettings?.hideVolumeSlider?.locked} > - @@ -103,7 +103,7 @@ export const GestureControls: React.FC = ({ ...props }) => { )} disabled={pluginSettings?.hideBrightnessSlider?.locked} > - diff --git a/components/settings/KefinTweaks.tsx b/components/settings/KefinTweaks.tsx index 0021859c..5a12cc69 100644 --- a/components/settings/KefinTweaks.tsx +++ b/components/settings/KefinTweaks.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Switch } from "react-native"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import { useSettings } from "@/utils/atoms/settings"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -17,7 +17,7 @@ export const KefinTweaksSettings = () => { title={t("home.settings.plugins.kefinTweaks.watchlist_enabler")} disabledByAdmin={locked} > - updateSettings({ useKefinTweaks: value })} diff --git a/components/settings/LocalNetworkSettings.tsx b/components/settings/LocalNetworkSettings.tsx index 8bf99dbd..804185c7 100644 --- a/components/settings/LocalNetworkSettings.tsx +++ b/components/settings/LocalNetworkSettings.tsx @@ -2,8 +2,9 @@ import { Ionicons } from "@expo/vector-icons"; import type React from "react"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Switch, TouchableOpacity, View } from "react-native"; +import { TouchableOpacity, View } from "react-native"; import { toast } from "sonner-native"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import { useWifiSSID } from "@/hooks/useWifiSSID"; import { useServerUrl } from "@/providers/ServerUrlProvider"; import { storage } from "@/utils/mmkv"; @@ -147,7 +148,10 @@ export function LocalNetworkSettings(): React.ReactElement | null { title={t("home.settings.network.auto_switch_enabled")} subtitle={t("home.settings.network.auto_switch_description")} > - + diff --git a/components/settings/MpvSubtitleSettings.tsx b/components/settings/MpvSubtitleSettings.tsx index c481a080..e3b2feb8 100644 --- a/components/settings/MpvSubtitleSettings.tsx +++ b/components/settings/MpvSubtitleSettings.tsx @@ -1,7 +1,8 @@ import { Ionicons } from "@expo/vector-icons"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -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 { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; @@ -126,7 +127,7 @@ export const MpvSubtitleSettings: React.FC = ({ ...props }) => { )} - updateSettings({ mpvSubtitleBackgroundEnabled: value }) diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index bd339c54..2bb26e6c 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -3,8 +3,9 @@ import { TFunction } from "i18next"; import type React from "react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Linking, Switch, View } from "react-native"; +import { Linking, View } from "react-native"; import { BITRATES } from "@/components/BitrateSelector"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import { PlatformDropdown } from "@/components/PlatformDropdown"; import DisabledSetting from "@/components/settings/DisabledSetting"; import useRouter from "@/hooks/useAppRouter"; @@ -132,7 +133,7 @@ export const OtherSettings: React.FC = () => { title={t("home.settings.other.safe_area_in_controls")} disabled={pluginSettings?.safeAreaInControlsEnabled?.locked} > - @@ -150,7 +151,7 @@ export const OtherSettings: React.FC = () => { ) } > - @@ -188,7 +189,7 @@ export const OtherSettings: React.FC = () => { title={t("home.settings.other.disable_haptic_feedback")} disabled={pluginSettings?.disableHapticFeedback?.locked} > - diff --git a/components/settings/PlaybackControlsSettings.tsx b/components/settings/PlaybackControlsSettings.tsx index 17ee5367..9fa39414 100644 --- a/components/settings/PlaybackControlsSettings.tsx +++ b/components/settings/PlaybackControlsSettings.tsx @@ -3,8 +3,9 @@ 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 { View } from "react-native"; import { BITRATES } from "@/components/BitrateSelector"; +import { SettingSwitch } from "@/components/common/SettingSwitch"; import { PlatformDropdown } from "@/components/PlatformDropdown"; import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector"; import DisabledSetting from "@/components/settings/DisabledSetting"; @@ -115,7 +116,7 @@ export const PlaybackControlsSettings: React.FC = () => { return ( - + { title={t("home.settings.other.safe_area_in_controls")} disabled={pluginSettings?.safeAreaInControlsEnabled?.locked} > - @@ -205,7 +206,7 @@ export const PlaybackControlsSettings: React.FC = () => { title={t("home.settings.other.disable_haptic_feedback")} disabled={pluginSettings?.disableHapticFeedback?.locked} > - @@ -218,7 +219,7 @@ export const PlaybackControlsSettings: React.FC = () => { title={t("home.settings.other.auto_play_next_episode")} disabled={pluginSettings?.autoPlayNextEpisode?.locked} > - diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 82a97d50..e61586cf 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -125,7 +125,7 @@ export const StorageSettings = () => { {!Platform.isTV && ( - + = ({ ...props }) => { return ( @@ -152,7 +153,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { title={t("home.settings.subtitles.set_subtitle_track")} disabled={pluginSettings?.rememberSubtitleSelections?.locked} > -