diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 35477faa..ce640fe9 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -735,6 +735,10 @@ "device": "device", "enabled": "Casting to device 🎬" }, + "skipIntro": { + "feedback": "Was this skip helpful?", + "skip": "Skip Intro" + }, "menus": { "downloads": { "button": "Attempt download", diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index ad256aa0..5aa08d87 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -83,6 +83,8 @@ export enum Icons { RELOAD = "reload", REPEAT = "repeat", PLUS = "plus", + THUMBS_UP = "thumbsUp", + THUMBS_DOWN = "thumbsDown", } export interface IconProps { @@ -183,6 +185,8 @@ const iconList: Record = { reload: ``, repeat: ``, plus: ``, + thumbsUp: ``, + thumbsDown: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/player/atoms/SkipIntroButton.tsx b/src/components/player/atoms/SkipIntroButton.tsx index 2f95158e..3b030f74 100644 --- a/src/components/player/atoms/SkipIntroButton.tsx +++ b/src/components/player/atoms/SkipIntroButton.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; -import { useCallback } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Icon, Icons } from "@/components/Icon"; import { useSkipTracking } from "@/components/player/hooks/useSkipTracking"; @@ -50,6 +51,14 @@ export function SkipIntroButton(props: { const display = usePlayerStore((s) => s.display); const meta = usePlayerStore((s) => s.meta); const { addSkipEvent } = useSkipTracking(20); + const [showFeedback, setShowFeedback] = useState(false); + const [feedbackSubmitted, setFeedbackSubmitted] = useState(false); + const timeoutRef = useRef | null>(null); + const pendingSkipDataRef = useRef<{ + startTime: number; + endTime: number; + skipDuration: number; + } | null>(null); const showingState = shouldShowSkipButton(time, props.skipTime); const animation = showingState === "hover" ? "slide-up" : "fade"; let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]"; @@ -59,20 +68,19 @@ export function SkipIntroButton(props: { : "bottom-[calc(3rem+env(safe-area-inset-bottom))]"; } - const handleSkip = useCallback(() => { - if (typeof props.skipTime === "number" && display) { - const startTime = time; - const endTime = props.skipTime; - const skipDuration = endTime - startTime; + const { t } = useTranslation(); - display.setTime(props.skipTime); + const reportSkip = useCallback( + (confidence: number) => { + if (!pendingSkipDataRef.current) return; + + const { startTime, endTime, skipDuration } = pendingSkipDataRef.current; - // Add manual skip event with high confidence (user explicitly clicked skip intro) addSkipEvent({ startTime, endTime, skipDuration, - confidence: 0.95, // High confidence for explicit user action + confidence, meta: meta ? { title: @@ -87,15 +95,99 @@ export function SkipIntroButton(props: { : undefined, }); + // eslint-disable-next-line no-console + console.log( + `Skip intro reported: ${skipDuration}s total, confidence: ${confidence}`, + ); + + // Clean up + pendingSkipDataRef.current = null; + setShowFeedback(false); + setFeedbackSubmitted(true); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }, + [addSkipEvent, meta], + ); + + const handleThumbsUp = useCallback(() => { + reportSkip(0.95); + }, [reportSkip]); + + const handleThumbsDown = useCallback(() => { + reportSkip(0.7); + }, [reportSkip]); + + const handleSkip = useCallback(() => { + if (typeof props.skipTime === "number" && display) { + const startTime = time; + const endTime = props.skipTime; + const skipDuration = endTime - startTime; + + display.setTime(props.skipTime); + + // Store skip data temporarily + pendingSkipDataRef.current = { + startTime, + endTime, + skipDuration, + }; + + // Show feedback UI + setShowFeedback(true); + setFeedbackSubmitted(false); + + // Start 10-second timeout + timeoutRef.current = setTimeout(() => { + // Hide component immediately to prevent flicker + setShowFeedback(false); + setFeedbackSubmitted(true); + reportSkip(0.8); + }, 10000); + // eslint-disable-next-line no-console console.log(`Skip intro button used: ${skipDuration}s total`); } - }, [props.skipTime, display, time, addSkipEvent, meta]); + }, [props.skipTime, display, time, reportSkip]); + + // Reset feedback state when content changes + useEffect(() => { + setShowFeedback(false); + setFeedbackSubmitted(false); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + pendingSkipDataRef.current = null; + }, [meta?.tmdbId]); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + if (!props.inControl) return null; let show = false; - if (showingState === "always") show = true; - else if (showingState === "hover" && props.controlsShowing) show = true; + // Don't show anything if feedback has been submitted + if (feedbackSubmitted) { + show = false; + } else if (showFeedback) { + // Always show feedback UI when active + show = true; + } else if (showingState === "always") { + // Show skip button when always visible + show = true; + } else if (showingState === "hover" && props.controlsShowing) { + // Show skip button on hover when controls are showing + show = true; + } if (status !== "playing") show = false; return ( @@ -106,17 +198,52 @@ export function SkipIntroButton(props: { >
- + {showFeedback ? ( + <> +
+ {t("player.skipIntro.feedback")} +
+
+ + +
+ + ) : ( + + )}
);