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.account.Account
data class ErrorResult(
val values: Map<Feed, Exception>
)
typealias ErrorResult = Map<Feed, Exception>
abstract class ARepository(
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 =

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

View File

@ -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<TimelineScreenModel>()
@ -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 = {

View File

@ -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,

View File

@ -142,5 +142,18 @@
<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="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>

View File

@ -148,4 +148,18 @@
<string name="mark_items_read">Mark items read on scroll</string>
<string name="new_articles">New articles</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>