Add initial FreshRSS login with new kotlin repository

This commit is contained in:
Shinokuni 2024-04-30 22:40:38 +02:00
parent a7c0749641
commit f14ed7f331
8 changed files with 151 additions and 32 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.components.errorText
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 = errorText(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(