fix: update okhttp v5 and fix android download crash issues (#1203)

This commit is contained in:
Fredrik Burmester
2025-11-15 11:07:35 +01:00
committed by GitHub
parent 9a906e6d39
commit 1439bcee0d
5 changed files with 69 additions and 40 deletions

View File

@@ -132,13 +132,15 @@ export const DownloadItems: React.FC<DownloadProps> = ({
return itemsNotDownloaded.length === 0; return itemsNotDownloaded.length === 0;
}, [items, itemsNotDownloaded]); }, [items, itemsNotDownloaded]);
const itemsProcesses = useMemo( const itemsProcesses = useMemo(
() => processes?.filter((p) => itemIds.includes(p.item.Id)), () =>
processes?.filter((p) => p?.item?.Id && itemIds.includes(p.item.Id)) ||
[],
[processes, itemIds], [processes, itemIds],
); );
const progress = useMemo(() => { const progress = useMemo(() => {
if (itemIds.length === 1) if (itemIds.length === 1)
return itemsProcesses.reduce((acc, p) => acc + p.progress, 0); return itemsProcesses.reduce((acc, p) => acc + (p.progress || 0), 0);
return ( return (
((itemIds.length - ((itemIds.length -
queue.filter((q) => itemIds.includes(q.item.Id)).length) / queue.filter((q) => itemIds.includes(q.item.Id)).length) /
@@ -262,9 +264,9 @@ export const DownloadItems: React.FC<DownloadProps> = ({
closeModal(); closeModal();
// Wait for modal dismiss animation to complete // Wait for modal dismiss animation to complete
requestAnimationFrame(() => { setTimeout(() => {
initiateDownload(...itemsToDownload); initiateDownload(...itemsToDownload);
}); }, 300);
} else { } else {
toast.error( toast.error(
t("home.downloads.toasts.you_are_not_allowed_to_download_files"), t("home.downloads.toasts.you_are_not_allowed_to_download_files"),

View File

@@ -9,7 +9,11 @@ interface ActiveDownloadsProps extends ViewProps {}
export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
const { processes } = useDownload(); const { processes } = useDownload();
if (processes?.length === 0)
// Filter out any invalid processes before rendering
const validProcesses = processes?.filter((p) => p?.item?.Id) || [];
if (validProcesses.length === 0)
return ( return (
<View {...props} className='bg-neutral-900 p-4 rounded-2xl'> <View {...props} className='bg-neutral-900 p-4 rounded-2xl'>
<Text className='text-lg font-bold'> <Text className='text-lg font-bold'>
@@ -27,8 +31,8 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
{t("home.downloads.active_downloads")} {t("home.downloads.active_downloads")}
</Text> </Text>
<View className='gap-y-2'> <View className='gap-y-2'>
{processes?.map((p: JobStatus) => ( {validProcesses.map((p: JobStatus) => (
<DownloadCard key={p.item.Id} process={p} /> <DownloadCard key={p.id} process={p} />
))} ))}
</View> </View>
</View> </View>

View File

@@ -51,7 +51,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
}; };
const eta = useMemo(() => { const eta = useMemo(() => {
if (!process.estimatedTotalSizeBytes || !process.bytesDownloaded) { if (!process?.estimatedTotalSizeBytes || !process?.bytesDownloaded) {
return null; return null;
} }
@@ -66,13 +66,14 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
} }
return formatTimeString(secondsRemaining, "s"); return formatTimeString(secondsRemaining, "s");
}, [process.id, process.bytesDownloaded, process.estimatedTotalSizeBytes]); }, [process?.id, process?.bytesDownloaded, process?.estimatedTotalSizeBytes]);
const estimatedSize = useMemo(() => { const estimatedSize = useMemo(() => {
if (process.estimatedTotalSizeBytes) return process.estimatedTotalSizeBytes; if (process?.estimatedTotalSizeBytes)
return process.estimatedTotalSizeBytes;
// Calculate from bitrate + duration (only if bitrate value is defined) // Calculate from bitrate + duration (only if bitrate value is defined)
if (process.maxBitrate.value) { if (process?.maxBitrate?.value && process?.item?.RunTimeTicks) {
return estimateDownloadSize( return estimateDownloadSize(
process.maxBitrate.value, process.maxBitrate.value,
process.item.RunTimeTicks, process.item.RunTimeTicks,
@@ -81,32 +82,43 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
return undefined; return undefined;
}, [ }, [
process.maxBitrate.value, process?.maxBitrate?.value,
process.item.RunTimeTicks, process?.item?.RunTimeTicks,
process.estimatedTotalSizeBytes, process?.estimatedTotalSizeBytes,
]); ]);
const isTranscoding = process.isTranscoding || false; const isTranscoding = process?.isTranscoding || false;
const downloadedAmount = useMemo(() => { const downloadedAmount = useMemo(() => {
if (!process.bytesDownloaded) return null; if (!process?.bytesDownloaded) return null;
return formatBytes(process.bytesDownloaded); return formatBytes(process.bytesDownloaded);
}, [process.bytesDownloaded]); }, [process?.bytesDownloaded]);
const base64Image = useMemo(() => { const base64Image = useMemo(() => {
return storage.getString(process.item.Id!); try {
}, []); const itemId = process?.item?.Id;
if (!itemId) return undefined;
return storage.getString(itemId);
} catch {
return undefined;
}
}, [process?.item?.Id]);
// Sanitize progress to ensure it's within valid bounds // Sanitize progress to ensure it's within valid bounds
const sanitizedProgress = useMemo(() => { const sanitizedProgress = useMemo(() => {
if ( if (
typeof process.progress !== "number" || typeof process?.progress !== "number" ||
Number.isNaN(process.progress) Number.isNaN(process.progress)
) { ) {
return 0; return 0;
} }
return Math.max(0, Math.min(100, process.progress)); return Math.max(0, Math.min(100, process.progress));
}, [process.progress]); }, [process?.progress]);
// Return null after all hooks have been called
if (!process || !process.item || !process.item.Id) {
return null;
}
return ( return (
<TouchableOpacity <TouchableOpacity

View File

@@ -35,7 +35,7 @@ android {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.squareup.okhttp3:okhttp:5.3.0"
} }
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {

View File

@@ -58,31 +58,42 @@ function useDownloadProvider() {
| Partial<JobStatus> | Partial<JobStatus>
| ((current: JobStatus) => Partial<JobStatus>), | ((current: JobStatus) => Partial<JobStatus>),
) => { ) => {
setProcesses((prev) => setProcesses((prev) => {
prev.map((p) => { const processIndex = prev.findIndex((p) => p.id === processId);
if (p.id !== processId) return p; if (processIndex === -1) return prev;
const newStatus =
typeof updater === "function" ? updater(p) : updater; const currentProcess = prev[processIndex];
return { if (!currentProcess) return prev;
...p,
...newStatus, const newStatus =
}; typeof updater === "function" ? updater(currentProcess) : updater;
}),
); // Create new array with updated process
const newProcesses = [...prev];
newProcesses[processIndex] = {
...currentProcess,
...newStatus,
};
return newProcesses;
});
}, },
[setProcesses], [setProcesses],
); );
const removeProcess = useCallback( const removeProcess = useCallback(
(id: string) => { (id: string) => {
setProcesses((prev) => prev.filter((process) => process.id !== id)); // Use setTimeout to defer removal and avoid race conditions during rendering
setTimeout(() => {
setProcesses((prev) => prev.filter((process) => process.id !== id));
// Find and remove from task map // Find and remove from task map
taskMapRef.current.forEach((processId, taskId) => { taskMapRef.current.forEach((processId, taskId) => {
if (processId === id) { if (processId === id) {
taskMapRef.current.delete(taskId); taskMapRef.current.delete(taskId);
} }
}); });
}, 0);
}, },
[setProcesses], [setProcesses],
); );