mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-19 02:34:17 +01:00
Compare commits
4 Commits
fix/settin
...
fix/skip-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
980245f19d | ||
|
|
228e847a4e | ||
|
|
15c66bc714 | ||
|
|
c4209cbf8e |
@@ -854,13 +854,6 @@ export default function SettingsTV() {
|
||||
updateSettings({ mergeNextUpAndContinueWatching: value })
|
||||
}
|
||||
/>
|
||||
<TVSettingsToggle
|
||||
label={t("home.settings.appearance.use_episode_images_next_up")}
|
||||
value={settings.useEpisodeImagesForNextUp}
|
||||
onToggle={(value) =>
|
||||
updateSettings({ useEpisodeImagesForNextUp: value })
|
||||
}
|
||||
/>
|
||||
<TVSettingsToggle
|
||||
label={t("home.settings.appearance.show_home_backdrop")}
|
||||
value={settings.showHomeBackdrop}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { ScrollView, Switch, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
@@ -51,12 +50,12 @@ export default function AppearanceHideLibrariesPage() {
|
||||
>
|
||||
<DisabledSetting
|
||||
disabled={pluginSettings?.hiddenLibraries?.locked === true}
|
||||
className='px-4 pt-4'
|
||||
className='px-4'
|
||||
>
|
||||
<ListGroup title={t("home.settings.other.hide_libraries")}>
|
||||
{data?.map((view) => (
|
||||
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||
onValueChange={(value) => {
|
||||
updateSettings({
|
||||
|
||||
@@ -2,8 +2,7 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Switch, View } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
@@ -47,7 +46,7 @@ export default function HideLibrariesPage() {
|
||||
<ListGroup>
|
||||
{data?.map((view) => (
|
||||
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||
onValueChange={(value) => {
|
||||
updateSettings({
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, ScrollView, View } from "react-native";
|
||||
import { Switch } from "react-native-gesture-handler";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { toast } from "sonner-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
@@ -136,7 +136,7 @@ export default function MusicSettingsPage() {
|
||||
title={t("home.settings.music.prefer_downloaded")}
|
||||
disabled={pluginSettings?.preferLocalAudio?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.preferLocalAudio}
|
||||
disabled={pluginSettings?.preferLocalAudio?.locked}
|
||||
onValueChange={(value) =>
|
||||
@@ -159,7 +159,7 @@ export default function MusicSettingsPage() {
|
||||
title={t("home.settings.music.lookahead_enabled")}
|
||||
disabled={pluginSettings?.audioLookaheadEnabled?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.audioLookaheadEnabled}
|
||||
disabled={pluginSettings?.audioLookaheadEnabled?.locked}
|
||||
onValueChange={(value) =>
|
||||
@@ -233,7 +233,7 @@ export default function MusicSettingsPage() {
|
||||
})}
|
||||
/>
|
||||
</ListGroup>
|
||||
<ListGroup className='mt-4'>
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
textColor='red'
|
||||
onPress={onDeleteDownloadedSongsClicked}
|
||||
|
||||
@@ -17,14 +17,13 @@ export default function PlaybackControlsPage() {
|
||||
contentContainerStyle={{
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className='p-4 flex flex-col'
|
||||
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||
>
|
||||
<View>
|
||||
<View className='mb-4'>
|
||||
<MediaProvider>
|
||||
<MediaToggles className='mb-4' />
|
||||
<GestureControls className='mb-4' />
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
Switch,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { toast } from "sonner-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
@@ -72,7 +72,7 @@ export default function MarlinSearchPage() {
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
>
|
||||
<DisabledSetting disabled={disabled} className='px-4 pt-4'>
|
||||
<DisabledSetting disabled={disabled} className='px-4'>
|
||||
<ListGroup>
|
||||
<DisabledSetting
|
||||
disabled={
|
||||
@@ -90,7 +90,7 @@ export default function MarlinSearchPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||
}}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.searchEngine === "Marlin"}
|
||||
disabled={!!pluginSettings?.streamyStatsServerUrl?.value}
|
||||
onValueChange={(value) => {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
Switch,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { toast } from "sonner-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
@@ -134,7 +134,7 @@ export default function StreamystatsPage() {
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
>
|
||||
<View className='px-4 pt-4'>
|
||||
<View className='px-4'>
|
||||
<ListGroup className='flex-1'>
|
||||
<ListItem
|
||||
title={t("home.settings.plugins.streamystats.url")}
|
||||
@@ -173,7 +173,7 @@ export default function StreamystatsPage() {
|
||||
title={t("home.settings.plugins.streamystats.enable_search")}
|
||||
disabledByAdmin={pluginSettings?.searchEngine?.locked === true}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={useForSearch}
|
||||
disabled={!isStreamystatsEnabled}
|
||||
onValueChange={setUseForSearch}
|
||||
@@ -187,7 +187,7 @@ export default function StreamystatsPage() {
|
||||
pluginSettings?.streamyStatsMovieRecommendations?.locked === true
|
||||
}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={movieRecs}
|
||||
onValueChange={setMovieRecs}
|
||||
disabled={!isStreamystatsEnabled}
|
||||
@@ -201,7 +201,7 @@ export default function StreamystatsPage() {
|
||||
pluginSettings?.streamyStatsSeriesRecommendations?.locked === true
|
||||
}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={seriesRecs}
|
||||
onValueChange={setSeriesRecs}
|
||||
disabled={!isStreamystatsEnabled}
|
||||
@@ -215,7 +215,7 @@ export default function StreamystatsPage() {
|
||||
pluginSettings?.streamyStatsPromotedWatchlists?.locked === true
|
||||
}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={promotedWatchlists}
|
||||
onValueChange={setPromotedWatchlists}
|
||||
disabled={!isStreamystatsEnabled}
|
||||
@@ -225,7 +225,7 @@ export default function StreamystatsPage() {
|
||||
title={t("home.settings.plugins.streamystats.hide_watchlists_tab")}
|
||||
disabledByAdmin={pluginSettings?.hideWatchlistsTab?.locked === true}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={hideWatchlistsTab}
|
||||
onValueChange={setHideWatchlistsTab}
|
||||
disabled={!isStreamystatsEnabled}
|
||||
|
||||
@@ -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 { Text } from "@/components/common/Text";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { Colors } from "../../constants/Colors";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||
@@ -86,7 +85,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
}, [isSuccess, onLoaded]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { settings } = useSettings();
|
||||
|
||||
// Flatten all pages into a single array (and de-dupe by Id to avoid UI duplicates)
|
||||
const allItems = useMemo(() => {
|
||||
@@ -188,10 +186,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
`}
|
||||
>
|
||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||
<ContinueWatchingPoster
|
||||
item={item}
|
||||
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
||||
/>
|
||||
<ContinueWatchingPoster item={item} />
|
||||
)}
|
||||
{item.Type === "Episode" && orientation === "vertical" && (
|
||||
<SeriesPoster item={item} />
|
||||
|
||||
@@ -24,7 +24,6 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { scaleSize } from "@/utils/scaleSize";
|
||||
|
||||
// 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 { settings } = useSettings();
|
||||
|
||||
const allItems = useMemo(() => {
|
||||
const items = data?.pages.flat() ?? [];
|
||||
@@ -233,7 +231,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
hasTVPreferredFocus={isFirstItem}
|
||||
onFocus={() => handleItemFocus(item)}
|
||||
width={itemWidth}
|
||||
preferEpisodeImage={settings?.useEpisodeImagesForNextUp}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -246,7 +243,6 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
showItemActions,
|
||||
handleItemFocus,
|
||||
ITEM_GAP,
|
||||
settings?.useEpisodeImagesForNextUp,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ScrollView, View, type ViewProps } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { useInView } from "@/hooks/useInView";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||
import { ItemCardText } from "../ItemCardText";
|
||||
@@ -51,7 +50,6 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { settings } = useSettings();
|
||||
|
||||
// Show skeleton if loading OR if lazy loading is enabled and not in view yet
|
||||
const shouldShowSkeleton = isLoading || (enableLazyLoading && !isInView);
|
||||
@@ -110,10 +108,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
`}
|
||||
>
|
||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||
<ContinueWatchingPoster
|
||||
item={item}
|
||||
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
||||
/>
|
||||
<ContinueWatchingPoster item={item} />
|
||||
)}
|
||||
{item.Type === "Episode" && orientation === "vertical" && (
|
||||
<SeriesPoster item={item} />
|
||||
|
||||
@@ -23,11 +23,9 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
{title ? (
|
||||
<Text className='ml-4 mb-1 uppercase text-[#8E8D91] text-xs'>
|
||||
{title}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text className='ml-4 mb-1 uppercase text-[#8E8D91] text-xs'>
|
||||
{title}
|
||||
</Text>
|
||||
<View
|
||||
style={[]}
|
||||
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
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";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
@@ -34,17 +34,12 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
||||
}) => {
|
||||
const effectiveSubtitle = disabledByAdmin ? "Disabled by admin" : subtitle;
|
||||
const isDisabled = disabled || disabledByAdmin;
|
||||
// Keep the row floor uniform; Android trims padding slightly (its native
|
||||
// controls sit taller). Switch height is capped via SettingSwitch so toggle
|
||||
// rows match non-toggle rows.
|
||||
const rowSizing =
|
||||
Platform.OS === "android" ? "min-h-[42px] py-1.5" : "min-h-[42px] py-2";
|
||||
if (onPress)
|
||||
return (
|
||||
<TouchableOpacity
|
||||
disabled={isDisabled}
|
||||
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)}
|
||||
>
|
||||
<ListItemContent
|
||||
@@ -63,7 +58,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
||||
);
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<ListItemContent
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Linking } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Linking, Switch } from "react-native";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
@@ -28,7 +27,6 @@ export const AppearanceSettings: React.FC = () => {
|
||||
<ListGroup title={t("home.settings.appearance.title")} className=''>
|
||||
<ListItem
|
||||
title={t("home.settings.other.show_custom_menu_links")}
|
||||
subtitle={t("home.settings.other.show_custom_menu_links_hint")}
|
||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
@@ -36,7 +34,7 @@ export const AppearanceSettings: React.FC = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.showCustomMenuLinks}
|
||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||
onValueChange={(value) =>
|
||||
@@ -46,51 +44,31 @@ export const AppearanceSettings: React.FC = () => {
|
||||
</ListItem>
|
||||
<ListItem
|
||||
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}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ mergeNextUpAndContinueWatching: value })
|
||||
}
|
||||
/>
|
||||
</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
|
||||
onPress={() =>
|
||||
router.push("/settings/appearance/hide-libraries/page")
|
||||
}
|
||||
title={t("home.settings.other.hide_libraries")}
|
||||
subtitle={t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||
showArrow
|
||||
/>
|
||||
<ListItem
|
||||
title={t("home.settings.appearance.hide_remote_session_button")}
|
||||
>
|
||||
<Switch
|
||||
value={settings.hideRemoteSessionButton}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ hideRemoteSessionButton: value })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
</DisabledSetting>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Switch } from "react-native-gesture-handler";
|
||||
import { AudioTranscodeMode, useSettings } from "@/utils/atoms/settings";
|
||||
import { Text } from "../common/Text";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
@@ -135,7 +135,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
||||
title={t("home.settings.audio.set_audio_track")}
|
||||
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.rememberAudioSelections}
|
||||
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
||||
onValueChange={(value) =>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { View } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Switch, View } from "react-native";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
@@ -10,7 +9,7 @@ export const ChromecastSettings: React.FC = ({ ...props }) => {
|
||||
<View {...props}>
|
||||
<ListGroup title={"Chromecast"}>
|
||||
<ListItem title={"Enable H265 for Chromecast"}>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.enableH265ForChromecast}
|
||||
onValueChange={(enableH265ForChromecast) =>
|
||||
updateSettings({ enableH265ForChromecast })
|
||||
|
||||
@@ -2,7 +2,7 @@ import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ViewProps } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Switch } from "react-native";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
@@ -39,7 +39,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
||||
)}
|
||||
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.enableHorizontalSwipeSkip}
|
||||
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
||||
onValueChange={(enableHorizontalSwipeSkip) =>
|
||||
@@ -55,7 +55,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
||||
)}
|
||||
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.enableLeftSideBrightnessSwipe}
|
||||
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
||||
onValueChange={(enableLeftSideBrightnessSwipe) =>
|
||||
@@ -71,7 +71,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
||||
)}
|
||||
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.enableRightSideVolumeSwipe}
|
||||
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
||||
onValueChange={(enableRightSideVolumeSwipe) =>
|
||||
@@ -87,7 +87,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
||||
)}
|
||||
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.hideVolumeSlider}
|
||||
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
||||
onValueChange={(hideVolumeSlider) =>
|
||||
@@ -103,7 +103,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
||||
)}
|
||||
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.hideBrightnessSlider}
|
||||
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||
onValueChange={(hideBrightnessSlider) =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Switch, Text, View } from "react-native";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
|
||||
export const KefinTweaksSettings = () => {
|
||||
@@ -21,7 +20,7 @@ export const KefinTweaksSettings = () => {
|
||||
{isEnabled ? t("Watchlist On") : t("Watchlist Off")}
|
||||
</Text>
|
||||
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
onValueChange={(value) => updateSettings({ useKefinTweaks: value })}
|
||||
trackColor={{ false: "#555", true: "purple" }}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Switch, TouchableOpacity, View } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { useWifiSSID } from "@/hooks/useWifiSSID";
|
||||
import { useServerUrl } from "@/providers/ServerUrlProvider";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
@@ -148,10 +147,7 @@ export function LocalNetworkSettings(): React.ReactElement | null {
|
||||
title={t("home.settings.network.auto_switch_enabled")}
|
||||
subtitle={t("home.settings.network.auto_switch_description")}
|
||||
>
|
||||
<SettingSwitch
|
||||
value={config.enabled}
|
||||
onValueChange={handleToggleEnabled}
|
||||
/>
|
||||
<Switch value={config.enabled} onValueChange={handleToggleEnabled} />
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useMemo } from "react";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Platform, Switch, View, type ViewProps } from "react-native";
|
||||
import { Stepper } from "@/components/inputs/Stepper";
|
||||
import { Text } from "../common/Text";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
@@ -123,7 +122,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
||||
)}
|
||||
|
||||
<ListItem title='Opaque Background'>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ mpvSubtitleBackgroundEnabled: value })
|
||||
|
||||
@@ -3,9 +3,8 @@ import { TFunction } from "i18next";
|
||||
import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Linking, View } from "react-native";
|
||||
import { Linking, Switch, View } from "react-native";
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
@@ -133,7 +132,7 @@ export const OtherSettings: React.FC = () => {
|
||||
title={t("home.settings.other.safe_area_in_controls")}
|
||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.safeAreaInControlsEnabled}
|
||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||
onValueChange={(value) =>
|
||||
@@ -151,7 +150,7 @@ export const OtherSettings: React.FC = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.showCustomMenuLinks}
|
||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||
onValueChange={(value) =>
|
||||
@@ -189,7 +188,7 @@ export const OtherSettings: React.FC = () => {
|
||||
title={t("home.settings.other.disable_haptic_feedback")}
|
||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.disableHapticFeedback}
|
||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||
onValueChange={(disableHapticFeedback) =>
|
||||
|
||||
@@ -3,9 +3,8 @@ import { TFunction } from "i18next";
|
||||
import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Switch, View } from "react-native";
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
@@ -116,7 +115,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
||||
|
||||
return (
|
||||
<DisabledSetting disabled={disabled}>
|
||||
<ListGroup title={t("home.settings.other.other_title")} className='mb-4'>
|
||||
<ListGroup title={t("home.settings.other.other_title")} className=''>
|
||||
<ListItem
|
||||
title={t("home.settings.other.video_orientation")}
|
||||
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
||||
@@ -147,7 +146,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
||||
title={t("home.settings.other.safe_area_in_controls")}
|
||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.safeAreaInControlsEnabled}
|
||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||
onValueChange={(value) =>
|
||||
@@ -206,7 +205,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
||||
title={t("home.settings.other.disable_haptic_feedback")}
|
||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.disableHapticFeedback}
|
||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||
onValueChange={(disableHapticFeedback) =>
|
||||
@@ -219,7 +218,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
||||
title={t("home.settings.other.auto_play_next_episode")}
|
||||
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.autoPlayNextEpisode}
|
||||
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||
onValueChange={(autoPlayNextEpisode) =>
|
||||
|
||||
@@ -125,7 +125,7 @@ export const StorageSettings = () => {
|
||||
</View>
|
||||
</View>
|
||||
{!Platform.isTV && (
|
||||
<ListGroup className={Platform.OS === "android" ? "mt-4" : undefined}>
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
textColor='red'
|
||||
onPress={onDeleteClicked}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import { Switch } from "react-native-gesture-handler";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { SettingSwitch } from "@/components/common/SettingSwitch";
|
||||
import { Stepper } from "@/components/inputs/Stepper";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { Text } from "../common/Text";
|
||||
@@ -98,7 +98,6 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
||||
return (
|
||||
<View {...props}>
|
||||
<ListGroup
|
||||
className='mb-4'
|
||||
title={t("home.settings.subtitles.subtitle_title")}
|
||||
description={
|
||||
<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")}
|
||||
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
||||
>
|
||||
<SettingSwitch
|
||||
<Switch
|
||||
value={settings.rememberSubtitleSelections}
|
||||
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
||||
onValueChange={(value) =>
|
||||
|
||||
@@ -72,9 +72,6 @@ export interface TVPosterCardProps {
|
||||
|
||||
/** Custom image URL getter - if not provided, uses smart URL logic */
|
||||
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",
|
||||
scaleAmount = 1.05,
|
||||
imageUrlGetter,
|
||||
preferEpisodeImage = false,
|
||||
}) => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
@@ -143,10 +139,6 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
||||
if (orientation === "horizontal") {
|
||||
// Episode: prefer series thumb image for consistent look (like hero section)
|
||||
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).
|
||||
// Matched pair: ParentThumbItemId owns the Thumb tag, not ParentBackdropItemId.
|
||||
if (item.ParentThumbItemId && item.ParentThumbImageTag) {
|
||||
@@ -182,7 +174,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
||||
item,
|
||||
width: width * 2, // 2x for quality on large screens
|
||||
});
|
||||
}, [api, item, orientation, width, imageUrlGetter, preferEpisodeImage]);
|
||||
}, [api, item, orientation, width, imageUrlGetter]);
|
||||
|
||||
// Progress calculation
|
||||
const progress = useMemo(() => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||
import SkipButton from "./SkipButton";
|
||||
import { TimeDisplay } from "./TimeDisplay";
|
||||
import { TrickplayBubble } from "./TrickplayBubble";
|
||||
|
||||
@@ -34,11 +33,8 @@ interface BottomControlsProps {
|
||||
showRemoteBubble: boolean;
|
||||
currentTime: number;
|
||||
remainingTime: number;
|
||||
showSkipButton: boolean;
|
||||
showSkipCreditButton: boolean;
|
||||
hasContentAfterCredits: boolean;
|
||||
skipIntro: () => void;
|
||||
skipCredit: () => void;
|
||||
nextItem?: BaseItemDto | null;
|
||||
handleNextEpisodeAutoPlay: () => void;
|
||||
handleNextEpisodeManual: () => void;
|
||||
@@ -86,11 +82,8 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
||||
showRemoteBubble,
|
||||
currentTime,
|
||||
remainingTime,
|
||||
showSkipButton,
|
||||
showSkipCreditButton,
|
||||
hasContentAfterCredits,
|
||||
skipIntro,
|
||||
skipCredit,
|
||||
nextItem,
|
||||
handleNextEpisodeAutoPlay,
|
||||
handleNextEpisodeManual,
|
||||
@@ -180,21 +173,11 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
||||
) : null}
|
||||
</View>
|
||||
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
||||
<SkipButton
|
||||
showButton={showSkipButton}
|
||||
onPress={skipIntro}
|
||||
buttonText='Skip Intro'
|
||||
/>
|
||||
{/* Smart Skip Credits behavior:
|
||||
- Show "Skip Credits" if there's content after credits OR no next episode
|
||||
- Show "Next Episode" if credits extend to video end AND next episode exists */}
|
||||
<SkipButton
|
||||
showButton={
|
||||
showSkipCreditButton && (hasContentAfterCredits || !nextItem)
|
||||
}
|
||||
onPress={skipCredit}
|
||||
buttonText='Skip Credits'
|
||||
/>
|
||||
{/* Smart Skip Credits behavior (drives Next Episode timing):
|
||||
- "Skip Credits" button itself is rendered in SkipSegmentOverlay
|
||||
(independent of controls visibility).
|
||||
- "Next Episode" shows when credits extend to video end AND a next
|
||||
episode exists. */}
|
||||
{settings.autoPlayNextEpisode !== false &&
|
||||
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
||||
settings.autoPlayEpisodeCount <
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useRemoteControl } from "./hooks/useRemoteControl";
|
||||
import { useVideoNavigation } from "./hooks/useVideoNavigation";
|
||||
import { useVideoSlider } from "./hooks/useVideoSlider";
|
||||
import { useVideoTime } from "./hooks/useVideoTime";
|
||||
import { SkipSegmentOverlay } from "./SkipSegmentOverlay";
|
||||
import { TechnicalInfoOverlay } from "./TechnicalInfoOverlay";
|
||||
import { useControlsTimeout } from "./useControlsTimeout";
|
||||
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
||||
@@ -338,6 +339,16 @@ export const Controls: FC<Props> = ({
|
||||
maxMs,
|
||||
);
|
||||
|
||||
// Whether the "Next Episode" countdown will actually be rendered. The Skip
|
||||
// Credits button yields to it only when this is true; if autoplay is
|
||||
// disabled or its episode limit is reached, Skip Credits must stay available
|
||||
// (mirrors the NextEpisodeCountDownButton mount gate in BottomControls).
|
||||
const willShowNextEpisode =
|
||||
!!nextItem &&
|
||||
settings.autoPlayNextEpisode !== false &&
|
||||
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
||||
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value);
|
||||
|
||||
const goToItemCommon = useCallback(
|
||||
(item: BaseItemDto) => {
|
||||
if (!item || !settings) {
|
||||
@@ -570,11 +581,8 @@ export const Controls: FC<Props> = ({
|
||||
showRemoteBubble={showRemoteBubble}
|
||||
currentTime={currentTime}
|
||||
remainingTime={remainingTime}
|
||||
showSkipButton={showSkipButton}
|
||||
showSkipCreditButton={showSkipCreditButton}
|
||||
hasContentAfterCredits={hasContentAfterCredits}
|
||||
skipIntro={skipIntro}
|
||||
skipCredit={skipCredit}
|
||||
nextItem={nextItem}
|
||||
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
|
||||
handleNextEpisodeManual={handleNextEpisodeManual}
|
||||
@@ -594,6 +602,17 @@ export const Controls: FC<Props> = ({
|
||||
time={isSliding || showRemoteBubble ? time : remoteTime}
|
||||
/>
|
||||
</Animated.View>
|
||||
{/* Skip Intro / Skip Credits float independently of the controls so
|
||||
they're visible (and tappable) without summoning the controls. */}
|
||||
<SkipSegmentOverlay
|
||||
showSkipButton={showSkipButton}
|
||||
showSkipCreditButton={showSkipCreditButton}
|
||||
hasContentAfterCredits={hasContentAfterCredits}
|
||||
willShowNextEpisode={willShowNextEpisode}
|
||||
skipIntro={skipIntro}
|
||||
skipCredit={skipCredit}
|
||||
controlsVisible={showControls}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
|
||||
|
||||
125
components/video-player/controls/SkipSegmentOverlay.tsx
Normal file
125
components/video-player/controls/SkipSegmentOverlay.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||
import SkipButton from "./SkipButton";
|
||||
|
||||
interface Props {
|
||||
showSkipButton: boolean;
|
||||
showSkipCreditButton: boolean;
|
||||
hasContentAfterCredits: boolean;
|
||||
willShowNextEpisode: boolean;
|
||||
skipIntro: () => void;
|
||||
skipCredit: () => void;
|
||||
controlsVisible: boolean;
|
||||
}
|
||||
|
||||
// Offsets are relative to the safe-area insets so they hold in both portrait
|
||||
// and landscape (the insets move with the notch / home indicator).
|
||||
//
|
||||
// Hidden: low, far-right — nothing else is drawn there, this is the familiar
|
||||
// spot and was working fine.
|
||||
// Visible: shifted up (clear of the horizontal progress bar) and left (clear
|
||||
// of the chapters icon), while staying below the vertical volume
|
||||
// slider which sits at the vertical middle of the screen.
|
||||
const HIDDEN_BOTTOM = 24;
|
||||
const HIDDEN_RIGHT = 12;
|
||||
const VISIBLE_BOTTOM = 65;
|
||||
const VISIBLE_RIGHT = 42;
|
||||
const ANIM_DURATION = 250;
|
||||
|
||||
// Keeps `value` true for `duration` ms after it turns false. SkipButton hides
|
||||
// itself instantly via a `hidden` (display:none) class, which would preempt
|
||||
// the parent's opacity fade-out — lagging the flag keeps the button rendered
|
||||
// (and visible) while the fade plays out.
|
||||
const useDelayedHide = (value: boolean, duration: number): boolean => {
|
||||
const [display, setDisplay] = useState(value);
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setDisplay(true);
|
||||
return;
|
||||
}
|
||||
const t = setTimeout(() => setDisplay(false), duration);
|
||||
return () => clearTimeout(t);
|
||||
}, [value, duration]);
|
||||
return value || display;
|
||||
};
|
||||
|
||||
/**
|
||||
* Floating Skip Intro / Skip Credits buttons shown independently of the
|
||||
* player controls. They appear on their own during an intro or credits segment
|
||||
* without the user having to summon the controls.
|
||||
*/
|
||||
export const SkipSegmentOverlay: FC<Props> = ({
|
||||
showSkipButton,
|
||||
showSkipCreditButton,
|
||||
hasContentAfterCredits,
|
||||
willShowNextEpisode,
|
||||
skipIntro,
|
||||
skipCredit,
|
||||
controlsVisible,
|
||||
}) => {
|
||||
const insets = useControlsSafeAreaInsets();
|
||||
|
||||
const showCredit =
|
||||
showSkipCreditButton && (hasContentAfterCredits || !willShowNextEpisode);
|
||||
const visible = showSkipButton || showCredit;
|
||||
|
||||
// Drive each SkipButton with a lagged flag so it stays visible while the
|
||||
// opacity fade-out plays, instead of disappearing the instant its segment
|
||||
// ends. `visible` above still drives opacity/pointerEvents immediately.
|
||||
const renderSkip = useDelayedHide(showSkipButton, ANIM_DURATION);
|
||||
const renderCredit = useDelayedHide(showCredit, ANIM_DURATION);
|
||||
|
||||
const opacity = useSharedValue(visible ? 1 : 0);
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withTiming(visible ? 1 : 0, {
|
||||
duration: ANIM_DURATION,
|
||||
easing: Easing.out(Easing.quad),
|
||||
});
|
||||
}, [visible, opacity]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
// Position is recomputed on render (no slide animation) so the button never
|
||||
// sweeps through an overlap zone while the controls toggle.
|
||||
const bottom =
|
||||
insets.bottom + (controlsVisible ? VISIBLE_BOTTOM : HIDDEN_BOTTOM);
|
||||
const right = insets.right + (controlsVisible ? VISIBLE_RIGHT : HIDDEN_RIGHT);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[styles.container, { right, bottom }, animatedStyle]}
|
||||
pointerEvents={visible ? "box-none" : "none"}
|
||||
>
|
||||
<SkipButton
|
||||
showButton={renderSkip}
|
||||
onPress={skipIntro}
|
||||
buttonText='Skip Intro'
|
||||
/>
|
||||
<SkipButton
|
||||
showButton={renderCredit}
|
||||
onPress={skipCredit}
|
||||
buttonText='Skip Credits'
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
zIndex: 15,
|
||||
},
|
||||
});
|
||||
@@ -137,11 +137,7 @@
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"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_hint": "Hide the remote-sessions button from the home header.",
|
||||
"show_home_backdrop": "Dynamic home backdrop",
|
||||
"show_hero_carousel": "Hero carousel",
|
||||
"show_series_poster_on_episode": "Show series poster on episodes",
|
||||
@@ -300,7 +296,6 @@
|
||||
},
|
||||
"safe_area_in_controls": "Safe area in controls",
|
||||
"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)",
|
||||
"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.",
|
||||
|
||||
@@ -274,9 +274,6 @@ export type Settings = {
|
||||
hideBrightnessSlider: boolean;
|
||||
usePopularPlugin: 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
|
||||
showHomeBackdrop: boolean;
|
||||
showTVHeroCarousel: boolean;
|
||||
@@ -385,7 +382,6 @@ export const defaultValues: Settings = {
|
||||
hideBrightnessSlider: false,
|
||||
usePopularPlugin: true,
|
||||
mergeNextUpAndContinueWatching: false,
|
||||
useEpisodeImagesForNextUp: false,
|
||||
// TV-specific settings
|
||||
showHomeBackdrop: true,
|
||||
showTVHeroCarousel: true,
|
||||
|
||||
Reference in New Issue
Block a user