Display synchronization errors for individual feeds in TimelineTab

This commit is contained in:
Shinokuni 2024-03-25 22:58:17 +01:00
parent c3026f0fdb
commit b4ac021159
8 changed files with 160 additions and 17 deletions

View File

@ -7,9 +7,7 @@ import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.Account
data class ErrorResult( typealias ErrorResult = Map<Feed, Exception>
val values: Map<Feed, Exception>
)
abstract class ARepository( abstract class ARepository(
val database: Database, val database: Database,

View File

@ -61,7 +61,7 @@ class LocalRSSRepository(
} }
return Pair(syncResult, ErrorResult(errors)) return Pair(syncResult, errors)
} }
override suspend fun synchronize(): SyncResult = override suspend fun synchronize(): SyncResult =

View File

@ -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}"
}

View File

@ -9,6 +9,7 @@ import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.readrops.app.compose.base.TabScreenModel import com.readrops.app.compose.base.TabScreenModel
import com.readrops.app.compose.repositories.ErrorResult
import com.readrops.app.compose.repositories.GetFoldersWithFeeds import com.readrops.app.compose.repositories.GetFoldersWithFeeds
import com.readrops.db.Database import com.readrops.db.Database
import com.readrops.db.entities.Feed import com.readrops.db.entities.Feed
@ -85,13 +86,18 @@ class TimelineScreenModel(
screenModelScope.launch(dispatcher) { screenModelScope.launch(dispatcher) {
val selectedFeeds = if (currentAccount!!.isLocal) { val selectedFeeds = if (currentAccount!!.isLocal) {
when (filters.value.subFilter) { when (filters.value.subFilter) {
SubFilter.FEED -> listOf(database.newFeedDao().selectFeed(filters.value.filterFeedId)) SubFilter.FEED -> listOf(
SubFilter.FOLDER -> database.newFeedDao().selectFeedsByFolder(filters.value.filterFolderId) database.newFeedDao().selectFeed(filters.value.filterFeedId)
)
SubFilter.FOLDER -> database.newFeedDao()
.selectFeedsByFolder(filters.value.filterFolderId)
else -> listOf() else -> listOf()
} }
} else listOf() } else listOf()
repository?.synchronize( val results = repository?.synchronize(
selectedFeeds = selectedFeeds, selectedFeeds = selectedFeeds,
onUpdate = { } onUpdate = { }
) )
@ -99,7 +105,8 @@ class TimelineScreenModel(
_timelineState.update { _timelineState.update {
it.copy( it.copy(
isRefreshing = false, 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 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) { fun setShowReadItemsState(showReadItems: Boolean) {
_timelineState.update { _timelineState.update {
@ -259,6 +272,7 @@ data class TimelineState(
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val isDrawerOpen: Boolean = false, val isDrawerOpen: Boolean = false,
val endSynchronizing: Boolean = false, val endSynchronizing: Boolean = false,
val synchronizationErrors: ErrorResult? = null,
val filters: QueryFilters = QueryFilters(), val filters: QueryFilters = QueryFilters(),
val filterFeedName: String = "", val filterFeedName: String = "",
val filterFolderName: String = "", val filterFolderName: String = "",
@ -273,4 +287,5 @@ data class TimelineState(
sealed interface DialogState { sealed interface DialogState {
object ConfirmDialog : DialogState object ConfirmDialog : DialogState
object FilterSheet : DialogState object FilterSheet : DialogState
class ErrorList(val errorResult: ErrorResult) : DialogState
} }

View File

@ -18,6 +18,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold 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.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
@ -26,6 +30,8 @@ import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll 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.ListSortType
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import com.readrops.db.filters.SubFilter import com.readrops.db.filters.SubFilter
import kotlinx.coroutines.launch
object TimelineTab : Tab { object TimelineTab : Tab {
@ -69,6 +76,7 @@ object TimelineTab : Tab {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val viewModel = getScreenModel<TimelineScreenModel>() val viewModel = getScreenModel<TimelineScreenModel>()
@ -77,6 +85,7 @@ object TimelineTab : Tab {
val scrollState = rememberLazyListState() val scrollState = rememberLazyListState()
val swipeState = rememberPullToRefreshState() val swipeState = rememberPullToRefreshState()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(state.isRefreshing) { LaunchedEffect(state.isRefreshing) {
if (state.isRefreshing) { if (state.isRefreshing) {
@ -120,8 +129,27 @@ object TimelineTab : Tab {
} }
} }
when (state.dialog) { LaunchedEffect(state.synchronizationErrors) {
DialogState.ConfirmDialog -> { 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( TwoChoicesDialog(
title = "Mark all items as read", title = "Mark all items as read",
text = "Do you really want to 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( FilterBottomSheet(
filters = state.filters, filters = state.filters,
onSetShowReadItemsState = { onSetShowReadItemsState = {
@ -150,9 +178,14 @@ object TimelineTab : Tab {
ListSortType.NEWEST_TO_OLDEST ListSortType.NEWEST_TO_OLDEST
) )
}, },
onDismiss = { onDismiss = { viewModel.closeDialog() }
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 = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {

View File

@ -25,13 +25,15 @@ fun BaseDialog(
title: String, title: String,
icon: Painter, icon: Painter,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
Dialog( Dialog(
onDismissRequest = onDismiss onDismissRequest = onDismiss
) { ) {
Card( Card(
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(24.dp),
modifier = modifier
) { ) {
Column( Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,

View File

@ -142,5 +142,18 @@
<string name="hide_feeds">Cacher les flux sans nouveaux items</string> <string name="hide_feeds">Cacher les flux sans nouveaux items</string>
<string name="mark_items_read">Marquer les items comme lus pendant le défilement</string> <string name="mark_items_read">Marquer les items comme lus pendant le défilement</string>
<string name="filters">Filtres</string> <string name="filters">Filtres</string>
<plurals name="error_occurred">
<item quantity="one">Une erreur s\'est produite</item>
<item quantity="other">Des erreurs se sont produites</item>
</plurals>
<string name="details">Détails</string>
<string name="synchronization_errors">Erreurs de synchronisation</string>
<plurals name="error_occurred_feed">
<item quantity="one">Une erreur s\'est produite pour le flux suivant :</item>
<item quantity="other">Des erreurs se sont produites pour les flux suivants :</item>
</plurals>
<string name="unreachable_feed_http_error">Flux non attaignable, erreur HTTP %1$s</string>
<string name="network_failure">Erreur réseau: %1$s</string>
<string name="processing_feed_error">Erreur de traitement du flux</string>
<string name="unreachable_feed">Flux non attaignable</string>
</resources> </resources>

View File

@ -148,4 +148,18 @@
<string name="mark_items_read">Mark items read on scroll</string> <string name="mark_items_read">Mark items read on scroll</string>
<string name="new_articles">New articles</string> <string name="new_articles">New articles</string>
<string name="filters">Filters</string> <string name="filters">Filters</string>
<plurals name="error_occurred">
<item quantity="one">An error occurred</item>
<item quantity="other">Some errors occurred</item>
</plurals>
<string name="details">Details</string>
<string name="synchronization_errors">Synchronization errors</string>
<plurals name="error_occurred_feed">
<item quantity="one">An error occurred for the following feed:</item>
<item quantity="other">Some errors occurred for the following feeds:</item>
</plurals>
<string name="unreachable_feed_http_error">Unreachable feed, HTTP error %1$s</string>
<string name="network_failure">Network failure: %1$s</string>
<string name="processing_feed_error">Processing feed error</string>
<string name="unreachable_feed">Unreachable feed</string>
</resources> </resources>