mirror of https://github.com/readrops/Readrops.git
Add account creation screens with global account state management
This commit is contained in:
parent
c9db21e881
commit
dac54a307f
|
@ -1,24 +1,26 @@
|
|||
package com.readrops.app.compose
|
||||
|
||||
import com.readrops.app.compose.account.AccountViewModel
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
||||
import com.readrops.app.compose.feeds.FeedViewModel
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.app.compose.repositories.LocalRSSRepository
|
||||
import com.readrops.app.compose.timelime.TimelineViewModel
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val composeAppModule = module {
|
||||
|
||||
viewModel { TimelineViewModel(get(), get()) }
|
||||
viewModel { TimelineViewModel(get()) }
|
||||
|
||||
viewModel { FeedViewModel(get(), get()) }
|
||||
viewModel { FeedViewModel(get()) }
|
||||
|
||||
viewModel { AccountViewModel(get()) }
|
||||
viewModel { AccountSelectionViewModel(get()) }
|
||||
|
||||
// repositories
|
||||
|
||||
single<BaseRepository> { LocalRSSRepository(get(), get(), Account(id = 1, isCurrentAccount = true, accountType = AccountType.LOCAL)) }
|
||||
factory<BaseRepository> { (account: Account) ->
|
||||
LocalRSSRepository(get(), get(), account)
|
||||
}
|
||||
|
||||
}
|
|
@ -4,26 +4,12 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountBox
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import cafe.adriel.voyager.navigator.CurrentScreen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||
import cafe.adriel.voyager.transitions.FadeTransition
|
||||
import cafe.adriel.voyager.transitions.SlideTransition
|
||||
import com.readrops.app.compose.account.AccountSelectionScreen
|
||||
import com.readrops.app.compose.account.AccountTab
|
||||
import com.readrops.app.compose.account.AccountViewModel
|
||||
import com.readrops.app.compose.feeds.FeedTab
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionScreen
|
||||
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
|
||||
import com.readrops.app.compose.home.HomeScreen
|
||||
import com.readrops.app.compose.more.MoreTab
|
||||
import com.readrops.app.compose.timelime.TimelineTab
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
@ -32,7 +18,7 @@ class MainActivity : ComponentActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val viewModel = getViewModel<AccountViewModel>()
|
||||
val viewModel = getViewModel<AccountSelectionViewModel>()
|
||||
val accountExists = viewModel.accountExists()
|
||||
|
||||
setContent {
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
package com.readrops.app.compose.account
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
|
||||
class AccountSelectionScreen : AndroidScreen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column {
|
||||
Text(text = "account selection")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package com.readrops.app.compose.account
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.readrops.db.Database
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AccountViewModel(
|
||||
private val database: Database,
|
||||
) : ViewModel() {
|
||||
|
||||
fun accountExists(): Boolean {
|
||||
val accountCount = runBlocking {
|
||||
database.newAccountDao().selectAccountCount()
|
||||
}
|
||||
|
||||
return accountCount > 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.readrops.app.compose.account.credentials
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.readrops.app.compose.home.HomeScreen
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
|
||||
class AccountCredentialsScreen(
|
||||
private val accountType: AccountType,
|
||||
private val account: Account? = null,
|
||||
) : AndroidScreen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "AccountCredentialsScreen"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Button(onClick = { navigator.replaceAll(HomeScreen()) }) {
|
||||
Text(
|
||||
text = "skip"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package com.readrops.app.compose.account.selection
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.androidx.AndroidScreen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
|
||||
import com.readrops.app.compose.home.HomeScreen
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
class AccountSelectionScreen : AndroidScreen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val viewModel = getViewModel<AccountSelectionViewModel>()
|
||||
val navState by viewModel.navState.collectAsState()
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Text(text = "Choose an account")
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
AccountType.values().forEach { accountType ->
|
||||
Row(
|
||||
modifier = Modifier.clickable { viewModel.createAccount(accountType) }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_freshrss),
|
||||
contentDescription = accountType.name,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
|
||||
Text(text = accountType.name)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
when (navState) {
|
||||
is AccountSelectionViewModel.NavState.GoToHomeScreen -> {
|
||||
// using replace makes the app crash due to a screen key conflict
|
||||
navigator.replaceAll(HomeScreen())
|
||||
}
|
||||
|
||||
is AccountSelectionViewModel.NavState.GoToAccountCredentialsScreen -> {
|
||||
val accountType = (navState as AccountSelectionViewModel.NavState.GoToAccountCredentialsScreen).accountType
|
||||
|
||||
navigator.push(AccountCredentialsScreen(accountType))
|
||||
viewModel.resetNavState()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package com.readrops.app.compose.account.selection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class AccountSelectionViewModel(
|
||||
private val database: Database,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : ViewModel(), KoinComponent {
|
||||
|
||||
private val _navState = MutableStateFlow<NavState>(NavState.Idle)
|
||||
val navState = _navState.asStateFlow()
|
||||
|
||||
fun accountExists(): Boolean {
|
||||
val accountCount = runBlocking {
|
||||
database.newAccountDao().selectAccountCount()
|
||||
}
|
||||
|
||||
return accountCount > 0
|
||||
}
|
||||
|
||||
fun createAccount(accountType: AccountType) {
|
||||
if (accountType == AccountType.LOCAL) {
|
||||
createLocalAccount()
|
||||
} else {
|
||||
_navState.update { NavState.GoToAccountCredentialsScreen(accountType) }
|
||||
}
|
||||
}
|
||||
|
||||
fun resetNavState() {
|
||||
_navState.update { NavState.Idle }
|
||||
}
|
||||
|
||||
private fun createLocalAccount() {
|
||||
val context = get<Context>()
|
||||
val account = Account(
|
||||
url = null,
|
||||
accountName = context.getString(AccountType.LOCAL.typeName),
|
||||
accountType = AccountType.LOCAL,
|
||||
isCurrentAccount = true
|
||||
)
|
||||
|
||||
viewModelScope.launch(dispatcher) {
|
||||
database.newAccountDao().insert(account)
|
||||
|
||||
_navState.update { NavState.GoToHomeScreen }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sealed class NavState {
|
||||
object Idle : NavState()
|
||||
object GoToHomeScreen : NavState()
|
||||
class GoToAccountCredentialsScreen(val accountType: AccountType) : NavState()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package com.readrops.app.compose.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.app.compose.repositories.LocalRSSRepository
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.account.Account
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
/**
|
||||
* Custom ViewModel for Tab screens handling account change
|
||||
*/
|
||||
abstract class TabViewModel(
|
||||
private val database: Database,
|
||||
) : ViewModel(), KoinComponent {
|
||||
|
||||
/**
|
||||
* Repository intended to be rebuilt when the current account changes
|
||||
*/
|
||||
protected var repository: BaseRepository? = null
|
||||
|
||||
protected var currentAccount: Account? = null
|
||||
|
||||
/**
|
||||
* This method is called when the repository has been rebuilt from the new current account
|
||||
*/
|
||||
abstract fun invalidate()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
database.newAccountDao()
|
||||
.selectCurrentAccount()
|
||||
.distinctUntilChanged()
|
||||
.collect { account ->
|
||||
currentAccount = account
|
||||
repository = get(parameters = { parametersOf(account) })
|
||||
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.readrops.app.compose.feeds
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.readrops.app.compose.base.TabViewModel
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
|
@ -13,8 +14,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class FeedViewModel(
|
||||
private val database: Database,
|
||||
private val repository: BaseRepository,
|
||||
) : ViewModel() {
|
||||
) : TabViewModel(database) {
|
||||
|
||||
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState)
|
||||
val feedsState = _feedsState.asStateFlow()
|
||||
|
@ -29,9 +29,13 @@ class FeedViewModel(
|
|||
|
||||
fun insertFeed(url: String) {
|
||||
viewModelScope.launch(context = Dispatchers.IO) {
|
||||
repository.insertNewFeeds(listOf(url))
|
||||
repository?.insertNewFeeds(listOf(url))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sealed class FeedsState {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package com.readrops.app.compose.timelime
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.app.compose.base.TabViewModel
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Item
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -12,9 +11,8 @@ import kotlinx.coroutines.flow.catch
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
class TimelineViewModel(
|
||||
private val database: Database,
|
||||
private val repository: BaseRepository,
|
||||
) : ViewModel() {
|
||||
private val database: Database,
|
||||
) : TabViewModel(database) {
|
||||
|
||||
private val _timelineState = MutableStateFlow<TimelineState>(TimelineState.InitialState)
|
||||
val timelineState = _timelineState.asStateFlow()
|
||||
|
@ -25,23 +23,27 @@ class TimelineViewModel(
|
|||
init {
|
||||
viewModelScope.launch(context = Dispatchers.IO) {
|
||||
database.newItemDao().selectAll()
|
||||
.catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) }
|
||||
.collect {
|
||||
_timelineState.value = TimelineState.LoadedState(it)
|
||||
}
|
||||
.catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) }
|
||||
.collect {
|
||||
_timelineState.value = TimelineState.LoadedState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshTimeline() {
|
||||
_isRefreshing.value = true
|
||||
viewModelScope.launch(context = Dispatchers.IO) {
|
||||
repository.synchronize(null) {
|
||||
repository?.synchronize(null) {
|
||||
|
||||
}
|
||||
|
||||
_isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
refreshTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TimelineState {
|
||||
|
|
|
@ -2,11 +2,15 @@ package com.readrops.db.dao.newdao
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.readrops.db.entities.account.Account
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface NewAccountDao {
|
||||
interface NewAccountDao : NewBaseDao<Account> {
|
||||
|
||||
@Query("Select Count(*) From Account")
|
||||
suspend fun selectAccountCount(): Int
|
||||
|
||||
@Query("Select * From Account Where current_account = 1")
|
||||
fun selectCurrentAccount(): Flow<Account>
|
||||
}
|
Loading…
Reference in New Issue