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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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="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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user