From 3c5782936074ecb63614e187bd4a1c0b61843990 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 16 Nov 2025 21:47:51 +0100 Subject: [PATCH] feat: prefer downloaded file (#1217) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/Button.tsx | 83 +++++++++++++++++++------ components/PlayButton.tsx | 125 ++++++++++++++++++++++++++++++++++++-- translations/en.json | 7 ++- 3 files changed, 190 insertions(+), 25 deletions(-) diff --git a/components/Button.tsx b/components/Button.tsx index bbd0082a..03df8967 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -2,7 +2,6 @@ import type React from "react"; import { type PropsWithChildren, type ReactNode, - useMemo, useRef, useState, } from "react"; @@ -18,6 +17,58 @@ import { import { useHaptic } from "@/hooks/useHaptic"; import { Loader } from "./Loader"; +const getColorClasses = ( + color: "purple" | "red" | "black" | "transparent" | "white", + variant: "solid" | "border", + focused: boolean, +): string => { + if (variant === "border") { + switch (color) { + case "purple": + return focused + ? "bg-transparent border-2 border-purple-400" + : "bg-transparent border-2 border-purple-600"; + case "red": + return focused + ? "bg-transparent border-2 border-red-400" + : "bg-transparent border-2 border-red-600"; + case "black": + return focused + ? "bg-transparent border-2 border-neutral-700" + : "bg-transparent border-2 border-neutral-900"; + case "white": + return focused + ? "bg-transparent border-2 border-gray-100" + : "bg-transparent border-2 border-white"; + case "transparent": + return focused + ? "bg-transparent border-2 border-gray-400" + : "bg-transparent border-2 border-gray-600"; + default: + return ""; + } + } else { + switch (color) { + case "purple": + return focused + ? "bg-purple-500 border-2 border-white" + : "bg-purple-600 border border-purple-700"; + case "red": + return "bg-red-600"; + case "black": + return "bg-neutral-900"; + case "white": + return focused + ? "bg-gray-100 border-2 border-gray-300" + : "bg-white border border-gray-200"; + case "transparent": + return "bg-transparent"; + default: + return ""; + } + } +}; + export interface ButtonProps extends React.ComponentProps { onPress?: () => void; @@ -26,7 +77,8 @@ export interface ButtonProps disabled?: boolean; children?: string | ReactNode; loading?: boolean; - color?: "purple" | "red" | "black" | "transparent"; + color?: "purple" | "red" | "black" | "transparent" | "white"; + variant?: "solid" | "border"; iconRight?: ReactNode; iconLeft?: ReactNode; justify?: "center" | "between"; @@ -39,6 +91,7 @@ export const Button: React.FC> = ({ disabled = false, loading = false, color = "purple", + variant = "solid", iconRight, iconLeft, children, @@ -56,23 +109,13 @@ export const Button: React.FC> = ({ useNativeDriver: true, }).start(); - const colorClasses = useMemo(() => { - switch (color) { - case "purple": - return focused - ? "bg-purple-500 border-2 border-white" - : "bg-purple-600 border border-purple-700"; - case "red": - return "bg-red-600"; - case "black": - return "bg-neutral-900"; - case "transparent": - return "bg-transparent"; - } - }, [color, focused]); + const colorClasses = getColorClasses(color, variant, focused); const lightHapticFeedback = useHaptic("light"); + const textColorClass = + color === "white" && variant === "solid" ? "text-black" : "text-white"; + return Platform.isTV ? ( > = ({ > - {children} + + {children} + @@ -135,7 +180,7 @@ export const Button: React.FC> = ({ {iconLeft ? iconLeft : } { @@ -55,6 +60,7 @@ export const PlayButton: React.FC = ({ const client = useRemoteMediaClient(); const mediaStatus = useMediaStatus(); const { t } = useTranslation(); + const { showModal, hideModal } = useGlobalModal(); const [globalColorAtom] = useAtom(itemThemeColorAtom); const api = useAtomValue(apiAtom); @@ -84,12 +90,9 @@ export const PlayButton: React.FC = ({ [router, isOffline], ); - const onPress = useCallback(async () => { - console.log("onPress"); + const handleNormalPlayFlow = useCallback(async () => { if (!item) return; - lightHapticFeedback(); - const queryParams = new URLSearchParams({ itemId: item.Id!, audioIndex: selectedOptions.audioIndex?.toString() ?? "", @@ -271,6 +274,118 @@ export const PlayButton: React.FC = ({ showActionSheetWithOptions, mediaStatus, selectedOptions, + goToPlayer, + isOffline, + t, + ]); + + const onPress = useCallback(async () => { + console.log("onPress"); + if (!item) return; + + lightHapticFeedback(); + + // Check if item is downloaded + const downloadedItem = item.Id ? getDownloadedItemById(item.Id) : undefined; + + if (downloadedItem) { + if (Platform.OS === "android") { + // Show bottom sheet for Android + showModal( + + + + + {t("player.downloaded_file_title")} + + + {t("player.downloaded_file_message")} + + + + + + + + , + { + snapPoints: ["35%"], + enablePanDownToClose: true, + }, + ); + } else { + // Show alert for iOS + Alert.alert( + t("player.downloaded_file_title"), + t("player.downloaded_file_message"), + [ + { + text: t("player.downloaded_file_yes"), + onPress: () => { + const queryParams = new URLSearchParams({ + itemId: item.Id!, + offline: "true", + playbackPosition: + item.UserData?.PlaybackPositionTicks?.toString() ?? "0", + }); + goToPlayer(queryParams.toString()); + }, + isPreferred: true, + }, + { + text: t("player.downloaded_file_no"), + onPress: () => { + handleNormalPlayFlow(); + }, + }, + { + text: t("player.downloaded_file_cancel"), + style: "cancel", + }, + ], + ); + } + return; + } + + // If not downloaded, proceed with normal flow + handleNormalPlayFlow(); + }, [ + item, + lightHapticFeedback, + handleNormalPlayFlow, + goToPlayer, + t, + showModal, + hideModal, + effectiveColors, ]); const derivedTargetWidth = useDerivedValue(() => { diff --git a/translations/en.json b/translations/en.json index 4266ee69..e1596030 100644 --- a/translations/en.json +++ b/translations/en.json @@ -429,7 +429,12 @@ "playback_state": "Playback State:", "index": "Index:", "continue_watching": "Continue Watching", - "go_back": "Go Back" + "go_back": "Go Back", + "downloaded_file_title": "You have this file downloaded", + "downloaded_file_message": "Do you want to play the downloaded file?", + "downloaded_file_yes": "Yes", + "downloaded_file_no": "No", + "downloaded_file_cancel": "Cancel" }, "item_card": { "next_up": "Next Up",