Use Voyager on Repo screen
This commit is contained in:
parent
02954670d4
commit
4a1a1301ff
@ -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(
|
|
||||||
state = presenter,
|
|
||||||
lazyListState = lazyListState,
|
|
||||||
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
|
|
||||||
)
|
)
|
||||||
}
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
|
||||||
}
|
|
@ -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) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user