From 43ce50fe70a84cd05623c4b629e18aa500b544b9 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Wed, 15 Jul 2026 10:50:47 +0200 Subject: [PATCH 1/2] fix(notifications): drop deprecated handler flags and payload logging (#1805) Co-authored-by: lance chant <13349722+lancechant@users.noreply.github.com> --- app/_layout.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index ed6a5beec..d3e6fd5d9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -85,7 +85,8 @@ configureReanimatedLogger({ if (!Platform.isTV) { Notifications.setNotificationHandler({ handleNotification: async () => ({ - shouldShowAlert: true, + shouldShowBanner: true, + shouldShowList: true, shouldPlaySound: true, shouldSetBadge: false, }), @@ -350,9 +351,12 @@ function Layout() { notificationListener.current = Notifications?.addNotificationReceivedListener( (notification: Notification) => { + // Log only the title — serializing the whole notification touches + // the deprecated dataString getter (deprecation warning) and dumps + // noisy payloads into the console. console.log( - "Notification received while app running", - notification, + "Notification received while app running:", + notification.request.content.title, ); }, ); From 2e79815d7894f0044707a478ab7b4c263c9c825a Mon Sep 17 00:00:00 2001 From: Gauvain Date: Wed, 15 Jul 2026 11:19:51 +0200 Subject: [PATCH 2/2] fix(downloads): delete-all confirmation, live storage usage, series poster cache (#1808) --- components/downloads/SeriesCard.tsx | 7 ++- components/settings/StorageSettings.tsx | 43 ++++++++++++++----- providers/Downloads/fileOperations.ts | 21 ++++++++- .../Downloads/hooks/useDownloadOperations.ts | 21 ++++++++- translations/en.json | 2 + 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index 60ae71be2..09b13d275 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -16,9 +16,12 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => { const { showActionSheetWithOptions } = useActionSheet(); const router = useRouter(); + // Keyed on SeriesId so recycled FlashList cells re-read the correct poster + // instead of freezing the first-rendered series' image (empty deps bug). const base64Image = useMemo(() => { - return storage.getString(items[0].SeriesId!); - }, []); + const seriesId = items[0]?.SeriesId; + return seriesId ? storage.getString(seriesId) : undefined; + }, [items[0]?.SeriesId]); const deleteSeries = useCallback( async () => diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 117152fc1..82a97d509 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -1,6 +1,6 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { Platform, View } from "react-native"; +import { Alert, Platform, View } from "react-native"; import { toast } from "sonner-native"; import { Text } from "@/components/common/Text"; import { Colors } from "@/constants/Colors"; @@ -12,6 +12,7 @@ import { ListItem } from "../list/ListItem"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); const { t } = useTranslation(); + const queryClient = useQueryClient(); const successHapticFeedback = useHaptic("success"); const errorHapticFeedback = useHaptic("error"); @@ -27,16 +28,38 @@ export const StorageSettings = () => { used: (app.total - app.remaining) / app.total, }; }, + // Keep the bar moving while a download is writing to disk. + refetchInterval: 10 * 1000, }); - const onDeleteClicked = async () => { - try { - await deleteAllFiles(); - successHapticFeedback(); - } catch (_e) { - errorHapticFeedback(); - toast.error(t("home.settings.toasts.error_deleting_files")); - } + const onDeleteClicked = () => { + Alert.alert( + t("home.settings.storage.delete_all_downloaded_files_confirm"), + t("home.settings.storage.delete_all_downloaded_files_confirm_desc"), + [ + { + text: t("common.cancel"), + style: "cancel", + }, + { + text: t("common.ok"), + style: "destructive", + onPress: async () => { + try { + await deleteAllFiles(); + successHapticFeedback(); + } catch (_e) { + errorHapticFeedback(); + toast.error(t("home.settings.toasts.error_deleting_files")); + } finally { + // Reflect the freed space immediately instead of waiting for + // the next poll. + queryClient.invalidateQueries({ queryKey: ["appSize"] }); + } + }, + }, + ], + ); }; const calculatePercentage = (value: number, total: number) => { diff --git a/providers/Downloads/fileOperations.ts b/providers/Downloads/fileOperations.ts index 8ef5deb8d..052409986 100644 --- a/providers/Downloads/fileOperations.ts +++ b/providers/Downloads/fileOperations.ts @@ -96,5 +96,24 @@ export function getDownloadedItemSize(id: string): number { */ export function calculateTotalDownloadedSize(): number { const items = getAllDownloadedItems(); - return items.reduce((sum, item) => sum + (item.videoFileSize || 0), 0); + return items.reduce((sum, item) => { + // Trickplay bytes count too — getDownloadedItemSize models per-item size + // as video + trickplay, the total must match. + const trickplaySize = item.trickPlayData?.size ?? 0; + // Read the live file size on disk so the total reflects actual usage and + // self-heals items whose stored videoFileSize is 0 (old schema, or + // `fileInfo.size` was undefined at download time). Fall back to the stored + // value if the file can't be stat'd. + if (item.videoFilePath) { + try { + const file = new File(filePathToUri(item.videoFilePath)); + if (file.exists) { + return sum + (file.size ?? item.videoFileSize ?? 0) + trickplaySize; + } + } catch (error) { + console.warn("Failed to stat downloaded file for size:", error); + } + } + return sum + (item.videoFileSize ?? 0) + trickplaySize; + }, 0); } diff --git a/providers/Downloads/hooks/useDownloadOperations.ts b/providers/Downloads/hooks/useDownloadOperations.ts index 90e8036cb..0710263bf 100644 --- a/providers/Downloads/hooks/useDownloadOperations.ts +++ b/providers/Downloads/hooks/useDownloadOperations.ts @@ -289,7 +289,24 @@ export function useDownloadOperations({ ); const appSizeUsage = useCallback(async () => { - const totalSize = calculateTotalDownloadedSize(); + let totalSize = calculateTotalDownloadedSize(); + + // Also count in-progress downloads (they write straight to their final + // path) so the growing file shows up as app usage instead of drifting + // into the generic device share until completion. + for (const process of processes) { + try { + const file = new File( + Paths.document, + `${generateFilename(process.item)}.mp4`, + ); + if (file.exists) { + totalSize += file.size ?? 0; + } + } catch { + // File not created yet — ignore. + } + } try { const [freeDiskStorage, totalDiskCapacity] = await Promise.all([ @@ -310,7 +327,7 @@ export function useDownloadOperations({ appSize: totalSize, }; } - }, []); + }, [processes]); return { startBackgroundDownload, diff --git a/translations/en.json b/translations/en.json index 5c92531e0..692532eb7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -384,6 +384,8 @@ "device_usage": "Device {{availableSpace}}%", "size_used": "{{used}} of {{total}} used", "delete_all_downloaded_files": "Delete all downloaded files", + "delete_all_downloaded_files_confirm": "Delete All Downloaded Files?", + "delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.", "music_cache_title": "Music cache", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "clear_music_cache": "Clear music cache",