Add OPML import in AccountSelectionScreen
This commit is contained in:
parent
36d9aef2bb
commit
7ad5e0bc54
@ -2,6 +2,8 @@ package com.readrops.app.account.selection
|
|||||||
|
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -11,9 +13,14 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
@ -29,11 +36,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import cafe.adriel.voyager.koin.getScreenModel
|
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.api.utils.ApiUtils
|
||||||
import com.readrops.app.BuildConfig
|
import com.readrops.app.BuildConfig
|
||||||
import com.readrops.app.R
|
import com.readrops.app.R
|
||||||
|
import com.readrops.app.account.OPMLImportProgressDialog
|
||||||
import com.readrops.app.account.credentials.AccountCredentialsScreen
|
import com.readrops.app.account.credentials.AccountCredentialsScreen
|
||||||
import com.readrops.app.account.credentials.AccountCredentialsScreenMode
|
import com.readrops.app.account.credentials.AccountCredentialsScreenMode
|
||||||
import com.readrops.app.home.HomeScreen
|
import com.readrops.app.home.HomeScreen
|
||||||
|
import com.readrops.app.util.ErrorMessage
|
||||||
import com.readrops.app.util.components.AndroidScreen
|
import com.readrops.app.util.components.AndroidScreen
|
||||||
import com.readrops.app.util.components.SelectableImageText
|
import com.readrops.app.util.components.SelectableImageText
|
||||||
import com.readrops.app.util.theme.LargeSpacer
|
import com.readrops.app.util.theme.LargeSpacer
|
||||||
@ -48,28 +58,49 @@ class AccountSelectionScreen : AndroidScreen() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val screenModel = getScreenModel<AccountSelectionScreenModel>()
|
val screenModel = getScreenModel<AccountSelectionScreenModel>()
|
||||||
val state by screenModel.state.collectAsStateWithLifecycle()
|
val state by screenModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
when (state) {
|
val opmlImportLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||||
|
uri?.let { screenModel.parseOPMLFile(uri, context) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
if (state.showOPMLImportDialog) {
|
||||||
|
OPMLImportProgressDialog(
|
||||||
|
currentFeed = state.currentFeed,
|
||||||
|
feedCount = state.feedCount,
|
||||||
|
feedMax = state.feedMax
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.exception) {
|
||||||
|
if (state.exception != null) {
|
||||||
|
snackbarHostState.showSnackbar(ErrorMessage.get(state.exception!!, context))
|
||||||
|
screenModel.resetException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state.navState) {
|
||||||
is NavState.GoToHomeScreen -> {
|
is NavState.GoToHomeScreen -> {
|
||||||
// using replace makes the app crash due to a screen key conflict
|
// using replace makes the app crash due to a screen key conflict
|
||||||
navigator.replaceAll(HomeScreen())
|
navigator.replaceAll(HomeScreen())
|
||||||
}
|
}
|
||||||
|
|
||||||
is NavState.GoToAccountCredentialsScreen -> {
|
is NavState.GoToAccountCredentialsScreen -> {
|
||||||
val accountType = (state as NavState.GoToAccountCredentialsScreen).accountType
|
val accountType =
|
||||||
|
(state.navState as NavState.GoToAccountCredentialsScreen).accountType
|
||||||
val account = Account(
|
val account = Account(
|
||||||
accountType = accountType,
|
accountType = accountType,
|
||||||
accountName = stringResource(id = accountType.typeName)
|
accountName = stringResource(id = accountType.typeName)
|
||||||
)
|
)
|
||||||
|
|
||||||
navigator.push(
|
navigator.push(
|
||||||
AccountCredentialsScreen(
|
AccountCredentialsScreen(account, AccountCredentialsScreenMode.NEW_CREDENTIALS)
|
||||||
account,
|
|
||||||
AccountCredentialsScreenMode.NEW_CREDENTIALS
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
screenModel.resetNavState()
|
screenModel.resetNavState()
|
||||||
}
|
}
|
||||||
@ -77,105 +108,112 @@ class AccountSelectionScreen : AndroidScreen() {
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize()
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
) {
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.weight(1f)
|
.padding(paddingValues)
|
||||||
.padding(MaterialTheme.spacing.mediumSpacing)
|
|
||||||
) {
|
) {
|
||||||
Image(
|
Column(
|
||||||
painter = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
contentDescription = null,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.size(64.dp)
|
modifier = Modifier
|
||||||
)
|
.fillMaxSize()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(MaterialTheme.spacing.mediumSpacing)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
|
||||||
ShortSpacer()
|
ShortSpacer()
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.app_name),
|
text = stringResource(id = R.string.app_name),
|
||||||
style = MaterialTheme.typography.headlineLarge
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
)
|
)
|
||||||
|
|
||||||
LargeSpacer()
|
LargeSpacer()
|
||||||
|
|
||||||
Card {
|
Card {
|
||||||
Column(
|
Column {
|
||||||
modifier = Modifier.padding(MaterialTheme.spacing.largeSpacing)
|
Text(
|
||||||
) {
|
text = stringResource(id = R.string.choose_account),
|
||||||
Text(
|
style = MaterialTheme.typography.titleMedium,
|
||||||
text = stringResource(id = R.string.choose_account),
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
style = MaterialTheme.typography.titleMedium,
|
.padding(top = MaterialTheme.spacing.mediumSpacing)
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
MediumSpacer()
|
MediumSpacer()
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.local),
|
text = stringResource(id = R.string.local),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
modifier = Modifier.padding(start = MaterialTheme.spacing.mediumSpacing)
|
||||||
|
)
|
||||||
|
|
||||||
SelectableImageText(
|
SelectableImageText(
|
||||||
image = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
image = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
||||||
text = stringResource(id = AccountType.LOCAL.typeName),
|
text = stringResource(id = AccountType.LOCAL.typeName),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||||
padding = MaterialTheme.spacing.mediumSpacing,
|
padding = MaterialTheme.spacing.mediumSpacing,
|
||||||
imageSize = 24.dp,
|
imageSize = 24.dp,
|
||||||
onClick = { screenModel.createAccount(AccountType.LOCAL) }
|
onClick = { screenModel.createAccount(AccountType.LOCAL) }
|
||||||
)
|
)
|
||||||
|
|
||||||
SelectableImageText(
|
SelectableImageText(
|
||||||
image = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
image = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
|
||||||
text = stringResource(id = R.string.opml_import),
|
text = stringResource(id = R.string.opml_import),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||||
padding = MaterialTheme.spacing.mediumSpacing,
|
padding = MaterialTheme.spacing.mediumSpacing,
|
||||||
imageSize = 24.dp,
|
imageSize = 24.dp,
|
||||||
onClick = { }
|
onClick = { opmlImportLauncher.launch(ApiUtils.OPML_MIMETYPES.toTypedArray()) }
|
||||||
)
|
)
|
||||||
|
|
||||||
MediumSpacer()
|
MediumSpacer()
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "External",
|
text = "External",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
modifier = Modifier.padding(start = MaterialTheme.spacing.mediumSpacing)
|
||||||
|
)
|
||||||
|
|
||||||
ShortSpacer()
|
AccountType.entries.filter { it != AccountType.LOCAL }
|
||||||
|
.forEach { accountType ->
|
||||||
AccountType.entries.filter { it != AccountType.LOCAL }
|
SelectableImageText(
|
||||||
.forEach { accountType ->
|
image = adaptiveIconPainterResource(id = accountType.iconRes),
|
||||||
SelectableImageText(
|
text = stringResource(id = accountType.typeName),
|
||||||
image = adaptiveIconPainterResource(id = accountType.iconRes),
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
text = stringResource(id = accountType.typeName),
|
imageSize = 24.dp,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
spacing = MaterialTheme.spacing.mediumSpacing,
|
||||||
imageSize = 24.dp,
|
padding = MaterialTheme.spacing.mediumSpacing,
|
||||||
spacing = MaterialTheme.spacing.mediumSpacing,
|
onClick = { screenModel.createAccount(accountType) }
|
||||||
padding = MaterialTheme.spacing.mediumSpacing,
|
)
|
||||||
onClick = { screenModel.createAccount(accountType) }
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "v${BuildConfig.VERSION_NAME}",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(bottom = MaterialTheme.spacing.veryShortSpacing)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "v${BuildConfig.VERSION_NAME}",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
.padding(bottom = MaterialTheme.spacing.largeSpacing)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
package com.readrops.app.account.selection
|
package com.readrops.app.account.selection
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toFile
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import com.readrops.api.opml.OPMLParser
|
||||||
|
import com.readrops.app.repositories.BaseRepository
|
||||||
import com.readrops.db.Database
|
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.Account
|
||||||
import com.readrops.db.entities.account.AccountType
|
import com.readrops.db.entities.account.AccountType
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
@ -13,11 +19,12 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class AccountSelectionScreenModel(
|
class AccountSelectionScreenModel(
|
||||||
private val database: Database,
|
private val database: Database,
|
||||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
) : StateScreenModel<NavState>(NavState.Idle), KoinComponent {
|
) : StateScreenModel<AccountSelectionState>(AccountSelectionState()), KoinComponent {
|
||||||
|
|
||||||
fun accountExists(): Boolean {
|
fun accountExists(): Boolean {
|
||||||
val accountCount = runBlocking {
|
val accountCount = runBlocking {
|
||||||
@ -29,35 +36,98 @@ class AccountSelectionScreenModel(
|
|||||||
|
|
||||||
fun createAccount(accountType: AccountType) {
|
fun createAccount(accountType: AccountType) {
|
||||||
if (accountType == AccountType.LOCAL) {
|
if (accountType == AccountType.LOCAL) {
|
||||||
createLocalAccount()
|
screenModelScope.launch(dispatcher) {
|
||||||
|
createLocalAccount()
|
||||||
|
mutableState.update { it.copy(navState = NavState.GoToHomeScreen) }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mutableState.update { NavState.GoToAccountCredentialsScreen(accountType) }
|
mutableState.update {
|
||||||
|
it.copy(navState = NavState.GoToAccountCredentialsScreen(accountType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetNavState() {
|
fun resetNavState() {
|
||||||
mutableState.update { NavState.Idle }
|
mutableState.update { it.copy(navState = NavState.Idle) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLocalAccount() {
|
private suspend fun createLocalAccount(): Account {
|
||||||
val context = get<Context>()
|
val context = get<Context>()
|
||||||
val account = Account(
|
val account = Account(
|
||||||
url = null,
|
url = null,
|
||||||
accountName = context.getString(AccountType.LOCAL.typeName),
|
accountName = context.getString(AccountType.LOCAL.typeName),
|
||||||
accountType = AccountType.LOCAL,
|
accountType = AccountType.LOCAL,
|
||||||
isCurrentAccount = true
|
isCurrentAccount = true
|
||||||
)
|
)
|
||||||
|
|
||||||
screenModelScope.launch(dispatcher) {
|
account.id = database.accountDao().insert(account).toInt()
|
||||||
database.accountDao().insert(account)
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
mutableState.update { NavState.GoToHomeScreen }
|
fun parseOPMLFile(uri: Uri, context: Context) {
|
||||||
|
screenModelScope.launch(dispatcher) {
|
||||||
|
val foldersAndFeeds: Map<Folder?, List<Feed>>
|
||||||
|
|
||||||
|
try {
|
||||||
|
val stream = context.contentResolver.openInputStream(uri)
|
||||||
|
if (stream == null) {
|
||||||
|
mutableState.update { it.copy(exception = NoSuchFileException(uri.toFile())) }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
foldersAndFeeds = OPMLParser.read(stream)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mutableState.update { it.copy(exception = e) }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
showOPMLImportDialog = true,
|
||||||
|
currentFeed = foldersAndFeeds.values.first().first().name!!,
|
||||||
|
feedCount = 0,
|
||||||
|
feedMax = foldersAndFeeds.values.flatten().size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val account = createLocalAccount()
|
||||||
|
val repository = get<BaseRepository> { parametersOf(account) }
|
||||||
|
|
||||||
|
repository.insertOPMLFoldersAndFeeds(
|
||||||
|
foldersAndFeeds = foldersAndFeeds,
|
||||||
|
onUpdate = { feed ->
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
currentFeed = feed.name!!,
|
||||||
|
feedCount = it.feedCount + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
showOPMLImportDialog = false,
|
||||||
|
navState = NavState.GoToHomeScreen
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetException() = mutableState.update { it.copy(exception = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AccountSelectionState(
|
||||||
|
val showOPMLImportDialog: Boolean = false,
|
||||||
|
val navState: NavState = NavState.Idle,
|
||||||
|
val exception: Exception? = null,
|
||||||
|
val currentFeed: String = "",
|
||||||
|
val feedCount: Int = 0,
|
||||||
|
val feedMax: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
sealed class NavState {
|
sealed class NavState {
|
||||||
object Idle : NavState()
|
data object Idle : NavState()
|
||||||
object GoToHomeScreen : NavState()
|
data object GoToHomeScreen : NavState()
|
||||||
class GoToAccountCredentialsScreen(val accountType: AccountType) : NavState()
|
class GoToAccountCredentialsScreen(val accountType: AccountType) : NavState()
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user