diff --git a/app/_layout.tsx b/app/_layout.tsx index 304a40be7..bd53c6c8a 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -86,7 +86,8 @@ configureReanimatedLogger({ if (!Platform.isTV) { Notifications.setNotificationHandler({ handleNotification: async () => ({ - shouldShowAlert: true, + shouldShowBanner: true, + shouldShowList: true, shouldPlaySound: true, shouldSetBadge: false, }), @@ -351,9 +352,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, ); }, ); 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 74c906f5a..cebca976f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -387,6 +387,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",