Use Voyager on Repo screen

This commit is contained in:
Jobobby04 2022-11-10 23:33:17 -05:00
parent 02954670d4
commit 4a1a1301ff
7 changed files with 218 additions and 175 deletions

View File

@ -1,31 +1,27 @@
package eu.kanade.presentation.category package eu.kanade.presentation.category
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.repo.SourceRepoContent import eu.kanade.presentation.category.components.repo.SourceRepoContent
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter import eu.kanade.tachiyomi.ui.category.repos.RepoScreenState
import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter.Dialog
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourceRepoScreen( fun SourceRepoScreen(
presenter: RepoPresenter, state: RepoScreenState.Success,
onClickCreate: () -> Unit,
onClickDelete: (String) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -40,57 +36,23 @@ fun SourceRepoScreen(
floatingActionButton = { floatingActionButton = {
CategoryFloatingActionButton( CategoryFloatingActionButton(
lazyListState = lazyListState, lazyListState = lazyListState,
onCreate = { presenter.dialog = Dialog.Create }, onCreate = onClickCreate,
) )
}, },
) { paddingValues -> ) { paddingValues ->
val context = LocalContext.current if (state.isEmpty) {
when { EmptyScreen(
presenter.isLoading -> LoadingScreen() textResource = R.string.information_empty_category,
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_category) modifier = Modifier.padding(paddingValues),
else -> { )
SourceRepoContent( return@Scaffold
state = presenter,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
)
}
} }
val onDismissRequest = { presenter.dialog = null } SourceRepoContent(
when (val dialog = presenter.dialog) { repos = state.repos,
Dialog.Create -> { lazyListState = lazyListState,
CategoryCreateDialog( paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onDismissRequest = onDismissRequest, onClickDelete = onClickDelete,
onCreate = { presenter.createRepo(it) }, )
title = stringResource(R.string.action_add_repo),
extraMessage = stringResource(R.string.action_add_repo_message),
)
}
is Dialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = onDismissRequest,
onDelete = { presenter.deleteRepos(listOf(dialog.repo)) },
title = stringResource(R.string.delete_repo),
text = stringResource(R.string.delete_repo_confirmation, dialog.repo),
)
}
else -> {}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
is RepoPresenter.Event.RepoExists -> {
context.toast(R.string.error_repo_exists)
}
is RepoPresenter.Event.InternalError -> {
context.toast(R.string.internal_error)
}
is RepoPresenter.Event.InvalidName -> {
context.toast(R.string.invalid_repo_name)
}
}
}
}
} }
} }

View File

@ -1,27 +0,0 @@
package eu.kanade.presentation.category
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter
@Stable
interface SourceRepoState {
val isLoading: Boolean
var dialog: RepoPresenter.Dialog?
val repos: List<String>
val isEmpty: Boolean
}
fun SourceRepoState(): SourceRepoState {
return SourceRepoStateImpl()
}
class SourceRepoStateImpl : SourceRepoState {
override var isLoading: Boolean by mutableStateOf(true)
override var dialog: RepoPresenter.Dialog? by mutableStateOf(null)
override var repos: List<String> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { repos.isEmpty() }
}

View File

@ -7,17 +7,15 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.category.SourceRepoState
import eu.kanade.presentation.components.LazyColumn import eu.kanade.presentation.components.LazyColumn
import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter
@Composable @Composable
fun SourceRepoContent( fun SourceRepoContent(
state: SourceRepoState, repos: List<String>,
lazyListState: LazyListState, lazyListState: LazyListState,
paddingValues: PaddingValues, paddingValues: PaddingValues,
onClickDelete: (String) -> Unit,
) { ) {
val repos = state.repos
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,
contentPadding = paddingValues, contentPadding = paddingValues,
@ -27,7 +25,7 @@ fun SourceRepoContent(
SourceRepoListItem( SourceRepoListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
repo = repo, repo = repo,
onDelete = { state.dialog = RepoPresenter.Dialog.Delete(repo) }, onDelete = { onClickDelete(repo) },
) )
} }
} }

View File

@ -1,21 +1,20 @@
package eu.kanade.tachiyomi.ui.category.repos package eu.kanade.tachiyomi.ui.category.repos
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import eu.kanade.presentation.category.SourceRepoScreen import androidx.compose.runtime.CompositionLocalProvider
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
/** /**
* Controller to manage the categories for the users' library. * Controller to manage the categories for the users' library.
*/ */
class RepoController : FullComposeController<RepoPresenter>() { class RepoController : BasicFullComposeController() {
override fun createPresenter() = RepoPresenter()
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
SourceRepoScreen( CompositionLocalProvider(LocalRouter provides router) {
presenter = presenter, Navigator(screen = RepoScreen())
navigateUp = router::popCurrentController, }
)
} }
} }

View File

@ -1,77 +0,0 @@
package eu.kanade.tachiyomi.ui.category.repos
import android.os.Bundle
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteSourceRepos
import eu.kanade.domain.source.interactor.GetSourceRepos
import eu.kanade.presentation.category.SourceRepoState
import eu.kanade.presentation.category.SourceRepoStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [RepoController]. Used to manage the repos for the extensions.
*/
class RepoPresenter(
private val state: SourceRepoStateImpl = SourceRepoState() as SourceRepoStateImpl,
private val getSourceRepos: GetSourceRepos = Injekt.get(),
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(),
) : BasePresenter<RepoController>(), SourceRepoState by state {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events = _events.consumeAsFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
getSourceRepos.subscribe()
.collectLatest {
state.isLoading = false
state.repos = it
}
}
}
/**
* Creates and adds a new repo to the database.
*
* @param name The name of the repo to create.
*/
fun createRepo(name: String) {
presenterScope.launchIO {
when (createSourceRepo.await(name)) {
is CreateSourceRepo.Result.RepoExists -> _events.send(Event.RepoExists)
is CreateSourceRepo.Result.InvalidName -> _events.send(Event.InvalidName)
else -> {}
}
}
}
/**
* Deletes the given repos from the database.
*
* @param repos The list of repos to delete.
*/
fun deleteRepos(repos: List<String>) {
presenterScope.launchIO {
deleteSourceRepos.await(repos)
}
}
sealed class Event {
object RepoExists : Event()
object InvalidName : Event()
object InternalError : Event()
}
sealed class Dialog {
object Create : Dialog()
data class Delete(val repo: String) : Dialog()
}
}

View File

@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.ui.category.repos
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.category.SourceRepoScreen
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
class RepoScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { RepoScreenModel() }
val state by screenModel.state.collectAsState()
if (state is RepoScreenState.Loading) {
LoadingScreen()
return
}
val successState = state as RepoScreenState.Success
SourceRepoScreen(
state = successState,
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
navigateUp = router::popCurrentController,
)
when (val dialog = successState.dialog) {
null -> {}
RepoDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(it) },
title = stringResource(R.string.action_add_repo),
extraMessage = stringResource(R.string.action_add_repo_message),
)
}
is RepoDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteRepos(listOf(dialog.repo)) },
title = stringResource(R.string.delete_repo),
text = stringResource(R.string.delete_repo_confirmation, dialog.repo),
)
}
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
if (event is RepoEvent.LocalizedMessage) {
context.toast(event.stringRes)
}
}
}
}
}

View File

@ -0,0 +1,115 @@
package eu.kanade.tachiyomi.ui.category.repos
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteSourceRepos
import eu.kanade.domain.source.interactor.GetSourceRepos
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [RepoController]. Used to manage the repos for the extensions.
*/
class RepoScreenModel(
private val getSourceRepos: GetSourceRepos = Injekt.get(),
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(),
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
val events = _events.consumeAsFlow()
init {
coroutineScope.launchIO {
getSourceRepos.subscribe()
.collectLatest { repos ->
mutableState.update {
RepoScreenState.Success(
repos = repos,
)
}
}
}
}
/**
* Creates and adds a new repo to the database.
*
* @param name The name of the repo to create.
*/
fun createRepo(name: String) {
coroutineScope.launchIO {
when (createSourceRepo.await(name)) {
is CreateSourceRepo.Result.RepoExists -> _events.send(RepoEvent.RepoExists)
is CreateSourceRepo.Result.InvalidName -> _events.send(RepoEvent.InvalidName)
else -> {}
}
}
}
/**
* Deletes the given repos from the database.
*
* @param repos The list of repos to delete.
*/
fun deleteRepos(repos: List<String>) {
coroutineScope.launchIO {
deleteSourceRepos.await(repos)
}
}
fun showDialog(dialog: RepoDialog) {
mutableState.update {
when (it) {
RepoScreenState.Loading -> it
is RepoScreenState.Success -> it.copy(dialog = dialog)
}
}
}
fun dismissDialog() {
mutableState.update {
when (it) {
RepoScreenState.Loading -> it
is RepoScreenState.Success -> it.copy(dialog = null)
}
}
}
}
sealed class RepoEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : RepoEvent()
object RepoExists : LocalizedMessage(R.string.error_repo_exists)
object InvalidName : LocalizedMessage(R.string.invalid_repo_name)
object InternalError : LocalizedMessage(R.string.internal_error)
}
sealed class RepoDialog {
object Create : RepoDialog()
data class Delete(val repo: String) : RepoDialog()
}
sealed class RepoScreenState {
@Immutable
object Loading : RepoScreenState()
@Immutable
data class Success(
val repos: List<String>,
val dialog: RepoDialog? = null,
) : RepoScreenState() {
val isEmpty: Boolean
get() = repos.isEmpty()
}
}