From 0a1574df0d3a2b04c7acf3f9d69a3e9e624134bb Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 1 Apr 2024 13:02:59 +0200 Subject: [PATCH] Show opml import errors in AccountTab --- .../app/compose/account/AccountScreenModel.kt | 41 ++++++++++-- .../app/compose/account/AccountTab.kt | 65 ++++++++++++++++++- .../app/compose/repositories/ARepository.kt | 10 ++- .../repositories/LocalRSSRepository.kt | 27 ++++---- .../app/compose/timelime/ErrorListDialog.kt | 16 +---- .../app/compose/timelime/TimelineTab.kt | 30 ++++----- .../compose/util/components/ErrorDialog.kt | 37 +++++++++++ appcompose/src/main/res/values-fr/strings.xml | 1 + appcompose/src/main/res/values/strings.xml | 1 + 9 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorDialog.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt index f941c7a0..e1311732 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreenModel.kt @@ -2,10 +2,14 @@ package com.readrops.app.compose.account import android.content.Context import android.net.Uri +import androidx.core.net.toFile import cafe.adriel.voyager.core.model.screenModelScope import com.readrops.api.opml.OPMLParser import com.readrops.app.compose.base.TabScreenModel +import com.readrops.app.compose.repositories.ErrorResult import com.readrops.db.Database +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType import kotlinx.coroutines.Dispatchers @@ -38,7 +42,15 @@ class AccountScreenModel( fun openDialog(dialog: DialogState) = _accountState.update { it.copy(dialog = dialog) } - fun closeDialog() = _accountState.update { it.copy(dialog = null) } + fun closeDialog(dialog: DialogState? = null) { + if (dialog is DialogState.ErrorList) { + _accountState.update { it.copy(synchronizationErrors = null) } + } else if (dialog is DialogState.Error) { + _accountState.update { it.copy(opmlImportError = null) } + } + + _accountState.update { it.copy(dialog = null) } + } fun deleteAccount() { screenModelScope.launch(Dispatchers.IO) { @@ -51,8 +63,20 @@ class AccountScreenModel( fun parseOPMLFile(uri: Uri, context: Context) { screenModelScope.launch(Dispatchers.IO) { - val stream = context.contentResolver.openInputStream(uri)!! - val foldersAndFeeds = OPMLParser.read(stream) + val foldersAndFeeds: Map> + + try { + val stream = context.contentResolver.openInputStream(uri) + if (stream == null) { + _accountState.update { it.copy(opmlImportError = NoSuchFileException(uri.toFile())) } + return@launch + } + + foldersAndFeeds = OPMLParser.read(stream) + } catch (e: Exception) { + _accountState.update { it.copy(opmlImportError = e) } + return@launch + } openDialog( DialogState.OPMLImport( @@ -62,7 +86,7 @@ class AccountScreenModel( ) ) - repository?.insertOPMLFoldersAndFeeds( + val errors = repository?.insertOPMLFoldersAndFeeds( foldersAndFeeds = foldersAndFeeds, onUpdate = { feed -> _accountState.update { @@ -79,6 +103,10 @@ class AccountScreenModel( ) closeDialog() + + _accountState.update { + it.copy(synchronizationErrors = if (errors!!.isNotEmpty()) errors else null) + } } } } @@ -86,6 +114,8 @@ class AccountScreenModel( data class AccountState( val account: Account = Account(accountName = "account", accountType = AccountType.LOCAL), val dialog: DialogState? = null, + val synchronizationErrors: ErrorResult? = null, + val opmlImportError: Exception? = null ) sealed interface DialogState { @@ -93,4 +123,7 @@ sealed interface DialogState { object NewAccount : DialogState data class OPMLImport(val currentFeed: String, val feedCount: Int, val feedMax: Int) : DialogState + + data class ErrorList(val errorResult: ErrorResult) : DialogState + data class Error(val exception: Exception) : DialogState } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index 6d781e9d..e064786f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -19,10 +19,16 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter @@ -41,6 +47,8 @@ import com.readrops.app.compose.R import com.readrops.app.compose.account.credentials.AccountCredentialsScreen import com.readrops.app.compose.account.selection.AccountSelectionDialog import com.readrops.app.compose.account.selection.AccountSelectionScreen +import com.readrops.app.compose.timelime.ErrorListDialog +import com.readrops.app.compose.util.components.ErrorDialog import com.readrops.app.compose.util.components.SelectableIconText import com.readrops.app.compose.util.components.TwoChoicesDialog import com.readrops.app.compose.util.theme.LargeSpacer @@ -66,6 +74,8 @@ object AccountTab : Tab { val closeHome by viewModel.closeHome.collectAsStateWithLifecycle() val state by viewModel.accountState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + if (closeHome) { navigator.replaceAll(AccountSelectionScreen()) } @@ -75,6 +85,44 @@ object AccountTab : Tab { uri?.let { viewModel.parseOPMLFile(uri, context) } } + LaunchedEffect(state.opmlImportError) { + if (state.opmlImportError != null) { + val action = snackbarHostState.showSnackbar( + message = context.resources.getQuantityString( + R.plurals.error_occurred, + 1 + ), + actionLabel = context.getString(R.string.details), + duration = SnackbarDuration.Short + ) + + if (action == SnackbarResult.ActionPerformed) { + viewModel.openDialog(DialogState.Error(state.opmlImportError!!)) + } else { + viewModel.closeDialog(DialogState.Error(state.opmlImportError!!)) + } + } + } + + LaunchedEffect(state.synchronizationErrors) { + if (state.synchronizationErrors != null) { + val action = snackbarHostState.showSnackbar( + message = context.resources.getQuantityString( + R.plurals.error_occurred, + state.synchronizationErrors!!.size + ), + actionLabel = context.getString(R.string.details), + duration = SnackbarDuration.Short + ) + + if (action == SnackbarResult.ActionPerformed) { + viewModel.openDialog(DialogState.ErrorList(state.synchronizationErrors!!)) + } else { + viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!)) + } + } + } + when (val dialog = state.dialog) { is DialogState.DeleteAccount -> { TwoChoicesDialog( @@ -109,6 +157,20 @@ object AccountTab : Tab { ) } + is DialogState.ErrorList -> { + ErrorListDialog( + errorResult = dialog.errorResult, + onDismiss = { viewModel.closeDialog(dialog) } + ) + } + + is DialogState.Error -> { + ErrorDialog( + exception = dialog.exception, + onDismiss = { viewModel.closeDialog(dialog) } + ) + } + else -> {} } @@ -137,7 +199,8 @@ object AccountTab : Tab { contentDescription = null ) } - } + }, + snackbarHost = { SnackbarHost(snackbarHostState) } ) { paddingValues -> Column( modifier = Modifier diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index c3909fb4..79c1e4f4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -37,7 +37,7 @@ abstract class ARepository( */ abstract suspend fun synchronize(): SyncResult - abstract suspend fun insertNewFeeds(newFeeds: List, onUpdate: (Feed) -> Unit) + abstract suspend fun insertNewFeeds(newFeeds: List, onUpdate: (Feed) -> Unit): ErrorResult } abstract class BaseRepository( @@ -87,7 +87,9 @@ abstract class BaseRepository( suspend fun insertOPMLFoldersAndFeeds( foldersAndFeeds: Map>, onUpdate: (Feed) -> Unit - ) { + ): ErrorResult { + val errors = mutableMapOf() + for ((folder, feeds) in foldersAndFeeds) { if (folder != null) { folder.accountId = account.id @@ -103,10 +105,12 @@ abstract class BaseRepository( feeds.forEach { it.folderId = folder?.id } - insertNewFeeds( + errors += insertNewFeeds( newFeeds = feeds, onUpdate = onUpdate ) } + + return errors } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index 0bd18690..ff0e622e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -1,6 +1,5 @@ package com.readrops.app.compose.repositories -import android.util.Log import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.SyncResult import com.readrops.api.utils.ApiUtils @@ -69,20 +68,26 @@ class LocalRSSRepository( throw NotImplementedError("This method can't be called here") - override suspend fun insertNewFeeds(newFeeds: List, onUpdate: (Feed) -> Unit) = withContext(Dispatchers.IO) { - for (newFeed in newFeeds) { - onUpdate(newFeed) + override suspend fun insertNewFeeds( + newFeeds: List, + onUpdate: (Feed) -> Unit + ): ErrorResult = withContext(Dispatchers.IO) { + val errors = mutableMapOf() - try { - val result = dataSource.queryRSSResource(newFeed.url!!, null)!! - insertFeed(result.first.also { it.folderId = newFeed.folderId }) - } catch (e: Exception) { - Log.d("LocalRSSRepository", e.message.orEmpty()) - //throw e - } + for (newFeed in newFeeds) { + onUpdate(newFeed) + + try { + val result = dataSource.queryRSSResource(newFeed.url!!, null)!! + insertFeed(result.first.also { it.folderId = newFeed.folderId }) + } catch (e: Exception) { + errors[newFeed] = e } } + return@withContext errors + } + private suspend fun insertNewItems(items: List, feed: Feed) { items.sortedWith(Item::compareTo) // TODO Check if ordering is useful in this situation val itemsToInsert = mutableListOf() diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/ErrorListDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/ErrorListDialog.kt index 4df2e5d8..3fdd5bde 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/ErrorListDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/ErrorListDialog.kt @@ -11,16 +11,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.readrops.api.utils.exceptions.HttpException -import com.readrops.api.utils.exceptions.ParseException -import com.readrops.api.utils.exceptions.UnknownFormatException import com.readrops.app.compose.R import com.readrops.app.compose.repositories.ErrorResult import com.readrops.app.compose.util.components.BaseDialog +import com.readrops.app.compose.util.components.errorText import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.ShortSpacer -import java.io.IOException -import java.net.UnknownHostException @Composable fun ErrorListDialog( @@ -54,14 +50,4 @@ fun ErrorListDialog( } } } -} - -// TODO check compatibility with other accounts errors -@Composable -fun errorText(exception: Exception) = when (exception) { - is HttpException -> stringResource(id = R.string.unreachable_feed_http_error, exception.code.toString()) - is UnknownHostException -> stringResource(R.string.unreachable_feed) - is IOException -> stringResource(R.string.network_failure, exception.message.orEmpty()) - is ParseException, is UnknownFormatException -> stringResource(R.string.processing_feed_error) - else -> "${exception.javaClass.simpleName}: ${exception.message}" } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 36303d88..efea00cd 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -61,7 +60,6 @@ import com.readrops.app.compose.util.theme.spacing import com.readrops.db.filters.ListSortType import com.readrops.db.filters.MainFilter import com.readrops.db.filters.SubFilter -import kotlinx.coroutines.launch object TimelineTab : Tab { @@ -78,7 +76,6 @@ object TimelineTab : Tab { override fun Content() { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() val viewModel = getScreenModel() val state by viewModel.timelineState.collectAsStateWithLifecycle() @@ -132,19 +129,20 @@ object TimelineTab : Tab { LaunchedEffect(state.synchronizationErrors) { if (state.synchronizationErrors != null) { - coroutineScope.launch { - val action = snackbarHostState.showSnackbar( - message = context.resources.getQuantityString(R.plurals.error_occurred, state.synchronizationErrors!!.size), - actionLabel = context.getString(R.string.details), - duration = SnackbarDuration.Short - ) + val action = snackbarHostState.showSnackbar( + message = context.resources.getQuantityString( + R.plurals.error_occurred, + state.synchronizationErrors!!.size + ), + actionLabel = context.getString(R.string.details), + duration = SnackbarDuration.Short + ) - if (action == SnackbarResult.ActionPerformed) { - viewModel.openDialog(DialogState.ErrorList(state.synchronizationErrors!!)) - } else { - // remove errors from state - viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!)) - } + if (action == SnackbarResult.ActionPerformed) { + viewModel.openDialog(DialogState.ErrorList(state.synchronizationErrors!!)) + } else { + // remove errors from state + viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!)) } } } @@ -186,7 +184,7 @@ object TimelineTab : Tab { is DialogState.ErrorList -> { ErrorListDialog( errorResult = dialog.errorResult, - onDismiss = { viewModel.closeDialog(state.dialog) } + onDismiss = { viewModel.closeDialog(dialog) } ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorDialog.kt new file mode 100644 index 00000000..a10e010f --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorDialog.kt @@ -0,0 +1,37 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.readrops.api.utils.exceptions.HttpException +import com.readrops.api.utils.exceptions.ParseException +import com.readrops.api.utils.exceptions.UnknownFormatException +import com.readrops.app.compose.R +import java.io.IOException +import java.net.UnknownHostException + +@Composable +fun ErrorDialog( + exception: Exception, + onDismiss: () -> Unit +) { + BaseDialog( + title = stringResource(id = R.string.error_occured), + icon = painterResource(id = R.drawable.ic_error), + onDismiss = onDismiss + ) { + Text(text = errorText(exception = exception)) + } +} + +// TODO check compatibility with other accounts errors +@Composable +fun errorText(exception: Exception) = when (exception) { + is HttpException -> stringResource(id = R.string.unreachable_feed_http_error, exception.code.toString()) + is UnknownHostException -> stringResource(R.string.unreachable_feed) + is NoSuchFileException -> stringResource(R.string.unable_open_file) + is IOException -> stringResource(R.string.network_failure, exception.message.orEmpty()) + is ParseException, is UnknownFormatException -> stringResource(R.string.processing_feed_error) + else -> "${exception.javaClass.simpleName}: ${exception.message}" +} \ No newline at end of file diff --git a/appcompose/src/main/res/values-fr/strings.xml b/appcompose/src/main/res/values-fr/strings.xml index ff91d6c5..7905f03d 100644 --- a/appcompose/src/main/res/values-fr/strings.xml +++ b/appcompose/src/main/res/values-fr/strings.xml @@ -156,4 +156,5 @@ Erreur réseau: %1$s Erreur de traitement du flux Flux non attaignable + Impossible d\'ouvrir le fichier \ No newline at end of file diff --git a/appcompose/src/main/res/values/strings.xml b/appcompose/src/main/res/values/strings.xml index f333cbbe..e58d7604 100644 --- a/appcompose/src/main/res/values/strings.xml +++ b/appcompose/src/main/res/values/strings.xml @@ -162,4 +162,5 @@ Network failure: %1$s Processing feed error Unreachable feed + Unable to open file \ No newline at end of file