Add biometric lock times
This commit is contained in:
parent
86defec57c
commit
035fb9e755
@ -310,4 +310,6 @@ object PreferenceKeys {
|
|||||||
const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level"
|
const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level"
|
||||||
|
|
||||||
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
||||||
|
|
||||||
|
const val biometricTimeRanges = "biometric_time_ranges"
|
||||||
}
|
}
|
||||||
|
@ -415,4 +415,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0)
|
fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0)
|
||||||
|
|
||||||
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
||||||
|
|
||||||
|
fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,316 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category.biometric
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
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 eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
|
import eu.davidea.flexibleadapter.helpers.UndoHelper
|
||||||
|
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.util.system.toast
|
||||||
|
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller to manage the lock times for the biometric lock.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
class BiometricTimesController :
|
||||||
|
NucleusController<CategoriesControllerBinding, BiometricTimesPresenter>(),
|
||||||
|
FabController,
|
||||||
|
ActionMode.Callback,
|
||||||
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
|
BiometricTimesCreateDialog.Listener,
|
||||||
|
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()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the toolbar title to show when this controller is attached.
|
||||||
|
*/
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return resources?.getString(R.string.biometric_lock_times)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view of this controller.
|
||||||
|
*
|
||||||
|
* @param inflater The layout inflater to create the view from XML.
|
||||||
|
* @param container The parent view for this one.
|
||||||
|
*/
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
binding = CategoriesControllerBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after view inflation. Used to initialize the view.
|
||||||
|
*
|
||||||
|
* @param view The view of this controller.
|
||||||
|
*/
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
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.clicks()
|
||||||
|
.onEach {
|
||||||
|
BiometricTimesCreateDialog(this@BiometricTimesController).showDialog(router, null)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||||
|
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!!.root_coordinator,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startNextDialog(startTime: Duration?) {
|
||||||
|
if (startTime != null) {
|
||||||
|
BiometricTimesCreateDialog(this@BiometricTimesController, startTime).showDialog(router, null)
|
||||||
|
} else activity?.toast(R.string.biometric_lock_invalid_time_selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createTimeRange(startTime: Duration?, endTime: Duration?) {
|
||||||
|
if (startTime != null && endTime != null) {
|
||||||
|
presenter.createTimeRange(TimeRange(startTime, endTime))
|
||||||
|
} else activity?.toast(R.string.biometric_lock_invalid_time_selected)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category.biometric
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.datetime.timePicker
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import java.util.Calendar
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.hours
|
||||||
|
import kotlin.time.minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog to create a new category for the library.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
class BiometricTimesCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
|
where T : Controller, T : BiometricTimesCreateDialog.Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the new category. Value updated with each input from the user.
|
||||||
|
*/
|
||||||
|
private var startTime: Duration? = null
|
||||||
|
|
||||||
|
private var endTime: Duration? = null
|
||||||
|
|
||||||
|
constructor(target: T) : this() {
|
||||||
|
targetController = target
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(target: T, startTime: Duration) : this() {
|
||||||
|
targetController = target
|
||||||
|
this.startTime = startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when creating the dialog for this controller.
|
||||||
|
*
|
||||||
|
* @param savedViewState The saved state of this dialog.
|
||||||
|
* @return a new dialog instance.
|
||||||
|
*/
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog(activity!!)
|
||||||
|
.title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
|
||||||
|
.timePicker(show24HoursView = false) { _, datetime ->
|
||||||
|
val hour = datetime.get(Calendar.HOUR_OF_DAY)
|
||||||
|
XLog.nst().d(hour)
|
||||||
|
val minute = datetime.get(Calendar.MINUTE)
|
||||||
|
XLog.nst().d(minute)
|
||||||
|
if (hour !in 0..24 || minute !in 0..60) return@timePicker
|
||||||
|
if (startTime != null) {
|
||||||
|
endTime = hour.hours + minute.minutes
|
||||||
|
} else {
|
||||||
|
startTime = hour.hours + minute.minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.negativeButton(android.R.string.cancel)
|
||||||
|
.positiveButton(android.R.string.ok) {
|
||||||
|
if (endTime != null) {
|
||||||
|
(targetController as? Listener)?.createTimeRange(startTime, endTime)
|
||||||
|
} else {
|
||||||
|
(targetController as? Listener)?.startNextDialog(startTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun startNextDialog(startTime: Duration?)
|
||||||
|
fun createTimeRange(startTime: Duration?, endTime: Duration?)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category.biometric
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlinx.android.synthetic.main.categories_item.reorder
|
||||||
|
import kotlinx.android.synthetic.main.categories_item.title
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) : BaseFlexibleViewHolder(view, adapter) {
|
||||||
|
/**
|
||||||
|
* Binds this holder with the given category.
|
||||||
|
*
|
||||||
|
* @param timeRange The category to bind.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
fun bind(timeRange: TimeRange) {
|
||||||
|
// Set capitalized title.
|
||||||
|
title.text = timeRange.getFormattedString(itemView.context)
|
||||||
|
reorder.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
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
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category item for a recycler view.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category.biometric
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import rx.Observable
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenter of [BiometricTimesController]. Used to manage the categories of the library.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing categories.
|
||||||
|
*/
|
||||||
|
private var timeRanges: List<TimeRange> = emptyList()
|
||||||
|
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the presenter is created.
|
||||||
|
*
|
||||||
|
* @param savedState The saved state of this presenter.
|
||||||
|
*/
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges ->
|
||||||
|
timeRanges = prefTimeRanges.toList()
|
||||||
|
.mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { XLog.nst().d(it) }
|
||||||
|
|
||||||
|
Observable.just(timeRanges)
|
||||||
|
.map { it.map(::BiometricTimesItem) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeLatestCache(BiometricTimesController::setBiometricTimeItems)
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and adds a new category to the database.
|
||||||
|
*
|
||||||
|
* @param name The name of the category to create.
|
||||||
|
*/
|
||||||
|
fun createTimeRange(timeRange: TimeRange) {
|
||||||
|
// Do not allow duplicate categories.
|
||||||
|
if (timeRangeConflicts(timeRange)) {
|
||||||
|
Observable.just(Unit).subscribeFirst({ view, _ -> view.onTimeRangeConflictsError() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XLog.nst().d(timeRange)
|
||||||
|
|
||||||
|
preferences.biometricTimeRanges() += timeRange.toPreferenceString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given categories from the database.
|
||||||
|
*
|
||||||
|
* @param timeRanges The list of categories to delete.
|
||||||
|
*/
|
||||||
|
fun deleteTimeRanges(timeRanges: List<TimeRange>) {
|
||||||
|
preferences.biometricTimeRanges().set(
|
||||||
|
this.timeRanges.filterNot { it in timeRanges }.map { it.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) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category.biometric
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.SimpleTimeZone
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.hours
|
||||||
|
import kotlin.time.milliseconds
|
||||||
|
import kotlin.time.minutes
|
||||||
|
|
||||||
|
@ExperimentalTime
|
||||||
|
data class TimeRange(val startTime: Duration, val endTime: Duration) {
|
||||||
|
override fun toString(): String {
|
||||||
|
val startHour = floor(startTime.inHours).roundToInt()
|
||||||
|
val startMinute = (startTime - floor(startTime.inHours).hours).inMinutes.roundToInt()
|
||||||
|
val endHour = floor(endTime.inHours).roundToInt()
|
||||||
|
val endMinute = (endTime - floor(endTime.inHours).hours).inMinutes.roundToInt()
|
||||||
|
return String.format("%02d:%02d - %02d:%02d", startHour, startMinute, endHour, endMinute)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFormattedString(context: Context): String {
|
||||||
|
val startDate = Date(startTime.toLongMilliseconds())
|
||||||
|
val endDate = Date(endTime.toLongMilliseconds())
|
||||||
|
val format = DateFormat.getTimeFormat(context)
|
||||||
|
format.timeZone = SimpleTimeZone(0, "UTC")
|
||||||
|
|
||||||
|
return format.format(startDate) + " - " + format.format(endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toPreferenceString(): String {
|
||||||
|
return "${startTime.inMinutes},${endTime.inMinutes}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun conflictsWith(other: TimeRange): Boolean {
|
||||||
|
return startTime in other.startTime..other.endTime || endTime in other.startTime..other.endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromPreferenceString(timeRange: String): TimeRange? {
|
||||||
|
return timeRange.split(",").mapNotNull { it.toDoubleOrNull() }.let {
|
||||||
|
if (it.size != 2) null else {
|
||||||
|
TimeRange(it[0].minutes, it[1].minutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,12 @@ import androidx.biometric.BiometricManager
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.ui.category.biometric.TimeRange
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.hours
|
||||||
|
import kotlin.time.minutes
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -19,7 +24,10 @@ class SecureActivityDelegate(private val activity: FragmentActivity) {
|
|||||||
preferences.secureScreen().asFlow()
|
preferences.secureScreen().asFlow()
|
||||||
.onEach {
|
.onEach {
|
||||||
if (it) {
|
if (it) {
|
||||||
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
activity.window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
|
WindowManager.LayoutParams.FLAG_SECURE
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
}
|
}
|
||||||
@ -40,12 +48,19 @@ class SecureActivityDelegate(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
private fun isAppLocked(): Boolean {
|
private fun isAppLocked(): Boolean {
|
||||||
return locked &&
|
return locked &&
|
||||||
(
|
(
|
||||||
preferences.lockAppAfter().get() <= 0 ||
|
preferences.lockAppAfter().get() <= 0 ||
|
||||||
Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get()
|
Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get()
|
||||||
)
|
) && preferences.biometricTimeRanges().get().mapNotNull { TimeRange.fromPreferenceString(it) }.let { timeRanges ->
|
||||||
|
if (timeRanges.isNotEmpty()) {
|
||||||
|
val today: Calendar = Calendar.getInstance()
|
||||||
|
val now = today.get(Calendar.HOUR_OF_DAY).hours + today.get(Calendar.MINUTE).minutes
|
||||||
|
timeRanges.any { now in it.startTime..it.endTime }
|
||||||
|
} else true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -5,8 +5,12 @@ import androidx.preference.PreferenceScreen
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesController
|
||||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||||
import eu.kanade.tachiyomi.util.preference.intListPreference
|
import eu.kanade.tachiyomi.util.preference.intListPreference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.onClick
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||||
@ -54,5 +58,15 @@ class SettingsSecurityController : SettingsController() {
|
|||||||
titleRes = R.string.hide_notification_content
|
titleRes = R.string.hide_notification_content
|
||||||
defaultValue = false
|
defaultValue = false
|
||||||
}
|
}
|
||||||
|
preference {
|
||||||
|
titleRes = R.string.action_edit_biometric_lock_times
|
||||||
|
|
||||||
|
val timeRanges = preferences.biometricTimeRanges().get().count()
|
||||||
|
summary = context.resources.getQuantityString(R.plurals.num_lock_times, timeRanges, timeRanges)
|
||||||
|
|
||||||
|
onClick {
|
||||||
|
router.pushController(BiometricTimesController().withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,6 +187,20 @@
|
|||||||
<string name="save_chapter_as_cbz">Save Chapters as CBZ</string>
|
<string name="save_chapter_as_cbz">Save Chapters as CBZ</string>
|
||||||
<string name="save_chapter_as_cbz_level">CBZ Compression level</string>
|
<string name="save_chapter_as_cbz_level">CBZ Compression level</string>
|
||||||
|
|
||||||
|
<!-- Security settings -->
|
||||||
|
<string name="biometric_lock_times">Biometric lock times</string>
|
||||||
|
<string name="action_edit_biometric_lock_times">Edit lock times</string>
|
||||||
|
<string name="biometric_lock_times_empty">You have biometric lock times. Tap the plus button to create one.</string>
|
||||||
|
<string name="biometric_lock_time_conflicts">A lock time conflicts with one that already exists!</string>
|
||||||
|
<plurals name="num_lock_times">
|
||||||
|
<item quantity="one">%d lock time</item>
|
||||||
|
<item quantity="other">%d lock times</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="biometric_lock_start_time">Enter starting time</string>
|
||||||
|
<string name="biometric_lock_end_time">Enter end time</string>
|
||||||
|
<string name="biometric_lock_time_deleted_snack">Biometric lock time completed</string>
|
||||||
|
<string name="biometric_lock_invalid_time_selected">Invalid time selected</string>
|
||||||
|
|
||||||
<!-- Reader Settings -->
|
<!-- Reader Settings -->
|
||||||
<string name="download_threads">Download threads</string>
|
<string name="download_threads">Download threads</string>
|
||||||
<string name="download_threads_summary">Higher values can speed up image downloading significantly, but can also trigger bans. Recommended value is 2 or 3. Current value is: %s</string>
|
<string name="download_threads_summary">Higher values can speed up image downloading significantly, but can also trigger bans. Recommended value is 2 or 3. Current value is: %s</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user