Convert biometric times to compose
This commit is contained in:
parent
fc44ffa5af
commit
eba7d137ee
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,27 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.category.biometric
|
||||
|
||||
import android.view.LayoutInflater
|
||||
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 androidx.compose.runtime.Composable
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.davidea.flexibleadapter.helpers.UndoHelper
|
||||
import eu.kanade.presentation.category.BiometricTimesScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
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.Companion.hours
|
||||
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.
|
||||
*/
|
||||
class BiometricTimesController :
|
||||
NucleusController<CategoriesControllerBinding, BiometricTimesPresenter>(),
|
||||
FabController,
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
UndoHelper.OnActionListener {
|
||||
class BiometricTimesController : FullComposeController<BiometricTimesPresenter>() {
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
/**
|
||||
* Returns the toolbar title to show when this controller is attached.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.biometric_lock_times)
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
BiometricTimesScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = router::popCurrentController,
|
||||
openCreateDialog = ::showTimePicker,
|
||||
)
|
||||
|
||||
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) {
|
||||
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)
|
||||
@ -306,11 +34,15 @@ class BiometricTimesController :
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
val timeRange = picker.hour.hours + picker.minute.minutes
|
||||
if (startTime != null) {
|
||||
presenter.dialog = null
|
||||
presenter.createTimeRange(TimeRange(startTime, timeRange))
|
||||
} else {
|
||||
showTimePicker(timeRange)
|
||||
}
|
||||
}
|
||||
picker.addOnDismissListener {
|
||||
presenter.dialog = null
|
||||
}
|
||||
picker.show((activity as MainActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,44 +1,44 @@
|
||||
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.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
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 : BasePresenter<BiometricTimesController>() {
|
||||
|
||||
/**
|
||||
* List containing categories.
|
||||
*/
|
||||
private var timeRanges: List<TimeRange> = emptyList()
|
||||
class BiometricTimesPresenter(
|
||||
private val state: BiometricTimesStateImpl = BiometricTimesState() as BiometricTimesStateImpl,
|
||||
) : BasePresenter<BiometricTimesController>(), BiometricTimesState by state {
|
||||
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
/**
|
||||
* Called when the presenter is created.
|
||||
*
|
||||
* @param savedState The saved state of this presenter.
|
||||
*/
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.consumeAsFlow()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
preferences.authenticatorTimeRanges().asFlow().onEach { prefTimeRanges ->
|
||||
timeRanges = prefTimeRanges.toList()
|
||||
presenterScope.launchIO {
|
||||
// todo usecase
|
||||
preferences.authenticatorTimeRanges().asFlow()
|
||||
.collectLatest {
|
||||
val context = view?.activity ?: Injekt.get<Application>()
|
||||
state.isLoading = false
|
||||
state.timeRanges = it.toList()
|
||||
.mapNotNull(TimeRange::fromPreferenceString)
|
||||
|
||||
withUIContext {
|
||||
view?.setBiometricTimeItems(timeRanges.map(::BiometricTimesItem))
|
||||
.map { TimeRangeItem(it, it.getFormattedString(context)) }
|
||||
}
|
||||
}
|
||||
}.launchIn(presenterScope)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,32 +47,46 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
|
||||
* @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)) {
|
||||
launchUI {
|
||||
view?.onTimeRangeConflictsError()
|
||||
}
|
||||
return
|
||||
_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(timeRanges: List<TimeRange>) {
|
||||
fun deleteTimeRanges(timeRange: TimeRangeItem) {
|
||||
// todo usecase
|
||||
presenterScope.launchIO {
|
||||
preferences.authenticatorTimeRanges().set(
|
||||
this.timeRanges.filterNot { it in timeRanges }.map(TimeRange::toPreferenceString).toSet(),
|
||||
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) }
|
||||
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,3 @@
|
||||
package eu.kanade.tachiyomi.ui.category.biometric
|
||||
|
||||
data class TimeRangeItem(val timeRange: TimeRange, val formattedString: String)
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -394,6 +394,8 @@
|
||||
<string name="snack_tags_deleted">Tags deleted</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_time_range">Delete time range</string>
|
||||
<string name="delete_time_range_confirmation">Do you wish to delete the time range %s?</string>
|
||||
|
||||
<!-- Extension section -->
|
||||
<string name="ext_redundant">Redundant</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user