Use Voyager on Biometric Times screen
This commit is contained in:
parent
fd99a5f502
commit
206aab6755
@ -1,33 +1,29 @@
|
||||
package eu.kanade.presentation.category
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.category.components.CategoryDeleteDialog
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.category.components.biometric.BiometricTimesContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.topPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesPresenter
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesPresenter.Dialog
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlin.time.Duration
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreenState
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.TimeRangeItem
|
||||
|
||||
@Composable
|
||||
fun BiometricTimesScreen(
|
||||
presenter: BiometricTimesPresenter,
|
||||
state: BiometricTimesScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickDelete: (TimeRangeItem) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
openCreateDialog: (Duration?) -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
Scaffold(
|
||||
@ -41,51 +37,23 @@ fun BiometricTimesScreen(
|
||||
floatingActionButton = {
|
||||
CategoryFloatingActionButton(
|
||||
lazyListState = lazyListState,
|
||||
onCreate = { presenter.dialog = Dialog.Create },
|
||||
onCreate = onClickCreate,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
val context = LocalContext.current
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(textResource = R.string.biometric_lock_times_empty)
|
||||
else -> {
|
||||
BiometricTimesContent(
|
||||
state = presenter,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
|
||||
)
|
||||
}
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.biometric_lock_times_empty,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
val onDismissRequest = { presenter.dialog = null }
|
||||
when (val dialog = presenter.dialog) {
|
||||
Dialog.Create -> {
|
||||
LaunchedEffect(Unit) {
|
||||
openCreateDialog(null)
|
||||
}
|
||||
}
|
||||
is Dialog.Delete -> {
|
||||
CategoryDeleteDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDelete = { presenter.deleteTimeRanges(dialog.timeRange) },
|
||||
title = stringResource(R.string.delete_time_range),
|
||||
text = stringResource(R.string.delete_time_range_confirmation, dialog.timeRange.formattedString),
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest { event ->
|
||||
when (event) {
|
||||
is BiometricTimesPresenter.Event.TimeConflicts -> {
|
||||
context.toast(R.string.biometric_lock_time_conflicts)
|
||||
}
|
||||
is BiometricTimesPresenter.Event.InternalError -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BiometricTimesContent(
|
||||
timeRanges = state.timeRanges,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
|
||||
onClickDelete = onClickDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +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.biometric.BiometricTimesPresenter
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.TimeRangeItem
|
||||
|
||||
@Stable
|
||||
interface BiometricTimesState {
|
||||
val isLoading: Boolean
|
||||
var dialog: BiometricTimesPresenter.Dialog?
|
||||
val timeRanges: List<TimeRangeItem>
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun BiometricTimesState(): BiometricTimesState {
|
||||
return BiometricTimesStateImpl()
|
||||
}
|
||||
|
||||
class BiometricTimesStateImpl : BiometricTimesState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var dialog: BiometricTimesPresenter.Dialog? by mutableStateOf(null)
|
||||
override var timeRanges: List<TimeRangeItem> by mutableStateOf(emptyList())
|
||||
override val isEmpty: Boolean by derivedStateOf { timeRanges.isEmpty() }
|
||||
}
|
@ -7,27 +7,26 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.category.BiometricTimesState
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesPresenter
|
||||
import eu.kanade.tachiyomi.ui.category.biometric.TimeRangeItem
|
||||
|
||||
@Composable
|
||||
fun BiometricTimesContent(
|
||||
state: BiometricTimesState,
|
||||
timeRanges: List<TimeRangeItem>,
|
||||
lazyListState: LazyListState,
|
||||
paddingValues: PaddingValues,
|
||||
onClickDelete: (TimeRangeItem) -> Unit,
|
||||
) {
|
||||
val timeRanges = state.timeRanges
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(timeRanges) { timeRange ->
|
||||
items(timeRanges, key = { it.formattedString }) { timeRange ->
|
||||
BiometricTimesListItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
timeRange = timeRange,
|
||||
onDelete = { state.dialog = BiometricTimesPresenter.Dialog.Delete(timeRange) },
|
||||
onDelete = { onClickDelete(timeRange) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.category.biometric
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import eu.kanade.presentation.category.BiometricTimesScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
|
||||
/**
|
||||
* Controller to manage the lock times for the biometric lock.
|
||||
*/
|
||||
class BiometricTimesController : FullComposeController<BiometricTimesPresenter>() {
|
||||
|
||||
override fun createPresenter() = BiometricTimesPresenter()
|
||||
class BiometricTimesController : BasicFullComposeController() {
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
BiometricTimesScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = router::popCurrentController,
|
||||
openCreateDialog = ::showTimePicker,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showTimePicker(startTime: Duration? = null) {
|
||||
val picker = MaterialTimePicker.Builder()
|
||||
.setTitleText(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
|
||||
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
|
||||
.build()
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
val timeRange = picker.hour.hours + picker.minute.minutes
|
||||
if (startTime != null) {
|
||||
presenter.dialog = null
|
||||
presenter.createTimeRange(TimeRange(startTime, timeRange))
|
||||
} else {
|
||||
showTimePicker(timeRange)
|
||||
}
|
||||
CompositionLocalProvider(LocalRouter provides router) {
|
||||
Navigator(screen = BiometricTimesScreen())
|
||||
}
|
||||
picker.addOnDismissListener {
|
||||
presenter.dialog = null
|
||||
}
|
||||
picker.show((activity as MainActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
@ -1,91 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.category.biometric
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import eu.kanade.presentation.category.BiometricTimesState
|
||||
import eu.kanade.presentation.category.BiometricTimesStateImpl
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
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 [BiometricTimesController]. Used to manage the categories of the library.
|
||||
*/
|
||||
class BiometricTimesPresenter(
|
||||
private val state: BiometricTimesStateImpl = BiometricTimesState() as BiometricTimesStateImpl,
|
||||
private val preferences: SecurityPreferences = Injekt.get(),
|
||||
) : BasePresenter<BiometricTimesController>(), BiometricTimesState 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 {
|
||||
// todo usecase
|
||||
preferences.authenticatorTimeRanges().changes()
|
||||
.collectLatest {
|
||||
val context = view?.activity ?: Injekt.get<Application>()
|
||||
state.isLoading = false
|
||||
state.timeRanges = it.toList()
|
||||
.mapNotNull(TimeRange::fromPreferenceString)
|
||||
.map { TimeRangeItem(it, it.getFormattedString(context)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new category to the database.
|
||||
*
|
||||
* @param name The name of the category to create.
|
||||
*/
|
||||
fun createTimeRange(timeRange: TimeRange) {
|
||||
// todo usecase
|
||||
presenterScope.launchIO {
|
||||
// Do not allow duplicate categories.
|
||||
if (timeRangeConflicts(timeRange)) {
|
||||
_events.send(Event.TimeConflicts)
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
preferences.authenticatorTimeRanges() += timeRange.toPreferenceString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given categories from the database.
|
||||
*
|
||||
* @param timeRanges The list of categories to delete.
|
||||
*/
|
||||
fun deleteTimeRanges(timeRange: TimeRangeItem) {
|
||||
// todo usecase
|
||||
presenterScope.launchIO {
|
||||
preferences.authenticatorTimeRanges().set(
|
||||
state.timeRanges.filterNot { it == timeRange }.map { it.timeRange.toPreferenceString() }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a category with the given name already exists.
|
||||
*/
|
||||
private fun timeRangeConflicts(timeRange: TimeRange): Boolean {
|
||||
return timeRanges.any { timeRange.conflictsWith(it.timeRange) }
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
object TimeConflicts : Event()
|
||||
object InternalError : Event()
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
object Create : Dialog()
|
||||
data class Delete(val timeRange: TimeRangeItem) : Dialog()
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package eu.kanade.tachiyomi.ui.category.biometric
|
||||
|
||||
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 com.google.android.material.timepicker.MaterialTimePicker
|
||||
import eu.kanade.presentation.category.BiometricTimesScreen
|
||||
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.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class BiometricTimesScreen : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val screenModel = rememberScreenModel { BiometricTimesScreenModel() }
|
||||
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
if (state is BiometricTimesScreenState.Loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val successState = state as BiometricTimesScreenState.Success
|
||||
|
||||
BiometricTimesScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(BiometricTimesDialog.Create) },
|
||||
onClickDelete = { screenModel.showDialog(BiometricTimesDialog.Delete(it)) },
|
||||
navigateUp = router::popCurrentController,
|
||||
)
|
||||
|
||||
fun showTimePicker(startTime: Duration? = null) {
|
||||
val activity = context as? MainActivity ?: return
|
||||
val picker = MaterialTimePicker.Builder()
|
||||
.setTitleText(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
|
||||
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
|
||||
.build()
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
val timeRange = picker.hour.hours + picker.minute.minutes
|
||||
if (startTime != null) {
|
||||
screenModel.dismissDialog()
|
||||
screenModel.createTimeRange(TimeRange(startTime, timeRange))
|
||||
} else {
|
||||
showTimePicker(timeRange)
|
||||
}
|
||||
}
|
||||
picker.addOnDismissListener {
|
||||
screenModel.dismissDialog()
|
||||
}
|
||||
picker.show(activity.supportFragmentManager, null)
|
||||
}
|
||||
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
BiometricTimesDialog.Create -> {
|
||||
LaunchedEffect(Unit) {
|
||||
showTimePicker()
|
||||
}
|
||||
}
|
||||
is BiometricTimesDialog.Delete -> {
|
||||
CategoryDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteTimeRanges(dialog.timeRange) },
|
||||
title = stringResource(R.string.delete_time_range),
|
||||
text = stringResource(R.string.delete_time_range_confirmation, dialog.timeRange.formattedString),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.events.collectLatest { event ->
|
||||
if (event is BiometricTimesEvent.LocalizedMessage) {
|
||||
context.toast(event.stringRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package eu.kanade.tachiyomi.ui.category.biometric
|
||||
|
||||
import android.app.Application
|
||||
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.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
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 [BiometricTimesController]. Used to manage the categories of the library.
|
||||
*/
|
||||
class BiometricTimesScreenModel(
|
||||
private val preferences: SecurityPreferences = Injekt.get(),
|
||||
) : StateScreenModel<BiometricTimesScreenState>(BiometricTimesScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<BiometricTimesEvent> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.consumeAsFlow()
|
||||
|
||||
init {
|
||||
coroutineScope.launchIO {
|
||||
// todo usecase
|
||||
preferences.authenticatorTimeRanges().changes()
|
||||
.collectLatest { times ->
|
||||
val context = Injekt.get<Application>()
|
||||
mutableState.update {
|
||||
BiometricTimesScreenState.Success(
|
||||
timeRanges = times.toList()
|
||||
.mapNotNull(TimeRange::fromPreferenceString)
|
||||
.map { TimeRangeItem(it, it.getFormattedString(context)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new category to the database.
|
||||
*
|
||||
* @param name The name of the category to create.
|
||||
*/
|
||||
fun createTimeRange(timeRange: TimeRange) {
|
||||
// todo usecase
|
||||
coroutineScope.launchIO {
|
||||
// Do not allow duplicate categories.
|
||||
if (timeRangeConflicts(timeRange)) {
|
||||
_events.send(BiometricTimesEvent.TimeConflicts)
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
preferences.authenticatorTimeRanges() += timeRange.toPreferenceString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given categories from the database.
|
||||
*
|
||||
* @param timeRanges The list of categories to delete.
|
||||
*/
|
||||
fun deleteTimeRanges(timeRange: TimeRangeItem) {
|
||||
// todo usecase
|
||||
coroutineScope.launchIO {
|
||||
val state = state.value as? BiometricTimesScreenState.Success ?: return@launchIO
|
||||
preferences.authenticatorTimeRanges().set(
|
||||
state.timeRanges.filterNot { it == timeRange }.map { it.timeRange.toPreferenceString() }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a category with the given name already exists.
|
||||
*/
|
||||
private fun timeRangeConflicts(timeRange: TimeRange): Boolean {
|
||||
val state = state.value as? BiometricTimesScreenState.Success ?: return false
|
||||
return state.timeRanges.any { timeRange.conflictsWith(it.timeRange) }
|
||||
}
|
||||
|
||||
fun showDialog(dialog: BiometricTimesDialog) {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
BiometricTimesScreenState.Loading -> it
|
||||
is BiometricTimesScreenState.Success -> it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
BiometricTimesScreenState.Loading -> it
|
||||
is BiometricTimesScreenState.Success -> it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class BiometricTimesEvent {
|
||||
sealed class LocalizedMessage(@StringRes val stringRes: Int) : BiometricTimesEvent()
|
||||
object TimeConflicts : LocalizedMessage(R.string.biometric_lock_time_conflicts)
|
||||
object InternalError : LocalizedMessage(R.string.internal_error)
|
||||
}
|
||||
|
||||
sealed class BiometricTimesDialog {
|
||||
object Create : BiometricTimesDialog()
|
||||
data class Delete(val timeRange: TimeRangeItem) : BiometricTimesDialog()
|
||||
}
|
||||
|
||||
sealed class BiometricTimesScreenState {
|
||||
|
||||
@Immutable
|
||||
object Loading : BiometricTimesScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val timeRanges: List<TimeRangeItem>,
|
||||
val dialog: BiometricTimesDialog? = null,
|
||||
) : BiometricTimesScreenState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = timeRanges.isEmpty()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user