mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-18 18:24:18 +01:00
Compare commits
2 Commits
fix/skip-b
...
renovate/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07272f04e2 | ||
|
|
e79ae53e4e |
6
.github/workflows/ci-codeql.yml
vendored
6
.github/workflows/ci-codeql.yml
vendored
@@ -30,13 +30,13 @@ jobs:
|
|||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: 🏁 Initialize CodeQL
|
- name: 🏁 Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended,security-and-quality
|
queries: +security-extended,security-and-quality
|
||||||
|
|
||||||
- name: 🛠️ Autobuild
|
- name: 🛠️ Autobuild
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
|
|
||||||
- name: 🧪 Perform CodeQL Analysis
|
- name: 🧪 Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
|
|||||||
2
.github/workflows/crowdin.yml
vendored
2
.github/workflows/crowdin.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 🌐 Sync Translations with Crowdin
|
- name: 🌐 Sync Translations with Crowdin
|
||||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
uses: crowdin/github-action@e0c8f73cdc0fafde9396e056c5038217000a32d1 # v2.16.4
|
||||||
with:
|
with:
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: true
|
upload_translations: true
|
||||||
|
|||||||
4
.github/workflows/pr-title-comment.yml
vendored
4
.github/workflows/pr-title-comment.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 💬 Post title error
|
- name: 💬 Post title error
|
||||||
if: steps.result.outputs.has_error == 'true'
|
if: steps.result.outputs.has_error == 'true'
|
||||||
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
|
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
|
||||||
with:
|
with:
|
||||||
number: ${{ steps.result.outputs.number }}
|
number: ${{ steps.result.outputs.number }}
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 🧹 Clear title error
|
- name: 🧹 Clear title error
|
||||||
if: steps.result.outputs.has_error == 'false'
|
if: steps.result.outputs.has_error == 'false'
|
||||||
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
|
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
|
||||||
with:
|
with:
|
||||||
number: ${{ steps.result.outputs.number }}
|
number: ${{ steps.result.outputs.number }}
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
|
|||||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
output: trivy-results.sarif
|
output: trivy-results.sarif
|
||||||
|
|
||||||
- name: 📤 Upload results to code scanning
|
- name: 📤 Upload results to code scanning
|
||||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
sarif_file: trivy-results.sarif
|
sarif_file: trivy-results.sarif
|
||||||
category: trivy-fs
|
category: trivy-fs
|
||||||
|
|||||||
@@ -854,6 +854,13 @@ 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}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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";
|
||||||
@@ -85,6 +86,7 @@ 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(() => {
|
||||||
@@ -186,7 +188,10 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||||
<ContinueWatchingPoster item={item} />
|
<ContinueWatchingPoster
|
||||||
|
item={item}
|
||||||
|
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{item.Type === "Episode" && orientation === "vertical" && (
|
{item.Type === "Episode" && orientation === "vertical" && (
|
||||||
<SeriesPoster item={item} />
|
<SeriesPoster item={item} />
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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
|
||||||
@@ -165,6 +166,7 @@ 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() ?? [];
|
||||||
@@ -231,6 +233,7 @@ 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>
|
||||||
);
|
);
|
||||||
@@ -243,6 +246,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
showItemActions,
|
showItemActions,
|
||||||
handleItemFocus,
|
handleItemFocus,
|
||||||
ITEM_GAP,
|
ITEM_GAP,
|
||||||
|
settings?.useEpisodeImagesForNextUp,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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";
|
||||||
@@ -50,6 +51,7 @@ 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);
|
||||||
@@ -108,7 +110,10 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||||
<ContinueWatchingPoster item={item} />
|
<ContinueWatchingPoster
|
||||||
|
item={item}
|
||||||
|
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{item.Type === "Episode" && orientation === "vertical" && (
|
{item.Type === "Episode" && orientation === "vertical" && (
|
||||||
<SeriesPoster item={item} />
|
<SeriesPoster item={item} />
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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(
|
||||||
@@ -44,6 +45,9 @@ 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",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.mergeNextUpAndContinueWatching}
|
value={settings.mergeNextUpAndContinueWatching}
|
||||||
@@ -52,15 +56,32 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.appearance.use_episode_images_next_up")}
|
||||||
|
subtitle={t(
|
||||||
|
"home.settings.appearance.use_episode_images_next_up_hint",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={settings.useEpisodeImagesForNextUp}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ useEpisodeImagesForNextUp: 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
|
<ListItem
|
||||||
title={t("home.settings.appearance.hide_remote_session_button")}
|
title={t("home.settings.appearance.hide_remote_session_button")}
|
||||||
|
subtitle={t(
|
||||||
|
"home.settings.appearance.hide_remote_session_button_hint",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.hideRemoteSessionButton}
|
value={settings.hideRemoteSessionButton}
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +111,7 @@ 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();
|
||||||
@@ -139,6 +143,10 @@ 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) {
|
||||||
@@ -174,7 +182,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]);
|
}, [api, item, orientation, width, imageUrlGetter, preferEpisodeImage]);
|
||||||
|
|
||||||
// Progress calculation
|
// Progress calculation
|
||||||
const progress = useMemo(() => {
|
const progress = useMemo(() => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
|
import SkipButton from "./SkipButton";
|
||||||
import { TimeDisplay } from "./TimeDisplay";
|
import { TimeDisplay } from "./TimeDisplay";
|
||||||
import { TrickplayBubble } from "./TrickplayBubble";
|
import { TrickplayBubble } from "./TrickplayBubble";
|
||||||
|
|
||||||
@@ -33,8 +34,11 @@ interface BottomControlsProps {
|
|||||||
showRemoteBubble: boolean;
|
showRemoteBubble: boolean;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
remainingTime: number;
|
remainingTime: number;
|
||||||
|
showSkipButton: boolean;
|
||||||
showSkipCreditButton: boolean;
|
showSkipCreditButton: boolean;
|
||||||
hasContentAfterCredits: boolean;
|
hasContentAfterCredits: boolean;
|
||||||
|
skipIntro: () => void;
|
||||||
|
skipCredit: () => void;
|
||||||
nextItem?: BaseItemDto | null;
|
nextItem?: BaseItemDto | null;
|
||||||
handleNextEpisodeAutoPlay: () => void;
|
handleNextEpisodeAutoPlay: () => void;
|
||||||
handleNextEpisodeManual: () => void;
|
handleNextEpisodeManual: () => void;
|
||||||
@@ -82,8 +86,11 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
showRemoteBubble,
|
showRemoteBubble,
|
||||||
currentTime,
|
currentTime,
|
||||||
remainingTime,
|
remainingTime,
|
||||||
|
showSkipButton,
|
||||||
showSkipCreditButton,
|
showSkipCreditButton,
|
||||||
hasContentAfterCredits,
|
hasContentAfterCredits,
|
||||||
|
skipIntro,
|
||||||
|
skipCredit,
|
||||||
nextItem,
|
nextItem,
|
||||||
handleNextEpisodeAutoPlay,
|
handleNextEpisodeAutoPlay,
|
||||||
handleNextEpisodeManual,
|
handleNextEpisodeManual,
|
||||||
@@ -173,11 +180,21 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
||||||
{/* Smart Skip Credits behavior (drives Next Episode timing):
|
<SkipButton
|
||||||
- "Skip Credits" button itself is rendered in SkipSegmentOverlay
|
showButton={showSkipButton}
|
||||||
(independent of controls visibility).
|
onPress={skipIntro}
|
||||||
- "Next Episode" shows when credits extend to video end AND a next
|
buttonText='Skip Intro'
|
||||||
episode exists. */}
|
/>
|
||||||
|
{/* 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'
|
||||||
|
/>
|
||||||
{settings.autoPlayNextEpisode !== false &&
|
{settings.autoPlayNextEpisode !== false &&
|
||||||
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
||||||
settings.autoPlayEpisodeCount <
|
settings.autoPlayEpisodeCount <
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import { useRemoteControl } from "./hooks/useRemoteControl";
|
|||||||
import { useVideoNavigation } from "./hooks/useVideoNavigation";
|
import { useVideoNavigation } from "./hooks/useVideoNavigation";
|
||||||
import { useVideoSlider } from "./hooks/useVideoSlider";
|
import { useVideoSlider } from "./hooks/useVideoSlider";
|
||||||
import { useVideoTime } from "./hooks/useVideoTime";
|
import { useVideoTime } from "./hooks/useVideoTime";
|
||||||
import { SkipSegmentOverlay } from "./SkipSegmentOverlay";
|
|
||||||
import { TechnicalInfoOverlay } from "./TechnicalInfoOverlay";
|
import { TechnicalInfoOverlay } from "./TechnicalInfoOverlay";
|
||||||
import { useControlsTimeout } from "./useControlsTimeout";
|
import { useControlsTimeout } from "./useControlsTimeout";
|
||||||
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
||||||
@@ -339,16 +338,6 @@ export const Controls: FC<Props> = ({
|
|||||||
maxMs,
|
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(
|
const goToItemCommon = useCallback(
|
||||||
(item: BaseItemDto) => {
|
(item: BaseItemDto) => {
|
||||||
if (!item || !settings) {
|
if (!item || !settings) {
|
||||||
@@ -581,8 +570,11 @@ export const Controls: FC<Props> = ({
|
|||||||
showRemoteBubble={showRemoteBubble}
|
showRemoteBubble={showRemoteBubble}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
remainingTime={remainingTime}
|
remainingTime={remainingTime}
|
||||||
|
showSkipButton={showSkipButton}
|
||||||
showSkipCreditButton={showSkipCreditButton}
|
showSkipCreditButton={showSkipCreditButton}
|
||||||
hasContentAfterCredits={hasContentAfterCredits}
|
hasContentAfterCredits={hasContentAfterCredits}
|
||||||
|
skipIntro={skipIntro}
|
||||||
|
skipCredit={skipCredit}
|
||||||
nextItem={nextItem}
|
nextItem={nextItem}
|
||||||
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
|
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
|
||||||
handleNextEpisodeManual={handleNextEpisodeManual}
|
handleNextEpisodeManual={handleNextEpisodeManual}
|
||||||
@@ -602,17 +594,6 @@ export const Controls: FC<Props> = ({
|
|||||||
time={isSliding || showRemoteBubble ? time : remoteTime}
|
time={isSliding || showRemoteBubble ? time : remoteTime}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</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 && (
|
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
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,7 +137,11 @@
|
|||||||
"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",
|
||||||
@@ -296,6 +300,7 @@
|
|||||||
},
|
},
|
||||||
"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,6 +274,9 @@ 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;
|
||||||
@@ -382,6 +385,7 @@ 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