Create shared custom view for library items

nightly-build-test
Tiger Oakes 5 years ago committed by Emily Kager
parent ed60bdf470
commit 2467588c4a

@ -0,0 +1,69 @@
/* 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/. */
package org.mozilla.fenix.library
import android.content.Context
import android.view.LayoutInflater
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.library_site_item.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView
class LibrarySiteItemView(
context: Context
) : ConstraintLayout(context) {
val titleView: TextView get() = title
val urlView: TextView get() = url
val iconView: ImageView get() = favicon
val overflowView: ImageButton get() = overflow_menu
init {
LayoutInflater.from(context).inflate(R.layout.library_site_item, this, true)
overflow_menu.increaseTapArea(OVERFLOW_EXTRA_DIPS)
}
/**
* Change visibility of parts of this view based on what type of item is being represented.
*/
fun displayAs(mode: ItemType) {
favicon.isVisible = mode != ItemType.SEPARATOR
title.isVisible = mode != ItemType.SEPARATOR
url.isVisible = mode == ItemType.SITE
overflow_menu.isVisible = mode != ItemType.SEPARATOR
separator.isVisible = mode == ItemType.SEPARATOR
isClickable = mode != ItemType.SEPARATOR
isFocusable = mode != ItemType.SEPARATOR
}
/**
* Changes the icon to show a check mark if [isSelected]
*/
fun changeSelected(isSelected: Boolean) {
icon.displayedChild = if (isSelected) 1 else 0
}
fun loadFavicon(url: String) {
context.components.core.icons.loadIntoView(favicon, url)
}
enum class ItemType {
SITE, FOLDER, SEPARATOR;
}
companion object {
private const val OVERFLOW_EXTRA_DIPS = 16
}
}

@ -4,24 +4,21 @@
package org.mozilla.fenix.library.bookmarks package org.mozilla.fenix.library.bookmarks
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.bookmark_row.*
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.concept.storage.BookmarkNodeType
import org.jetbrains.anko.image
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView
class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteractor) : class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteractor) :
RecyclerView.Adapter<BookmarkAdapter.BookmarkNodeViewHolder>() { RecyclerView.Adapter<BookmarkAdapter.BookmarkNodeViewHolder>() {
@ -76,29 +73,28 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkNodeViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkNodeViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.bookmark_row, parent, false) val view = LibrarySiteItemView(parent.context).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
return when (viewType) { return when (viewType) {
BookmarkItemViewHolder.viewType.ordinal -> BookmarkItemViewHolder( LibrarySiteItemView.ItemType.SITE.ordinal ->
view, interactor BookmarkItemViewHolder(view, interactor)
) LibrarySiteItemView.ItemType.FOLDER.ordinal ->
BookmarkFolderViewHolder.viewType.ordinal -> BookmarkFolderViewHolder( BookmarkFolderViewHolder(view, interactor)
view, interactor LibrarySiteItemView.ItemType.SEPARATOR.ordinal ->
) BookmarkSeparatorViewHolder(view, interactor)
BookmarkSeparatorViewHolder.viewType.ordinal -> BookmarkSeparatorViewHolder(
view, interactor
)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder") else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
} }
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when (tree[position].type) { return when (tree[position].type) {
BookmarkNodeType.ITEM -> ViewType.ITEM.ordinal BookmarkNodeType.ITEM -> LibrarySiteItemView.ItemType.SITE
BookmarkNodeType.FOLDER -> ViewType.FOLDER.ordinal BookmarkNodeType.FOLDER -> LibrarySiteItemView.ItemType.FOLDER
BookmarkNodeType.SEPARATOR -> ViewType.SEPARATOR.ordinal BookmarkNodeType.SEPARATOR -> LibrarySiteItemView.ItemType.SEPARATOR
else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType") else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType")
} }.ordinal
} }
override fun getItemCount(): Int = tree.size override fun getItemCount(): Int = tree.size
@ -111,95 +107,63 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
) )
} }
open class BookmarkNodeViewHolder( abstract class BookmarkNodeViewHolder(
view: View, val view: LibrarySiteItemView,
val interactor: BookmarkViewInteractor, val interactor: BookmarkViewInteractor
override val containerView: View? = view
) : RecyclerView.ViewHolder(view), LayoutContainer {
open fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {}
}
class BookmarkItemViewHolder(
view: View,
interactor: BookmarkViewInteractor,
override val containerView: View? = view
) : ) :
BookmarkNodeViewHolder(view, interactor, containerView) { RecyclerView.ViewHolder(view), LayoutContainer {
@Suppress("ComplexMethod") override val containerView get() = view
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
val shiftTwoDp = TypedValue.applyDimension( abstract fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean)
TypedValue.COMPLEX_UNIT_DIP, TWO_DIGIT_MARGIN, containerView!!.context.resources.displayMetrics
).toInt() protected fun setupMenu(item: BookmarkNode) {
val params = bookmark_title.layoutParams as ViewGroup.MarginLayoutParams val bookmarkItemMenu = BookmarkItemMenu(view.context, item) {
params.topMargin = shiftTwoDp
bookmark_title.layoutParams = params
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_url.visibility = View.VISIBLE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
when (it) { when (it) {
is BookmarkItemMenu.Item.Edit -> { is BookmarkItemMenu.Item.Edit -> interactor.edit(item)
interactor.edit(item) is BookmarkItemMenu.Item.Select -> interactor.select(item)
} is BookmarkItemMenu.Item.Copy -> interactor.copy(item)
is BookmarkItemMenu.Item.Select -> { is BookmarkItemMenu.Item.Share -> interactor.share(item)
interactor.select(item) is BookmarkItemMenu.Item.OpenInNewTab -> interactor.openInNewTab(item)
} is BookmarkItemMenu.Item.OpenInPrivateTab -> interactor.openInPrivateTab(item)
is BookmarkItemMenu.Item.Copy -> { is BookmarkItemMenu.Item.Delete -> interactor.delete(item)
interactor.copy(item)
}
is BookmarkItemMenu.Item.Share -> {
interactor.share(item)
}
is BookmarkItemMenu.Item.OpenInNewTab -> {
interactor.openInNewTab(item)
}
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
interactor.openInPrivateTab(item)
}
is BookmarkItemMenu.Item.Delete -> {
interactor.delete(item)
}
} }
} }
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips) view.overflowView.setOnClickListener {
bookmark_overflow.setOnClickListener { bookmarkItemMenu.menuBuilder.build(view.context).show(
bookmarkItemMenu.menuBuilder.build(containerView.context).show(anchor = it) anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
} }
bookmark_title.text = if (item.title.isNullOrBlank()) item.url else item.title
bookmark_url.text = item.url
updateUrl(item, mode, selected)
} }
}
private fun updateUrl(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) { class BookmarkItemViewHolder(
setClickListeners(mode, item, selected) view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) :
BookmarkNodeViewHolder(view, interactor) {
setColorsAndIcons(selected, item) @Suppress("ComplexMethod")
} override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
private fun setColorsAndIcons(selected: Boolean, item: BookmarkNode) { view.displayAs(LibrarySiteItemView.ItemType.SITE)
val backgroundTint =
if (selected) { setupMenu(item)
ThemeManager.resolveAttribute(R.attr.accentHighContrast, containerView!!.context) view.titleView.text = if (item.title.isNullOrBlank()) item.url else item.title
} else { view.urlView.text = item.url
ThemeManager.resolveAttribute(R.attr.neutral, containerView!!.context)
}
val backgroundTintList = ContextCompat.getColorStateList(containerView.context, backgroundTint) setClickListeners(mode, item, selected)
bookmark_favicon.backgroundTintList = backgroundTintList view.changeSelected(selected)
if (selected) bookmark_favicon.setImageResource(R.drawable.mozac_ic_check) setColorsAndIcons(item.url)
}
val url = item.url ?: return private fun setColorsAndIcons(url: String?) {
if (!selected && url.startsWith("http")) { if (url != null && url.startsWith("http")) {
bookmark_layout.context.components.core.icons.loadIntoView(bookmark_favicon, url) view.loadFavicon(url)
} else {
view.iconView.setImageDrawable(null)
} }
} }
@ -208,102 +172,47 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
item: BookmarkNode, item: BookmarkNode,
selected: Boolean selected: Boolean
) { ) {
bookmark_layout.setOnClickListener { view.setOnClickListener {
if (mode == BookmarkState.Mode.Normal) { when {
interactor.open(item) mode == BookmarkState.Mode.Normal -> interactor.open(item)
} else { selected -> interactor.deselect(item)
if (selected) interactor.deselect(item) else interactor.select(item) else -> interactor.select(item)
} }
} }
bookmark_layout.setOnLongClickListener { view.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal) { if (mode == BookmarkState.Mode.Normal) {
if (selected) interactor.deselect(item) else interactor.select(item) interactor.select(item)
true true
} else false } else false
} }
} }
companion object {
internal const val TWO_DIGIT_MARGIN = 2F
val viewType = ViewType.ITEM
}
} }
class BookmarkFolderViewHolder( class BookmarkFolderViewHolder(
view: View, view: LibrarySiteItemView,
interactor: BookmarkViewInteractor, interactor: BookmarkViewInteractor
override val containerView: View? = view
) : ) :
BookmarkNodeViewHolder(view, interactor, containerView) { BookmarkNodeViewHolder(view, interactor) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) { override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
containerView?.context?.let {
val drawable = it.getDrawable(R.drawable.ic_folder_icon)
drawable?.setTint(
ContextCompat.getColor(
it,
R.color.primary_text_light_theme
)
)
bookmark_favicon.setImageDrawable(drawable)
}
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_url.visibility = View.GONE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
setClickListeners(mode, item, selected) view.displayAs(LibrarySiteItemView.ItemType.FOLDER)
setMenu(item, containerView!!) setClickListeners(mode, item, selected)
val backgroundTint = if (selected) { if (!item.inRoots()) {
ThemeManager.resolveAttribute(R.attr.accentHighContrast, containerView.context) setupMenu(item)
view.setOnLongClickListener(null)
} else { } else {
ThemeManager.resolveAttribute(R.attr.neutral, containerView.context) view.overflowView.visibility = View.GONE
} }
val backgroundTintList = ContextCompat.getColorStateList(containerView.context, backgroundTint) view.changeSelected(selected)
bookmark_favicon.backgroundTintList = backgroundTintList view.iconView.image = view.context.getDrawable(R.drawable.ic_folder_icon)?.apply {
val res = if (selected) R.drawable.mozac_ic_check else R.drawable.ic_folder_icon setTint(ContextCompat.getColor(view.context, R.color.primary_text_light_theme))
bookmark_favicon.setImageResource(res)
bookmark_title?.text = item.title
}
private fun setMenu(
item: BookmarkNode,
containerView: View
) {
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
when (it) {
is BookmarkItemMenu.Item.Edit -> {
interactor.edit(item)
}
is BookmarkItemMenu.Item.Select -> {
interactor.select(item)
}
is BookmarkItemMenu.Item.Delete -> {
interactor.delete(item)
}
}
}
if (!item.inRoots()) {
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
bookmark_layout.setOnLongClickListener(null)
} else {
bookmark_overflow.visibility = View.GONE
} }
view.titleView.text = item.title
} }
private fun setClickListeners( private fun setClickListeners(
@ -311,71 +220,33 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
item: BookmarkNode, item: BookmarkNode,
selected: Boolean selected: Boolean
) { ) {
bookmark_layout.setOnClickListener { view.setOnClickListener {
if (mode == BookmarkState.Mode.Normal) { when {
interactor.expand(item) mode == BookmarkState.Mode.Normal -> interactor.expand(item)
} else { selected -> interactor.deselect(item)
if (selected) interactor.deselect(item) else interactor.select(item) else -> interactor.select(item)
} }
} }
bookmark_layout.setOnLongClickListener { view.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal && !item.inRoots()) { if (mode == BookmarkState.Mode.Normal && !item.inRoots()) {
if (selected) interactor.deselect(item) else interactor.select(item) interactor.select(item)
true true
} else false } else false
} }
} }
companion object {
val viewType = ViewType.FOLDER
}
} }
class BookmarkSeparatorViewHolder( class BookmarkSeparatorViewHolder(
view: View, view: LibrarySiteItemView,
interactor: BookmarkViewInteractor, interactor: BookmarkViewInteractor
override val containerView: View? = view ) : BookmarkNodeViewHolder(view, interactor) {
) : BookmarkNodeViewHolder(view, interactor, containerView) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) { override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
view.displayAs(LibrarySiteItemView.ItemType.SEPARATOR)
bookmark_favicon.visibility = View.GONE setupMenu(item)
bookmark_title.visibility = View.GONE
bookmark_url.visibility = View.GONE
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.visibility = View.GONE
bookmark_separator.visibility = View.VISIBLE
bookmark_layout.isClickable = false
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
when (it) {
is BookmarkItemMenu.Item.Delete -> {
interactor.delete(item)
}
}
}
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
}
companion object {
val viewType = ViewType.SEPARATOR
} }
} }
companion object {
private const val bookmarkOverflowExtraDips = 16
}
enum class ViewType {
ITEM, FOLDER, SEPARATOR
}
} }
fun BookmarkNode.inRoots() = enumValues<BookmarkRoot>().any { it.id == guid } fun BookmarkNode.inRoots() = enumValues<BookmarkRoot>().any { it.id == guid }

@ -31,7 +31,7 @@ class BookmarkItemMenu(
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy { private val menuItems by lazy {
listOf( listOfNotNull(
if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) { if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) { SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Edit) onItemTapped.invoke(BookmarkItemMenu.Item.Edit)
@ -63,6 +63,6 @@ class BookmarkItemMenu(
) { ) {
onItemTapped.invoke(BookmarkItemMenu.Item.Delete) onItemTapped.invoke(BookmarkItemMenu.Item.Delete)
} }
).filterNotNull() )
} }
} }

@ -4,19 +4,15 @@
package org.mozilla.fenix.library.bookmarks.selectfolder package org.mozilla.fenix.library.bookmarks.selectfolder
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.bookmark_row.*
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getColorResFromAttr import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedViewModel) : class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedViewModel) :
@ -30,21 +26,9 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkFolderViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkFolderViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.bookmark_row, parent, false) val view = LibrarySiteItemView(parent.context)
return when (viewType) { return BookmarkFolderViewHolder(view)
BookmarkFolderViewHolder.viewType -> SelectBookmarkFolderAdapter.BookmarkFolderViewHolder(
view
)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
}
}
override fun getItemViewType(position: Int): Int {
return when (tree[position].node.type) {
BookmarkNodeType.FOLDER -> BookmarkFolderViewHolder.viewType
else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType")
}
} }
override fun getItemCount(): Int = tree.size override fun getItemCount(): Int = tree.size
@ -52,63 +36,40 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi
override fun onBindViewHolder(holder: BookmarkFolderViewHolder, position: Int) { override fun onBindViewHolder(holder: BookmarkFolderViewHolder, position: Int) {
holder.bind( holder.bind(
tree[position], tree[position],
tree[position].node == sharedViewModel.selectedFolder, tree[position].node == sharedViewModel.selectedFolder
object : SelectionInterface { ) { node ->
override fun itemSelected(node: BookmarkNode) { sharedViewModel.apply {
sharedViewModel.apply { when (selectedFolder) {
when (selectedFolder) { node -> selectedFolder = null
node -> selectedFolder = null else -> selectedFolder = node
else -> selectedFolder = node
}
}
notifyDataSetChanged()
} }
} }
) notifyDataSetChanged()
} }
interface SelectionInterface {
fun itemSelected(node: BookmarkNode)
} }
class BookmarkFolderViewHolder( class BookmarkFolderViewHolder(
view: View, val view: LibrarySiteItemView
override val containerView: View? = view
) : ) :
RecyclerView.ViewHolder(view), LayoutContainer { RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView get() = view
init { init {
bookmark_favicon.visibility = View.VISIBLE view.displayAs(LibrarySiteItemView.ItemType.FOLDER)
bookmark_title.visibility = View.VISIBLE view.overflowView.visibility = View.GONE
bookmark_url.visibility = View.GONE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
} }
fun bind(folder: BookmarkNodeWithDepth, selected: Boolean, selectionInterface: SelectionInterface) { fun bind(folder: BookmarkNodeWithDepth, selected: Boolean, onSelect: (BookmarkNode) -> Unit) {
val backgroundTintAttr = if (selected) R.attr.accentBright else R.attr.neutral view.changeSelected(selected)
view.iconView.setImageResource(R.drawable.ic_folder_icon)
// Center the bookmark title since we don't have a url view.titleView.text = folder.node.title
val constraintSet = ConstraintSet() view.setOnClickListener {
constraintSet.clone(bookmark_layout) onSelect(folder.node)
constraintSet.connect(
bookmark_title.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM
)
constraintSet.applyTo(bookmark_layout)
val backgroundTint = containerView!!.context.getColorResFromAttr(backgroundTintAttr)
val backgroundTintList = ContextCompat.getColorStateList(containerView.context, backgroundTint)
bookmark_favicon.backgroundTintList = backgroundTintList
val res = if (selected) R.drawable.mozac_ic_check else R.drawable.ic_folder_icon
bookmark_favicon.setImageResource(res)
bookmark_overflow.visibility = View.GONE
bookmark_title?.text = folder.node.title
bookmark_layout.setOnClickListener {
selectionInterface.itemSelected(folder.node)
} }
val pxToIndent = dpsToIndent.dpToPx(containerView.resources.displayMetrics) val pxToIndent = dpsToIndent.dpToPx(view.context.resources.displayMetrics)
val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth
bookmark_layout.setPadding(padding, 0, 0, 0) view.setPadding(padding, 0, 0, 0)
} }
companion object { companion object {
@ -118,17 +79,12 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi
data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?) data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?)
private fun BookmarkNode?.convertToFolderDepthTree( private fun BookmarkNode.convertToFolderDepthTree(depth: Int = 0): List<BookmarkNodeWithDepth> {
depth: Int = 0, val newList = listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid))
list: List<BookmarkNodeWithDepth> = listOf() return newList + children
): List<BookmarkNodeWithDepth> { ?.filter { it.type == BookmarkNodeType.FOLDER }
return if (this != null) { ?.flatMap { it.convertToFolderDepthTree(depth = depth + 1) }
val newList = list.plus(listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid))) .orEmpty()
newList.plus(
children?.filter { it.type == BookmarkNodeType.FOLDER }
?.flatMap { it.convertToFolderDepthTree(depth + 1) }
?: listOf())
} else listOf()
} }
companion object { companion object {

@ -24,12 +24,12 @@ import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile import mozilla.components.concept.sync.Profile
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getColorFromAttr import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
@ -75,26 +75,22 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
context?.let { context?.let { setRootTitles(it, showMobileRoot = true) }
setRootTitles(it, showMobileRoot = true) activity?.title = getString(R.string.bookmark_select_folder_fragment_label)
} (activity as? AppCompatActivity)?.supportActionBar?.show()
(activity as AppCompatActivity).title =
getString(R.string.bookmark_select_folder_fragment_label)
(activity as AppCompatActivity).supportActionBar?.show()
folderGuid = SelectBookmarkFolderFragmentArgs.fromBundle(arguments!!).folderGuid ?: BookmarkRoot.Root.id folderGuid = SelectBookmarkFolderFragmentArgs.fromBundle(arguments!!).folderGuid ?: BookmarkRoot.Root.id
checkIfSignedIn() checkIfSignedIn()
lifecycleScope.launch(IO) { lifecycleScope.launch(Main) {
bookmarkNode = bookmarkNode = withContext(IO) {
requireComponents.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true) requireComponents.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true)
.withOptionalDesktopFolders(context, showMobileRoot = true) .withOptionalDesktopFolders(context, showMobileRoot = true)
launch(Main) {
(activity as HomeActivity).title = bookmarkNode?.title ?: getString(R.string.library_bookmarks)
val adapter = SelectBookmarkFolderAdapter(sharedViewModel)
recylerView_bookmark_folders.adapter = adapter
adapter.updateData(bookmarkNode)
} }
activity?.title = bookmarkNode?.title ?: getString(R.string.library_bookmarks)
val adapter = SelectBookmarkFolderAdapter(sharedViewModel)
recylerView_bookmark_folders.adapter = adapter
adapter.updateData(bookmarkNode)
} }
} }

@ -8,9 +8,12 @@ import android.content.Context
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder
import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder
import org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder import org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder
@ -149,18 +152,23 @@ class HistoryAdapter(private val historyInteractor: HistoryInteractor) :
return when (historyList.items[position]) { return when (historyList.items[position]) {
is AdapterItem.DeleteButton -> HistoryDeleteButtonViewHolder.LAYOUT_ID is AdapterItem.DeleteButton -> HistoryDeleteButtonViewHolder.LAYOUT_ID
is AdapterItem.SectionHeader -> HistoryHeaderViewHolder.LAYOUT_ID is AdapterItem.SectionHeader -> HistoryHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> HistoryListItemViewHolder.LAYOUT_ID is AdapterItem.Item -> HistoryListItemViewHolder.ID
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return if (viewType == HistoryListItemViewHolder.ID) {
val view = LibrarySiteItemView(parent.context).apply {
return when (viewType) { layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, historyInteractor) }
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view) HistoryListItemViewHolder(view, historyInteractor)
HistoryListItemViewHolder.LAYOUT_ID -> HistoryListItemViewHolder(view, historyInteractor) } else {
else -> throw IllegalStateException() val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
when (viewType) {
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, historyInteractor)
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
else -> throw IllegalStateException()
}
} }
} }

@ -4,47 +4,22 @@
package org.mozilla.fenix.library.history.viewholders package org.mozilla.fenix.library.history.viewholders
import android.view.View
import android.widget.CompoundButton
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenu
import org.mozilla.fenix.R import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.library.history.HistoryInteractor import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryItem import org.mozilla.fenix.library.history.HistoryItem
import org.mozilla.fenix.library.history.HistoryItemMenu import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.library.history.HistoryState import org.mozilla.fenix.library.history.HistoryState
class HistoryListItemViewHolder( class HistoryListItemViewHolder(
view: View, val view: LibrarySiteItemView,
private val historyInteractor: HistoryInteractor private val historyInteractor: HistoryInteractor
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private val favicon = view.history_favicon
private val title = view.history_title
private val url = view.history_url
private val menuButton = view.history_item_overflow
private var item: HistoryItem? = null private var item: HistoryItem? = null
private lateinit var historyMenu: HistoryItemMenu private lateinit var historyMenu: HistoryItemMenu
private var mode: HistoryState.Mode = HistoryState.Mode.Normal private var mode: HistoryState.Mode = HistoryState.Mode.Normal
private val checkListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (mode is HistoryState.Mode.Normal) {
return@OnCheckedChangeListener
}
item?.apply {
if (isChecked) {
historyInteractor.onItemAddedForRemoval(this)
} else {
historyInteractor.onItemRemovedForRemoval(this)
}
}
}
init { init {
setupMenu() setupMenu()
@ -57,55 +32,33 @@ class HistoryListItemViewHolder(
true true
} }
menuButton.setOnClickListener { view.overflowView.setOnClickListener {
historyMenu.menuBuilder.build(view.context).show( historyMenu.menuBuilder.build(view.context).show(
anchor = it, anchor = it,
orientation = BrowserMenu.Orientation.DOWN orientation = BrowserMenu.Orientation.DOWN
) )
} }
view.displayAs(LibrarySiteItemView.ItemType.SITE)
} }
fun bind(item: HistoryItem, mode: HistoryState.Mode) { fun bind(item: HistoryItem, mode: HistoryState.Mode) {
this.item = item this.item = item
this.mode = mode this.mode = mode
title.text = item.title view.titleView.text = item.title
url.text = item.url view.urlView.text = item.url
val selected = when (mode) { val selected = mode is HistoryState.Mode.Editing && mode.selectedItems.contains(item)
is HistoryState.Mode.Editing -> mode.selectedItems.contains(item)
else -> false
}
setClickListeners(item, selected) setClickListeners(item, selected)
if (mode is HistoryState.Mode.Editing) { view.changeSelected(selected)
val backgroundTint = view.loadFavicon(item.url)
if (selected) {
ThemeManager.resolveAttribute(R.attr.accentHighContrast, itemView.context)
} else {
ThemeManager.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 {
updateFavIcon(item.url)
}
} else {
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList
updateFavIcon(item.url)
}
} }
private fun setupMenu() { private fun setupMenu() {
this.historyMenu = HistoryItemMenu(itemView.context) { historyMenu = HistoryItemMenu(view.context) {
when (it) { when (it) {
is HistoryItemMenu.Item.Delete -> { is HistoryItemMenu.Item.Delete -> {
item?.apply { historyInteractor.onDeleteOne(this) } item?.apply { historyInteractor.onDeleteOne(this) }
@ -114,28 +67,20 @@ class HistoryListItemViewHolder(
} }
} }
private fun updateFavIcon(url: String) {
favicon.context.components.core.icons.loadIntoView(favicon, url)
}
private fun setClickListeners( private fun setClickListeners(
item: HistoryItem, item: HistoryItem,
selected: Boolean selected: Boolean
) { ) {
itemView.history_layout.setOnClickListener { view.setOnClickListener {
if (mode == HistoryState.Mode.Normal) { when {
historyInteractor.onHistoryItemOpened(item) mode == HistoryState.Mode.Normal -> historyInteractor.onHistoryItemOpened(item)
} else { selected -> historyInteractor.onItemRemovedForRemoval(item)
if (selected) { else -> historyInteractor.onItemAddedForRemoval(item)
historyInteractor.onItemRemovedForRemoval(item)
} else {
historyInteractor.onItemAddedForRemoval(item)
}
} }
} }
} }
companion object { companion object {
const val LAYOUT_ID = R.layout.history_list_item val ID = LibrarySiteItemView.ItemType.SITE.ordinal
} }
} }

@ -1,87 +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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bookmark_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/bookmark_favicon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="20dp"
android:background="@drawable/favicon_background"
android:importantForAccessibility="no"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_folder_icon" />
<TextView
android:id="@+id/bookmark_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/bookmark_url"
app:layout_constraintEnd_toStartOf="@id/bookmark_overflow"
app:layout_constraintStart_toEndOf="@id/bookmark_favicon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Internet" />
<TextView
android:id="@+id/bookmark_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/bookmark_overflow"
app:layout_constraintStart_toEndOf="@id/bookmark_favicon"
app:layout_constraintTop_toBottomOf="@id/bookmark_title"
tools:text="https://github.com/mozilla-mobile/fenix" />
<ImageButton
android:id="@+id/bookmark_overflow"
android:layout_width="@dimen/glyph_button_width"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/bookmark_menu_content_description"
android:src="@drawable/ic_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/bookmark_separator"
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?neutralFaded"
android:importantForAccessibility="no"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,67 +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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/history_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="4dp"
android:paddingStart="20dp"
android:paddingEnd="0dp">
<ImageButton
android:id="@+id/history_item_overflow"
android:layout_width="@dimen/glyph_button_width"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/content_description_history_menu"
android:src="@drawable/ic_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/history_favicon"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:background="@drawable/favicon_background"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/history_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
app:layout_constraintStart_toEndOf="@id/history_favicon"
app:layout_constraintTop_toBottomOf="@id/history_title" />
<TextView
android:id="@+id/history_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:textSize="18sp"
android:layout_marginTop="2dp"
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
app:layout_constraintStart_toEndOf="@id/history_favicon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,98 @@
<?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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/library_item_height"
android:background="?android:attr/selectableItemBackground">
<ImageSwitcher
android:id="@+id/icon"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:layout_marginStart="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/favicon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:background="@drawable/favicon_background"
android:backgroundTint="?neutral"
android:importantForAccessibility="no"
tools:src="@drawable/ic_folder_icon" />
<ImageView
android:id="@+id/checkmark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:background="@drawable/favicon_background"
android:backgroundTint="?accentHighContrast"
android:src="@drawable/mozac_ic_check" />
</ImageSwitcher>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textSize="18sp"
android:textColor="?primaryText"
tools:text="Example site"
app:layout_constraintEnd_toStartOf="@id/overflow_menu"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/url"
app:layout_constraintVertical_chainStyle="packed"/>
<TextView
android:id="@+id/url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
tools:text="https://example.com/"
app:layout_constraintEnd_toStartOf="@id/overflow_menu"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/overflow_menu"
android:layout_width="@dimen/glyph_button_width"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/content_description_menu"
android:src="@drawable/ic_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<View
android:id="@+id/separator"
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="?neutralFaded"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save