diff --git a/android/app/build.gradle b/android/app/build.gradle index 1ca68471..c490dc33 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -95,8 +95,8 @@ android { applicationId 'com.nuvio.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 37 - versionName "1.4.1" + versionCode 38 + versionName "1.4.2" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" } @@ -118,7 +118,7 @@ android { def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] applicationVariants.all { variant -> variant.outputs.each { output -> - def baseVersionCode = 37 // Current versionCode 37 from defaultConfig + def baseVersionCode = 38 // Current versionCode 38 from defaultConfig def abiName = output.getFilter(com.android.build.OutputFile.ABI) def versionCode = baseVersionCode * 100 // Base multiplier diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index f3acf267..836f69b4 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -3,5 +3,5 @@ contain false dark - 1.4.1 + 1.4.2 \ No newline at end of file diff --git a/app.json b/app.json index 8593f64c..a73174b9 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Nuvio", "slug": "nuvio", - "version": "1.4.1", + "version": "1.4.2", "orientation": "default", "backgroundColor": "#020404", "icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png", @@ -17,7 +17,7 @@ "ios": { "supportsTablet": true, "icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png", - "buildNumber": "37", + "buildNumber": "38", "infoPlist": { "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true @@ -60,7 +60,7 @@ "android.permission.WRITE_SETTINGS" ], "package": "com.nuvio.app", - "versionCode": 37, + "versionCode": 38, "architectures": [ "arm64-v8a", "armeabi-v7a", @@ -113,6 +113,6 @@ "fallbackToCacheTimeout": 30000, "url": "https://ota.nuvioapp.space/api/manifest" }, - "runtimeVersion": "1.4.1" + "runtimeVersion": "1.4.2" } } diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index eaae9f2f..24d1caaa 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleSignature ???? CFBundleURLTypes @@ -39,7 +39,7 @@ CFBundleVersion - 37 + 38 LSApplicationQueriesSchemes vlc diff --git a/src/contexts/DownloadsContext.tsx b/src/contexts/DownloadsContext.tsx index 4f25e2a7..9a1b64b4 100644 --- a/src/contexts/DownloadsContext.tsx +++ b/src/contexts/DownloadsContext.tsx @@ -35,6 +35,7 @@ export interface DownloadItem { sourceUrl: string; // stream url headers?: Record; fileUri?: string; // local file uri once downloading/finished + relativeFilePath?: string; // stable path under the app documents dir (survives sandbox path changes) createdAt: number; updatedAt: number; // Additional metadata for progress tracking @@ -231,6 +232,55 @@ function toFileUri(pathOrUri: string): string { return pathOrUri; } +function normalizeRelativePath(path: string): string { + return path.replace(/\\/g, '/').replace(/^\/+/, ''); +} + +function getDocumentsDirPath(): string { + return stripFileScheme(String((directories as any).documents || (FileSystem as any).documentDirectory || '')); +} + +function getRelativeDownloadPath(pathOrUri?: string | null): string | undefined { + if (!pathOrUri) return undefined; + + const withoutScheme = stripFileScheme(String(pathOrUri)).replace(/\\/g, '/').trim(); + if (!withoutScheme) return undefined; + + const relativeCandidate = normalizeRelativePath(withoutScheme); + if (!withoutScheme.startsWith('/') && relativeCandidate.startsWith('downloads/')) { + return relativeCandidate; + } + + const downloadsMatch = withoutScheme.match(/(?:^|\/)(downloads\/.+)$/); + if (downloadsMatch?.[1]) { + return normalizeRelativePath(downloadsMatch[1]); + } + + const documentsDir = getDocumentsDirPath().replace(/\\/g, '/').replace(/\/+$/, ''); + if (documentsDir && withoutScheme.startsWith(`${documentsDir}/`)) { + return normalizeRelativePath(withoutScheme.slice(documentsDir.length + 1)); + } + + if (!withoutScheme.startsWith('/') && !withoutScheme.includes('://')) { + return relativeCandidate; + } + + return undefined; +} + +function resolveDownloadFileUri(relativeFilePath?: string | null, fileUri?: string | null): string | undefined { + const relativePath = getRelativeDownloadPath(relativeFilePath) || getRelativeDownloadPath(fileUri); + if (relativePath) { + const documentsDir = getDocumentsDirPath(); + if (documentsDir) { + return toFileUri(`${documentsDir}/${relativePath}`); + } + } + + if (fileUri) return toFileUri(String(fileUri)); + return undefined; +} + export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [downloads, setDownloads] = useState([]); const downloadsRef = useRef(downloads); @@ -272,7 +322,8 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi posterUrl: (d.posterUrl as any) ?? null, sourceUrl: String(d.sourceUrl ?? ''), headers: (d.headers as any) ?? undefined, - fileUri: d.fileUri ? String(d.fileUri) : undefined, + fileUri: resolveDownloadFileUri((d as any).relativeFilePath, d.fileUri), + relativeFilePath: getRelativeDownloadPath((d as any).relativeFilePath) || getRelativeDownloadPath(d.fileUri), createdAt: typeof d.createdAt === 'number' ? d.createdAt : Date.now(), updatedAt: typeof d.updatedAt === 'number' ? d.updatedAt : Date.now(), // Restore metadata for progress tracking @@ -463,6 +514,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi .done(({ location, bytesDownloaded, bytesTotal }: any) => { const finalPath = location ? String(location) : ''; const finalUri = finalPath ? toFileUri(finalPath) : undefined; + const relativeFilePath = getRelativeDownloadPath(finalPath || finalUri); updateDownload(taskId, (d) => ({ ...d, @@ -472,6 +524,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi progress: 100, updatedAt: Date.now(), fileUri: finalUri || d.fileUri, + relativeFilePath: relativeFilePath || d.relativeFilePath, resumeData: undefined, })); @@ -539,7 +592,8 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi posterUrl: meta.posterUrl ?? null, sourceUrl: String(meta.sourceUrl ?? ''), headers: meta.headers, - fileUri: meta.fileUri, + fileUri: resolveDownloadFileUri(meta.relativeFilePath, meta.fileUri), + relativeFilePath: getRelativeDownloadPath(meta.relativeFilePath) || getRelativeDownloadPath(meta.fileUri), createdAt, updatedAt: createdAt, imdbId: meta.imdbId, @@ -564,11 +618,12 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi const list = downloadsRef.current; await Promise.all( list.map(async (d) => { - if (!d.fileUri) return; + const resolvedFileUri = resolveDownloadFileUri(d.relativeFilePath, d.fileUri); + if (!resolvedFileUri) return; if (d.status === 'completed' || d.status === 'queued') return; try { - const info = await FileSystem.getInfoAsync(d.fileUri); + const info = await FileSystem.getInfoAsync(resolvedFileUri); if (!info.exists || typeof info.size !== 'number') return; let totalBytes = d.totalBytes; @@ -588,6 +643,8 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi totalBytes: totalBytes || prev.totalBytes, progress: looksComplete ? 100 : Math.min(99, Math.max(prev.progress, progress)), status: looksComplete ? 'completed' : prev.status, + fileUri: resolvedFileUri, + relativeFilePath: prev.relativeFilePath || getRelativeDownloadPath(resolvedFileUri), resumeData: looksComplete ? undefined : prev.resumeData, updatedAt: Date.now(), })); @@ -595,7 +652,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi if (looksComplete) { const done = downloadsRef.current.find(x => x.id === d.id); if (done) { - notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: d.fileUri } as DownloadItem); + notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: resolvedFileUri } as DownloadItem); stopLiveActivityForDownload(d.id, { title: done.title, subtitle: 'Completed', progressPercent: 100 }); } else { stopLiveActivityForDownload(d.id, { subtitle: 'Completed', progressPercent: 100 }); @@ -693,7 +750,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } } - const documentsDir = stripFileScheme(String((directories as any).documents || '')); + const documentsDir = getDocumentsDirPath(); if (!documentsDir) throw new Error('Downloads directory is not available'); const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`; @@ -713,6 +770,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } catch { } const fileUri = toFileUri(destinationPath); + const relativeFilePath = getRelativeDownloadPath(destinationPath); const createdAt = Date.now(); const newItem: DownloadItem = { @@ -737,6 +795,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi sourceUrl: input.url, headers: input.headers, fileUri, + relativeFilePath, createdAt, updatedAt: createdAt, // Store metadata for progress tracking @@ -770,6 +829,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi sourceUrl: input.url, headers: input.headers, fileUri, + relativeFilePath, imdbId: input.imdbId, tmdbId: input.tmdbId, }, @@ -823,8 +883,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } const item = downloadsRef.current.find(d => d.id === id); - if (item?.fileUri) { - await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => { }); + const resolvedFileUri = resolveDownloadFileUri(item?.relativeFilePath, item?.fileUri); + if (resolvedFileUri) { + await FileSystem.deleteAsync(resolvedFileUri, { idempotent: true }).catch(() => { }); } setDownloads(prev => prev.filter(d => d.id !== id)); }, [stopLiveActivityForDownload]); @@ -832,8 +893,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi const removeDownload = useCallback(async (id: string) => { const item = downloadsRef.current.find(d => d.id === id); await stopLiveActivityForDownload(id, { title: item?.title, subtitle: 'Removed', progressPercent: item?.progress }); - if (item?.fileUri && item.status === 'completed') { - await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => { }); + const resolvedFileUri = resolveDownloadFileUri(item?.relativeFilePath, item?.fileUri); + if (resolvedFileUri && item?.status === 'completed') { + await FileSystem.deleteAsync(resolvedFileUri, { idempotent: true }).catch(() => { }); } setDownloads(prev => prev.filter(d => d.id !== id)); }, [stopLiveActivityForDownload]); @@ -863,4 +925,3 @@ export function useDownloads(): DownloadsContextValue { return ctx; } - diff --git a/src/utils/version.ts b/src/utils/version.ts index 96fe25be..ab5af826 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -1,7 +1,7 @@ // Single source of truth for the app version displayed in Settings // Update this when bumping app version -export const APP_VERSION = '1.4.1'; +export const APP_VERSION = '1.4.2'; export function getDisplayedAppVersion(): string { return APP_VERSION;