For #356 - Gives a user the ability to delete their history
parent
6491adf029
commit
00ad9d3f6f
|
@ -13,8 +13,6 @@ import android.widget.CompoundButton
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
import io.reactivex.Observer
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.android.synthetic.main.history_delete.view.*
|
|
||||||
import kotlinx.android.synthetic.main.history_header.view.*
|
import kotlinx.android.synthetic.main.history_header.view.*
|
||||||
import kotlinx.android.synthetic.main.history_list_item.view.*
|
import kotlinx.android.synthetic.main.history_list_item.view.*
|
||||||
import mozilla.components.browser.menu.BrowserMenu
|
import mozilla.components.browser.menu.BrowserMenu
|
||||||
|
@ -226,48 +224,6 @@ class HistoryAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryDeleteViewHolder(
|
|
||||||
view: View,
|
|
||||||
private val actionEmitter: Observer<HistoryAction>
|
|
||||||
) : RecyclerView.ViewHolder(view) {
|
|
||||||
private lateinit var mode: HistoryState.Mode
|
|
||||||
|
|
||||||
private val button = view.delete_history_button.apply {
|
|
||||||
setOnClickListener {
|
|
||||||
val mode = mode
|
|
||||||
if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
|
|
||||||
actionEmitter.onNext(HistoryAction.Delete.Some(mode.selectedItems))
|
|
||||||
} else {
|
|
||||||
actionEmitter.onNext(HistoryAction.Delete.All)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val text = view.delete_history_button_text.apply {
|
|
||||||
val color = ContextCompat.getColor(context, R.color.photonRed60)
|
|
||||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_delete)
|
|
||||||
drawable?.setTint(color)
|
|
||||||
this.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(mode: HistoryState.Mode) {
|
|
||||||
this.mode = mode
|
|
||||||
|
|
||||||
val text = if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
|
|
||||||
text.context.resources.getString(R.string.history_delete_some, mode.selectedItems.size)
|
|
||||||
} else {
|
|
||||||
text.context.resources.getString(R.string.history_delete_all)
|
|
||||||
}
|
|
||||||
|
|
||||||
button.contentDescription = text
|
|
||||||
this.text.text = text
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LAYOUT_ID = R.layout.history_delete
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var historyList: HistoryList = HistoryList(emptyList())
|
private var historyList: HistoryList = HistoryList(emptyList())
|
||||||
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ class HistoryComponent(
|
||||||
|
|
||||||
override val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
|
override val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
|
||||||
when (change) {
|
when (change) {
|
||||||
is HistoryChange.Change -> state.copy(items = change.list)
|
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
|
||||||
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
|
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
|
||||||
is HistoryChange.AddItemForRemoval -> {
|
is HistoryChange.AddItemForRemoval -> {
|
||||||
val mode = state.mode
|
val mode = state.mode
|
||||||
|
|
|
@ -17,9 +17,10 @@ import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import kotlinx.android.synthetic.main.fragment_history.view.*
|
import kotlinx.android.synthetic.main.fragment_history.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import mozilla.components.support.base.feature.BackHandler
|
import mozilla.components.support.base.feature.BackHandler
|
||||||
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -29,6 +30,7 @@ import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
|
class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
@ -81,18 +83,13 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val items = requireComponents.core.historyStorage.getDetailedVisits(0)
|
reloadData()
|
||||||
.asReversed()
|
|
||||||
.mapIndexed { id, item -> HistoryItem(id, item.url, item.visitTime) }
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
getManagedEmitter<HistoryChange>().onNext(HistoryChange.Change(items))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method triggers the complexity warning. However it's actually not that hard to understand.
|
||||||
|
@SuppressWarnings("ComplexMethod")
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
getAutoDisposeObservable<HistoryAction>()
|
getAutoDisposeObservable<HistoryAction>()
|
||||||
|
@ -107,6 +104,20 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
|
||||||
.onNext(HistoryChange.RemoveItemForRemoval(it.item))
|
.onNext(HistoryChange.RemoveItemForRemoval(it.item))
|
||||||
is HistoryAction.BackPressed -> getManagedEmitter<HistoryChange>()
|
is HistoryAction.BackPressed -> getManagedEmitter<HistoryChange>()
|
||||||
.onNext(HistoryChange.ExitEditMode)
|
.onNext(HistoryChange.ExitEditMode)
|
||||||
|
is HistoryAction.Delete.All -> launch(Dispatchers.IO) {
|
||||||
|
requireComponents.core.historyStorage.deleteEverything()
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
is HistoryAction.Delete.One -> launch(Dispatchers.IO) {
|
||||||
|
requireComponents.core.historyStorage.deleteVisit(it.item.url, it.item.visitedAt)
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
is HistoryAction.Delete.Some -> launch(Dispatchers.IO) {
|
||||||
|
it.items.forEach { item ->
|
||||||
|
requireComponents.core.historyStorage.deleteVisit(item.url, item.visitedAt)
|
||||||
|
}
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,4 +139,16 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean = (historyComponent.uiView as HistoryUIView).onBackPressed()
|
override fun onBackPressed(): Boolean = (historyComponent.uiView as HistoryUIView).onBackPressed()
|
||||||
|
|
||||||
|
private suspend fun reloadData() {
|
||||||
|
val items = requireComponents.core.historyStorage.getDetailedVisits(0)
|
||||||
|
.asReversed()
|
||||||
|
.mapIndexed { id, item -> HistoryItem(id, item.url, item.visitTime) }
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
getManagedEmitter<HistoryChange>().onNext(HistoryChange.Change(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,12 @@ package org.mozilla.fenix.library.history
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Observer
|
import io.reactivex.Observer
|
||||||
import io.reactivex.functions.Consumer
|
import io.reactivex.functions.Consumer
|
||||||
|
import kotlinx.android.synthetic.main.component_history.view.*
|
||||||
import mozilla.components.support.base.feature.BackHandler
|
import mozilla.components.support.base.feature.BackHandler
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.mvi.UIView
|
import org.mozilla.fenix.mvi.UIView
|
||||||
|
@ -26,22 +27,48 @@ class HistoryUIView(
|
||||||
var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override val view: RecyclerView = LayoutInflater.from(container.context)
|
override val view: NestedScrollView = LayoutInflater.from(container.context)
|
||||||
.inflate(R.layout.component_history, container, true)
|
.inflate(R.layout.component_history, container, true)
|
||||||
.findViewById(R.id.history_list)
|
.findViewById(R.id.history_wrapper)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
view.apply {
|
view.history_list.apply {
|
||||||
adapter = HistoryAdapter(actionEmitter)
|
adapter = HistoryAdapter(actionEmitter)
|
||||||
layoutManager = LinearLayoutManager(container.context)
|
layoutManager = LinearLayoutManager(container.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.delete_history_button.setOnClickListener {
|
||||||
|
val mode = mode
|
||||||
|
val action = when (mode) {
|
||||||
|
is HistoryState.Mode.Normal -> HistoryAction.Delete.All
|
||||||
|
is HistoryState.Mode.Editing -> HistoryAction.Delete.Some(mode.selectedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionEmitter.onNext(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateView() = Consumer<HistoryState> {
|
override fun updateView() = Consumer<HistoryState> {
|
||||||
mode = it.mode
|
mode = it.mode
|
||||||
(view.adapter as HistoryAdapter).updateData(it.items, it.mode)
|
updateDeleteButton()
|
||||||
|
(view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateDeleteButton() {
|
||||||
|
val mode = mode
|
||||||
|
|
||||||
|
val text = if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
|
||||||
|
view.delete_history_button_text.context.resources.getString(
|
||||||
|
R.string.history_delete_some,
|
||||||
|
mode.selectedItems.size
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
view.delete_history_button_text.context.resources.getString(R.string.history_delete_all)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.delete_history_button.contentDescription = text
|
||||||
|
view.delete_history_button_text.text = text
|
||||||
|
}
|
||||||
override fun onBackPressed(): Boolean {
|
override fun onBackPressed(): Boolean {
|
||||||
if (mode is HistoryState.Mode.Editing) {
|
if (mode is HistoryState.Mode.Editing) {
|
||||||
actionEmitter.onNext(HistoryAction.BackPressed)
|
actionEmitter.onNext(HistoryAction.BackPressed)
|
||||||
|
|
|
@ -3,8 +3,42 @@
|
||||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.core.widget.NestedScrollView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
android:id="@+id/history_wrapper"
|
||||||
android:id="@+id/history_list"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/delete_history_button"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:background="@drawable/button_background"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/delete_history_button_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/history_delete_all"
|
||||||
|
android:textColor="@color/photonRed60"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
</FrameLayout>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/history_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/delete_history_button"
|
|
||||||
android:foreground="?android:attr/selectableItemBackground"
|
|
||||||
android:background="@drawable/button_background"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/delete_history_button_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/history_delete_all"
|
|
||||||
android:textColor="@color/photonRed60"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:clickable="false"
|
|
||||||
android:focusable="false"
|
|
||||||
android:layout_gravity="center" />
|
|
||||||
</FrameLayout>
|
|
Loading…
Reference in New Issue