mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-19 02:34:17 +01:00
Compare commits
6 Commits
fix/settin
...
fix/networ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4813fdab81 | ||
|
|
fd504cef14 | ||
|
|
e0017b8092 | ||
|
|
4c5a3dc658 | ||
|
|
c4b6390a5e | ||
|
|
64b9224b7a |
@@ -854,13 +854,6 @@ export default function SettingsTV() {
|
|||||||
updateSettings({ mergeNextUpAndContinueWatching: value })
|
updateSettings({ mergeNextUpAndContinueWatching: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TVSettingsToggle
|
|
||||||
label={t("home.settings.appearance.use_episode_images_next_up")}
|
|
||||||
value={settings.useEpisodeImagesForNextUp}
|
|
||||||
onToggle={(value) =>
|
|
||||||
updateSettings({ useEpisodeImagesForNextUp: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TVSettingsToggle
|
<TVSettingsToggle
|
||||||
label={t("home.settings.appearance.show_home_backdrop")}
|
label={t("home.settings.appearance.show_home_backdrop")}
|
||||||
value={settings.showHomeBackdrop}
|
value={settings.showHomeBackdrop}
|
||||||
|
|||||||
@@ -2,9 +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 { ScrollView, View } from "react-native";
|
import { ScrollView, Switch, 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";
|
||||||
@@ -51,12 +50,12 @@ export default function AppearanceHideLibrariesPage() {
|
|||||||
>
|
>
|
||||||
<DisabledSetting
|
<DisabledSetting
|
||||||
disabled={pluginSettings?.hiddenLibraries?.locked === true}
|
disabled={pluginSettings?.hiddenLibraries?.locked === true}
|
||||||
className='px-4 pt-4'
|
className='px-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={() => {}}>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ 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 { View } from "react-native";
|
import { Switch, 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";
|
||||||
@@ -47,7 +46,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={() => {}}>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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 className='mt-4'>
|
<ListGroup>
|
||||||
<ListItem
|
<ListItem
|
||||||
textColor='red'
|
textColor='red'
|
||||||
onPress={onDeleteDownloadedSongsClicked}
|
onPress={onDeleteDownloadedSongsClicked}
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ 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>
|
<View className='mb-4'>
|
||||||
<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 pt-4'>
|
<DisabledSetting disabled={disabled} className='px-4'>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<DisabledSetting
|
<DisabledSetting
|
||||||
disabled={
|
disabled={
|
||||||
@@ -90,7 +90,7 @@ export default function MarlinSearchPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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 pt-4'>
|
<View className='px-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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={hideWatchlistsTab}
|
value={hideWatchlistsTab}
|
||||||
onValueChange={setHideWatchlistsTab}
|
onValueChange={setHideWatchlistsTab}
|
||||||
disabled={!isStreamystatsEnabled}
|
disabled={!isStreamystatsEnabled}
|
||||||
|
|||||||
274
app/_layout.tsx
274
app/_layout.tsx
@@ -27,6 +27,7 @@ import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
|
|||||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||||
import { ServerUrlProvider } from "@/providers/ServerUrlProvider";
|
import { ServerUrlProvider } from "@/providers/ServerUrlProvider";
|
||||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||||
|
import { WifiSsidProvider } from "@/providers/WifiSsidProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import {
|
import {
|
||||||
BACKGROUND_FETCH_TASK,
|
BACKGROUND_FETCH_TASK,
|
||||||
@@ -425,143 +426,146 @@ function Layout() {
|
|||||||
>
|
>
|
||||||
<JellyfinProvider>
|
<JellyfinProvider>
|
||||||
<InactivityProvider>
|
<InactivityProvider>
|
||||||
<ServerUrlProvider>
|
<WifiSsidProvider>
|
||||||
<NetworkStatusProvider>
|
<ServerUrlProvider>
|
||||||
<PlaySettingsProvider>
|
<NetworkStatusProvider>
|
||||||
<LogProvider>
|
<PlaySettingsProvider>
|
||||||
<WebSocketProvider>
|
<LogProvider>
|
||||||
<DownloadProvider>
|
<WebSocketProvider>
|
||||||
<MusicPlayerProvider>
|
<DownloadProvider>
|
||||||
<GlobalModalProvider>
|
<MusicPlayerProvider>
|
||||||
<BottomSheetModalProvider>
|
<GlobalModalProvider>
|
||||||
<IntroSheetProvider>
|
<BottomSheetModalProvider>
|
||||||
<ThemeProvider value={DarkTheme}>
|
<IntroSheetProvider>
|
||||||
<SystemBars style='light' hidden={false} />
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack initialRouteName='(auth)/(tabs)'>
|
<SystemBars style='light' hidden={false} />
|
||||||
<Stack.Screen
|
<Stack initialRouteName='(auth)/(tabs)'>
|
||||||
name='(auth)/(tabs)'
|
<Stack.Screen
|
||||||
options={{
|
name='(auth)/(tabs)'
|
||||||
headerShown: false,
|
options={{
|
||||||
title: "",
|
headerShown: false,
|
||||||
header: () => null,
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/player'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/now-playing'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "modal",
|
||||||
|
gestureEnabled: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='login'
|
||||||
|
options={{
|
||||||
|
headerShown: true,
|
||||||
|
title: "",
|
||||||
|
headerTransparent:
|
||||||
|
Platform.OS === "ios",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name='+not-found' />
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-option-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-subtitle-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-request-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-season-select-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-series-season-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='tv-account-action-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='tv-account-select-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-user-switch-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Toaster
|
||||||
|
duration={4000}
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderColor: "#363639",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
titleStyle: {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
closeButton
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
{!Platform.isTV && <GlobalModal />}
|
||||||
name='(auth)/player'
|
</ThemeProvider>
|
||||||
options={{
|
</IntroSheetProvider>
|
||||||
headerShown: false,
|
</BottomSheetModalProvider>
|
||||||
title: "",
|
</GlobalModalProvider>
|
||||||
header: () => null,
|
</MusicPlayerProvider>
|
||||||
}}
|
</DownloadProvider>
|
||||||
/>
|
</WebSocketProvider>
|
||||||
<Stack.Screen
|
</LogProvider>
|
||||||
name='(auth)/now-playing'
|
</PlaySettingsProvider>
|
||||||
options={{
|
</NetworkStatusProvider>
|
||||||
headerShown: false,
|
</ServerUrlProvider>
|
||||||
presentation: "modal",
|
</WifiSsidProvider>
|
||||||
gestureEnabled: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='login'
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: "",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name='+not-found' />
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-option-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-subtitle-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-request-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-season-select-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-series-season-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='tv-account-action-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='tv-account-select-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-user-switch-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Toaster
|
|
||||||
duration={4000}
|
|
||||||
toastOptions={{
|
|
||||||
style: {
|
|
||||||
backgroundColor: "#262626",
|
|
||||||
borderColor: "#363639",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
titleStyle: {
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
closeButton
|
|
||||||
/>
|
|
||||||
{!Platform.isTV && <GlobalModal />}
|
|
||||||
</ThemeProvider>
|
|
||||||
</IntroSheetProvider>
|
|
||||||
</BottomSheetModalProvider>
|
|
||||||
</GlobalModalProvider>
|
|
||||||
</MusicPlayerProvider>
|
|
||||||
</DownloadProvider>
|
|
||||||
</WebSocketProvider>
|
|
||||||
</LogProvider>
|
|
||||||
</PlaySettingsProvider>
|
|
||||||
</NetworkStatusProvider>
|
|
||||||
</ServerUrlProvider>
|
|
||||||
</InactivityProvider>
|
</InactivityProvider>
|
||||||
</JellyfinProvider>
|
</JellyfinProvider>
|
||||||
</PersistQueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
import { SectionHeader } from "@/components/common/SectionHeader";
|
import { SectionHeader } from "@/components/common/SectionHeader";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import MoviePoster from "@/components/posters/MoviePoster";
|
import MoviePoster from "@/components/posters/MoviePoster";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { Colors } from "../../constants/Colors";
|
import { Colors } from "../../constants/Colors";
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
@@ -86,7 +85,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
}, [isSuccess, onLoaded]);
|
}, [isSuccess, onLoaded]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { settings } = useSettings();
|
|
||||||
|
|
||||||
// Flatten all pages into a single array (and de-dupe by Id to avoid UI duplicates)
|
// Flatten all pages into a single array (and de-dupe by Id to avoid UI duplicates)
|
||||||
const allItems = useMemo(() => {
|
const allItems = useMemo(() => {
|
||||||
@@ -188,10 +186,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||||
<ContinueWatchingPoster
|
<ContinueWatchingPoster item={item} />
|
||||||
item={item}
|
|
||||||
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{item.Type === "Episode" && orientation === "vertical" && (
|
{item.Type === "Episode" && orientation === "vertical" && (
|
||||||
<SeriesPoster item={item} />
|
<SeriesPoster item={item} />
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
|
|||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||||
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { scaleSize } from "@/utils/scaleSize";
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
||||||
@@ -166,7 +165,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { settings } = useSettings();
|
|
||||||
|
|
||||||
const allItems = useMemo(() => {
|
const allItems = useMemo(() => {
|
||||||
const items = data?.pages.flat() ?? [];
|
const items = data?.pages.flat() ?? [];
|
||||||
@@ -233,7 +231,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
hasTVPreferredFocus={isFirstItem}
|
hasTVPreferredFocus={isFirstItem}
|
||||||
onFocus={() => handleItemFocus(item)}
|
onFocus={() => handleItemFocus(item)}
|
||||||
width={itemWidth}
|
width={itemWidth}
|
||||||
preferEpisodeImage={settings?.useEpisodeImagesForNextUp}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -246,7 +243,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
showItemActions,
|
showItemActions,
|
||||||
handleItemFocus,
|
handleItemFocus,
|
||||||
ITEM_GAP,
|
ITEM_GAP,
|
||||||
settings?.useEpisodeImagesForNextUp,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { ScrollView, View, type ViewProps } from "react-native";
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import MoviePoster from "@/components/posters/MoviePoster";
|
import MoviePoster from "@/components/posters/MoviePoster";
|
||||||
import { useInView } from "@/hooks/useInView";
|
import { useInView } from "@/hooks/useInView";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
@@ -51,7 +50,6 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { settings } = useSettings();
|
|
||||||
|
|
||||||
// Show skeleton if loading OR if lazy loading is enabled and not in view yet
|
// Show skeleton if loading OR if lazy loading is enabled and not in view yet
|
||||||
const shouldShowSkeleton = isLoading || (enableLazyLoading && !isInView);
|
const shouldShowSkeleton = isLoading || (enableLazyLoading && !isInView);
|
||||||
@@ -110,10 +108,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||||
<ContinueWatchingPoster
|
<ContinueWatchingPoster item={item} />
|
||||||
item={item}
|
|
||||||
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{item.Type === "Episode" && orientation === "vertical" && (
|
{item.Type === "Episode" && orientation === "vertical" && (
|
||||||
<SeriesPoster item={item} />
|
<SeriesPoster item={item} />
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
{title ? (
|
<Text className='ml-4 mb-1 uppercase text-[#8E8D91] text-xs'>
|
||||||
<Text className='ml-4 mb-1 uppercase text-[#8E8D91] text-xs'>
|
{title}
|
||||||
{title}
|
</Text>
|
||||||
</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 { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
|
import { 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,17 +34,12 @@ 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 ${rowSizing} pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
||||||
{...(viewProps as any)}
|
{...(viewProps as any)}
|
||||||
>
|
>
|
||||||
<ListItemContent
|
<ListItemContent
|
||||||
@@ -63,7 +58,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 ${rowSizing} pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
||||||
{...viewProps}
|
{...viewProps}
|
||||||
>
|
>
|
||||||
<ListItemContent
|
<ListItemContent
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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 } from "react-native";
|
import { Linking, Switch } 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";
|
||||||
@@ -28,7 +27,6 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
<ListGroup title={t("home.settings.appearance.title")} className=''>
|
<ListGroup title={t("home.settings.appearance.title")} className=''>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.show_custom_menu_links")}
|
title={t("home.settings.other.show_custom_menu_links")}
|
||||||
subtitle={t("home.settings.other.show_custom_menu_links_hint")}
|
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
Linking.openURL(
|
Linking.openURL(
|
||||||
@@ -36,7 +34,7 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.showCustomMenuLinks}
|
value={settings.showCustomMenuLinks}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -46,51 +44,31 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.appearance.merge_next_up_continue_watching")}
|
title={t("home.settings.appearance.merge_next_up_continue_watching")}
|
||||||
subtitle={t(
|
|
||||||
"home.settings.appearance.merge_next_up_continue_watching_hint",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.mergeNextUpAndContinueWatching}
|
value={settings.mergeNextUpAndContinueWatching}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ mergeNextUpAndContinueWatching: value })
|
updateSettings({ mergeNextUpAndContinueWatching: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.appearance.use_episode_images_next_up")}
|
|
||||||
subtitle={t(
|
|
||||||
"home.settings.appearance.use_episode_images_next_up_hint",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SettingSwitch
|
|
||||||
value={settings.useEpisodeImagesForNextUp}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateSettings({ useEpisodeImagesForNextUp: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.appearance.hide_remote_session_button")}
|
|
||||||
subtitle={t(
|
|
||||||
"home.settings.appearance.hide_remote_session_button_hint",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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")
|
||||||
}
|
}
|
||||||
title={t("home.settings.other.hide_libraries")}
|
title={t("home.settings.other.hide_libraries")}
|
||||||
subtitle={t("home.settings.other.select_libraries_you_want_to_hide")}
|
|
||||||
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 { SettingSwitch } from "@/components/common/SettingSwitch";
|
import { Switch } from "react-native-gesture-handler";
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.rememberAudioSelections}
|
value={settings.rememberAudioSelections}
|
||||||
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { View } from "react-native";
|
import { Switch, 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";
|
||||||
@@ -10,7 +9,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"}>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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 { SettingSwitch } from "@/components/common/SettingSwitch";
|
import { Switch } from "react-native";
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.hideBrightnessSlider}
|
value={settings.hideBrightnessSlider}
|
||||||
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||||
onValueChange={(hideBrightnessSlider) =>
|
onValueChange={(hideBrightnessSlider) =>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Text, View } from "react-native";
|
import { Switch, 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 = () => {
|
||||||
@@ -21,7 +20,7 @@ export const KefinTweaksSettings = () => {
|
|||||||
{isEnabled ? t("Watchlist On") : t("Watchlist Off")}
|
{isEnabled ? t("Watchlist On") : t("Watchlist Off")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<SettingSwitch
|
<Switch
|
||||||
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,10 +2,10 @@ 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 { TouchableOpacity, View } from "react-native";
|
import { Switch, 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 { openLocationSettings } from "@/modules/wifi-ssid";
|
||||||
import { useServerUrl } from "@/providers/ServerUrlProvider";
|
import { useServerUrl } from "@/providers/ServerUrlProvider";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import {
|
import {
|
||||||
@@ -27,16 +27,26 @@ const DEFAULT_CONFIG: LocalNetworkConfig = {
|
|||||||
|
|
||||||
interface StatusDisplayProps {
|
interface StatusDisplayProps {
|
||||||
currentSSID: string | null;
|
currentSSID: string | null;
|
||||||
|
connectedToWifi: boolean;
|
||||||
isUsingLocalUrl: boolean;
|
isUsingLocalUrl: boolean;
|
||||||
|
locationBlocked: boolean;
|
||||||
|
onOpenLocationSettings: () => void;
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusDisplay({
|
function StatusDisplay({
|
||||||
currentSSID,
|
currentSSID,
|
||||||
|
connectedToWifi,
|
||||||
isUsingLocalUrl,
|
isUsingLocalUrl,
|
||||||
|
locationBlocked,
|
||||||
|
onOpenLocationSettings,
|
||||||
t,
|
t,
|
||||||
}: StatusDisplayProps): React.ReactElement {
|
}: StatusDisplayProps): React.ReactElement {
|
||||||
const wifiStatus = currentSSID ?? t("home.settings.network.not_connected");
|
const wifiStatus = currentSSID
|
||||||
|
? currentSSID
|
||||||
|
: connectedToWifi
|
||||||
|
? t("home.settings.network.ssid_hidden")
|
||||||
|
: t("home.settings.network.not_connected");
|
||||||
const urlType = isUsingLocalUrl
|
const urlType = isUsingLocalUrl
|
||||||
? t("home.settings.network.local")
|
? t("home.settings.network.local")
|
||||||
: t("home.settings.network.remote");
|
: t("home.settings.network.remote");
|
||||||
@@ -56,6 +66,23 @@ function StatusDisplay({
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className={urlTypeColor}>{urlType}</Text>
|
<Text className={urlTypeColor}>{urlType}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{locationBlocked && (
|
||||||
|
<View className='mt-2 pt-2 border-t border-neutral-800'>
|
||||||
|
<Text className='text-xs text-amber-400'>
|
||||||
|
{t("home.settings.network.location_off_description")}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onOpenLocationSettings}
|
||||||
|
className='mt-2 self-start'
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
<Text className='text-xs text-blue-400 font-semibold'>
|
||||||
|
{t("home.settings.network.open_location_settings")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,7 +90,17 @@ function StatusDisplay({
|
|||||||
export function LocalNetworkSettings(): React.ReactElement | null {
|
export function LocalNetworkSettings(): React.ReactElement | null {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { permissionStatus, requestPermission } = useWifiSSID();
|
const { permissionStatus, requestPermission } = useWifiSSID();
|
||||||
const { isUsingLocalUrl, currentSSID, refreshUrlState } = useServerUrl();
|
const { isUsingLocalUrl, currentSSID, connectedToWifi, refreshUrlState } =
|
||||||
|
useServerUrl();
|
||||||
|
|
||||||
|
// Connected to Wi-Fi and have permission, but the OS won't reveal the SSID —
|
||||||
|
// on Android this means device Location services are off.
|
||||||
|
const locationBlocked =
|
||||||
|
permissionStatus === "granted" && connectedToWifi && !currentSSID;
|
||||||
|
|
||||||
|
const handleOpenLocationSettings = useCallback(() => {
|
||||||
|
openLocationSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const remoteUrl = storage.getString("serverUrl");
|
const remoteUrl = storage.getString("serverUrl");
|
||||||
const [config, setConfig] = useState<LocalNetworkConfig>(DEFAULT_CONFIG);
|
const [config, setConfig] = useState<LocalNetworkConfig>(DEFAULT_CONFIG);
|
||||||
@@ -148,10 +185,7 @@ 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")}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch value={config.enabled} onValueChange={handleToggleEnabled} />
|
||||||
value={config.enabled}
|
|
||||||
onValueChange={handleToggleEnabled}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
@@ -199,18 +233,23 @@ export function LocalNetworkSettings(): React.ReactElement | null {
|
|||||||
)}
|
)}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
<View className='py-2'>
|
{!locationBlocked && (
|
||||||
<Button
|
<View className='py-2'>
|
||||||
onPress={handleAddCurrentNetwork}
|
<Button
|
||||||
disabled={!currentSSID || permissionStatus !== "granted"}
|
onPress={handleAddCurrentNetwork}
|
||||||
>
|
disabled={!currentSSID || permissionStatus !== "granted"}
|
||||||
{addNetworkButtonText}
|
>
|
||||||
</Button>
|
{addNetworkButtonText}
|
||||||
</View>
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatusDisplay
|
<StatusDisplay
|
||||||
currentSSID={currentSSID}
|
currentSSID={currentSSID}
|
||||||
|
connectedToWifi={connectedToWifi}
|
||||||
isUsingLocalUrl={isUsingLocalUrl}
|
isUsingLocalUrl={isUsingLocalUrl}
|
||||||
|
locationBlocked={locationBlocked}
|
||||||
|
onOpenLocationSettings={handleOpenLocationSettings}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Platform, View, type ViewProps } from "react-native";
|
import { Platform, Switch, 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";
|
||||||
@@ -123,7 +122,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ListItem title='Opaque Background'>
|
<ListItem title='Opaque Background'>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ mpvSubtitleBackgroundEnabled: value })
|
updateSettings({ mpvSubtitleBackgroundEnabled: value })
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ 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, View } from "react-native";
|
import { Linking, Switch, 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";
|
||||||
@@ -133,7 +132,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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.safeAreaInControlsEnabled}
|
value={settings.safeAreaInControlsEnabled}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -151,7 +150,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.showCustomMenuLinks}
|
value={settings.showCustomMenuLinks}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -189,7 +188,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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.disableHapticFeedback}
|
value={settings.disableHapticFeedback}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
onValueChange={(disableHapticFeedback) =>
|
onValueChange={(disableHapticFeedback) =>
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ 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 { View } from "react-native";
|
import { Switch, 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";
|
||||||
@@ -116,7 +115,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={disabled}>
|
<DisabledSetting disabled={disabled}>
|
||||||
<ListGroup title={t("home.settings.other.other_title")} className='mb-4'>
|
<ListGroup title={t("home.settings.other.other_title")} className=''>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.video_orientation")}
|
title={t("home.settings.other.video_orientation")}
|
||||||
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
||||||
@@ -147,7 +146,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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.safeAreaInControlsEnabled}
|
value={settings.safeAreaInControlsEnabled}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -206,7 +205,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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.disableHapticFeedback}
|
value={settings.disableHapticFeedback}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
onValueChange={(disableHapticFeedback) =>
|
onValueChange={(disableHapticFeedback) =>
|
||||||
@@ -219,7 +218,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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.autoPlayNextEpisode}
|
value={settings.autoPlayNextEpisode}
|
||||||
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||||
onValueChange={(autoPlayNextEpisode) =>
|
onValueChange={(autoPlayNextEpisode) =>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const StorageSettings = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<ListGroup className={Platform.OS === "android" ? "mt-4" : undefined}>
|
<ListGroup>
|
||||||
<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,7 +98,6 @@ 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'>
|
||||||
@@ -153,7 +152,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}
|
||||||
>
|
>
|
||||||
<SettingSwitch
|
<Switch
|
||||||
value={settings.rememberSubtitleSelections}
|
value={settings.rememberSubtitleSelections}
|
||||||
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -72,9 +72,6 @@ export interface TVPosterCardProps {
|
|||||||
|
|
||||||
/** Custom image URL getter - if not provided, uses smart URL logic */
|
/** Custom image URL getter - if not provided, uses smart URL logic */
|
||||||
imageUrlGetter?: (item: BaseItemDto) => string | undefined;
|
imageUrlGetter?: (item: BaseItemDto) => string | undefined;
|
||||||
|
|
||||||
/** For horizontal episodes, prefer the episode's own image over the series thumb */
|
|
||||||
preferEpisodeImage?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,7 +108,6 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
glowColor = "white",
|
glowColor = "white",
|
||||||
scaleAmount = 1.05,
|
scaleAmount = 1.05,
|
||||||
imageUrlGetter,
|
imageUrlGetter,
|
||||||
preferEpisodeImage = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const posterSizes = useScaledTVPosterSizes();
|
const posterSizes = useScaledTVPosterSizes();
|
||||||
@@ -143,10 +139,6 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
if (orientation === "horizontal") {
|
if (orientation === "horizontal") {
|
||||||
// Episode: prefer series thumb image for consistent look (like hero section)
|
// Episode: prefer series thumb image for consistent look (like hero section)
|
||||||
if (item.Type === "Episode") {
|
if (item.Type === "Episode") {
|
||||||
// Opt-in: use the episode's own image instead of the series thumb.
|
|
||||||
if (preferEpisodeImage && item.ImageTags?.Primary) {
|
|
||||||
return `${api.basePath}/Items/${item.Id}/Images/Primary?fillHeight=600&quality=80&tag=${item.ImageTags.Primary}`;
|
|
||||||
}
|
|
||||||
// First try parent/series thumb (horizontal series artwork).
|
// First try parent/series thumb (horizontal series artwork).
|
||||||
// Matched pair: ParentThumbItemId owns the Thumb tag, not ParentBackdropItemId.
|
// Matched pair: ParentThumbItemId owns the Thumb tag, not ParentBackdropItemId.
|
||||||
if (item.ParentThumbItemId && item.ParentThumbImageTag) {
|
if (item.ParentThumbItemId && item.ParentThumbImageTag) {
|
||||||
@@ -182,7 +174,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
item,
|
item,
|
||||||
width: width * 2, // 2x for quality on large screens
|
width: width * 2, // 2x for quality on large screens
|
||||||
});
|
});
|
||||||
}, [api, item, orientation, width, imageUrlGetter, preferEpisodeImage]);
|
}, [api, item, orientation, width, imageUrlGetter]);
|
||||||
|
|
||||||
// Progress calculation
|
// Progress calculation
|
||||||
const progress = useMemo(() => {
|
const progress = useMemo(() => {
|
||||||
|
|||||||
@@ -1,125 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
export {
|
||||||
import { Platform } from "react-native";
|
type PermissionStatus,
|
||||||
import { getSSID } from "@/modules/wifi-ssid";
|
type UseWifiSSIDReturn,
|
||||||
|
useWifiSsid as useWifiSSID,
|
||||||
export type PermissionStatus =
|
} from "@/providers/WifiSsidProvider";
|
||||||
| "granted"
|
|
||||||
| "denied"
|
|
||||||
| "undetermined"
|
|
||||||
| "unavailable";
|
|
||||||
|
|
||||||
export interface UseWifiSSIDReturn {
|
|
||||||
ssid: string | null;
|
|
||||||
permissionStatus: PermissionStatus;
|
|
||||||
requestPermission: () => Promise<boolean>;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// WiFi SSID is not available on tvOS
|
|
||||||
if (Platform.isTV) {
|
|
||||||
// Export a stub hook for tvOS
|
|
||||||
module.exports = {
|
|
||||||
useWifiSSID: (): UseWifiSSIDReturn => ({
|
|
||||||
ssid: null,
|
|
||||||
permissionStatus: "unavailable" as PermissionStatus,
|
|
||||||
requestPermission: async () => false,
|
|
||||||
isLoading: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only import Location on non-TV platforms
|
|
||||||
const Location = Platform.isTV ? null : require("expo-location");
|
|
||||||
|
|
||||||
function mapLocationStatus(status: number | undefined): PermissionStatus {
|
|
||||||
if (!Location) return "unavailable";
|
|
||||||
switch (status) {
|
|
||||||
case Location.PermissionStatus?.GRANTED:
|
|
||||||
return "granted";
|
|
||||||
case Location.PermissionStatus?.DENIED:
|
|
||||||
return "denied";
|
|
||||||
default:
|
|
||||||
return "undetermined";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWifiSSID(): UseWifiSSIDReturn {
|
|
||||||
const [ssid, setSSID] = useState<string | null>(null);
|
|
||||||
const [permissionStatus, setPermissionStatus] = useState<PermissionStatus>(
|
|
||||||
Platform.isTV ? "unavailable" : "undetermined",
|
|
||||||
);
|
|
||||||
const [isLoading, setIsLoading] = useState(!Platform.isTV);
|
|
||||||
|
|
||||||
const fetchSSID = useCallback(async () => {
|
|
||||||
if (Platform.isTV) return;
|
|
||||||
const result = await getSSID();
|
|
||||||
setSSID(result);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const requestPermission = useCallback(async (): Promise<boolean> => {
|
|
||||||
if (Platform.isTV || !Location) {
|
|
||||||
setPermissionStatus("unavailable");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
||||||
const newStatus = mapLocationStatus(status);
|
|
||||||
setPermissionStatus(newStatus);
|
|
||||||
|
|
||||||
if (newStatus === "granted") {
|
|
||||||
await fetchSSID();
|
|
||||||
}
|
|
||||||
|
|
||||||
return newStatus === "granted";
|
|
||||||
} catch {
|
|
||||||
setPermissionStatus("unavailable");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [fetchSSID]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (Platform.isTV || !Location) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initialize() {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const { status } = await Location.getForegroundPermissionsAsync();
|
|
||||||
const mappedStatus = mapLocationStatus(status);
|
|
||||||
setPermissionStatus(mappedStatus);
|
|
||||||
|
|
||||||
if (mappedStatus === "granted") {
|
|
||||||
await fetchSSID();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setPermissionStatus("unavailable");
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize();
|
|
||||||
}, [fetchSSID]);
|
|
||||||
|
|
||||||
// Refresh SSID when permission status changes to granted
|
|
||||||
useEffect(() => {
|
|
||||||
if (Platform.isTV) return;
|
|
||||||
|
|
||||||
if (permissionStatus === "granted") {
|
|
||||||
fetchSSID();
|
|
||||||
|
|
||||||
// Also set up an interval to periodically check SSID
|
|
||||||
const interval = setInterval(fetchSSID, 10000); // Check every 10 seconds
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [permissionStatus, fetchSSID]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ssid,
|
|
||||||
permissionStatus,
|
|
||||||
requestPermission,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
19
modules/wifi-ssid/android/build.gradle
Normal file
19
modules/wifi-ssid/android/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apply plugin: 'expo-module-gradle-plugin'
|
||||||
|
|
||||||
|
group = 'expo.modules.wifissid'
|
||||||
|
version = '1.0.0'
|
||||||
|
|
||||||
|
expoModule {
|
||||||
|
canBePublished false
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "expo.modules.wifissid"
|
||||||
|
defaultConfig {
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
}
|
||||||
9
modules/wifi-ssid/android/src/main/AndroidManifest.xml
Normal file
9
modules/wifi-ssid/android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Reading the connected Wi-Fi SSID requires ACCESS_WIFI_STATE, and (since
|
||||||
|
Android 8) ACCESS_FINE_LOCATION granted at runtime. Location is requested
|
||||||
|
at runtime by the app via expo-location; the manifest declares the
|
||||||
|
Wi-Fi/network permissions this module needs directly. -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package expo.modules.wifissid
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.wifi.WifiInfo
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import expo.modules.kotlin.modules.Module
|
||||||
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
|
||||||
|
class WifiSsidModule : Module() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WifiSsid"
|
||||||
|
private const val UNKNOWN_SSID = "<unknown ssid>"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val context
|
||||||
|
get() = requireNotNull(appContext.reactContext)
|
||||||
|
|
||||||
|
override fun definition() = ModuleDefinition {
|
||||||
|
Name("WifiSsid")
|
||||||
|
|
||||||
|
AsyncFunction("getSSID") {
|
||||||
|
val (ssid, connectedToWifi) = readWifi()
|
||||||
|
mapOf(
|
||||||
|
"ssid" to ssid,
|
||||||
|
"connectedToWifi" to connectedToWifi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getSSIDSync") {
|
||||||
|
val (ssid, connectedToWifi) = readWifi()
|
||||||
|
mapOf(
|
||||||
|
"ssid" to ssid,
|
||||||
|
"connectedToWifi" to connectedToWifi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("openLocationSettings") {
|
||||||
|
openLocationSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current Wi-Fi SSID (or null when it can't be read) and whether
|
||||||
|
* the device is connected to a Wi-Fi network. When the device is connected but
|
||||||
|
* the SSID is null, the OS is withholding the network name — on Android this
|
||||||
|
* typically means device Location services are off (the SSID is treated as
|
||||||
|
* location-sensitive).
|
||||||
|
*/
|
||||||
|
private fun readWifi(): Pair<String?, Boolean> {
|
||||||
|
val androidContext = context.applicationContext
|
||||||
|
var connectedToWifi = false
|
||||||
|
var ssid: String? = null
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
try {
|
||||||
|
val cm = androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
val caps = cm?.let { manager ->
|
||||||
|
manager.activeNetwork?.let { net -> manager.getNetworkCapabilities(net) }
|
||||||
|
}
|
||||||
|
connectedToWifi = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||||
|
if (hasFineLocationPermission()) {
|
||||||
|
ssid = (caps?.transportInfo as? WifiInfo)?.ssid?.sanitize()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error reading Wi-Fi via ConnectivityManager", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pre-Q (API 26–28): NetworkCapabilities/transportInfo aren't available, so
|
||||||
|
// detect a Wi-Fi connection via the legacy active-network API. Without this,
|
||||||
|
// connectedToWifi stays false on Android 8/9 even when connected, so the
|
||||||
|
// "connected but SSID hidden" state never surfaces there.
|
||||||
|
try {
|
||||||
|
val cm = androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
connectedToWifi = cm?.activeNetworkInfo?.type == ConnectivityManager.TYPE_WIFI
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error reading Wi-Fi via ConnectivityManager (legacy)", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ssid == null && hasFineLocationPermission()) {
|
||||||
|
try {
|
||||||
|
val wm = androidContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
ssid = wm?.connectionInfo?.ssid?.sanitize()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error reading SSID via WifiManager", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(ssid, connectedToWifi)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openLocationSettings() {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.applicationContext.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open Location settings", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasFineLocationPermission(): Boolean =
|
||||||
|
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
private fun String.sanitize(): String? {
|
||||||
|
val cleaned = trim('"')
|
||||||
|
return if (cleaned.isBlank() || cleaned == UNKNOWN_SSID) null else cleaned
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "wifi-ssid",
|
"name": "wifi-ssid",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"platforms": ["ios"],
|
"platforms": ["ios", "android"],
|
||||||
"ios": {
|
"ios": {
|
||||||
"modules": ["WifiSsidModule"]
|
"modules": ["WifiSsidModule"]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"modules": ["expo.modules.wifissid.WifiSsidModule"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,92 @@
|
|||||||
import { Platform, requireNativeModule } from "expo-modules-core";
|
import { Platform, requireNativeModule } from "expo-modules-core";
|
||||||
|
import { Linking } from "react-native";
|
||||||
|
|
||||||
// Only load the native module on iOS
|
export interface WifiSsidResult {
|
||||||
const WifiSsidModule =
|
ssid: string | null;
|
||||||
Platform.OS === "ios" ? requireNativeModule("WifiSsid") : null;
|
/**
|
||||||
|
* Whether the device is connected to a Wi-Fi network. When true but `ssid` is
|
||||||
|
* null, the OS is withholding the network name — on Android this happens when
|
||||||
|
* device Location services are off (the SSID is location-sensitive).
|
||||||
|
*/
|
||||||
|
connectedToWifi: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the native module on iOS and Android (not TV/web). Wrapped so a
|
||||||
|
// missing/unlinked module degrades to null instead of crashing the bundle.
|
||||||
|
const WifiSsidModule = (() => {
|
||||||
|
if (Platform.OS !== "ios" && Platform.OS !== "android") return null;
|
||||||
|
try {
|
||||||
|
return requireNativeModule("WifiSsid");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current WiFi SSID on iOS.
|
* Get the current WiFi SSID and whether the device is on a Wi-Fi network.
|
||||||
* Returns null on Android or if not connected to WiFi.
|
|
||||||
*
|
*
|
||||||
* Requires:
|
* iOS requirements:
|
||||||
* - Location permission granted
|
* - Location permission granted
|
||||||
* - com.apple.developer.networking.wifi-info entitlement
|
* - com.apple.developer.networking.wifi-info entitlement
|
||||||
* - Access WiFi Information capability enabled in Apple Developer Portal
|
*
|
||||||
|
* Android requirements:
|
||||||
|
* - ACCESS_FINE_LOCATION granted at runtime, and device Location services on
|
||||||
*/
|
*/
|
||||||
export async function getSSID(): Promise<string | null> {
|
export async function getSSID(): Promise<WifiSsidResult> {
|
||||||
if (!WifiSsidModule) {
|
if (!WifiSsidModule) {
|
||||||
return null;
|
return { ssid: null, connectedToWifi: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ssid = await WifiSsidModule.getSSID();
|
return normalize(await WifiSsidModule.getSSID());
|
||||||
return ssid ?? null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[WifiSsid] Error getting SSID:", error);
|
console.error("[WifiSsid] Error getting SSID:", error);
|
||||||
return null;
|
return { ssid: null, connectedToWifi: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Synchronous version (uses the older CNCopyCurrentNetworkInfo API on iOS). */
|
||||||
|
export function getSSIDSync(): WifiSsidResult {
|
||||||
|
if (!WifiSsidModule) {
|
||||||
|
return { ssid: null, connectedToWifi: false };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return normalize(WifiSsidModule.getSSIDSync());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WifiSsid] Error getting SSID (sync):", error);
|
||||||
|
return { ssid: null, connectedToWifi: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous version - uses older CNCopyCurrentNetworkInfo API
|
* Open the system Location settings so the user can enable Location services
|
||||||
|
* (required on Android for the OS to reveal the Wi-Fi SSID). Falls back to the
|
||||||
|
* app's settings page where there is no dedicated native action (e.g. iOS).
|
||||||
*/
|
*/
|
||||||
export function getSSIDSync(): string | null {
|
export function openLocationSettings(): void {
|
||||||
if (!WifiSsidModule) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return WifiSsidModule.getSSIDSync() ?? null;
|
if (WifiSsidModule?.openLocationSettings) {
|
||||||
|
WifiSsidModule.openLocationSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[WifiSsid] Error getting SSID (sync):", error);
|
console.error("[WifiSsid] Error opening location settings:", error);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
Linking.openSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both native modules return { ssid, connectedToWifi }. The string branch is a
|
||||||
|
// defensive fallback in case a build returns the older plain-string shape.
|
||||||
|
function normalize(result: unknown): WifiSsidResult {
|
||||||
|
if (typeof result === "string") {
|
||||||
|
const ssid = result || null;
|
||||||
|
return { ssid, connectedToWifi: ssid !== null };
|
||||||
|
}
|
||||||
|
if (result && typeof result === "object") {
|
||||||
|
const obj = result as { ssid?: string | null; connectedToWifi?: boolean };
|
||||||
|
return {
|
||||||
|
ssid: obj.ssid ?? null,
|
||||||
|
connectedToWifi: Boolean(obj.connectedToWifi),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ssid: null, connectedToWifi: false };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,113 @@
|
|||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
|
import Network
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
import SystemConfiguration.CaptiveNetwork
|
import SystemConfiguration.CaptiveNetwork
|
||||||
#endif
|
#endif
|
||||||
|
import UIKit
|
||||||
|
|
||||||
public class WifiSsidModule: Module {
|
public class WifiSsidModule: Module {
|
||||||
|
#if !os(tvOS)
|
||||||
|
// NWPathMonitor reports whether the active path uses Wi-Fi WITHOUT requiring
|
||||||
|
// the wifi-info entitlement, so we can tell "on Wi-Fi but can't read the SSID"
|
||||||
|
// apart from "not connected to Wi-Fi" even when fetchCurrent returns nil.
|
||||||
|
private let pathMonitor = NWPathMonitor()
|
||||||
|
private let monitorQueue = DispatchQueue(label: "expo.modules.wifissid.pathmonitor")
|
||||||
|
private let lock = NSLock()
|
||||||
|
private var cachedOnWifi = false
|
||||||
|
private var pathMonitorStarted = false
|
||||||
|
|
||||||
|
private var isOnWifi: Bool {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return cachedOnWifi
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startPathMonitorIfNeeded() {
|
||||||
|
lock.lock()
|
||||||
|
if pathMonitorStarted {
|
||||||
|
lock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pathMonitorStarted = true
|
||||||
|
lock.unlock()
|
||||||
|
|
||||||
|
pathMonitor.pathUpdateHandler = { [weak self] path in
|
||||||
|
guard let self else { return }
|
||||||
|
let onWifi = path.status == .satisfied && path.usesInterfaceType(.wifi)
|
||||||
|
self.lock.lock()
|
||||||
|
self.cachedOnWifi = onWifi
|
||||||
|
self.lock.unlock()
|
||||||
|
}
|
||||||
|
pathMonitor.start(queue: monitorQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
pathMonitor.cancel()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
public func definition() -> ModuleDefinition {
|
public func definition() -> ModuleDefinition {
|
||||||
Name("WifiSsid")
|
Name("WifiSsid")
|
||||||
|
|
||||||
// Get current WiFi SSID using NEHotspotNetwork (iOS 14+)
|
AsyncFunction("getSSID") { () -> [String: Any] in
|
||||||
// Not available on tvOS
|
|
||||||
AsyncFunction("getSSID") { () -> String? in
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
return nil
|
return ["ssid": NSNull(), "connectedToWifi": false]
|
||||||
#else
|
#else
|
||||||
return await withCheckedContinuation { continuation in
|
self.startPathMonitorIfNeeded()
|
||||||
NEHotspotNetwork.fetchCurrent { network in
|
let ssid = await self.resolveSSID()
|
||||||
if let ssid = network?.ssid {
|
var result: [String: Any] = ["connectedToWifi": self.isOnWifi]
|
||||||
print("[WifiSsid] Got SSID via NEHotspotNetwork: \(ssid)")
|
if let ssid = ssid {
|
||||||
continuation.resume(returning: ssid)
|
result["ssid"] = ssid
|
||||||
} else {
|
} else {
|
||||||
// Fallback to CNCopyCurrentNetworkInfo for older iOS
|
result["ssid"] = NSNull()
|
||||||
print("[WifiSsid] NEHotspotNetwork returned nil, trying CNCopyCurrentNetworkInfo")
|
|
||||||
let ssid = self.getSSIDViaCNCopy()
|
|
||||||
continuation.resume(returning: ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronous version using only CNCopyCurrentNetworkInfo
|
Function("getSSIDSync") { () -> [String: Any] in
|
||||||
// Not available on tvOS
|
|
||||||
Function("getSSIDSync") { () -> String? in
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
return nil
|
return ["ssid": NSNull(), "connectedToWifi": false]
|
||||||
#else
|
#else
|
||||||
return self.getSSIDViaCNCopy()
|
self.startPathMonitorIfNeeded()
|
||||||
|
let ssid = self.getSSIDViaCNCopy()
|
||||||
|
var result: [String: Any] = ["connectedToWifi": self.isOnWifi]
|
||||||
|
if let ssid = ssid {
|
||||||
|
result["ssid"] = ssid
|
||||||
|
} else {
|
||||||
|
result["ssid"] = NSNull()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("openLocationSettings") {
|
||||||
|
#if !os(tvOS)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
|
private func resolveSSID() async -> String? {
|
||||||
|
await withCheckedContinuation { continuation in
|
||||||
|
NEHotspotNetwork.fetchCurrent { network in
|
||||||
|
if let ssid = network?.ssid {
|
||||||
|
continuation.resume(returning: ssid)
|
||||||
|
} else {
|
||||||
|
continuation.resume(returning: self.getSSIDViaCNCopy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func getSSIDViaCNCopy() -> String? {
|
private func getSSIDViaCNCopy() -> String? {
|
||||||
guard let interfaces = CNCopySupportedInterfaces() as? [String] else {
|
guard let interfaces = CNCopySupportedInterfaces() as? [String] else {
|
||||||
print("[WifiSsid] CNCopySupportedInterfaces returned nil")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +117,10 @@ public class WifiSsidModule: Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let ssid = networkInfo[kCNNetworkInfoKeySSID as String] as? String {
|
if let ssid = networkInfo[kCNNetworkInfoKeySSID as String] as? String {
|
||||||
print("[WifiSsid] Got SSID via CNCopyCurrentNetworkInfo: \(ssid)")
|
|
||||||
return ssid
|
return ssid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print("[WifiSsid] No SSID found via CNCopyCurrentNetworkInfo")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -18,6 +19,7 @@ interface ServerUrlContextValue {
|
|||||||
effectiveServerUrl: string | null;
|
effectiveServerUrl: string | null;
|
||||||
isUsingLocalUrl: boolean;
|
isUsingLocalUrl: boolean;
|
||||||
currentSSID: string | null;
|
currentSSID: string | null;
|
||||||
|
connectedToWifi: boolean;
|
||||||
refreshUrlState: () => void;
|
refreshUrlState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ interface Props {
|
|||||||
export function ServerUrlProvider({ children }: Props): React.ReactElement {
|
export function ServerUrlProvider({ children }: Props): React.ReactElement {
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const { switchServerUrl } = useJellyfin();
|
const { switchServerUrl } = useJellyfin();
|
||||||
const { ssid, permissionStatus } = useWifiSSID();
|
const { ssid, connectedToWifi, permissionStatus } = useWifiSSID();
|
||||||
|
|
||||||
const [isUsingLocalUrl, setIsUsingLocalUrl] = useState(false);
|
const [isUsingLocalUrl, setIsUsingLocalUrl] = useState(false);
|
||||||
const [effectiveServerUrl, setEffectiveServerUrl] = useState<string | null>(
|
const [effectiveServerUrl, setEffectiveServerUrl] = useState<string | null>(
|
||||||
@@ -101,15 +103,25 @@ export function ServerUrlProvider({ children }: Props): React.ReactElement {
|
|||||||
};
|
};
|
||||||
}, [ssid, permissionStatus, evaluateAndSwitchUrl]);
|
}, [ssid, permissionStatus, evaluateAndSwitchUrl]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
effectiveServerUrl,
|
||||||
|
isUsingLocalUrl,
|
||||||
|
currentSSID: ssid,
|
||||||
|
connectedToWifi,
|
||||||
|
refreshUrlState,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
effectiveServerUrl,
|
||||||
|
isUsingLocalUrl,
|
||||||
|
ssid,
|
||||||
|
connectedToWifi,
|
||||||
|
refreshUrlState,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerUrlContext.Provider
|
<ServerUrlContext.Provider value={value}>
|
||||||
value={{
|
|
||||||
effectiveServerUrl,
|
|
||||||
isUsingLocalUrl,
|
|
||||||
currentSSID: ssid,
|
|
||||||
refreshUrlState,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ServerUrlContext.Provider>
|
</ServerUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
157
providers/WifiSsidProvider.tsx
Normal file
157
providers/WifiSsidProvider.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { getSSID } from "@/modules/wifi-ssid";
|
||||||
|
|
||||||
|
export type PermissionStatus =
|
||||||
|
| "granted"
|
||||||
|
| "denied"
|
||||||
|
| "undetermined"
|
||||||
|
| "unavailable";
|
||||||
|
|
||||||
|
export interface UseWifiSSIDReturn {
|
||||||
|
ssid: string | null;
|
||||||
|
connectedToWifi: boolean;
|
||||||
|
permissionStatus: PermissionStatus;
|
||||||
|
requestPermission: () => Promise<boolean>;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WifiSsidContext = createContext<UseWifiSSIDReturn | null>(null);
|
||||||
|
|
||||||
|
// WiFi SSID is not available on tvOS, and expo-location isn't needed there.
|
||||||
|
const Location = Platform.isTV ? null : require("expo-location");
|
||||||
|
|
||||||
|
const POLL_INTERVAL_MS = 10_000;
|
||||||
|
|
||||||
|
function mapLocationStatus(status: number | undefined): PermissionStatus {
|
||||||
|
if (!Location) return "unavailable";
|
||||||
|
switch (status) {
|
||||||
|
case Location.PermissionStatus?.GRANTED:
|
||||||
|
return "granted";
|
||||||
|
case Location.PermissionStatus?.DENIED:
|
||||||
|
return "denied";
|
||||||
|
default:
|
||||||
|
return "undetermined";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for the current WiFi SSID and the location permission
|
||||||
|
* that gates it. State is shared via context so that granting permission in one
|
||||||
|
* place (e.g. the network settings screen) is reflected everywhere else — most
|
||||||
|
* importantly ServerUrlProvider, which switches between the local and remote
|
||||||
|
* server URLs based on the connected SSID.
|
||||||
|
*
|
||||||
|
* Previously each consumer created its own useWifiSSID instance with independent
|
||||||
|
* state, so the provider never learned that permission had been granted later
|
||||||
|
* in the session and reported "not connected to WiFi" forever.
|
||||||
|
*/
|
||||||
|
export function WifiSsidProvider({ children }: Props): React.ReactElement {
|
||||||
|
const [ssid, setSSID] = useState<string | null>(null);
|
||||||
|
const [connectedToWifi, setConnectedToWifi] = useState(false);
|
||||||
|
const [permissionStatus, setPermissionStatus] = useState<PermissionStatus>(
|
||||||
|
Platform.isTV ? "unavailable" : "undetermined",
|
||||||
|
);
|
||||||
|
const [isLoading, setIsLoading] = useState(!Platform.isTV);
|
||||||
|
|
||||||
|
const fetchSSID = useCallback(async () => {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
const result = await getSSID();
|
||||||
|
setSSID(result.ssid);
|
||||||
|
setConnectedToWifi(result.connectedToWifi);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestPermission = useCallback(async (): Promise<boolean> => {
|
||||||
|
if (Platform.isTV || !Location) {
|
||||||
|
setPermissionStatus("unavailable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||||
|
const newStatus = mapLocationStatus(status);
|
||||||
|
setPermissionStatus(newStatus);
|
||||||
|
return newStatus === "granted";
|
||||||
|
} catch {
|
||||||
|
setPermissionStatus("unavailable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check the location permission once on mount. The poll effect below takes
|
||||||
|
// over fetching the SSID whenever permission becomes "granted".
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.isTV || !Location) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
async function initialize() {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { status } = await Location.getForegroundPermissionsAsync();
|
||||||
|
if (cancelled) return;
|
||||||
|
setPermissionStatus(mapLocationStatus(status));
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setPermissionStatus("unavailable");
|
||||||
|
}
|
||||||
|
if (!cancelled) setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch the SSID immediately, then poll, whenever permission is granted.
|
||||||
|
// Because permissionStatus is shared, this also covers the case where
|
||||||
|
// permission was granted later (from another consumer) than this provider's
|
||||||
|
// mount time — the original root cause of the "not connected to WiFi" bug.
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.isTV || permissionStatus !== "granted") return;
|
||||||
|
|
||||||
|
fetchSSID();
|
||||||
|
const interval = setInterval(fetchSSID, POLL_INTERVAL_MS);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [permissionStatus, fetchSSID]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
ssid,
|
||||||
|
connectedToWifi,
|
||||||
|
permissionStatus,
|
||||||
|
requestPermission,
|
||||||
|
isLoading,
|
||||||
|
}),
|
||||||
|
[ssid, connectedToWifi, permissionStatus, requestPermission, isLoading],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WifiSsidContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</WifiSsidContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWifiSsid(): UseWifiSSIDReturn {
|
||||||
|
const context = useContext(WifiSsidContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useWifiSsid must be used within WifiSsidProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -137,11 +137,7 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue watching & Next up",
|
"merge_next_up_continue_watching": "Merge Continue watching & Next up",
|
||||||
"merge_next_up_continue_watching_hint": "Combine Continue Watching and Next Up into a single home row.",
|
|
||||||
"use_episode_images_next_up": "Use episode images for Next Up & Continue Watching",
|
|
||||||
"use_episode_images_next_up_hint": "Show each episode's own thumbnail in the Next Up and Continue Watching rows instead of the series image.",
|
|
||||||
"hide_remote_session_button": "Hide remote session button",
|
"hide_remote_session_button": "Hide remote session button",
|
||||||
"hide_remote_session_button_hint": "Hide the remote-sessions button from the home header.",
|
|
||||||
"show_home_backdrop": "Dynamic home backdrop",
|
"show_home_backdrop": "Dynamic home backdrop",
|
||||||
"show_hero_carousel": "Hero carousel",
|
"show_hero_carousel": "Hero carousel",
|
||||||
"show_series_poster_on_episode": "Show series poster on episodes",
|
"show_series_poster_on_episode": "Show series poster on episodes",
|
||||||
@@ -178,7 +174,10 @@
|
|||||||
"network_already_added": "Network already added",
|
"network_already_added": "Network already added",
|
||||||
"no_wifi_connected": "Not connected to Wi-Fi",
|
"no_wifi_connected": "Not connected to Wi-Fi",
|
||||||
"permission_denied": "Location permission denied",
|
"permission_denied": "Location permission denied",
|
||||||
"permission_denied_explanation": "Location permission is required to detect the Wi-Fi network for auto-switching. Please enable it in Settings."
|
"permission_denied_explanation": "Location permission is required to detect the Wi-Fi network for auto-switching. Please enable it in Settings.",
|
||||||
|
"ssid_hidden": "SSID hidden",
|
||||||
|
"location_off_description": "Connected to Wi-Fi, but the network name is hidden. Enable device Location services and grant the app location permission to detect it.",
|
||||||
|
"open_location_settings": "Open Location settings"
|
||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"user_info_title": "User info",
|
"user_info_title": "User info",
|
||||||
@@ -300,7 +299,6 @@
|
|||||||
},
|
},
|
||||||
"safe_area_in_controls": "Safe area in controls",
|
"safe_area_in_controls": "Safe area in controls",
|
||||||
"show_custom_menu_links": "Show custom menu links",
|
"show_custom_menu_links": "Show custom menu links",
|
||||||
"show_custom_menu_links_hint": "Show the custom links your server administrator added in the web config.",
|
|
||||||
"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_libraries_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.",
|
||||||
|
|||||||
@@ -274,9 +274,6 @@ export type Settings = {
|
|||||||
hideBrightnessSlider: boolean;
|
hideBrightnessSlider: boolean;
|
||||||
usePopularPlugin: boolean;
|
usePopularPlugin: boolean;
|
||||||
mergeNextUpAndContinueWatching: boolean;
|
mergeNextUpAndContinueWatching: boolean;
|
||||||
// Use the episode's own image (instead of the series thumb) for the
|
|
||||||
// "Next Up" and "Continue Watching" home rows.
|
|
||||||
useEpisodeImagesForNextUp: boolean;
|
|
||||||
// TV-specific settings
|
// TV-specific settings
|
||||||
showHomeBackdrop: boolean;
|
showHomeBackdrop: boolean;
|
||||||
showTVHeroCarousel: boolean;
|
showTVHeroCarousel: boolean;
|
||||||
@@ -385,7 +382,6 @@ export const defaultValues: Settings = {
|
|||||||
hideBrightnessSlider: false,
|
hideBrightnessSlider: false,
|
||||||
usePopularPlugin: true,
|
usePopularPlugin: true,
|
||||||
mergeNextUpAndContinueWatching: false,
|
mergeNextUpAndContinueWatching: false,
|
||||||
useEpisodeImagesForNextUp: false,
|
|
||||||
// TV-specific settings
|
// TV-specific settings
|
||||||
showHomeBackdrop: true,
|
showHomeBackdrop: true,
|
||||||
showTVHeroCarousel: true,
|
showTVHeroCarousel: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user