1
0
Fork 0

For #563: Restyles history management (#2378)

master
Sawyer Blatz 2019-05-10 09:58:54 -07:00 committed by GitHub
parent 005f53965f
commit e1cdeffe8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 317 additions and 107 deletions

View File

@ -67,7 +67,7 @@ sealed class BookmarkAction : Action {
data class Deselect(val item: BookmarkNode) : BookmarkAction() data class Deselect(val item: BookmarkNode) : BookmarkAction()
data class Delete(val item: BookmarkNode) : BookmarkAction() data class Delete(val item: BookmarkNode) : BookmarkAction()
object BackPressed : BookmarkAction() object BackPressed : BookmarkAction()
object ModeChanged : BookmarkAction() object SwitchMode : BookmarkAction()
} }
sealed class BookmarkChange : Change { sealed class BookmarkChange : Change {

View File

@ -231,7 +231,7 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
refreshBookmarks(components) refreshBookmarks(components)
} }
} }
is BookmarkAction.ModeChanged -> activity?.invalidateOptionsMenu() is BookmarkAction.SwitchMode -> activity?.invalidateOptionsMenu()
} }
} }

View File

@ -63,7 +63,7 @@ class BookmarkUIView(
} }
if (it.mode != mode) { if (it.mode != mode) {
mode = it.mode mode = it.mode
actionEmitter.onNext(BookmarkAction.ModeChanged) actionEmitter.onNext(BookmarkAction.SwitchMode)
} }
bookmarkAdapter.updateData(it.tree, it.mode) bookmarkAdapter.updateData(it.tree, it.mode)
when (val modeCopy = mode) { when (val modeCopy = mode) {
@ -78,7 +78,7 @@ class BookmarkUIView(
mode = BookmarkState.Mode.Normal mode = BookmarkState.Mode.Normal
bookmarkAdapter.updateData(tree, mode) bookmarkAdapter.updateData(tree, mode)
setUIForNormalMode(tree) setUIForNormalMode(tree)
actionEmitter.onNext(BookmarkAction.ModeChanged) actionEmitter.onNext(BookmarkAction.SwitchMode)
true true
} }
canGoBack -> { canGoBack -> {
@ -113,7 +113,7 @@ class BookmarkUIView(
context.getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size) context.getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size)
setToolbarColors( setToolbarColors(
R.color.white_color, R.color.white_color,
R.attr.accentBright.getColorIntFromAttr(context!!) R.attr.accentHighContrast.getColorIntFromAttr(context!!)
) )
} }

View File

@ -98,10 +98,13 @@ class HistoryAdapter(
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
private lateinit var job: Job private lateinit var job: Job
var selected = listOf<HistoryItem>()
fun updateData(items: List<HistoryItem>, mode: HistoryState.Mode) { fun updateData(items: List<HistoryItem>, mode: HistoryState.Mode) {
this.historyList = HistoryList(items) this.historyList = HistoryList(items)
this.mode = mode this.mode = mode
this.selected = if (mode is HistoryState.Mode.Editing) mode.selectedItems else listOf()
notifyDataSetChanged() notifyDataSetChanged()
} }

View File

@ -30,6 +30,7 @@ class HistoryComponent(
bus.getManagedEmitter(HistoryAction::class.java), bus.getManagedEmitter(HistoryAction::class.java),
bus.getSafeManagedObservable(HistoryChange::class.java) bus.getSafeManagedObservable(HistoryChange::class.java)
) { ) {
override fun initView() = HistoryUIView(container, actionEmitter, changesObservable) override fun initView() = HistoryUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<HistoryState> = override fun render(): Observable<HistoryState> =
@ -51,11 +52,12 @@ data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : ViewStat
} }
sealed class HistoryAction : Action { sealed class HistoryAction : Action {
data class Select(val item: HistoryItem) : HistoryAction() data class Open(val item: HistoryItem) : HistoryAction()
data class EnterEditMode(val item: HistoryItem) : HistoryAction() data class EnterEditMode(val item: HistoryItem) : HistoryAction()
object BackPressed : HistoryAction() object BackPressed : HistoryAction()
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction() data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction() data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
object SwitchMode : HistoryAction()
sealed class Delete : HistoryAction() { sealed class Delete : HistoryAction() {
object All : Delete() object All : Delete()
@ -99,10 +101,13 @@ class HistoryViewModel(initialState: HistoryState, changesObservable: Observable
} }
} }
is HistoryChange.RemoveItemForRemoval -> { is HistoryChange.RemoveItemForRemoval -> {
val mode = state.mode var mode = state.mode
if (mode is HistoryState.Mode.Editing) { if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != change.item.id } val items = mode.selectedItems.filter { it.id != change.item.id }
state.copy(mode = mode.copy(selectedItems = items)) mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing(items)
state.copy(mode = mode)
} else { } else {
state state
} }

View File

@ -4,6 +4,8 @@
package org.mozilla.fenix.library.history package org.mozilla.fenix.library.history
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
@ -13,23 +15,30 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
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.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import mozilla.components.concept.storage.VisitType import mozilla.components.concept.storage.VisitType
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -39,6 +48,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
private lateinit var job: Job private lateinit var job: Job
private lateinit var historyComponent: HistoryComponent private lateinit var historyComponent: HistoryComponent
private val navigation by lazy { Navigation.findNavController(requireView()) }
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.Main + job
@ -66,7 +76,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
(activity as AppCompatActivity).supportActionBar?.show() (activity as AppCompatActivity).supportActionBar?.show()
} }
private fun selectItem(item: HistoryItem) { private fun openItem(item: HistoryItem) {
(activity as HomeActivity).openToBrowserAndLoad( (activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = item.url, searchTermOrURL = item.url,
newTab = false, newTab = false,
@ -79,7 +89,19 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library_menu, menu) when (val mode = (historyComponent.uiView as HistoryUIView).mode) {
HistoryState.Mode.Normal -> inflater.inflate(R.menu.library_menu, menu)
is HistoryState.Mode.Editing -> {
inflater.inflate(R.menu.history_select_multi, menu)
menu.findItem(R.id.share_history_multi_select)?.run {
isVisible = mode.selectedItems.isNotEmpty()
icon.colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(context!!, R.color.white_color),
PorterDuff.Mode.SRC_IN
)
}
}
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -96,7 +118,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
getAutoDisposeObservable<HistoryAction>() getAutoDisposeObservable<HistoryAction>()
.subscribe { .subscribe {
when (it) { when (it) {
is HistoryAction.Select -> selectItem(it.item) is HistoryAction.Open -> openItem(it.item)
is HistoryAction.EnterEditMode -> getManagedEmitter<HistoryChange>() is HistoryAction.EnterEditMode -> getManagedEmitter<HistoryChange>()
.onNext(HistoryChange.EnterEditMode(it.item)) .onNext(HistoryChange.EnterEditMode(it.item))
is HistoryAction.AddItemForRemoval -> getManagedEmitter<HistoryChange>() is HistoryAction.AddItemForRemoval -> getManagedEmitter<HistoryChange>()
@ -119,17 +141,59 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
} }
reloadData() reloadData()
} }
is HistoryAction.SwitchMode -> activity?.invalidateOptionsMenu()
} }
} }
} }
@Suppress("ComplexMethod")
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.share_history_multi_select -> {
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
when {
selectedHistory.size == 1 -> context?.share(selectedHistory.first().url)
selectedHistory.size > 1 -> ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "2377")
}
true
}
R.id.libraryClose -> { R.id.libraryClose -> {
Navigation.findNavController(requireActivity(), R.id.container) Navigation.findNavController(requireActivity(), R.id.container)
.popBackStack(R.id.libraryFragment, true) .popBackStack(R.id.libraryFragment, true)
true true
} }
R.id.delete_history_multi_select -> {
val components = context?.applicationContext?.components!!
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
CoroutineScope(Main).launch {
deleteSelectedHistory(selectedHistory, components)
reloadData()
}
true
}
R.id.open_history_in_new_tabs_multi_select -> {
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
selectedHistory.forEach {
requireComponents.useCases.tabsUseCases.addTab.invoke(it.url)
}
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Normal
(activity as HomeActivity).supportActionBar?.hide()
navigation.navigate(HistoryFragmentDirections.actionHistoryFragmentToHomeFragment())
true
}
R.id.open_history_in_private_tabs_multi_select -> {
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
selectedHistory.forEach {
requireComponents.useCases.tabsUseCases.addPrivateTab.invoke(it.url)
}
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
(activity as HomeActivity).supportActionBar?.hide()
navigation.navigate(HistoryFragmentDirections.actionHistoryFragmentToHomeFragment())
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@ -171,4 +235,13 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
} }
} }
} }
private suspend fun deleteSelectedHistory(
selected: List<HistoryItem>,
components: Components = requireComponents
) {
selected.forEach {
components.core.historyStorage.deleteVisit(it.url, it.visitedAt)
}
}
} }

View File

@ -4,9 +4,15 @@
package org.mozilla.fenix.library.history package org.mozilla.fenix.library.history
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Observer import io.reactivex.Observer
@ -14,6 +20,8 @@ import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.component_history.view.* 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.ext.asActivity
import org.mozilla.fenix.ext.getColorIntFromAttr
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
class HistoryUIView( class HistoryUIView(
@ -27,28 +35,103 @@ class HistoryUIView(
var mode: HistoryState.Mode = HistoryState.Mode.Normal var mode: HistoryState.Mode = HistoryState.Mode.Normal
private set private set
private val historyAdapter: HistoryAdapter
private var items: List<HistoryItem> = listOf()
private val context = container.context
private val activity = context?.asActivity()
fun getSelected(): List<HistoryItem> = historyAdapter.selected
override val view: LinearLayout = LayoutInflater.from(container.context) override val view: LinearLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_history, container, true) .inflate(R.layout.component_history, container, true)
.findViewById(R.id.history_wrapper) .findViewById(R.id.history_wrapper)
init { init {
view.history_list.apply { view.history_list.apply {
adapter = HistoryAdapter(actionEmitter) historyAdapter = HistoryAdapter(actionEmitter)
adapter = historyAdapter
layoutManager = LinearLayoutManager(container.context) layoutManager = LinearLayoutManager(container.context)
} }
} }
override fun updateView() = Consumer<HistoryState> { override fun updateView() = Consumer<HistoryState> {
mode = it.mode if (it.mode != mode) {
mode = it.mode
actionEmitter.onNext(HistoryAction.SwitchMode)
}
(view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode) (view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode)
items = it.items
when (val modeCopy = mode) {
is HistoryState.Mode.Normal -> setUIForNormalMode()
is HistoryState.Mode.Editing -> setUIForSelectingMode(modeCopy)
}
}
private fun setUIForSelectingMode(
mode: HistoryState.Mode.Editing
) {
(activity as? AppCompatActivity)?.title =
context.getString(R.string.history_multi_select_title, mode.selectedItems.size)
setToolbarColors(
R.color.white_color,
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
)
}
private fun setUIForNormalMode() {
(activity as? AppCompatActivity)?.title = context.getString(R.string.library_history)
setToolbarColors(
R.attr.primaryText.getColorIntFromAttr(context!!),
R.attr.foundation.getColorIntFromAttr(context)
)
}
private fun setToolbarColors(foreground: Int, background: Int) {
val toolbar = (activity as AppCompatActivity).findViewById<Toolbar>(R.id.navigationToolbar)
val colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(context, foreground), PorterDuff.Mode.SRC_IN
)
toolbar.setBackgroundColor(ContextCompat.getColor(context, background))
toolbar.setTitleTextColor(ContextCompat.getColor(context, foreground))
themeToolbar(
toolbar, foreground,
background, colorFilter
)
}
private fun themeToolbar(
toolbar: androidx.appcompat.widget.Toolbar,
textColor: Int,
backgroundColor: Int,
colorFilter: PorterDuffColorFilter? = null
) {
toolbar.setTitleTextColor(ContextCompat.getColor(context!!, textColor))
toolbar.setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
if (colorFilter == null) {
return
}
toolbar.overflowIcon?.colorFilter = colorFilter
(0 until toolbar.childCount).forEach {
when (val item = toolbar.getChildAt(it)) {
is ImageButton -> item.drawable.colorFilter = colorFilter
}
}
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
if (mode is HistoryState.Mode.Editing) { return when {
actionEmitter.onNext(HistoryAction.BackPressed) mode is HistoryState.Mode.Editing -> {
return true mode = HistoryState.Mode.Normal
historyAdapter.updateData(items, mode)
setUIForNormalMode()
actionEmitter.onNext(HistoryAction.SwitchMode)
true
}
else -> false
} }
return false
} }
} }

View File

@ -35,20 +35,19 @@ class HistoryDeleteButtonViewHolder(
fun bind(mode: HistoryState.Mode) { fun bind(mode: HistoryState.Mode) {
this.mode = mode this.mode = mode
val text = if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) { buttonView.run {
textView.context.resources.getString( if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
R.string.history_delete_some, isEnabled = false
mode.selectedItems.size alpha = DISABLED_ALPHA
) } else {
} else { isEnabled = true
textView.context.resources.getString(R.string.history_delete_all) alpha = 1f
}
} }
buttonView.contentDescription = text
textView.text = text
} }
companion object { companion object {
const val DISABLED_ALPHA = 0.4f
const val LAYOUT_ID = R.layout.delete_history_button const val LAYOUT_ID = R.layout.delete_history_button
} }
} }

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.library.history.viewholders
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer import io.reactivex.Observer
import kotlinx.android.synthetic.main.history_list_item.view.* import kotlinx.android.synthetic.main.history_list_item.view.*
@ -15,6 +16,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.icons.IconRequest import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenu
import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.library.history.HistoryAction import org.mozilla.fenix.library.history.HistoryAction
@ -32,7 +34,6 @@ class HistoryListItemViewHolder(
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job get() = Dispatchers.IO + job
private val checkbox = view.should_remove_checkbox
private val favicon = view.history_favicon private val favicon = view.history_favicon
private val title = view.history_title private val title = view.history_title
private val url = view.history_url private val url = view.history_url
@ -60,17 +61,6 @@ class HistoryListItemViewHolder(
init { init {
setupMenu() setupMenu()
view.setOnClickListener {
if (mode is HistoryState.Mode.Editing) {
checkbox.isChecked = !checkbox.isChecked
return@setOnClickListener
}
item?.apply {
actionEmitter.onNext(HistoryAction.Select(this))
}
}
view.setOnLongClickListener { view.setOnLongClickListener {
item?.apply { item?.apply {
actionEmitter.onNext(HistoryAction.EnterEditMode(this)) actionEmitter.onNext(HistoryAction.EnterEditMode(this))
@ -84,8 +74,6 @@ class HistoryListItemViewHolder(
anchor = it, anchor = it,
orientation = BrowserMenu.Orientation.DOWN) orientation = BrowserMenu.Orientation.DOWN)
} }
checkbox.setOnCheckedChangeListener(checkListener)
} }
fun bind(item: HistoryItem, mode: HistoryState.Mode) { fun bind(item: HistoryItem, mode: HistoryState.Mode) {
@ -95,23 +83,31 @@ class HistoryListItemViewHolder(
title.text = item.title title.text = item.title
url.text = item.url url.text = item.url
val isEditing = mode is HistoryState.Mode.Editing val selected = when (mode) {
checkbox.visibility = if (isEditing) View.VISIBLE else View.GONE is HistoryState.Mode.Editing -> mode.selectedItems.contains(item)
favicon.visibility = if (isEditing) View.INVISIBLE else View.VISIBLE HistoryState.Mode.Normal -> false
if (mode is HistoryState.Mode.Editing) {
checkbox.setOnCheckedChangeListener(null)
// Don't set the checkbox if it already contains the right value.
// This prevent us from cutting off the animation
val shouldCheck = mode.selectedItems.contains(item)
if (checkbox.isChecked != shouldCheck) {
checkbox.isChecked = shouldCheck
}
checkbox.setOnCheckedChangeListener(checkListener)
} }
updateFavIcon(item.url) setClickListeners(item, selected)
if (mode is HistoryState.Mode.Editing) {
val backgroundTint =
if (selected) {
DefaultThemeManager.resolveAttribute(R.attr.accentHighContrast, itemView.context)
} else {
DefaultThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
}
val backgroundTintList = ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList
if (selected) {
favicon.setImageResource(R.drawable.mozac_ic_check)
} else {
favicon.setImageResource(0)
}
} else {
updateFavIcon(item.url)
}
} }
private fun setupMenu() { private fun setupMenu() {
@ -134,6 +130,21 @@ class HistoryListItemViewHolder(
} }
} }
private fun setClickListeners(
item: HistoryItem,
selected: Boolean
) {
itemView.history_layout.setOnClickListener {
if (mode == HistoryState.Mode.Normal) {
actionEmitter.onNext(HistoryAction.Open(item))
} else {
if (selected) actionEmitter.onNext(HistoryAction.RemoveItemForRemoval(item)) else actionEmitter.onNext(
HistoryAction.AddItemForRemoval(item)
)
}
}
}
companion object { companion object {
const val LAYOUT_ID = R.layout.history_list_item const val LAYOUT_ID = R.layout.history_list_item
} }

View File

@ -5,5 +5,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp"/> <corners android:radius="4dp"/>
<solid android:color="@color/foundation_normal_theme" /> <solid android:color="?inset" />
</shape> </shape>

View File

@ -16,14 +16,18 @@
<TextView <TextView
android:id="@+id/delete_history_button_text" android:id="@+id/delete_history_button_text"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/history_delete_all" android:text="@string/history_delete_all"
android:textColor="?destructive"
android:drawablePadding="8dp"
android:textSize="16sp"
android:gravity="center" android:gravity="center"
android:textColor="?primaryText"
android:textSize="16sp"
android:textStyle="bold"
android:clickable="false" android:clickable="false"
android:focusable="false" android:focusable="false"
android:layout_gravity="center" /> android:drawableTint="?primaryText"
android:drawableStart="@drawable/ic_delete"
android:drawablePadding="12dp"/>
</FrameLayout> </FrameLayout>

View File

@ -4,6 +4,7 @@
- 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.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/history_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="56dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
@ -12,15 +13,6 @@
android:padding="4dp" android:padding="4dp"
android:paddingStart="20dp"> android:paddingStart="20dp">
<CheckBox
android:id="@+id/should_remove_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton <ImageButton
android:id="@+id/history_item_overflow" android:id="@+id/history_item_overflow"
android:layout_width="@dimen/glyph_button_width" android:layout_width="@dimen/glyph_button_width"

View File

@ -0,0 +1,36 @@
<?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/. -->
<menu 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">
<item
android:id="@+id/share_history_multi_select"
android:icon="@drawable/ic_hollow_share"
android:iconTint="?primaryText"
android:title="@string/browser_menu_share"
app:showAsAction="ifRoom"
tools:targetApi="o" />
<item
android:id="@+id/open_history_in_new_tabs_multi_select"
android:icon="@drawable/ic_new"
android:iconTint="?primaryText"
android:title="@string/bookmark_menu_open_in_new_tab_button"
app:showAsAction="never"
tools:targetApi="o" />
<item
android:id="@+id/open_history_in_private_tabs_multi_select"
android:icon="@drawable/ic_new"
android:iconTint="?primaryText"
android:title="@string/bookmark_menu_open_in_private_tab_button"
app:showAsAction="never"
tools:targetApi="o" />
<item
android:id="@+id/delete_history_multi_select"
android:icon="@drawable/ic_new"
android:iconTint="?primaryText"
android:title="@string/bookmark_menu_delete_button"
app:showAsAction="never"
tools:targetApi="o" />
</menu>

View File

@ -137,6 +137,7 @@
android:label="@string/library_history" android:label="@string/library_history"
tools:layout="@layout/fragment_history" > tools:layout="@layout/fragment_history" >
<action android:id="@+id/action_historyFragment_to_browserFragment" app:destination="@id/browserFragment"/> <action android:id="@+id/action_historyFragment_to_browserFragment" app:destination="@id/browserFragment"/>
<action android:id="@+id/action_historyFragment_to_homeFragment" app:destination="@id/homeFragment"/>
</fragment> </fragment>
<fragment <fragment

View File

@ -11,6 +11,10 @@
<string name="content_description_disable_private_browsing_button">Disable private browsing</string> <string name="content_description_disable_private_browsing_button">Disable private browsing</string>
<!-- Placeholder text shown in the search bar before a user enters text --> <!-- Placeholder text shown in the search bar before a user enters text -->
<string name="search_hint">Search or enter address</string> <string name="search_hint">Search or enter address</string>
<!-- No Open Tabs Message Header -->
<string name="no_open_tabs_header">No tabs opened</string>
<!-- No Open Tabs Message Description -->
<string name="no_open_tabs_description">Your open tabs will be shown here.</string>
<!-- Private Browsing --> <!-- Private Browsing -->
<!-- Title for private session option --> <!-- Title for private session option -->
@ -77,11 +81,9 @@
<!-- Button in the search view that lets a user navigate to the site in their clipboard --> <!-- Button in the search view that lets a user navigate to the site in their clipboard -->
<string name="awesomebar_clipboard_title">Fill link from clipboard</string> <string name="awesomebar_clipboard_title">Fill link from clipboard</string>
<!-- Settings Fragment --> <!-- Preferences -->
<!-- Title for the settings page--> <!-- Title for the settings page-->
<string name="settings">Settings</string> <string name="settings">Settings</string>
<!-- Preferences -->
<!-- Preference category for basic settings --> <!-- Preference category for basic settings -->
<string name="preferences_category_basics">Basics</string> <string name="preferences_category_basics">Basics</string>
<!-- Preference category for all links about Fenix --> <!-- Preference category for all links about Fenix -->
@ -281,16 +283,15 @@
The first parameter is a digit that shows the cardinal number of how many additional tabs the session has. --> The first parameter is a digit that shows the cardinal number of how many additional tabs the session has. -->
<string name="session_items_more">%1$d sites…</string> <string name="session_items_more">%1$d sites…</string>
<!-- No Open Tabs Message Header -->
<string name="no_open_tabs_header">No tabs opened</string>
<!-- No Open Tabs Message Description -->
<string name="no_open_tabs_description">Your open tabs will be shown here.</string>
<!-- History --> <!-- History -->
<!-- Text for the button to clear all history --> <!-- Text for the button to clear all history -->
<string name="history_delete_all">Delete history</string> <string name="history_delete_all">Delete history</string>
<!-- Text for the button to delete a single history item --> <!-- Text for the button to delete a single history item -->
<string name="history_delete_item">Delete</string> <string name="history_delete_item">Delete</string>
<!-- History multi select title in app bar
The first parameter is the number of bookmarks selected -->
<string name="history_multi_select_title">%1$d selected</string>
<!-- Text for the button to clear selected history items. The first parameter <!-- Text for the button to clear selected history items. The first parameter
is a digit showing the number of items you have selected --> is a digit showing the number of items you have selected -->
<string name="history_delete_some">Delete %1$d items</string> <string name="history_delete_some">Delete %1$d items</string>
@ -302,13 +303,10 @@
<string name="history_this_month">This month</string> <string name="history_this_month">This month</string>
<!-- Text for the header that groups the history older than the last month --> <!-- Text for the header that groups the history older than the last month -->
<string name="history_older">Older</string> <string name="history_older">Older</string>
<!-- Text displayed in a notification when the user enters full screen mode -->
<string name="full_screen_notification">Entering full screen mode</string>
<!-- Crashes --> <!-- Crashes -->
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) --> <!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->
<string name="tab_crash_title_2">Sorry. %1$s cant load that page.</string> <string name="tab_crash_title_2">Sorry. %1$s cant load that page.</string>
<!-- Description text displayed on the tab crash page --> <!-- Description text displayed on the tab crash page -->
<string name="tab_crash_description">You can attempt to restore or close this tab below.</string> <string name="tab_crash_description">You can attempt to restore or close this tab below.</string>
<!-- Send crash report checkbox text on the tab crash page --> <!-- Send crash report checkbox text on the tab crash page -->
@ -324,6 +322,7 @@
<!-- Content Description for session item share button --> <!-- Content Description for session item share button -->
<string name="content_description_session_share">Share session</string> <string name="content_description_session_share">Share session</string>
<!-- Bookmarks -->
<!-- Content description for bookmarks library menu --> <!-- Content description for bookmarks library menu -->
<string name="bookmark_menu_content_description">Bookmark menu</string> <string name="bookmark_menu_content_description">Bookmark menu</string>
<!-- Screen title for editing bookmarks --> <!-- Screen title for editing bookmarks -->
@ -338,7 +337,6 @@
<string name="bookmark_saved_snackbar">Bookmark saved!</string> <string name="bookmark_saved_snackbar">Bookmark saved!</string>
<!-- Snackbar edit button shown after a bookmark has been created. --> <!-- Snackbar edit button shown after a bookmark has been created. -->
<string name="edit_bookmark_snackbar_action">EDIT</string> <string name="edit_bookmark_snackbar_action">EDIT</string>
<!-- Bookmark overflow menu edit button --> <!-- Bookmark overflow menu edit button -->
<string name="bookmark_menu_edit_button">Edit</string> <string name="bookmark_menu_edit_button">Edit</string>
<!-- Bookmark overflow menu select button --> <!-- Bookmark overflow menu select button -->
@ -384,9 +382,6 @@
<!-- Bookmark undo button for deletion snackbar action --> <!-- Bookmark undo button for deletion snackbar action -->
<string name="bookmark_undo_deletion">UNDO</string> <string name="bookmark_undo_deletion">UNDO</string>
<!-- Message for copying the URL via long press on the toolbar -->
<string name="url_copied">URL copied</string>
<!-- Site Permissions --> <!-- Site Permissions -->
<!-- Button label that take the user to the Android App setting --> <!-- Button label that take the user to the Android App setting -->
<string name="phone_feature_go_to_settings">Go to Settings</string> <string name="phone_feature_go_to_settings">Go to Settings</string>
@ -434,58 +429,48 @@
<string name="no_collections_header">No collections</string> <string name="no_collections_header">No collections</string>
<!-- No Open Tabs Message Description --> <!-- No Open Tabs Message Description -->
<string name="no_collections_description">Your collections will be shown here.</string> <string name="no_collections_description">Your collections will be shown here.</string>
<!-- Title for the "select tabs" step of the collection creator --> <!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Select Tabs</string> <string name="create_collection_select_tabs">Select Tabs</string>
<!-- Title for the "select collection" step of the collection creator --> <!-- Title for the "select collection" step of the collection creator -->
<string name="create_collection_select_collection">Select collection</string> <string name="create_collection_select_collection">Select collection</string>
<!-- Title for the "name collection" step of the collection creator --> <!-- Title for the "name collection" step of the collection creator -->
<string name="create_collection_name_collection">Name collection</string> <string name="create_collection_name_collection">Name collection</string>
<!-- Button to add new collection for the "select collection" step of the collection creator --> <!-- Button to add new collection for the "select collection" step of the collection creator -->
<string name="create_collection_add_new_collection">Add new collection</string> <string name="create_collection_add_new_collection">Add new collection</string>
<!-- Button to select all tabs in the "select tabs" step of the collection creator --> <!-- Button to select all tabs in the "select tabs" step of the collection creator -->
<string name="create_collection_select_all">Select All</string> <string name="create_collection_select_all">Select All</string>
<!-- Text to prompt users to select the tabs to save in the "select tabs" step of the collection creator --> <!-- Text to prompt users to select the tabs to save in the "select tabs" step of the collection creator -->
<string name="create_collection_save_to_collection_empty">Select tabs to save</string> <string name="create_collection_save_to_collection_empty">Select tabs to save</string>
<!-- Text to show users how many tabs they have selected in the "select tabs" step of the collection creator. <!-- Text to show users how many tabs they have selected in the "select tabs" step of the collection creator.
%d is a placeholder for the number of tabs selected. --> %d is a placeholder for the number of tabs selected. -->
<string name="create_collection_save_to_collection_tabs_selected">%d tabs selected</string> <string name="create_collection_save_to_collection_tabs_selected">%d tabs selected</string>
<!-- Text to show users they have one tab selected in the "select tabs" step of the collection creator. <!-- Text to show users they have one tab selected in the "select tabs" step of the collection creator.
%d is a placeholder for the number of tabs selected. --> %d is a placeholder for the number of tabs selected. -->
<string name="create_collection_save_to_collection_tab_selected">%d tab selected</string> <string name="create_collection_save_to_collection_tab_selected">%d tab selected</string>
<!-- Text shown in snackbar when multiple tabs have been saved in a collection --> <!-- Text shown in snackbar when multiple tabs have been saved in a collection -->
<string name="create_collection_tabs_saved">Tabs saved!</string> <string name="create_collection_tabs_saved">Tabs saved!</string>
<!-- Text shown in snackbar when one tab has been saved in a collection --> <!-- Text shown in snackbar when one tab has been saved in a collection -->
<string name="create_collection_tab_saved">Tab saved!</string> <string name="create_collection_tab_saved">Tab saved!</string>
<!-- Content description (not visible, for screen readers etc.): button to close the collection creator --> <!-- Content description (not visible, for screen readers etc.): button to close the collection creator -->
<string name="create_collection_close">Close</string> <string name="create_collection_close">Close</string>
<!-- Button to save currently selected tabs in the "select tabs" step of the collection creator--> <!-- Button to save currently selected tabs in the "select tabs" step of the collection creator-->
<string name="create_collection_save">Save</string> <string name="create_collection_save">Save</string>
<!-- Default name for a new collection in "name new collection" step of the collection creator. %d is a placeholder for the number of collections--> <!-- Default name for a new collection in "name new collection" step of the collection creator. %d is a placeholder for the number of collections-->
<string name="create_collection_default_name">Collection %d</string> <string name="create_collection_default_name">Collection %d</string>
<!-- Notifications -->
<!-- Text shown in snackbar when user deletes a collection --> <!-- Text shown in snackbar when user deletes a collection -->
<string name="snackbar_collection_deleted">Collection deleted</string> <string name="snackbar_collection_deleted">Collection deleted</string>
<!-- Text shown in snackbar when user deletes a tab --> <!-- Text shown in snackbar when user deletes a tab -->
<string name="snackbar_tab_deleted">Tab deleted</string> <string name="snackbar_tab_deleted">Tab deleted</string>
<!-- Text for action to undo deleting a tab or collection shown in snackbar --> <!-- Text for action to undo deleting a tab or collection shown in snackbar -->
<string name="snackbar_deleted_undo">UNDO</string> <string name="snackbar_deleted_undo">UNDO</string>
<!-- QR code scanner prompt which appears after scanning a code, but before navigating to it <!-- QR code scanner prompt which appears after scanning a code, but before navigating to it
First parameter is the name of the app, second parameter is the URL or text scanned--> First parameter is the name of the app, second parameter is the URL or text scanned-->
<string name="qr_scanner_confirmation_dialog_message">Allow %1$s to open %2$s</string> <string name="qr_scanner_confirmation_dialog_message">Allow %1$s to open %2$s</string>
<!-- Text displayed in a notification when the user enters full screen mode -->
<string name="full_screen_notification">Entering full screen mode</string>
<!-- Message for copying the URL via long press on the toolbar -->
<string name="url_copied">URL copied</string>
</resources> </resources>

View File

@ -39,22 +39,40 @@ class HistoryComponentTest {
} }
@Test @Test
fun `add and remove one history item normally`() { fun `select two items for removal, then deselect one, then select it again`() {
val historyItem = HistoryItem(1, "Mozilla", "http://mozilla.org", 0)
val historyItem2 = HistoryItem(2, "Mozilla", "http://mozilla.org", 0)
emitter.onNext(HistoryChange.Change(listOf(historyItem, historyItem2)))
emitter.onNext(HistoryChange.EnterEditMode(historyItem))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem2))
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem))
emitter.onNext(HistoryChange.ExitEditMode)
historyObserver.assertSubscribed().awaitCount(7).assertNoErrors()
.assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem, historyItem2))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem2))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem2, historyItem))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Normal)
)
}
@Test
fun `deselecting all items triggers normal mode`() {
val historyItem = HistoryItem(123, "Mozilla", "http://mozilla.org", 0) val historyItem = HistoryItem(123, "Mozilla", "http://mozilla.org", 0)
emitter.onNext(HistoryChange.Change(listOf(historyItem))) emitter.onNext(HistoryChange.Change(listOf(historyItem)))
emitter.onNext(HistoryChange.EnterEditMode(historyItem)) emitter.onNext(HistoryChange.EnterEditMode(historyItem))
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem)) emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem))
emitter.onNext(HistoryChange.ExitEditMode)
historyObserver.assertSubscribed().awaitCount(6).assertNoErrors() historyObserver.assertSubscribed().awaitCount(6).assertNoErrors()
.assertValues( .assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal), HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem), HistoryState.Mode.Normal), HistoryState(listOf(historyItem), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))), HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf())),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem), HistoryState.Mode.Normal) HistoryState(listOf(historyItem), HistoryState.Mode.Normal)
) )
} }