From 3e0571bcb8273be686d437e8883a316e2a2c1dc8 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 28 Jun 2026 14:47:56 +0200 Subject: [PATCH] Show warning when download location is inaccessible --- .../org/jellyfin/mobile/app/StorageManager.kt | 5 + .../mobile/downloads/DownloadsViewModel.kt | 14 ++ .../ui/screens/downloads/DownloadsScreen.kt | 166 ++++++++++++++---- app/src/main/res/values/strings.xml | 4 + 4 files changed, 151 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/jellyfin/mobile/app/StorageManager.kt b/app/src/main/java/org/jellyfin/mobile/app/StorageManager.kt index 291c5cf7..4ca14616 100644 --- a/app/src/main/java/org/jellyfin/mobile/app/StorageManager.kt +++ b/app/src/main/java/org/jellyfin/mobile/app/StorageManager.kt @@ -22,6 +22,11 @@ class StorageManager( DocumentFile.fromTreeUri(context, it) } + fun isStorageLocationAccessible(): Boolean { + val documentFile = getStorageLocation() + return documentFile != null && documentFile.exists() && documentFile.canWrite() + } + fun changeStorageLocation(location: Uri): Boolean { if (appPreferences.storageLocation?.toUri() == location) return true diff --git a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadsViewModel.kt b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadsViewModel.kt index 946735ef..0c0485a1 100644 --- a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadsViewModel.kt +++ b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadsViewModel.kt @@ -3,8 +3,10 @@ package org.jellyfin.mobile.downloads import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -32,6 +34,12 @@ class DownloadsViewModel : ViewModel(), KoinComponent { .flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + private val _storageLocation = MutableStateFlow(storageManager.getStorageLocation()) + val storageLocation = _storageLocation.asStateFlow() + + private val _storageLocationAccessible = MutableStateFlow(storageManager.isStorageLocationAccessible()) + val storageLocationAccessible = _storageLocationAccessible.asStateFlow() + fun openDownload(download: DownloadEntity) { when (download.item.mediaType) { MediaType.VIDEO -> { @@ -80,4 +88,10 @@ class DownloadsViewModel : ViewModel(), KoinComponent { downloadManager.delete(download.id, deleteFiles) } } + + fun changeStorageLocation(uri: android.net.Uri) { + storageManager.changeStorageLocation(uri) + _storageLocation.value = storageManager.getStorageLocation() + _storageLocationAccessible.value = storageManager.isStorageLocationAccessible() + } } diff --git a/app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt b/app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt index 6755bb87..c703dc05 100644 --- a/app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt +++ b/app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt @@ -1,6 +1,8 @@ package org.jellyfin.mobile.ui.screens.downloads import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade @@ -15,9 +17,11 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.Card import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon @@ -25,6 +29,7 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack @@ -42,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -54,12 +60,18 @@ fun DownloadsScreen( onBackPressed: () -> Unit = {}, ) { val downloads by viewModel.downloads.collectAsState() + val storageLocation by viewModel.storageLocation.collectAsState() + val storageLocationAccessible by viewModel.storageLocationAccessible.collectAsState() val selection = remember { mutableStateSetOf() } var showDeleteConfirm by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) } val selectionMode = selection.isNotEmpty() + val storageLocationPicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + if (uri != null) viewModel.changeStorageLocation(uri) + } + BackHandler(enabled = selectionMode) { selection.clear() } @@ -193,53 +205,131 @@ fun DownloadsScreen( ) }, content = { innerPadding -> - if (downloads.isEmpty()) { - DownloadsEmpty( - contentPadding = innerPadding, - ) - } else { - DownloadsList( - downloads = downloads, - onOpen = { viewModel.openDownload(it) }, - onDownload = { viewModel.download(it) }, - selection = selection, - onToggleSelection = { download -> - if (selection.contains(download.id)) selection.remove(download.id) - else selection.add(download.id) - }, - contentPadding = innerPadding, - ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + if (storageLocation == null) { + StorageLocationNotSetCard( + onFix = { storageLocationPicker.launch(null) }, + modifier = Modifier.padding(16.dp), + ) + } else if (!storageLocationAccessible) { + StorageLocationInaccessibleCard( + onFix = { storageLocationPicker.launch(null) }, + modifier = Modifier.padding(16.dp), + ) + } + + if (downloads.isEmpty()) { + DownloadsEmpty(modifier = Modifier.weight(1f)) + } else { + DownloadsList( + downloads = downloads, + onOpen = { viewModel.openDownload(it) }, + onDownload = { viewModel.download(it) }, + selection = selection, + onToggleSelection = { download -> + if (selection.contains(download.id)) selection.remove(download.id) + else selection.add(download.id) + }, + modifier = Modifier.weight(1f), + ) + } } }, ) } @Composable -fun DownloadsEmpty( +fun StorageLocationNotSetCard( + onFix: () -> Unit, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues.Zero, +) = Card( + modifier = modifier.fillMaxWidth(), + elevation = 4.dp, + backgroundColor = MaterialTheme.colors.error.copy(alpha = 0.2f), ) { - Box( - modifier = modifier - .fillMaxSize() - .padding(contentPadding), - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.download_location_not_set), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(R.string.download_location_not_set_description), + style = MaterialTheme.typography.body2, + ) + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onFix, + modifier = Modifier.align(Alignment.End), ) { - Text( - text = stringResource(R.string.downloads_empty), - style = MaterialTheme.typography.h6, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.downloads_empty_description), - style = MaterialTheme.typography.body2, - textAlign = TextAlign.Center, - ) + Text(text = stringResource(R.string.select_folder)) } } } + +@Composable +fun StorageLocationInaccessibleCard( + onFix: () -> Unit, + modifier: Modifier = Modifier, +) = Card( + modifier = modifier.fillMaxWidth(), + elevation = 4.dp, + backgroundColor = MaterialTheme.colors.error.copy(alpha = 0.2f), +) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.download_location_inaccessible), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(R.string.download_location_inaccessible_description), + style = MaterialTheme.typography.body2, + ) + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onFix, + modifier = Modifier.align(Alignment.End), + ) { + Text(text = stringResource(R.string.select_folder)) + } + } +} + +@Composable +fun DownloadsEmpty( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues.Zero, +) = Box( + modifier = modifier + .fillMaxSize() + .padding(contentPadding), + contentAlignment = Alignment.Center, +) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.downloads_empty), + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.downloads_empty_description), + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c51aac69..5c150722 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -176,4 +176,8 @@ Select all Deselect all %1$d selected + No download location + To download media, please select a folder where the files should be stored. + Download location inaccessible + The selected download location is no longer accessible. Please select a different folder to resume downloads.