mirror of https://github.com/readrops/Readrops.git
Show opml import errors in AccountTab
This commit is contained in:
parent
e0874f2297
commit
0a1574df0d
|
@ -2,10 +2,14 @@ package com.readrops.app.compose.account
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.api.opml.OPMLParser
|
||||
import com.readrops.app.compose.base.TabScreenModel
|
||||
import com.readrops.app.compose.repositories.ErrorResult
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -38,7 +42,15 @@ class AccountScreenModel(
|
|||
|
||||
fun openDialog(dialog: DialogState) = _accountState.update { it.copy(dialog = dialog) }
|
||||
|
||||
fun closeDialog() = _accountState.update { it.copy(dialog = null) }
|
||||
fun closeDialog(dialog: DialogState? = null) {
|
||||
if (dialog is DialogState.ErrorList) {
|
||||
_accountState.update { it.copy(synchronizationErrors = null) }
|
||||
} else if (dialog is DialogState.Error) {
|
||||
_accountState.update { it.copy(opmlImportError = null) }
|
||||
}
|
||||
|
||||
_accountState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
fun deleteAccount() {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
|
@ -51,8 +63,20 @@ class AccountScreenModel(
|
|||
|
||||
fun parseOPMLFile(uri: Uri, context: Context) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
val stream = context.contentResolver.openInputStream(uri)!!
|
||||
val foldersAndFeeds = OPMLParser.read(stream)
|
||||
val foldersAndFeeds: Map<Folder?, List<Feed>>
|
||||
|
||||
try {
|
||||
val stream = context.contentResolver.openInputStream(uri)
|
||||
if (stream == null) {
|
||||
_accountState.update { it.copy(opmlImportError = NoSuchFileException(uri.toFile())) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
foldersAndFeeds = OPMLParser.read(stream)
|
||||
} catch (e: Exception) {
|
||||
_accountState.update { it.copy(opmlImportError = e) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
openDialog(
|
||||
DialogState.OPMLImport(
|
||||
|
@ -62,7 +86,7 @@ class AccountScreenModel(
|
|||
)
|
||||
)
|
||||
|
||||
repository?.insertOPMLFoldersAndFeeds(
|
||||
val errors = repository?.insertOPMLFoldersAndFeeds(
|
||||
foldersAndFeeds = foldersAndFeeds,
|
||||
onUpdate = { feed ->
|
||||
_accountState.update {
|
||||
|
@ -79,6 +103,10 @@ class AccountScreenModel(
|
|||
)
|
||||
|
||||
closeDialog()
|
||||
|
||||
_accountState.update {
|
||||
it.copy(synchronizationErrors = if (errors!!.isNotEmpty()) errors else null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +114,8 @@ class AccountScreenModel(
|
|||
data class AccountState(
|
||||
val account: Account = Account(accountName = "account", accountType = AccountType.LOCAL),
|
||||
val dialog: DialogState? = null,
|
||||
val synchronizationErrors: ErrorResult? = null,
|
||||
val opmlImportError: Exception? = null
|
||||
)
|
||||
|
||||
sealed interface DialogState {
|
||||
|
@ -93,4 +123,7 @@ sealed interface DialogState {
|
|||
object NewAccount : DialogState
|
||||
data class OPMLImport(val currentFeed: String, val feedCount: Int, val feedMax: Int) :
|
||||
DialogState
|
||||
|
||||
data class ErrorList(val errorResult: ErrorResult) : DialogState
|
||||
data class Error(val exception: Exception) : DialogState
|
||||
}
|
|
@ -19,10 +19,16 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
|
@ -41,6 +47,8 @@ import com.readrops.app.compose.R
|
|||
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionDialog
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionScreen
|
||||
import com.readrops.app.compose.timelime.ErrorListDialog
|
||||
import com.readrops.app.compose.util.components.ErrorDialog
|
||||
import com.readrops.app.compose.util.components.SelectableIconText
|
||||
import com.readrops.app.compose.util.components.TwoChoicesDialog
|
||||
import com.readrops.app.compose.util.theme.LargeSpacer
|
||||
|
@ -66,6 +74,8 @@ object AccountTab : Tab {
|
|||
val closeHome by viewModel.closeHome.collectAsStateWithLifecycle()
|
||||
val state by viewModel.accountState.collectAsStateWithLifecycle()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
if (closeHome) {
|
||||
navigator.replaceAll(AccountSelectionScreen())
|
||||
}
|
||||
|
@ -75,6 +85,44 @@ object AccountTab : Tab {
|
|||
uri?.let { viewModel.parseOPMLFile(uri, context) }
|
||||
}
|
||||
|
||||
LaunchedEffect(state.opmlImportError) {
|
||||
if (state.opmlImportError != null) {
|
||||
val action = snackbarHostState.showSnackbar(
|
||||
message = context.resources.getQuantityString(
|
||||
R.plurals.error_occurred,
|
||||
1
|
||||
),
|
||||
actionLabel = context.getString(R.string.details),
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
|
||||
if (action == SnackbarResult.ActionPerformed) {
|
||||
viewModel.openDialog(DialogState.Error(state.opmlImportError!!))
|
||||
} else {
|
||||
viewModel.closeDialog(DialogState.Error(state.opmlImportError!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.synchronizationErrors) {
|
||||
if (state.synchronizationErrors != null) {
|
||||
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 {
|
||||
viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is DialogState.DeleteAccount -> {
|
||||
TwoChoicesDialog(
|
||||
|
@ -109,6 +157,20 @@ object AccountTab : Tab {
|
|||
)
|
||||
}
|
||||
|
||||
is DialogState.ErrorList -> {
|
||||
ErrorListDialog(
|
||||
errorResult = dialog.errorResult,
|
||||
onDismiss = { viewModel.closeDialog(dialog) }
|
||||
)
|
||||
}
|
||||
|
||||
is DialogState.Error -> {
|
||||
ErrorDialog(
|
||||
exception = dialog.exception,
|
||||
onDismiss = { viewModel.closeDialog(dialog) }
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
@ -137,7 +199,8 @@ object AccountTab : Tab {
|
|||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -37,7 +37,7 @@ abstract class ARepository(
|
|||
*/
|
||||
abstract suspend fun synchronize(): SyncResult
|
||||
|
||||
abstract suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit)
|
||||
abstract suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit): ErrorResult
|
||||
}
|
||||
|
||||
abstract class BaseRepository(
|
||||
|
@ -87,7 +87,9 @@ abstract class BaseRepository(
|
|||
suspend fun insertOPMLFoldersAndFeeds(
|
||||
foldersAndFeeds: Map<Folder?, List<Feed>>,
|
||||
onUpdate: (Feed) -> Unit
|
||||
) {
|
||||
): ErrorResult {
|
||||
val errors = mutableMapOf<Feed, Exception>()
|
||||
|
||||
for ((folder, feeds) in foldersAndFeeds) {
|
||||
if (folder != null) {
|
||||
folder.accountId = account.id
|
||||
|
@ -103,10 +105,12 @@ abstract class BaseRepository(
|
|||
|
||||
feeds.forEach { it.folderId = folder?.id }
|
||||
|
||||
insertNewFeeds(
|
||||
errors += insertNewFeeds(
|
||||
newFeeds = feeds,
|
||||
onUpdate = onUpdate
|
||||
)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package com.readrops.app.compose.repositories
|
||||
|
||||
import android.util.Log
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
|
@ -69,7 +68,12 @@ class LocalRSSRepository(
|
|||
throw NotImplementedError("This method can't be called here")
|
||||
|
||||
|
||||
override suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit) = withContext(Dispatchers.IO) {
|
||||
override suspend fun insertNewFeeds(
|
||||
newFeeds: List<Feed>,
|
||||
onUpdate: (Feed) -> Unit
|
||||
): ErrorResult = withContext(Dispatchers.IO) {
|
||||
val errors = mutableMapOf<Feed, Exception>()
|
||||
|
||||
for (newFeed in newFeeds) {
|
||||
onUpdate(newFeed)
|
||||
|
||||
|
@ -77,10 +81,11 @@ class LocalRSSRepository(
|
|||
val result = dataSource.queryRSSResource(newFeed.url!!, null)!!
|
||||
insertFeed(result.first.also { it.folderId = newFeed.folderId })
|
||||
} catch (e: Exception) {
|
||||
Log.d("LocalRSSRepository", e.message.orEmpty())
|
||||
//throw e
|
||||
errors[newFeed] = e
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext errors
|
||||
}
|
||||
|
||||
private suspend fun insertNewItems(items: List<Item>, feed: Feed) {
|
||||
|
|
|
@ -11,16 +11,12 @@ 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.components.errorText
|
||||
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(
|
||||
|
@ -55,13 +51,3 @@ fun ErrorListDialog(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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}"
|
||||
}
|
|
@ -32,7 +32,6 @@ 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
|
||||
|
@ -61,7 +60,6 @@ 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 {
|
||||
|
@ -78,7 +76,6 @@ object TimelineTab : Tab {
|
|||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val viewModel = getScreenModel<TimelineScreenModel>()
|
||||
val state by viewModel.timelineState.collectAsStateWithLifecycle()
|
||||
|
@ -132,9 +129,11 @@ object TimelineTab : Tab {
|
|||
|
||||
LaunchedEffect(state.synchronizationErrors) {
|
||||
if (state.synchronizationErrors != null) {
|
||||
coroutineScope.launch {
|
||||
val action = snackbarHostState.showSnackbar(
|
||||
message = context.resources.getQuantityString(R.plurals.error_occurred, state.synchronizationErrors!!.size),
|
||||
message = context.resources.getQuantityString(
|
||||
R.plurals.error_occurred,
|
||||
state.synchronizationErrors!!.size
|
||||
),
|
||||
actionLabel = context.getString(R.string.details),
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
|
@ -147,7 +146,6 @@ object TimelineTab : Tab {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is DialogState.ConfirmDialog -> {
|
||||
|
@ -186,7 +184,7 @@ object TimelineTab : Tab {
|
|||
is DialogState.ErrorList -> {
|
||||
ErrorListDialog(
|
||||
errorResult = dialog.errorResult,
|
||||
onDismiss = { viewModel.closeDialog(state.dialog) }
|
||||
onDismiss = { viewModel.closeDialog(dialog) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package com.readrops.app.compose.util.components
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
@Composable
|
||||
fun ErrorDialog(
|
||||
exception: Exception,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
BaseDialog(
|
||||
title = stringResource(id = R.string.error_occured),
|
||||
icon = painterResource(id = R.drawable.ic_error),
|
||||
onDismiss = onDismiss
|
||||
) {
|
||||
Text(text = errorText(exception = exception))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 NoSuchFileException -> stringResource(R.string.unable_open_file)
|
||||
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}"
|
||||
}
|
|
@ -156,4 +156,5 @@
|
|||
<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>
|
||||
<string name="unable_open_file">Impossible d\'ouvrir le fichier</string>
|
||||
</resources>
|
|
@ -162,4 +162,5 @@
|
|||
<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>
|
||||
<string name="unable_open_file">Unable to open file</string>
|
||||
</resources>
|
Loading…
Reference in New Issue