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 f4da2b89..e9d2dcfa 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 @@ -7,9 +7,7 @@ import com.readrops.db.entities.Folder import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account -data class ErrorResult( - val values: Map -) +typealias ErrorResult = Map abstract class ARepository( val database: Database, 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 eff78b36..b2d320d0 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 @@ -61,7 +61,7 @@ class LocalRSSRepository( } - return Pair(syncResult, ErrorResult(errors)) + return Pair(syncResult, errors) } override suspend fun synchronize(): SyncResult = 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 new file mode 100644 index 00000000..4df2e5d8 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/ErrorListDialog.kt @@ -0,0 +1,67 @@ +package com.readrops.app.compose.timelime + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.theme.MediumSpacer +import com.readrops.app.compose.util.theme.ShortSpacer +import java.io.IOException +import java.net.UnknownHostException + +@Composable +fun ErrorListDialog( + errorResult: ErrorResult, + onDismiss: () -> Unit, +) { + val scrollableState = rememberScrollState() + + BaseDialog( + title = stringResource(R.string.synchronization_errors), + icon = painterResource(id = R.drawable.ic_error), + onDismiss = onDismiss, + modifier = Modifier.heightIn(max = 500.dp) + ) { + Text( + text = pluralStringResource( + id = R.plurals.error_occurred_feed, + count = errorResult.size + ) + ) + + MediumSpacer() + + Column( + modifier = Modifier.verticalScroll(scrollableState) + ) { + for (error in errorResult.entries) { + Text(text = "${error.key.name}: ${errorText(error.value)}") + + ShortSpacer() + } + } + } +} + +// 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/TimelineScreenModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreenModel.kt index e525748b..8f080f22 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreenModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreenModel.kt @@ -9,6 +9,7 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import cafe.adriel.voyager.core.model.screenModelScope import com.readrops.app.compose.base.TabScreenModel +import com.readrops.app.compose.repositories.ErrorResult import com.readrops.app.compose.repositories.GetFoldersWithFeeds import com.readrops.db.Database import com.readrops.db.entities.Feed @@ -85,13 +86,18 @@ class TimelineScreenModel( screenModelScope.launch(dispatcher) { val selectedFeeds = if (currentAccount!!.isLocal) { when (filters.value.subFilter) { - SubFilter.FEED -> listOf(database.newFeedDao().selectFeed(filters.value.filterFeedId)) - SubFilter.FOLDER -> database.newFeedDao().selectFeedsByFolder(filters.value.filterFolderId) + SubFilter.FEED -> listOf( + database.newFeedDao().selectFeed(filters.value.filterFeedId) + ) + + SubFilter.FOLDER -> database.newFeedDao() + .selectFeedsByFolder(filters.value.filterFolderId) + else -> listOf() } } else listOf() - repository?.synchronize( + val results = repository?.synchronize( selectedFeeds = selectedFeeds, onUpdate = { } ) @@ -99,7 +105,8 @@ class TimelineScreenModel( _timelineState.update { it.copy( isRefreshing = false, - endSynchronizing = true + endSynchronizing = true, + synchronizationErrors = if (results!!.second.isNotEmpty()) results.second else null ) } } @@ -225,7 +232,13 @@ class TimelineScreenModel( fun openDialog(dialog: DialogState) = _timelineState.update { it.copy(dialog = dialog) } - fun closeDialog() = _timelineState.update { it.copy(dialog = null) } + fun closeDialog(dialog: DialogState? = null) { + if (dialog is DialogState.ErrorList) { + _timelineState.update { it.copy(synchronizationErrors = null) } + } + + _timelineState.update { it.copy(dialog = null) } + } fun setShowReadItemsState(showReadItems: Boolean) { _timelineState.update { @@ -259,6 +272,7 @@ data class TimelineState( val isRefreshing: Boolean = false, val isDrawerOpen: Boolean = false, val endSynchronizing: Boolean = false, + val synchronizationErrors: ErrorResult? = null, val filters: QueryFilters = QueryFilters(), val filterFeedName: String = "", val filterFolderName: String = "", @@ -273,4 +287,5 @@ data class TimelineState( sealed interface DialogState { object ConfirmDialog : DialogState object FilterSheet : DialogState + class ErrorList(val errorResult: ErrorResult) : DialogState } 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 5354c97d..79de5f8d 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 @@ -18,6 +18,10 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalNavigationDrawer 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.material3.pulltorefresh.PullToRefreshContainer @@ -26,6 +30,8 @@ import androidx.compose.material3.rememberDrawerState 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 @@ -53,6 +59,7 @@ 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 { @@ -69,6 +76,7 @@ object TimelineTab : Tab { override fun Content() { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() val viewModel = getScreenModel() @@ -77,6 +85,7 @@ object TimelineTab : Tab { val scrollState = rememberLazyListState() val swipeState = rememberPullToRefreshState() + val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(state.isRefreshing) { if (state.isRefreshing) { @@ -120,8 +129,27 @@ object TimelineTab : Tab { } } - when (state.dialog) { - DialogState.ConfirmDialog -> { + 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 + ) + + if (action == SnackbarResult.ActionPerformed) { + viewModel.openDialog(DialogState.ErrorList(state.synchronizationErrors!!)) + } else { + // remove errors from state + viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!)) + } + } + } + } + + when (val dialog = state.dialog) { + is DialogState.ConfirmDialog -> { TwoChoicesDialog( title = "Mark all items as read", text = "Do you really want to mark all items as read?", @@ -136,7 +164,7 @@ object TimelineTab : Tab { ) } - DialogState.FilterSheet -> { + is DialogState.FilterSheet -> { FilterBottomSheet( filters = state.filters, onSetShowReadItemsState = { @@ -150,9 +178,14 @@ object TimelineTab : Tab { ListSortType.NEWEST_TO_OLDEST ) }, - onDismiss = { - viewModel.closeDialog() - } + onDismiss = { viewModel.closeDialog() } + ) + } + + is DialogState.ErrorList -> { + ErrorListDialog( + errorResult = dialog.errorResult, + onDismiss = { viewModel.closeDialog(state.dialog) } ) } @@ -236,6 +269,7 @@ object TimelineTab : Tab { } ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { FloatingActionButton( onClick = { diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt index 29132676..d9e3661c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt @@ -25,13 +25,15 @@ fun BaseDialog( title: String, icon: Painter, onDismiss: () -> Unit, + modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Dialog( onDismissRequest = onDismiss ) { Card( - shape = RoundedCornerShape(24.dp) + shape = RoundedCornerShape(24.dp), + modifier = modifier ) { Column( verticalArrangement = Arrangement.Center, diff --git a/appcompose/src/main/res/values-fr/strings.xml b/appcompose/src/main/res/values-fr/strings.xml index e0d77928..0ec147b2 100644 --- a/appcompose/src/main/res/values-fr/strings.xml +++ b/appcompose/src/main/res/values-fr/strings.xml @@ -142,5 +142,18 @@ Cacher les flux sans nouveaux items Marquer les items comme lus pendant le défilement Filtres - + + Une erreur s\'est produite + Des erreurs se sont produites + + Détails + Erreurs de synchronisation + + Une erreur s\'est produite pour le flux suivant : + Des erreurs se sont produites pour les flux suivants : + + Flux non attaignable, erreur HTTP %1$s + Erreur réseau: %1$s + Erreur de traitement du flux + Flux non attaignable \ 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 1f651913..f333cbbe 100644 --- a/appcompose/src/main/res/values/strings.xml +++ b/appcompose/src/main/res/values/strings.xml @@ -148,4 +148,18 @@ Mark items read on scroll New articles Filters + + An error occurred + Some errors occurred + + Details + Synchronization errors + + An error occurred for the following feed: + Some errors occurred for the following feeds: + + Unreachable feed, HTTP error %1$s + Network failure: %1$s + Processing feed error + Unreachable feed \ No newline at end of file