mirror of
https://github.com/readrops/Readrops.git
synced 2025-02-02 03:36:52 +01:00
Display synchronization errors for individual feeds in TimelineTab
This commit is contained in:
parent
c3026f0fdb
commit
b4ac021159
@ -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,
|
||||
|
@ -61,7 +61,7 @@ class LocalRSSRepository(
|
||||
|
||||
}
|
||||
|
||||
return Pair(syncResult, ErrorResult(errors))
|
||||
return Pair(syncResult, errors)
|
||||
}
|
||||
|
||||
override suspend fun synchronize(): SyncResult =
|
||||
|
@ -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}"
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user