mirror of
https://github.com/readrops/Readrops.git
synced 2025-02-02 11:46: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.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,
|
||||||
|
@ -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 =
|
||||||
|
@ -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 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
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user