mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-26 16:56:39 +01:00
feat(tv): add long-press mark as watched action using alert dialog
This commit is contained in:
@@ -36,6 +36,7 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
@@ -77,6 +78,7 @@ export const Home = () => {
|
||||
retryCheck,
|
||||
} = useNetworkStatus();
|
||||
const _invalidateCache = useInvalidatePlaybackProgressCache();
|
||||
const { showItemActions } = useTVItemActionModal();
|
||||
const [loadedSections, setLoadedSections] = useState<Set<string>>(new Set());
|
||||
|
||||
// Dynamic backdrop state with debounce
|
||||
@@ -745,7 +747,11 @@ export const Home = () => {
|
||||
>
|
||||
{/* Hero Carousel - Apple TV+ style featured content */}
|
||||
{showHero && heroItems && (
|
||||
<TVHeroCarousel items={heroItems} onItemFocus={handleItemFocus} />
|
||||
<TVHeroCarousel
|
||||
items={heroItems}
|
||||
onItemFocus={handleItemFocus}
|
||||
onItemLongPress={showItemActions}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View
|
||||
|
||||
@@ -21,6 +21,7 @@ import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster.tv";
|
||||
import SeriesPoster from "../posters/SeriesPoster.tv";
|
||||
@@ -202,6 +203,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
const effectivePageSize = Math.max(1, pageSize);
|
||||
const hasCalledOnLoaded = useRef(false);
|
||||
const router = useRouter();
|
||||
const { showItemActions } = useTVItemActionModal();
|
||||
const segments = useSegments();
|
||||
const from = (segments as string[])[2] || "(home)";
|
||||
|
||||
@@ -362,6 +364,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
<View style={{ marginRight: ITEM_GAP, width: itemWidth }}>
|
||||
<TVFocusablePoster
|
||||
onPress={() => handleItemPress(item)}
|
||||
onLongPress={() => showItemActions(item)}
|
||||
hasTVPreferredFocus={isFirstItem}
|
||||
onFocus={() => handleItemFocus(item)}
|
||||
onBlur={handleItemBlur}
|
||||
@@ -381,6 +384,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
isFirstSection,
|
||||
itemWidth,
|
||||
handleItemPress,
|
||||
showItemActions,
|
||||
handleItemFocus,
|
||||
handleItemBlur,
|
||||
typography,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||
@@ -70,6 +71,7 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
||||
const user = useAtomValue(userAtom);
|
||||
const { settings } = useSettings();
|
||||
const router = useRouter();
|
||||
const { showItemActions } = useTVItemActionModal();
|
||||
const segments = useSegments();
|
||||
const from = (segments as string[])[2] || "(home)";
|
||||
|
||||
@@ -142,6 +144,7 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
||||
<View style={{ marginRight: ITEM_GAP, width: posterSizes.poster }}>
|
||||
<TVFocusablePoster
|
||||
onPress={() => handleItemPress(item)}
|
||||
onLongPress={() => showItemActions(item)}
|
||||
onFocus={() => onItemFocus?.(item)}
|
||||
hasTVPreferredFocus={false}
|
||||
>
|
||||
@@ -152,7 +155,7 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[handleItemPress, onItemFocus, typography],
|
||||
[handleItemPress, showItemActions, onItemFocus, typography],
|
||||
);
|
||||
|
||||
if (!isLoading && (!items || items.length === 0)) return null;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||
@@ -74,6 +75,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
const user = useAtomValue(userAtom);
|
||||
const { settings } = useSettings();
|
||||
const router = useRouter();
|
||||
const { showItemActions } = useTVItemActionModal();
|
||||
const segments = useSegments();
|
||||
const from = (segments as string[])[2] || "(home)";
|
||||
|
||||
@@ -203,6 +205,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
<View style={{ marginRight: ITEM_GAP, width: posterSizes.poster }}>
|
||||
<TVFocusablePoster
|
||||
onPress={() => handleItemPress(item)}
|
||||
onLongPress={() => showItemActions(item)}
|
||||
onFocus={() => onItemFocus?.(item)}
|
||||
hasTVPreferredFocus={false}
|
||||
>
|
||||
@@ -213,7 +216,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[handleItemPress, onItemFocus, typography],
|
||||
[handleItemPress, showItemActions, onItemFocus, typography],
|
||||
);
|
||||
|
||||
if (!streamyStatsEnabled) return null;
|
||||
|
||||
@@ -43,6 +43,7 @@ const CARD_PADDING = 60;
|
||||
interface TVHeroCarouselProps {
|
||||
items: BaseItemDto[];
|
||||
onItemFocus?: (item: BaseItemDto) => void;
|
||||
onItemLongPress?: (item: BaseItemDto) => void;
|
||||
}
|
||||
|
||||
interface HeroCardProps {
|
||||
@@ -51,10 +52,11 @@ interface HeroCardProps {
|
||||
cardWidth: number;
|
||||
onFocus: (item: BaseItemDto) => void;
|
||||
onPress: (item: BaseItemDto) => void;
|
||||
onLongPress?: (item: BaseItemDto) => void;
|
||||
}
|
||||
|
||||
const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
({ item, isFirst, cardWidth, onFocus, onPress }) => {
|
||||
({ item, isFirst, cardWidth, onFocus, onPress, onLongPress }) => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new Animated.Value(1)).current;
|
||||
@@ -113,11 +115,16 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
onPress(item);
|
||||
}, [onPress, item]);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
onLongPress?.(item);
|
||||
}, [onLongPress, item]);
|
||||
|
||||
// Use glass poster for tvOS 26+
|
||||
if (useGlass) {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst}
|
||||
@@ -141,6 +148,7 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst}
|
||||
@@ -195,6 +203,7 @@ const BACKDROP_DEBOUNCE_MS = 300;
|
||||
export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
items,
|
||||
onItemFocus,
|
||||
onItemLongPress,
|
||||
}) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
@@ -359,9 +368,10 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
cardWidth={posterSizes.heroCard}
|
||||
onFocus={handleCardFocus}
|
||||
onPress={handleCardPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
),
|
||||
[handleCardFocus, handleCardPress, posterSizes.heroCard],
|
||||
[handleCardFocus, handleCardPress, onItemLongPress, posterSizes.heroCard],
|
||||
);
|
||||
|
||||
// Memoize keyExtractor
|
||||
|
||||
Reference in New Issue
Block a user