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

View File

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

View File

@ -1,16 +1,19 @@
package com.readrops.app.compose package com.readrops.app.compose
import com.readrops.api.services.Credentials
import com.readrops.app.compose.account.AccountScreenModel import com.readrops.app.compose.account.AccountScreenModel
import com.readrops.app.compose.account.credentials.AccountCredentialsScreenModel import com.readrops.app.compose.account.credentials.AccountCredentialsScreenModel
import com.readrops.app.compose.account.selection.AccountSelectionScreenModel import com.readrops.app.compose.account.selection.AccountSelectionScreenModel
import com.readrops.app.compose.feeds.FeedScreenModel import com.readrops.app.compose.feeds.FeedScreenModel
import com.readrops.app.compose.item.ItemScreenModel import com.readrops.app.compose.item.ItemScreenModel
import com.readrops.app.compose.repositories.BaseRepository 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.GetFoldersWithFeeds
import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineScreenModel import com.readrops.app.compose.timelime.TimelineScreenModel
import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType import com.readrops.db.entities.account.AccountType
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module import org.koin.dsl.module
val composeAppModule = module { val composeAppModule = module {
@ -23,18 +26,22 @@ val composeAppModule = module {
factory { AccountScreenModel(get()) } factory { AccountScreenModel(get()) }
factory { (itemId: Int) -> factory { (itemId: Int) -> ItemScreenModel(get(), itemId) }
ItemScreenModel(get(), itemId)
}
factory { (accountType: AccountType) -> AccountCredentialsScreenModel(accountType) } factory { (accountType: AccountType) -> AccountCredentialsScreenModel(accountType, get()) }
single { GetFoldersWithFeeds(get()) } single { GetFoldersWithFeeds(get()) }
// repositories // repositories
factory<BaseRepository> { (account: Account) -> 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.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.compose.R 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.AndroidScreen
import com.readrops.app.compose.util.components.errorText
import com.readrops.app.compose.util.theme.ShortSpacer import com.readrops.app.compose.util.theme.ShortSpacer
import com.readrops.app.compose.util.theme.VeryLargeSpacer import com.readrops.app.compose.util.theme.VeryLargeSpacer
import com.readrops.app.compose.util.theme.spacing import com.readrops.app.compose.util.theme.spacing
@ -54,6 +56,10 @@ class AccountCredentialsScreen(
val state by screenModel.state.collectAsStateWithLifecycle() val state by screenModel.state.collectAsStateWithLifecycle()
if (state.goToHomeScreen) {
navigator.replaceAll(HomeScreen())
}
Box( Box(
modifier = Modifier.imePadding() modifier = Modifier.imePadding()
) { ) {
@ -129,9 +135,9 @@ class AccountCredentialsScreen(
) { ) {
Icon( Icon(
painter = painterResource( painter = painterResource(
id = if (state.isPasswordVisible) id = if (state.isPasswordVisible) {
R.drawable.ic_visible_off R.drawable.ic_visible_off
else R.drawable.ic_visible } else R.drawable.ic_visible
), ),
contentDescription = null contentDescription = null
) )
@ -157,7 +163,7 @@ class AccountCredentialsScreen(
onClick = { screenModel.login() }, onClick = { screenModel.login() },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
if (state.isLoginStarted) { if (state.isLoginOnGoing) {
CircularProgressIndicator( CircularProgressIndicator(
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp, strokeWidth = 2.dp,
@ -167,6 +173,16 @@ class AccountCredentialsScreen(
Text(text = stringResource(id = R.string.validate)) 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 android.util.Patterns
import cafe.adriel.voyager.core.model.StateScreenModel 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.app.compose.util.components.TextFieldError
import com.readrops.db.Database
import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType import com.readrops.db.entities.account.AccountType
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update 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( class AccountCredentialsScreenModel(
private val accountType: AccountType private val accountType: AccountType,
) : StateScreenModel<AccountCredentialsState>(AccountCredentialsState(name = accountType.name)) { private val database: Database,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : StateScreenModel<AccountCredentialsState>(AccountCredentialsState(name = accountType.name)),
KoinComponent {
fun onEvent(event: Event): Unit = with(mutableState) { fun onEvent(event: Event): Unit = with(mutableState) {
when (event) { when (event) {
is Event.LoginEvent -> update { it.copy(login = event.value, loginError = null) } is Event.LoginEvent -> update { it.copy(login = event.value, loginError = null) }
is Event.NameEvent -> update { it.copy(name = event.value, nameError = 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) } is Event.URLEvent -> update { it.copy(url = event.value, urlError = null) }
} }
} }
@ -26,7 +44,7 @@ class AccountCredentialsScreenModel(
fun login() { fun login() {
if (validateFields()) { if (validateFields()) {
mutableState.update { it.copy(isLoginStarted = true) } mutableState.update { it.copy(isLoginOnGoing = true) }
with(state.value) { with(state.value) {
val account = Account( val account = Account(
@ -34,8 +52,29 @@ class AccountCredentialsScreenModel(
accountName = name, accountName = name,
login = login, login = login,
password = password, 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 password: String = "",
val passwordError: TextFieldError? = null, val passwordError: TextFieldError? = null,
val isPasswordVisible: Boolean = false, val isPasswordVisible: Boolean = false,
val isLoginStarted: Boolean = false val isLoginOnGoing: Boolean = false,
val goToHomeScreen: Boolean = false,
val loginException: Exception? = null
) { ) {
val isUrlError = urlError != null val isUrlError = urlError != null

View File

@ -17,7 +17,7 @@ abstract class ARepository(
/** /**
* This method is intended for remote accounts. * This method is intended for remote accounts.
*/ */
abstract suspend fun login() abstract suspend fun login(account: Account)
/** /**
* Global synchronization for the local 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 account: Account
) : BaseRepository(database, account), KoinComponent { ) : BaseRepository(database, account), KoinComponent {
override suspend fun login() { /* useless here */ override suspend fun login(account: Account) { /* useless here */
} }
override suspend fun synchronize( override suspend fun synchronize(