Convert biometric times to compose

This commit is contained in:
Jobobby04 2022-09-10 14:06:39 -04:00
parent fc44ffa5af
commit eba7d137ee
14 changed files with 269 additions and 529 deletions

View File

@ -0,0 +1,91 @@
package eu.kanade.presentation.category
import androidx.compose.foundation.layout.PaddingValues
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.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
@Composable
fun BiometricTimesScreen(
presenter: BiometricTimesPresenter,
navigateUp: () -> Unit,
openCreateDialog: (Duration?) -> Unit,
) {
val lazyListState = rememberLazyListState()
Scaffold(
topBar = { scrollBehavior ->
AppBar(
navigateUp = navigateUp,
title = stringResource(R.string.biometric_lock_times),
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
CategoryFloatingActionButton(
lazyListState = lazyListState,
onCreate = { presenter.dialog = Dialog.Create },
)
},
) { 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),
)
}
}
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)
}
}
}
}
}
}

View File

@ -0,0 +1,28 @@
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() }
}

View File

@ -0,0 +1,34 @@
package eu.kanade.presentation.category.components.biometric
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
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
@Composable
fun BiometricTimesContent(
state: BiometricTimesState,
lazyListState: LazyListState,
paddingValues: PaddingValues,
) {
val timeRanges = state.timeRanges
LazyColumn(
state = lazyListState,
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(timeRanges) { timeRange ->
BiometricTimesListItem(
modifier = Modifier.animateItemPlacement(),
timeRange = timeRange,
onDelete = { state.dialog = BiometricTimesPresenter.Dialog.Delete(timeRange) },
)
}
}
}

View File

@ -0,0 +1,45 @@
package eu.kanade.presentation.category.components.biometric
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.ui.category.biometric.TimeRangeItem
@Composable
fun BiometricTimesListItem(
modifier: Modifier,
timeRange: TimeRangeItem,
onDelete: () -> Unit,
) {
ElevatedCard(
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text(text = timeRange.formattedString, modifier = Modifier.padding(start = horizontalPadding))
}
Row {
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
}
}
}
}

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.ui.category.biometric
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
* Custom adapter for categories.
*
* @param controller The containing controller.
*/
class BiometricTimesAdapter(controller: BiometricTimesController) :
FlexibleAdapter<BiometricTimesItem>(null, controller, true) {
/**
* Clears the active selections from the list and the model.
*/
override fun clearSelection() {
super.clearSelection()
(0 until itemCount).forEach { getItem(it)?.isSelected = false }
}
/**
* Toggles the selection of the given position.
*
* @param position The position to toggle.
*/
override fun toggleSelection(position: Int) {
super.toggleSelection(position)
getItem(position)?.isSelected = isSelected(position)
}
}

View File

@ -1,27 +1,11 @@
package eu.kanade.tachiyomi.ui.category.biometric package eu.kanade.tachiyomi.ui.category.biometric
import android.view.LayoutInflater import androidx.compose.runtime.Composable
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import dev.chrisbanes.insetter.applyInsetter import eu.kanade.presentation.category.BiometricTimesScreen
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@ -29,276 +13,20 @@ import kotlin.time.Duration.Companion.minutes
/** /**
* Controller to manage the lock times for the biometric lock. * Controller to manage the lock times for the biometric lock.
*/ */
class BiometricTimesController : class BiometricTimesController : FullComposeController<BiometricTimesPresenter>() {
NucleusController<CategoriesControllerBinding, BiometricTimesPresenter>(),
FabController,
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
UndoHelper.OnActionListener {
/**
* Object used to show ActionMode toolbar.
*/
private var actionMode: ActionMode? = null
/**
* Adapter containing biometric lock time items.
*/
private var adapter: BiometricTimesAdapter? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/**
* Undo helper used for restoring a deleted lock time.
*/
private var undoHelper: UndoHelper? = null
/**
* Creates the presenter for this controller. Not to be manually called.
*/
override fun createPresenter() = BiometricTimesPresenter() override fun createPresenter() = BiometricTimesPresenter()
/** @Composable
* Returns the toolbar title to show when this controller is attached. override fun ComposeContent() {
*/ BiometricTimesScreen(
override fun getTitle(): String? { presenter = presenter,
return resources?.getString(R.string.biometric_lock_times) navigateUp = router::popCurrentController,
openCreateDialog = ::showTimePicker,
)
} }
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) private fun showTimePicker(startTime: Duration? = null) {
/**
* Called after view inflation. Used to initialize the view.
*
* @param view The view of this controller.
*/
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = BiometricTimesAdapter(this@BiometricTimesController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
binding.recycler.adapter = adapter
adapter?.isPermanentDelete = false
actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
actionFab = fab
fab.setText(R.string.action_add)
fab.setIconResource(R.drawable.ic_add_24dp)
fab.setOnClickListener {
showTimePicker()
}
}
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
fab.setOnClickListener(null)
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
actionFab = null
}
/**
* Called when the view is being destroyed. Used to release references and remove callbacks.
*
* @param view The view of this controller.
*/
override fun onDestroyView(view: View) {
// Manually call callback to delete lock times if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null
actionMode = null
adapter = null
super.onDestroyView(view)
}
/**
* Called from the presenter when the biometric lock times are updated.
*
* @param biometricTimeItems The new list of lock times to display.
*/
fun setBiometricTimeItems(biometricTimeItems: List<BiometricTimesItem>) {
actionMode?.finish()
adapter?.updateDataSet(biometricTimeItems)
if (biometricTimeItems.isNotEmpty()) {
binding.emptyView.hide()
val selected = biometricTimeItems.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(biometricTimeItems.indexOf(it)) }
}
} else {
binding.emptyView.show(R.string.biometric_lock_times_empty)
}
}
/**
* Called when action mode is first created. The menu supplied will be used to generate action
* buttons for the action mode.
*
* @param mode ActionMode being created.
* @param menu Menu used to populate action buttons.
* @return true if the action mode should be created, false if entering this mode should be
* aborted.
*/
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
// Inflate menu.
mode.menuInflater.inflate(R.menu.category_selection, menu)
// Enable adapter multi selection.
adapter?.mode = SelectableAdapter.Mode.MULTI
return true
}
/**
* Called to refresh an action mode's action menu whenever it is invalidated.
*
* @param mode ActionMode being prepared.
* @param menu Menu used to populate action buttons.
* @return true if the menu or action mode was updated, false otherwise.
*/
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val adapter = adapter ?: return false
val count = adapter.selectedItemCount
mode.title = count.toString()
// Show edit button only when one item is selected
val editItem = mode.menu.findItem(R.id.action_edit)
editItem.isVisible = false
return true
}
/**
* Called to report a user click on an action button.
*
* @param mode The current ActionMode.
* @param item The item that was clicked.
* @return true if this callback handled the event, false if the standard MenuItem invocation
* should continue.
*/
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val adapter = adapter ?: return false
when (item.itemId) {
R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this)
undoHelper?.start(
adapter.selectedPositions,
(activity as? MainActivity)?.binding?.rootCoordinator!!,
R.string.biometric_lock_time_deleted_snack,
R.string.action_undo,
3000,
)
mode.finish()
}
else -> return false
}
return true
}
/**
* Called when an action mode is about to be exited and destroyed.
*
* @param mode The current ActionMode being destroyed.
*/
override fun onDestroyActionMode(mode: ActionMode) {
// Reset adapter to single selection
adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection()
actionMode = null
}
/**
* Called when an item in the list is clicked.
*
* @param position The position of the clicked item.
* @return true if this click should enable selection mode.
*/
override fun onItemClick(view: View, position: Int): Boolean {
// Check if action mode is initialized and selected item exist.
return if (actionMode != null && position != RecyclerView.NO_POSITION) {
toggleSelection(position)
true
} else {
false
}
}
/**
* Called when an item in the list is long clicked.
*
* @param position The position of the clicked item.
*/
override fun onItemLongClick(position: Int) {
val activity = activity as? AppCompatActivity ?: return
// Check if action mode is initialized.
if (actionMode == null) {
// Initialize action mode
actionMode = activity.startSupportActionMode(this)
}
// Set item as selected
toggleSelection(position)
}
/**
* Toggle the selection state of an item.
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
*
* @param position The position of the item to toggle.
*/
private fun toggleSelection(position: Int) {
val adapter = adapter ?: return
// Mark the position selected
adapter.toggleSelection(position)
if (adapter.selectedItemCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
/**
* Called when the undo action is clicked in the snackbar.
*
* @param action The action performed.
*/
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
adapter?.restoreDeletedItems()
undoHelper = null
}
/**
* Called when the time to restore the items expires.
*
* @param action The action performed.
* @param event The event that triggered the action
*/
override fun onActionConfirmed(action: Int, event: Int) {
val adapter = adapter ?: return
presenter.deleteTimeRanges(adapter.deletedItems.map { it.timeRange })
undoHelper = null
}
/**
* Called from the presenter when a time range conflicts with another.
*/
fun onTimeRangeConflictsError() {
activity?.toast(R.string.biometric_lock_time_conflicts)
}
fun showTimePicker(startTime: Duration? = null) {
val picker = MaterialTimePicker.Builder() val picker = MaterialTimePicker.Builder()
.setTitleText(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time) .setTitleText(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK) .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
@ -306,11 +34,15 @@ class BiometricTimesController :
picker.addOnPositiveButtonClickListener { picker.addOnPositiveButtonClickListener {
val timeRange = picker.hour.hours + picker.minute.minutes val timeRange = picker.hour.hours + picker.minute.minutes
if (startTime != null) { if (startTime != null) {
presenter.dialog = null
presenter.createTimeRange(TimeRange(startTime, timeRange)) presenter.createTimeRange(TimeRange(startTime, timeRange))
} else { } else {
showTimePicker(timeRange) showTimePicker(timeRange)
} }
} }
picker.addOnDismissListener {
presenter.dialog = null
}
picker.show((activity as MainActivity).supportFragmentManager, null) picker.show((activity as MainActivity).supportFragmentManager, null)
} }
} }

View File

@ -1,33 +0,0 @@
package eu.kanade.tachiyomi.ui.category.biometric
import android.view.View
import androidx.core.view.isVisible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.CategoriesItemBinding
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlin.time.ExperimentalTime
/**
* Holder used to display category items.
*
* @param view The view used by category items.
* @param adapter The adapter containing this holder.
*/
class BiometricTimesHolder(view: View, val adapter: BiometricTimesAdapter) : FlexibleViewHolder(view, adapter) {
private val binding = CategoriesItemBinding.bind(view)
/**
* Binds this holder with the given category.
*
* @param timeRange The category to bind.
*/
@OptIn(ExperimentalTime::class)
fun bind(timeRange: TimeRange) {
binding.innerContainer.minimumHeight = 60.dpToPx
// Set capitalized title.
binding.title.text = timeRange.getFormattedString(itemView.context)
binding.reorder.isVisible = false
}
}

View File

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.ui.category.biometric
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Category item for a recycler view.
*/
class BiometricTimesItem(val timeRange: TimeRange) : AbstractFlexibleItem<BiometricTimesHolder>() {
/**
* Whether this item is currently selected.
*/
var isSelected = false
/**
* Returns the layout resource for this item.
*/
override fun getLayoutRes(): Int {
return R.layout.categories_item
}
/**
* Returns a new view holder for this item.
*
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): BiometricTimesHolder {
return BiometricTimesHolder(view, adapter as BiometricTimesAdapter)
}
/**
* Binds the given view holder with this item.
*
* @param adapter The adapter of this item.
* @param holder The holder to bind.
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: BiometricTimesHolder,
position: Int,
payloads: List<Any?>?,
) {
holder.bind(timeRange)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return false
}
override fun hashCode(): Int {
return timeRange.hashCode()
}
}

View File

@ -1,44 +1,44 @@
package eu.kanade.tachiyomi.ui.category.biometric package eu.kanade.tachiyomi.ui.category.biometric
import android.app.Application
import android.os.Bundle import android.os.Bundle
import eu.kanade.presentation.category.BiometricTimesState
import eu.kanade.presentation.category.BiometricTimesStateImpl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.plusAssign import eu.kanade.tachiyomi.util.preference.plusAssign
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
/** /**
* Presenter of [BiometricTimesController]. Used to manage the categories of the library. * Presenter of [BiometricTimesController]. Used to manage the categories of the library.
*/ */
class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() { class BiometricTimesPresenter(
private val state: BiometricTimesStateImpl = BiometricTimesState() as BiometricTimesStateImpl,
/** ) : BasePresenter<BiometricTimesController>(), BiometricTimesState by state {
* List containing categories.
*/
private var timeRanges: List<TimeRange> = emptyList()
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
/** private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
* Called when the presenter is created. val events = _events.consumeAsFlow()
*
* @param savedState The saved state of this presenter.
*/
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
presenterScope.launchIO {
preferences.authenticatorTimeRanges().asFlow().onEach { prefTimeRanges -> // todo usecase
timeRanges = prefTimeRanges.toList() preferences.authenticatorTimeRanges().asFlow()
.mapNotNull(TimeRange::fromPreferenceString) .collectLatest {
val context = view?.activity ?: Injekt.get<Application>()
withUIContext { state.isLoading = false
view?.setBiometricTimeItems(timeRanges.map(::BiometricTimesItem)) state.timeRanges = it.toList()
} .mapNotNull(TimeRange::fromPreferenceString)
}.launchIn(presenterScope) .map { TimeRangeItem(it, it.getFormattedString(context)) }
}
}
} }
/** /**
@ -47,15 +47,16 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
* @param name The name of the category to create. * @param name The name of the category to create.
*/ */
fun createTimeRange(timeRange: TimeRange) { fun createTimeRange(timeRange: TimeRange) {
// Do not allow duplicate categories. // todo usecase
if (timeRangeConflicts(timeRange)) { presenterScope.launchIO {
launchUI { // Do not allow duplicate categories.
view?.onTimeRangeConflictsError() if (timeRangeConflicts(timeRange)) {
_events.send(Event.TimeConflicts)
return@launchIO
} }
return
}
preferences.authenticatorTimeRanges() += timeRange.toPreferenceString() preferences.authenticatorTimeRanges() += timeRange.toPreferenceString()
}
} }
/** /**
@ -63,16 +64,29 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
* *
* @param timeRanges The list of categories to delete. * @param timeRanges The list of categories to delete.
*/ */
fun deleteTimeRanges(timeRanges: List<TimeRange>) { fun deleteTimeRanges(timeRange: TimeRangeItem) {
preferences.authenticatorTimeRanges().set( // todo usecase
this.timeRanges.filterNot { it in timeRanges }.map(TimeRange::toPreferenceString).toSet(), 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. * Returns true if a category with the given name already exists.
*/ */
private fun timeRangeConflicts(timeRange: TimeRange): Boolean { private fun timeRangeConflicts(timeRange: TimeRange): Boolean {
return timeRanges.any { timeRange.conflictsWith(it) } 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()
} }
} }

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.category.biometric
data class TimeRangeItem(val timeRange: TimeRange, val formattedString: String)

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoice"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_list_padding"
tools:listitem="@layout/categories_item" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?android:attr/colorBackground"
app:cardElevation="0dp"
app:cardForegroundColor="@color/draggable_card_foreground">
<LinearLayout
android:id="@+id/inner_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/reorder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:scaleType="center"
app:srcCompat="@drawable/ic_drag_handle_24dp"
app:tint="?android:attr/textColorHint"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="Category Title" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_edit"
android:icon="@drawable/ic_edit_24dp"
android:title="@string/action_edit"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_24dp"
android:title="@string/action_delete"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View File

@ -394,6 +394,8 @@
<string name="snack_tags_deleted">Tags deleted</string> <string name="snack_tags_deleted">Tags deleted</string>
<string name="delete_tag">Delete tag</string> <string name="delete_tag">Delete tag</string>
<string name="delete_tag_confirmation">Do you wish to delete the tag %s</string> <string name="delete_tag_confirmation">Do you wish to delete the tag %s</string>
<string name="delete_time_range">Delete time range</string>
<string name="delete_time_range_confirmation">Do you wish to delete the time range %s?</string>
<!-- Extension section --> <!-- Extension section -->
<string name="ext_redundant">Redundant</string> <string name="ext_redundant">Redundant</string>