Compare commits

...

5 Commits

Author SHA1 Message Date
Gauvain
c9b1f5e47b fix(downloads): add the delete-all confirmation strings 2026-07-15 00:34:38 +02:00
Gauvain
c4a5c05949 fix(downloads): include trickplay files in the storage usage total 2026-07-15 00:33:49 +02:00
Gauvain
9b07d39d4d fix(downloads): key the series poster cache read on SeriesId
The cached base64 poster was read in a useMemo with empty deps, so
recycled list cells kept showing the first-rendered series poster.
2026-07-15 00:32:58 +02:00
Gauvain
04cb88347a fix(downloads): compute storage usage from live file sizes
The storage bar showed 0.00% because calculateTotalDownloadedSize
summed the stored videoFileSize, which is 0 for items downloaded before
the size was recorded (or when fileInfo.size was undefined). Stat the
file on disk and fall back to the stored value.
2026-07-15 00:32:50 +02:00
Gauvain
6ddcc7be40 fix(downloads): confirm before deleting all downloaded files
The "Delete all downloaded files" row wiped everything on a single tap.
Ask for confirmation first (destructive action, cannot be undone).
2026-07-15 00:32:38 +02:00
5 changed files with 79 additions and 15 deletions

View File

@@ -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 () =>

View File

@@ -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) => {

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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",