Compare commits

...

2 Commits

Author SHA1 Message Date
Shinokuni e9536e99ed Improve http errors wording 2024-04-30 23:44:13 +02:00
Shinokuni f14ed7f331 Add initial FreshRSS login with new kotlin repository 2024-04-30 22:40:38 +02:00
13 changed files with 214 additions and 52 deletions

View File

@ -3,8 +3,13 @@ package com.readrops.api
import com.readrops.api.localfeed.LocalRSSDataSource
import com.readrops.api.services.Credentials
import com.readrops.api.services.freshrss.FreshRSSDataSource
import com.readrops.api.services.freshrss.FreshRSSService
import com.readrops.api.services.freshrss.adapters.*
import com.readrops.api.services.freshrss.NewFreshRSSDataSource
import com.readrops.api.services.freshrss.NewFreshRSSService
import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter
import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter
import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter
import com.readrops.api.services.freshrss.adapters.FreshRSSItemsIdsAdapter
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfoAdapter
import com.readrops.api.services.nextcloudnews.NextNewsDataSource
import com.readrops.api.services.nextcloudnews.NextNewsService
import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter
@ -27,12 +32,12 @@ val apiModule = module {
single {
OkHttpClient.Builder()
.callTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.HOURS)
.addInterceptor(get<AuthInterceptor>())
.addInterceptor(get<ErrorInterceptor>())
//.addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler"))
.build()
.callTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.HOURS)
.addInterceptor(get<AuthInterceptor>())
.addInterceptor(get<ErrorInterceptor>())
//.addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler"))
.build()
}
single { AuthInterceptor() }
@ -45,14 +50,15 @@ val apiModule = module {
factory { params -> FreshRSSDataSource(get(parameters = { params })) }
factory { params -> NewFreshRSSDataSource(get(parameters = { params })) }
factory { (credentials: Credentials) ->
Retrofit.Builder()
.baseUrl(credentials.url)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(get())
.addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi"))))
.build()
.create(FreshRSSService::class.java)
.baseUrl(credentials.url)
.client(get())
.addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi"))))
.build()
.create(NewFreshRSSService::class.java)
}
single(named("freshrssMoshi")) {

View File

@ -1,9 +1,10 @@
package com.readrops.api.utils.exceptions
import okhttp3.Response
import java.io.IOException
class HttpException(val response: Response) : Exception() {
class HttpException(val response: Response) : IOException() {
val code: Int
get() = response.code

View File

@ -1,16 +1,19 @@
package com.readrops.app.compose
import com.readrops.api.services.Credentials
import com.readrops.app.compose.account.AccountScreenModel
import com.readrops.app.compose.account.credentials.AccountCredentialsScreenModel
import com.readrops.app.compose.account.selection.AccountSelectionScreenModel
import com.readrops.app.compose.feeds.FeedScreenModel
import com.readrops.app.compose.item.ItemScreenModel
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.repositories.FreshRSSRepository
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineScreenModel
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module
val composeAppModule = module {
@ -23,18 +26,22 @@ val composeAppModule = module {
factory { AccountScreenModel(get()) }
factory { (itemId: Int) ->
ItemScreenModel(get(), itemId)
}
factory { (itemId: Int) -> ItemScreenModel(get(), itemId) }
factory { (accountType: AccountType) -> AccountCredentialsScreenModel(accountType) }
factory { (accountType: AccountType) -> AccountCredentialsScreenModel(accountType, get()) }
single { GetFoldersWithFeeds(get()) }
// repositories
factory<BaseRepository> { (account: Account) ->
LocalRSSRepository(get(), get(), account)
when (account.accountType) {
AccountType.LOCAL -> LocalRSSRepository(get(), get(), account)
AccountType.FRESHRSS -> FreshRSSRepository(
get(), account,
get(parameters = { parametersOf(Credentials.toCredentials(account)) })
)
else -> throw IllegalArgumentException("Unknown account type")
}
}
}

View File

@ -35,7 +35,9 @@ import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.compose.R
import com.readrops.app.compose.home.HomeScreen
import com.readrops.app.compose.util.components.AndroidScreen
import com.readrops.app.compose.util.ErrorMessage
import com.readrops.app.compose.util.theme.ShortSpacer
import com.readrops.app.compose.util.theme.VeryLargeSpacer
import com.readrops.app.compose.util.theme.spacing
@ -54,6 +56,10 @@ class AccountCredentialsScreen(
val state by screenModel.state.collectAsStateWithLifecycle()
if (state.goToHomeScreen) {
navigator.replaceAll(HomeScreen())
}
Box(
modifier = Modifier.imePadding()
) {
@ -129,9 +135,9 @@ class AccountCredentialsScreen(
) {
Icon(
painter = painterResource(
id = if (state.isPasswordVisible)
id = if (state.isPasswordVisible) {
R.drawable.ic_visible_off
else R.drawable.ic_visible
} else R.drawable.ic_visible
),
contentDescription = null
)
@ -157,7 +163,7 @@ class AccountCredentialsScreen(
onClick = { screenModel.login() },
modifier = Modifier.fillMaxWidth()
) {
if (state.isLoginStarted) {
if (state.isLoginOnGoing) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp,
@ -167,6 +173,16 @@ class AccountCredentialsScreen(
Text(text = stringResource(id = R.string.validate))
}
}
if (state.loginException != null) {
ShortSpacer()
Text(
text = ErrorMessage.get(exception = state.loginException!!),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
}
}
}

View File

@ -2,20 +2,38 @@ package com.readrops.app.compose.account.credentials
import android.util.Patterns
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.util.components.TextFieldError
import com.readrops.db.Database
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.parameter.parametersOf
class AccountCredentialsScreenModel(
private val accountType: AccountType
) : StateScreenModel<AccountCredentialsState>(AccountCredentialsState(name = accountType.name)) {
private val accountType: AccountType,
private val database: Database,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : StateScreenModel<AccountCredentialsState>(AccountCredentialsState(name = accountType.name)),
KoinComponent {
fun onEvent(event: Event): Unit = with(mutableState) {
when (event) {
is Event.LoginEvent -> update { it.copy(login = event.value, loginError = null) }
is Event.NameEvent -> update { it.copy(name = event.value, nameError = null) }
is Event.PasswordEvent -> update { it.copy(password = event.value, passwordError = null) }
is Event.PasswordEvent -> update {
it.copy(
password = event.value,
passwordError = null
)
}
is Event.URLEvent -> update { it.copy(url = event.value, urlError = null) }
}
}
@ -26,7 +44,7 @@ class AccountCredentialsScreenModel(
fun login() {
if (validateFields()) {
mutableState.update { it.copy(isLoginStarted = true) }
mutableState.update { it.copy(isLoginOnGoing = true) }
with(state.value) {
val account = Account(
@ -34,8 +52,29 @@ class AccountCredentialsScreenModel(
accountName = name,
login = login,
password = password,
accountType = accountType
accountType = accountType,
isCurrentAccount = true
)
val repository = get<BaseRepository> { parametersOf(account) }
screenModelScope.launch(dispatcher) {
try {
repository.login(account)
} catch (e: Exception) {
mutableState.update {
it.copy(
loginException = e,
isLoginOnGoing = false
)
}
return@launch
}
database.newAccountDao().insert(account)
mutableState.update { it.copy(goToHomeScreen = true) }
}
}
}
}
@ -82,7 +121,9 @@ data class AccountCredentialsState(
val password: String = "",
val passwordError: TextFieldError? = null,
val isPasswordVisible: Boolean = false,
val isLoginStarted: Boolean = false
val isLoginOnGoing: Boolean = false,
val goToHomeScreen: Boolean = false,
val loginException: Exception? = null
) {
val isUrlError = urlError != null

View File

@ -17,7 +17,7 @@ abstract class ARepository(
/**
* This method is intended for remote accounts.
*/
abstract suspend fun login()
abstract suspend fun login(account: Account)
/**
* Global synchronization for the local account.

View File

@ -0,0 +1,48 @@
package com.readrops.app.compose.repositories
import com.readrops.api.services.Credentials
import com.readrops.api.services.SyncResult
import com.readrops.api.services.freshrss.NewFreshRSSDataSource
import com.readrops.api.utils.AuthInterceptor
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.account.Account
import org.koin.core.component.KoinComponent
class FreshRSSRepository(
database: Database,
account: Account,
private val dataSource: NewFreshRSSDataSource,
) : BaseRepository(database, account), KoinComponent {
override suspend fun login(account: Account) {
val authInterceptor = getKoin().get<AuthInterceptor>()
authInterceptor.credentials = Credentials.toCredentials(account)
val authToken = dataSource.login(account.login!!, account.password!!)
account.token = authToken
// we got the authToken, time to provide it to make real calls
authInterceptor.credentials = Credentials.toCredentials(account)
val userInfo = dataSource.getUserInfo()
account.displayedName = userInfo.userName
}
override suspend fun synchronize(
selectedFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): Pair<SyncResult, ErrorResult> {
TODO("Not yet implemented")
}
override suspend fun synchronize(): SyncResult {
TODO("Not yet implemented")
}
override suspend fun insertNewFeeds(
newFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): ErrorResult {
TODO("Not yet implemented")
}
}

View File

@ -24,7 +24,7 @@ class LocalRSSRepository(
account: Account
) : BaseRepository(database, account), KoinComponent {
override suspend fun login() { /* useless here */
override suspend fun login(account: Account) { /* useless here */
}
override suspend fun synchronize(

View File

@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.readrops.app.compose.R
import com.readrops.app.compose.repositories.ErrorResult
import com.readrops.app.compose.util.ErrorMessage
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
@ -44,7 +44,7 @@ fun ErrorListDialog(
modifier = Modifier.verticalScroll(scrollableState)
) {
for (error in errorResult.entries) {
Text(text = "${error.key.name}: ${errorText(error.value)}")
Text(text = "${error.key.name}: ${ErrorMessage.get(error.value)}")
ShortSpacer()
}

View File

@ -0,0 +1,43 @@
package com.readrops.app.compose.util
import androidx.compose.runtime.Composable
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
object ErrorMessage {
@Composable
fun get(exception: Exception) = when (exception) {
is HttpException -> getHttpMessage(exception)
is UnknownHostException -> stringResource(R.string.unreachable_url)
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}"
}
@Composable
private fun getHttpMessage(exception: HttpException): String {
return when (exception.code) {
in 400..499 -> {
when (exception.code) {
400 -> stringResource(id = R.string.http_error_400)
401 -> stringResource(id = R.string.http_error_401)
403 -> stringResource(id = R.string.http_error_403)
404 -> stringResource(id = R.string.http_error_404)
else -> stringResource(id = R.string.http_error_4XX, exception.code)
}
}
in 500..599 -> {
stringResource(id = R.string.http_error_5XX, exception.code)
}
else -> stringResource(id = R.string.http_error, exception.code)
}
}
}

View File

@ -4,12 +4,8 @@ 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
import com.readrops.app.compose.util.ErrorMessage
@Composable
fun ErrorDialog(
@ -21,17 +17,7 @@ fun ErrorDialog(
icon = painterResource(id = R.drawable.ic_error),
onDismiss = onDismiss
) {
Text(text = errorText(exception = exception))
Text(text = ErrorMessage.get(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}"
}

View File

@ -155,6 +155,13 @@
<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>
<string name="unreachable_url">URL non attaignable</string>
<string name="unable_open_file">Impossible d\'ouvrir le fichier</string>
<string name="http_error_400">Erreur HTTP 400, veuillez vérifier l\'URL du serveur</string>
<string name="http_error_401">Erreur HTTP 401, veuillez vérifier vos identifiants</string>
<string name="http_error_403">Erreur HTTP 403, accès interdit</string>
<string name="http_error_404">Erreur HTTP 404, l\'URL n\'existe pas</string>
<string name="http_error_4XX">Erreur HTTP %1$s, veuillez vérifier vos champs</string>
<string name="http_error_5XX">Erreur HTTP %1$s, erreur serveur</string>
<string name="http_error">Erreur HTTP %1$s</string>
</resources>

View File

@ -161,6 +161,13 @@
<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>
<string name="unreachable_url">Unreachable URL</string>
<string name="unable_open_file">Unable to open file</string>
<string name="http_error_400">HTTP error 400, please check your server URL</string>
<string name="http_error_401">HTTP error 401, please check your credentials</string>
<string name="http_error_403">HTTP error 403, access forbidden</string>
<string name="http_error_404">HTTP error 404, URL not found</string>
<string name="http_error_4XX">HTTP error %1$s, please check your fields</string>
<string name="http_error_5XX">HTTP error %1$s, server error</string>
<string name="http_error">HTTP error %1$s</string>
</resources>