For #2205: Adds collections view to home fragment (#2249)

* For #1574: Adds collections to home view

* Adds colored icons and expansion

* Adds state change

* Adds more styling

* Adds ItsNotBrokenSnacks

* Adds chevron

* Improves styling of swipe to delete and adds delete action

* Fix nits

* Try to add real saving
nightly-build-test
Sawyer Blatz 5 years ago committed by GitHub
parent 282ad31345
commit 7d577e5953
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -60,7 +60,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.collections.CreateCollectionFragment
import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.collections.Tab
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.metrics.Event
@ -75,7 +74,8 @@ import org.mozilla.fenix.customtabs.CustomTabsIntegration
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share
import org.mozilla.fenix.ext.urlToHost
import org.mozilla.fenix.ext.urlToTrimmedHost
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
@ -606,7 +606,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope,
private fun showSaveToCollection() {
getSessionById()?.let {
val tabs = Tab(it.id, it.url, it.url.urlToHost(), it.title)
val tabs = Tab(it.id, it.url, it.url.urlToTrimmedHost(), it.title)
val viewModel = activity?.run {
ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java)
}

@ -5,6 +5,8 @@ package org.mozilla.fenix.collections
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.view.ViewGroup
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
@ -12,18 +14,6 @@ import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.ViewState
data class Tab(
val sessionId: String,
val url: String,
val hostname: String,
val title: String
)
data class Collection(
val collectionId: String,
val title: String
)
sealed class SaveCollectionStep {
object SelectTabs : SaveCollectionStep()
object SelectCollection : SaveCollectionStep()
@ -55,7 +45,7 @@ sealed class CollectionCreationAction : Action {
data class SaveCollectionName(val tabs: List<Tab>, val name: String) :
CollectionCreationAction()
data class SelectCollection(val collection: Collection) :
data class SelectCollection(val collection: TabCollection) :
CollectionCreationAction()
}

@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.icons.IconRequest
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.sessioncontrol.Tab
import kotlin.coroutines.CoroutineContext
class CollectionCreationTabListAdapter(

@ -25,6 +25,7 @@ import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.mvi.UIView
class CollectionCreationUIView(

@ -17,11 +17,15 @@ import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter
import java.util.Random
class CreateCollectionFragment : DialogFragment() {
// Temporary callback. In the future we will just directly add the collection to the core session manager.
var onCollectionSaved: ((TabCollection) -> Unit)? = null
private lateinit var collectionCreationComponent: CollectionCreationComponent
override fun onCreate(savedInstanceState: Bundle?) {
@ -91,6 +95,8 @@ class CreateCollectionFragment : DialogFragment() {
is CollectionCreationAction.SaveCollectionName -> {
showSavedSnackbar(it.tabs.size)
dismiss()
val newCollection = TabCollection(Random().nextInt(), it.name, it.tabs.toMutableList())
onCollectionSaved?.invoke(newCollection)
}
}
}

@ -5,6 +5,7 @@ package org.mozilla.fenix.collections
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import androidx.lifecycle.ViewModel
import org.mozilla.fenix.home.sessioncontrol.Tab
class CreateCollectionViewModel : ViewModel() {
var selectedTabs = setOf<Tab>()

@ -14,13 +14,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import kotlin.coroutines.CoroutineContext
class SaveCollectionListAdapter(
val actionEmitter: Observer<CollectionCreationAction>
) : RecyclerView.Adapter<CollectionViewHolder>() {
private var collections: List<Collection> = listOf()
private var collections: List<TabCollection> = listOf()
private lateinit var job: Job
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollectionViewHolder {
@ -58,7 +59,7 @@ class CollectionViewHolder(
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
private var collection: Collection? = null
private var collection: TabCollection? = null
private val listener = View.OnClickListener {
collection?.apply {
@ -71,7 +72,7 @@ class CollectionViewHolder(
view.setOnClickListener(listener)
}
fun bind(collection: Collection) {
fun bind(collection: TabCollection) {
this.collection = collection
view.collection_item.text = collection.title
}

@ -26,3 +26,20 @@ fun String?.urlToHost(): String {
""
}
}
fun String?.urlToTrimmedHost(): String {
return try {
val url = URL(this)
val firstIndex = url.host.indexOfFirst { it == '.' } + 1
val lastIndex = url.host.indexOfLast { it == '.' }
// Trim all but the title of the website from the hostname. 'www.mozilla.org' becomes 'mozilla'
when {
firstIndex - 1 == lastIndex -> url.host.substring(0, lastIndex)
firstIndex < lastIndex -> url.host.substring(firstIndex, lastIndex)
else -> url.host
}
} catch (e: MalformedURLException) {
""
}
}

@ -38,18 +38,20 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.collections.CreateCollectionFragment
import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.collections.Tab
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.allowUndo
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share
import org.mozilla.fenix.ext.urlToHost
import org.mozilla.fenix.ext.urlToTrimmedHost
import org.mozilla.fenix.home.sessioncontrol.Mode
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent
import org.mozilla.fenix.home.sessioncontrol.SessionControlState
import org.mozilla.fenix.home.sessioncontrol.TabAction
import org.mozilla.fenix.home.sessioncontrol.CollectionAction
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
@ -73,6 +75,9 @@ class HomeFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// TODO Remove this stub when we have the a-c version!
var storedCollections = mutableListOf<TabCollection>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -80,12 +85,12 @@ class HomeFragment : Fragment(), CoroutineScope {
): View? {
job = Job()
val view = inflater.inflate(R.layout.fragment_home, container, false)
val mode =
if ((activity as HomeActivity).browsingModeManager.isPrivate) Mode.Private else Mode.Normal
val mode = if ((activity as HomeActivity).browsingModeManager.isPrivate) Mode.Private else Mode.Normal
sessionControlComponent = SessionControlComponent(
view.homeLayout,
bus,
SessionControlState(listOf(), mode)
SessionControlState(listOf(), listOf(), mode)
)
view.homeLayout.applyConstraintSet {
@ -178,6 +183,7 @@ class HomeFragment : Fragment(), CoroutineScope {
.subscribe {
when (it) {
is SessionControlAction.Tab -> handleTabAction(it.action)
is SessionControlAction.Collection -> handleCollectionAction(it.action)
}
}
}
@ -244,6 +250,44 @@ class HomeFragment : Fragment(), CoroutineScope {
}
}
@Suppress("ComplexMethod")
private fun handleCollectionAction(action: CollectionAction) {
when (action) {
is CollectionAction.Expand -> {
storedCollections.find { it.id == action.collection.id }?.apply { expanded = true }
}
is CollectionAction.Collapse -> {
storedCollections.find { it.id == action.collection.id }?.apply { expanded = false }
}
is CollectionAction.Delete -> {
storedCollections.find { it.id == action.collection.id }?.let { storedCollections.remove(it) }
}
is CollectionAction.AddTab -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1575")
}
is CollectionAction.Rename -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1575")
}
is CollectionAction.OpenTabs -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "2205")
}
is CollectionAction.ShareTabs -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1585")
}
is CollectionAction.RemoveTab -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1578")
}
}
emitCollectionChange()
}
private fun emitCollectionChange() {
storedCollections.map { it.copy() }.let {
getManagedEmitter<SessionControlChange>().onNext(SessionControlChange.CollectionsChange(it))
}
}
override fun onPause() {
super.onPause()
sessionObserver?.let {
@ -326,7 +370,7 @@ class HomeFragment : Fragment(), CoroutineScope {
org.mozilla.fenix.home.sessioncontrol.Tab(
it.id,
it.url,
it.url.urlToHost(),
it.url.urlToTrimmedHost(),
it.title,
selected,
it.thumbnail
@ -364,7 +408,7 @@ class HomeFragment : Fragment(), CoroutineScope {
org.mozilla.fenix.home.sessioncontrol.Tab(
it.id,
it.url,
it.url.urlToHost(),
it.url.urlToTrimmedHost(),
it.title,
selected,
it.thumbnail
@ -376,7 +420,7 @@ class HomeFragment : Fragment(), CoroutineScope {
private fun showCollectionCreationFragment(selectedTabId: String?) {
val tabs = requireComponents.core.sessionManager.sessions
.map { Tab(it.id, it.url, it.url.urlToHost(), it.title) }
.map { Tab(it.id, it.url, it.url.urlToTrimmedHost(), it.title) }
val viewModel = activity?.run {
ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java)
@ -387,11 +431,17 @@ class HomeFragment : Fragment(), CoroutineScope {
viewModel?.selectedTabs = selectedSet
viewModel?.saveCollectionStep = SaveCollectionStep.SelectTabs
CreateCollectionFragment()
.show(
CreateCollectionFragment().also {
it.onCollectionSaved = {
storedCollections.add(it)
emitCollectionChange()
}
it.show(
requireActivity().supportFragmentManager,
CreateCollectionFragment.createCollectionTag
)
}
}
companion object {

@ -15,6 +15,10 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.NoTabMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import java.lang.IllegalStateException
sealed class AdapterItem {
@ -24,6 +28,10 @@ sealed class AdapterItem {
object PrivateBrowsingDescription : AdapterItem()
object SaveTabGroup : AdapterItem()
object DeleteTabs : AdapterItem()
object CollectionHeader : AdapterItem()
object NoCollectionMessage : AdapterItem()
data class CollectionItem(val collection: TabCollection) : AdapterItem()
data class TabInCollectionItem(val collection: TabCollection, val tab: Tab, val isLastTab: Boolean) : AdapterItem()
val viewType: Int
get() = when (this) {
@ -33,6 +41,10 @@ sealed class AdapterItem {
SaveTabGroup -> SaveTabGroupViewHolder.LAYOUT_ID
PrivateBrowsingDescription -> PrivateBrowsingDescriptionViewHolder.LAYOUT_ID
DeleteTabs -> DeleteTabsViewHolder.LAYOUT_ID
CollectionHeader -> CollectionHeaderViewHolder.LAYOUT_ID
NoCollectionMessage -> NoCollectionMessageViewHolder.LAYOUT_ID
is CollectionItem -> CollectionViewHolder.LAYOUT_ID
is TabInCollectionItem -> TabInCollectionViewHolder.LAYOUT_ID
}
}
@ -62,6 +74,12 @@ class SessionControlAdapter(
actionEmitter
)
DeleteTabsViewHolder.LAYOUT_ID -> DeleteTabsViewHolder(view, actionEmitter)
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
NoCollectionMessageViewHolder.LAYOUT_ID -> NoCollectionMessageViewHolder(
view
)
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, actionEmitter, job)
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, actionEmitter, job)
else -> throw IllegalStateException()
}
}
@ -85,6 +103,13 @@ class SessionControlAdapter(
is TabViewHolder -> holder.bindSession(
(items[position] as AdapterItem.TabItem).tab
)
is CollectionViewHolder -> holder.bindSession(
(items[position] as AdapterItem.CollectionItem).collection
)
is TabInCollectionViewHolder -> {
val item = (items[position] as AdapterItem.TabInCollectionItem)
holder.bindSession(item.collection, item.tab, item.isLastTab)
}
}
}
}

@ -17,7 +17,7 @@ import org.mozilla.fenix.mvi.ViewState
class SessionControlComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
override var initialState: SessionControlState = SessionControlState(emptyList(), Mode.Normal)
override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal)
) :
UIComponent<SessionControlState, SessionControlAction, SessionControlChange>(
bus.getManagedEmitter(SessionControlAction::class.java),
@ -26,6 +26,7 @@ class SessionControlComponent(
override val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
}
@ -45,10 +46,18 @@ data class Tab(
val url: String,
val hostname: String,
val title: String,
val selected: Boolean,
val selected: Boolean? = null,
val thumbnail: Bitmap? = null
)
data class TabCollection(
val id: Int,
val title: String,
val tabs: MutableList<Tab>,
val iconColor: Int = 0,
var expanded: Boolean = false
)
sealed class Mode {
object Normal : Mode()
object Private : Mode()
@ -56,6 +65,7 @@ sealed class Mode {
data class SessionControlState(
val tabs: List<Tab>,
val collections: List<TabCollection>,
val mode: Mode
) : ViewState
@ -70,15 +80,32 @@ sealed class TabAction : Action {
object PrivateBrowsingLearnMore : TabAction()
}
sealed class CollectionAction : Action {
data class Expand(val collection: TabCollection) : CollectionAction()
data class Collapse(val collection: TabCollection) : CollectionAction()
data class Delete(val collection: TabCollection) : CollectionAction()
data class AddTab(val collection: TabCollection) : CollectionAction()
data class Rename(val collection: TabCollection) : CollectionAction()
data class OpenTabs(val collection: TabCollection) : CollectionAction()
data class ShareTabs(val collection: TabCollection) : CollectionAction()
data class RemoveTab(val collection: TabCollection, val tab: Tab) : CollectionAction()
}
sealed class SessionControlAction : Action {
data class Tab(val action: TabAction) : SessionControlAction()
data class Collection(val action: CollectionAction) : SessionControlAction()
}
fun Observer<SessionControlAction>.onNext(tabAction: TabAction) {
onNext(SessionControlAction.Tab(tabAction))
}
fun Observer<SessionControlAction>.onNext(collectionAction: CollectionAction) {
onNext(SessionControlAction.Collection(collectionAction))
}
sealed class SessionControlChange : Change {
data class TabsChange(val tabs: List<Tab>) : SessionControlChange()
data class ModeChange(val mode: Mode) : SessionControlChange()
data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange()
}

@ -17,11 +17,12 @@ import androidx.recyclerview.widget.ItemTouchHelper
import org.mozilla.fenix.BuildConfig
// Convert HomeState into a data structure HomeAdapter understands
@SuppressWarnings("ComplexMethod")
@SuppressWarnings("ComplexMethod", "NestedBlockDepth")
private fun SessionControlState.toAdapterList(): List<AdapterItem> {
val items = mutableListOf<AdapterItem>()
items.add(AdapterItem.TabHeader)
// Populate tabs
if (tabs.isNotEmpty()) {
tabs.reversed().map(AdapterItem::TabItem).forEach { items.add(it) }
if (mode == Mode.Private) {
@ -36,9 +37,39 @@ private fun SessionControlState.toAdapterList(): List<AdapterItem> {
items.add(item)
}
// Populate collections
if (mode == Mode.Normal) {
items.add(AdapterItem.CollectionHeader)
if (collections.isNotEmpty()) {
// If the collection is expanded, we want to add all of its tabs beneath it in the adapter
collections.reversed().map(AdapterItem::CollectionItem).forEach {
if (it.collection.expanded) {
items.add(it)
addCollectionTabItems(it.collection, it.collection.tabs, items)
} else {
items.add(it)
}
}
} else {
items.add(AdapterItem.NoCollectionMessage)
}
}
return items
}
private fun addCollectionTabItems(
collection: TabCollection,
tabs: MutableList<Tab>,
itemList: MutableList<AdapterItem>
) {
for (tabIndex in 0 until tabs.size) {
itemList.add(AdapterItem.TabInCollectionItem
(collection, collection.tabs[tabIndex], tabIndex == collection.tabs.size - 1))
}
}
class SessionControlUIView(
container: ViewGroup,
actionEmitter: Observer<SessionControlAction>,

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
class SwipeToDeleteCallback(
@ -27,8 +28,11 @@ class SwipeToDeleteCallback(
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder is TabViewHolder) {
actionEmitter.onNext(TabAction.Close(viewHolder.tab?.sessionId!!))
when (viewHolder) {
is TabViewHolder -> actionEmitter.onNext(TabAction.Close(viewHolder.tab?.sessionId!!))
is TabInCollectionViewHolder -> {
actionEmitter.onNext(CollectionAction.RemoveTab(viewHolder.collection, viewHolder.tab))
}
}
}
@ -43,11 +47,18 @@ class SwipeToDeleteCallback(
) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val icon = ContextCompat.getDrawable(recyclerView.context, R.drawable.ic_delete)
val background = ContextCompat.getDrawable(
recyclerView.context,
R.drawable.session_background
)
val backgroundDrawable = when {
viewHolder is TabInCollectionViewHolder && viewHolder.isLastTab -> {
R.drawable.tab_in_collection_last_swipe_background
}
viewHolder is TabInCollectionViewHolder -> {
R.drawable.tab_in_collection_swipe_background
}
else -> R.drawable.session_background
}
val background = ContextCompat.getDrawable(recyclerView.context, backgroundDrawable)
background?.let {
icon?.let {
val itemView = viewHolder.itemView
@ -95,7 +106,7 @@ class SwipeToDeleteCallback(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return if (viewHolder is TabViewHolder) {
return if (viewHolder is TabViewHolder || viewHolder is TabInCollectionViewHolder) {
super.getSwipeDirs(recyclerView, viewHolder)
} else 0
}

@ -0,0 +1,17 @@
/* 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.home.sessioncontrol.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R
class CollectionHeaderViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
companion object {
const val LAYOUT_ID = R.layout.collection_header
}
}

@ -0,0 +1,215 @@
/* 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.home.sessioncontrol.viewholders
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.collection_home_list_row.*
import kotlinx.android.synthetic.main.collection_home_list_row.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.home.sessioncontrol.CollectionAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.home.sessioncontrol.onNext
import org.mozilla.fenix.utils.Settings
import kotlin.coroutines.CoroutineContext
class CollectionViewHolder(
val view: View,
val actionEmitter: Observer<SessionControlAction>,
val job: Job,
override val containerView: View? = view
) :
RecyclerView.ViewHolder(view), LayoutContainer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
private lateinit var collection: TabCollection
private var state = CollectionState.Collapsed
private var collectionMenu: CollectionItemMenu
init {
collectionMenu = CollectionItemMenu(view.context) {
when (it) {
is CollectionItemMenu.Item.DeleteCollection -> actionEmitter.onNext(CollectionAction.Delete(collection))
is CollectionItemMenu.Item.AddTab -> actionEmitter.onNext(CollectionAction.AddTab(collection))
is CollectionItemMenu.Item.RenameCollection -> actionEmitter.onNext(CollectionAction.Rename(collection))
is CollectionItemMenu.Item.OpenTabs -> actionEmitter.onNext(CollectionAction.OpenTabs(collection))
}
}
collection_overflow_button.run {
increaseTapArea(buttonIncreaseDps)
setOnClickListener {
collectionMenu.menuBuilder
.build(view.context)
.show(anchor = it, orientation = BrowserMenu.Orientation.DOWN)
}
}
collection_share_button.run {
increaseTapArea(buttonIncreaseDps)
setOnClickListener {
actionEmitter.onNext(CollectionAction.ShareTabs(collection))
}
}
view.setOnClickListener {
updateState()
}
view.collection_icon.setColorFilter(ContextCompat.getColor(
view.context,
getNextIconColor()),
android.graphics.PorterDuff.Mode.SRC_IN
)
}
fun bindSession(collection: TabCollection) {
this.collection = collection
updateCollectionUI()
}
private fun updateCollectionUI() {
view.collection_title.text = collection.title
var hostNameList = listOf<String>()
collection.tabs.forEach {
hostNameList += it.hostname.capitalize()
}
var tabsDisplayed = 0
val titleList = hostNameList.joinToString(", ") {
if (it.length > maxTitleLength) {
it.substring(0,
maxTitleLength
) + "..."
} else {
tabsDisplayed += 1
it
}
}
view.collection_description.text = titleList
if (collection.expanded) {
(view.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
collection_title.setPadding(0, 0, 0, EXPANDED_PADDING)
view.background = ContextCompat.getDrawable(view.context, R.drawable.rounded_top_corners)
view.collection_description.visibility = View.GONE
view.expand_button.setImageDrawable(ContextCompat.getDrawable(view.context, R.drawable.ic_chevron_up))
} else {
(view.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = COLLAPSED_MARGIN
view.background = ContextCompat.getDrawable(view.context, R.drawable.rounded_all_corners)
view.collection_description.visibility = View.VISIBLE
view.expand_button.setImageDrawable(ContextCompat.getDrawable(view.context, R.drawable.ic_chevron_down))
}
}
private fun updateState() {
state = when (state) {
CollectionState.Expanded -> {
actionEmitter.onNext(CollectionAction.Collapse(collection))
CollectionState.Collapsed
}
CollectionState.Collapsed -> {
actionEmitter.onNext(CollectionAction.Expand(collection))
CollectionState.Expanded
}
}
}
@Suppress("ComplexMethod", "MagicNumber")
private fun getNextIconColor(): Int {
with(view.context) {
var sessionColorIndex = Settings.getInstance(this).preferences
.getInt(getString(R.string.pref_key_collection_color), 0)
val iconResource = when (sessionColorIndex) {
0 -> R.color.collection_icon_color_violet
1 -> R.color.collection_icon_color_blue
2 -> R.color.collection_icon_color_pink
3 -> R.color.collection_icon_color_green
4 -> R.color.collection_icon_color_yellow
else -> R.color.white_color
}
if (sessionColorIndex >= MAX_COLOR_INDEX) { sessionColorIndex = 0 } else { sessionColorIndex += 1 }
Settings.getInstance(this).preferences.edit()
.putInt(getString(R.string.pref_key_collection_color), sessionColorIndex).apply()
return iconResource
}
}
companion object {
const val MAX_COLOR_INDEX = 4
const val EXPANDED_PADDING = 60
const val COLLAPSED_MARGIN = 12
const val LAYOUT_ID = R.layout.collection_home_list_row
const val maxTitleLength = 20
const val buttonIncreaseDps = 24
}
enum class CollectionState {
Expanded, Collapsed
}
}
class CollectionItemMenu(
private val context: Context,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object DeleteCollection : Item()
object AddTab : Item()
object RenameCollection : Item()
object OpenTabs : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.collection_delete),
textColorResource = DefaultThemeManager.resolveAttribute(R.attr.destructive, context)
) {
onItemTapped.invoke(Item.DeleteCollection)
},
SimpleBrowserMenuItem(
context.getString(R.string.add_tab)
) {
onItemTapped.invoke(Item.AddTab)
},
SimpleBrowserMenuItem(
context.getString(R.string.collection_rename)
) {
onItemTapped.invoke(Item.RenameCollection)
},
SimpleBrowserMenuItem(
context.getString(R.string.collection_open_tabs)
) {
onItemTapped.invoke(Item.OpenTabs)
}
)
}
}

@ -0,0 +1,17 @@
/* 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.home.sessioncontrol.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R
class NoCollectionMessageViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
companion object {
const val LAYOUT_ID = R.layout.no_collection_message
}
}

@ -0,0 +1,98 @@
/* 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.home.sessioncontrol.viewholders
import android.graphics.Outline
import android.view.View
import android.view.ViewOutlineProvider
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.tab_in_collection.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.browser.icons.IconRequest
import mozilla.components.support.ktx.android.content.res.pxToDp
import org.jetbrains.anko.backgroundColor
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.home.sessioncontrol.CollectionAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.home.sessioncontrol.onNext
import kotlin.coroutines.CoroutineContext
class TabInCollectionViewHolder(
val view: View,
val actionEmitter: Observer<SessionControlAction>,
val job: Job,
override val containerView: View? = view
) : RecyclerView.ViewHolder(view), LayoutContainer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
lateinit var collection: TabCollection
private set
lateinit var tab: Tab
private set
var isLastTab = false
init {
collection_tab_icon.clipToOutline = true
collection_tab_icon.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
outline?.setRoundRect(
0,
0,
view!!.width,
view.height,
view.context.resources.pxToDp(TabViewHolder.favIconBorderRadiusInPx).toFloat()
)
}
}
collection_tab_close_button.setOnClickListener {
actionEmitter.onNext(CollectionAction.RemoveTab(collection, tab))
}
}
fun bindSession(collection: TabCollection, tab: Tab, isLastTab: Boolean) {
this.collection = collection
this.tab = tab
this.isLastTab = isLastTab
updateTabUI()
}
private fun updateTabUI() {
collection_tab_hostname.text = tab.hostname
collection_tab_title.text = tab.title
launch(Dispatchers.IO) {
val bitmap = collection_tab_icon.context.components.utils.icons
.loadIcon(IconRequest(tab.url)).await().bitmap
launch(Dispatchers.Main) {
collection_tab_icon.setImageBitmap(bitmap)
}
}
// If I'm the last one...
if (isLastTab) {
view.background = ContextCompat.getDrawable(view.context, R.drawable.rounded_bottom_corners)
divider_line.visibility = View.GONE
} else {
view.backgroundColor = R.attr.above.getColorFromAttr(view.context)
divider_line.visibility = View.VISIBLE
}
}
companion object {
const val LAYOUT_ID = R.layout.tab_in_collection
}
}

@ -17,7 +17,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.support.ktx.android.content.res.pxToDp
@ -85,18 +84,18 @@ class TabViewHolder(
setOnClickListener {
tabMenu.menuBuilder
.build(view.context)
.show(anchor = it, orientation = BrowserMenu.Orientation.DOWN)
.show(anchor = it)
}
}
}
fun bindSession(tab: Tab) {
this.tab = tab
updateText(tab)
updateSelected(tab.selected)
updateTabUI(tab)
updateSelected(tab.selected ?: false)
}
fun updateText(tab: Tab) {
private fun updateTabUI(tab: Tab) {
hostname.text = tab.hostname
tab_title.text = tab.title
launch(Dispatchers.IO) {

@ -9,5 +9,5 @@
android:viewportHeight="24">
<path
android:pathData="M13,14.6449L14.2772,13.309C14.6588,12.9098 15.2918,12.8955 15.691,13.2772C16.0902,13.6588 16.1045,14.2918 15.7228,14.691L12.7903,17.7585C12.4026,18.1641 11.7571,18.1713 11.3604,17.7746L8.2929,14.7071C7.9024,14.3166 7.9024,13.6834 8.2929,13.2929C8.6834,12.9024 9.3166,12.9024 9.7071,13.2929L11,14.5858L11,11C11,10.4477 11.4477,10 12,10C12.5523,10 13,10.4477 13,11L13,14.6449ZM20,8L4,8L4,19C4,19.5523 4.4477,20 5,20L19,20C19.5523,20 20,19.5523 20,19L20,8ZM18.7908,6L17.333,4.3401C17.1432,4.1239 16.8694,4 16.5817,4L7.8284,4C7.5632,4 7.3089,4.1054 7.1213,4.2929L5.4142,6L18.7908,6ZM2,19L2,7.8284C2,7.0328 2.3161,6.2697 2.8787,5.7071L5.7071,2.8787C6.2697,2.3161 7.0328,2 7.8284,2L16.5817,2C17.4448,2 18.2662,2.3718 18.8358,3.0203L21.2541,5.7739C21.7349,6.3213 22,7.025 22,7.7536L22,19C22,20.6569 20.6569,22 19,22L5,22C3.3431,22 2,20.6569 2,19Z"
android:fillColor="?foundation" />
android:fillColor="?primaryText" />
</vector>

@ -0,0 +1,10 @@
<!-- 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/. -->
<vector android:height="6dp" android:viewportHeight="6"
android:viewportWidth="10" android:width="10dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M5,5.6667C4.8232,5.6666 4.6537,5.5964 4.5287,5.4713L0.5287,1.4713C0.276,1.2097 0.2796,0.7939 0.5368,0.5368C0.7939,0.2796 1.2097,0.276 1.4713,0.5287L5,4.0573L8.5287,0.5287C8.7903,0.276 9.2061,0.2796 9.4632,0.5368C9.7204,0.7939 9.724,1.2097 9.4713,1.4713L5.4713,5.4713C5.3463,5.5964 5.1768,5.6666 5,5.6667Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

@ -0,0 +1,10 @@
<!-- 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/. -->
<vector android:height="6dp" android:viewportHeight="6"
android:viewportWidth="10" android:width="10dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M5,0.3333C4.8232,0.3334 4.6537,0.4036 4.5287,0.5287L0.5287,4.5287C0.276,4.7903 0.2796,5.2061 0.5368,5.4632C0.7939,5.7204 1.2097,5.724 1.4713,5.4713L5,1.9427L8.5287,5.4713C8.7903,5.724 9.2061,5.7204 9.4632,5.4632C9.7204,5.2061 9.724,4.7903 9.4713,4.5287L5.4713,0.5287C5.3463,0.4036 5.1768,0.3334 5,0.3333Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

@ -0,0 +1,10 @@
<!-- 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/. -->
<vector android:height="20dp" android:viewportHeight="20"
android:viewportWidth="19" android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M6.92,12.7338C6.1902,13.5131 5.1519,14 4,14C1.7909,14 0,12.2091 0,10C0,7.7909 1.7909,6 4,6C5.0609,6 6.0783,6.4214 6.8284,7.1716C6.86,7.2031 6.8909,7.2351 6.9213,7.2676L11.1206,4.9785C11.0418,4.6654 11,4.3376 11,4C11,1.7909 12.7909,0 15,0C17.2091,0 19,1.7909 19,4C19,6.2091 17.2091,8 15,8C13.8481,8 12.8098,7.5131 12.08,6.7338L7.879,9.0237C7.9587,9.3403 8,9.668 8,10C8,10.3376 7.9582,10.6654 7.8794,10.9785L12.0787,13.2675C12.8087,12.4875 13.8474,12 15,12C17.2091,12 19,13.7909 19,16C19,18.2091 17.2091,20 15,20C12.7909,20 11,18.2091 11,16C11,15.6631 11.0417,15.3358 11.1201,15.0232L6.92,12.7338ZM15,2C13.8954,2 13,2.8954 13,4C13,5.1046 13.8954,6 15,6C16.1046,6 17,5.1046 17,4C17,2.8954 16.1046,2 15,2ZM4,8C2.8954,8 2,8.8954 2,10C2,11.1046 2.8954,12 4,12C5.1046,12 6,11.1046 6,10C6,8.8954 5.1046,8 4,8ZM15,14C13.8954,14 13,14.8954 13,16C13,17.1046 13.8954,18 15,18C16.1046,18 17,17.1046 17,16C17,14.8954 16.1046,14 15,14Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

@ -0,0 +1,9 @@
<?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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?above" />
<corners android:radius="@dimen/tab_corner_radius"/>
</shape>

@ -0,0 +1,9 @@
<?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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?above" />
<corners android:bottomLeftRadius="@dimen/tab_corner_radius" android:bottomRightRadius="@dimen/tab_corner_radius" />
</shape>

@ -0,0 +1,9 @@
<?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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?above" />
<corners android:topLeftRadius="@dimen/tab_corner_radius" android:topRightRadius="@dimen/tab_corner_radius" />
</shape>

@ -5,6 +5,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<corners android:radius="@dimen/tab_corner_radius" />
<solid android:color="@color/photonGrey30" />
</shape>

@ -0,0 +1,10 @@
<?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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:bottomLeftRadius="@dimen/tab_corner_radius" android:bottomRightRadius="@dimen/tab_corner_radius" />
<solid android:color="@color/photonGrey30" />
</shape>

@ -0,0 +1,9 @@
<?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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/photonGrey30" />
</shape>

@ -0,0 +1,33 @@
<?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/collections_header"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/divider_line"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:background="?neutralFaded"
android:layout_marginStart="23dp"
android:layout_marginEnd="23dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/collections_header_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/collections_header"
android:textAppearance="@style/HeaderTextStyle"
android:layout_marginTop="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_line" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,95 @@
<?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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_collection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:clickable="true"
android:clipToPadding="false"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/rounded_all_corners"
android:elevation="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/collection_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="18dp"
android:tint="@null"
android:src="@drawable/ic_archive"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/collection_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="17dp"
android:layout_marginStart="14dp"
android:ellipsize="end"
android:maxLines="1"
android:minLines="1"
android:textAppearance="@style/Header16TextStyle"
app:layout_constraintStart_toEndOf="@id/collection_icon"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="26dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_chevron_down"
app:layout_constraintStart_toEndOf="@id/collection_title"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/collection_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="2"
android:minLines="2"
android:textAppearance="@style/SubtitleTextStyle"
app:layout_constraintStart_toStartOf="@id/collection_title"
app:layout_constraintTop_toBottomOf="@id/collection_share_button"
app:layout_constraintEnd_toStartOf="@id/collection_share_button"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageButton
android:id="@+id/collection_share_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_menu"
android:src="@drawable/ic_hollow_share"
android:layout_marginEnd="29dp"
app:layout_constraintEnd_toStartOf="@id/collection_overflow_button"
app:layout_constraintTop_toTopOf="@id/collection_icon"/>
<ImageButton
android:id="@+id/collection_overflow_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_menu"
android:src="@drawable/ic_menu"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/collection_icon"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

@ -27,7 +27,7 @@
android:focusable="false"
android:textStyle="bold"
android:gravity="center"
android:text="@string/session_delete"
android:text="@string/collection_delete"
android:textColor="?contrastText"
android:textSize="16sp" />
</FrameLayout>

@ -0,0 +1,35 @@
<?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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/no_tabs_wrapper"
android:background="@drawable/empty_session_control_background"
android:layout_marginBottom="12dp"
android:padding="16dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/no_collection_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_archive"
android:drawableTint="?primaryText"
android:drawablePadding="8dp"
android:text="@string/no_collections_header"
android:textAppearance="@style/HeaderTextStyle"
android:textSize="16sp" />
<TextView
android:id="@+id/no_collection_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/no_collections_description"
android:textColor="?primaryText"
android:textSize="14sp"
android:textStyle="normal" />
</LinearLayout>

@ -33,7 +33,7 @@
android:contentDescription="@string/current_session_image"
android:paddingBottom="20dp"
android:src="@drawable/ic_session_thumbnail_placeholder_greyscale"
android:tint="@color/session_placeholder_blue"
android:tint="@color/collection_icon_color_blue"
android:tintMode="multiply"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

@ -0,0 +1,84 @@
<?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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/tab_in_collection_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:clipToPadding="false"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:background="?above"
android:elevation="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/collection_tab_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="25dp"
android:layout_marginStart="18dp"
android:tint="@null"
android:src="@drawable/ic_archive"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/collection_tab_hostname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="14dp"
android:layout_marginEnd="48dp"
android:ellipsize="end"
android:maxLines="1"
android:minLines="1"
android:textAppearance="@style/Header12TextStyle"
app:layout_constraintStart_toEndOf="@id/collection_tab_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/collection_tab_close_button"/>
<TextView
android:id="@+id/collection_tab_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:ellipsize="end"
android:maxLines="2"
android:minLines="2"
android:textAppearance="@style/Body14TextStyle"
app:layout_constraintStart_toStartOf="@id/collection_tab_hostname"
app:layout_constraintTop_toBottomOf="@id/collection_tab_hostname"
app:layout_constraintEnd_toEndOf="@id/collection_tab_hostname"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageButton
android:id="@+id/collection_tab_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/close_tab"
android:src="@drawable/ic_close"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:alpha="0.8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<View
android:id="@+id/divider_line"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:background="?neutralFaded"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

@ -7,7 +7,7 @@
android:id="@+id/item_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginBottom="12dp"
android:clickable="true"
android:clipToPadding="false"
android:focusable="true"
@ -40,9 +40,7 @@
android:layout_marginStart="8dp"
android:ellipsize="none"
android:singleLine="true"
android:textColor="?secondaryText"
android:textSize="12sp"
android:textStyle="bold"
android:textAppearance="@style/Header12TextStyle"
app:layout_constraintEnd_toStartOf="@id/close_tab_button"
app:layout_constraintStart_toEndOf="@id/favicon_image"
app:layout_constraintTop_toTopOf="@id/favicon_image" />
@ -58,7 +56,7 @@
android:maxLines="2"
android:minLines="2"
android:textColor="?primaryText"
android:textSize="15sp"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/favicon_image"
app:layout_constraintEnd_toEndOf="@id/hostname"
app:layout_constraintTop_toBottomOf="@id/hostname"

@ -74,12 +74,12 @@
<!-- Bookmark buttons -->
<color name="bookmark_favicon_background">#DFDFE3</color>
<!-- Session placeholder icons-->
<color name="session_placeholder_blue">#00B3F4</color>
<color name="session_placeholder_orange">#FF8A50</color>
<color name="session_placeholder_green">#54FFBD</color>
<color name="session_placeholder_purple">#AB71FF</color>
<color name="session_placeholder_pink">#FF4AA2</color>
<!-- Collection icons-->
<color name="collection_icon_color_violet">#7542E5</color>
<color name="collection_icon_color_blue">#0250BB</color>
<color name="collection_icon_color_pink">#E31587</color>
<color name="collection_icon_color_green">#2AC3A2</color>
<color name="collection_icon_color_yellow">#E27F2E</color>
<!-- Library buttons -->
<color name="library_sessions_icon_background">#B9F0FD</color>

@ -65,4 +65,6 @@
<string name="pref_key_tracking_protection_settings" translatable="false">pref_key_tracking_protection_settings</string>
<string name="pref_key_tracking_protection" translatable="false">pref_key_tracking_protection</string>
<string name="pref_key_tracking_protection_exceptions" translatable="false">pref_key_tracking_protection_exceptions</string>
<string name="pref_key_collection_color" translatable="false">pref_key_collection_color</string>
</resources>

@ -2,8 +2,6 @@
- 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/. -->
<resources>
<!-- Home Fragment -->
<!-- Content description (not visible, for screen readers etc.): "Three dot" menu button. -->
<string name="content_description_menu">More options</string>
@ -269,8 +267,12 @@
<string name="current_session_image">Current session image</string>
<!-- Button to save the current set of tabs into a collection -->
<string name="save_to_collection">Save to collection</string>
<!-- Text for the button to delete a session -->
<string name="session_delete">Delete session</string>
<!-- Text for the menu button to delete a collection -->
<string name="collection_delete">Delete collection</string>
<!-- Text for the menu button to rename a collection -->
<string name="collection_rename">Rename collection</string>
<!-- Text for the button to open tabs of the selected collection -->
<string name="collection_open_tabs">Open tabs</string>
<!-- Text for the button to delete a single session -->
<string name="session_item_delete">Delete</string>
<!-- Text to tell the user how many more tabs this session has.
@ -383,11 +385,9 @@
<!-- Message for copying the URL via long press on the toolbar -->
<string name="url_copied">URL copied</string>
<!-- Site Permissions -->
<!-- Button label that take the user to the Android App setting -->
<string name="phone_feature_go_to_settings">Go to Settings</string>
<!-- Content description (not visible, for screen readers etc.): Quick settings sheet
to give users access to site specific information / settings. For example:
Secure settings status and a button to modify site permissions -->
@ -427,6 +427,13 @@
<!-- Summary of tracking protection preference if tracking protection is set to off -->
<string name="tracking_protection_off">Off</string>
<!-- Collections -->
<!-- Collections header on home fragment -->
<string name="collections_header">Collections</string>
<!-- No Open Tabs Message Header -->
<string name="no_collections_header">No collections</string>
<!-- No Open Tabs Message Description -->
<string name="no_collections_description">Your collections will be shown here.</string>
<!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Select Tabs</string>
@ -442,7 +449,6 @@
<!-- Button to select all tabs in the "select tabs" step of the collection creator -->
<string name="create_collection_select_all">Select All</string>
<!-- Text to prompt users to select the tabs to save in the "select tabs" stepof the collection creator -->
<string name="create_collection_save_to_collection_empty">Select tabs to save</string>

@ -166,12 +166,34 @@
<item name="android:letterSpacing">0.03</item>
</style>
<style name="Header16TextStyle" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?primaryText</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Header14TextStyle" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:textColor">?primaryText</item>
<item name="android:textSize">14sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Header12TextStyle" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:textColor">?secondaryText</item>
<item name="android:textSize">12sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Body14TextStyle" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:textColor">?primaryText</item>
<item name="android:textSize">14sp</item>
</style>
<style name="SubtitleTextStyle" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?secondaryText</item>
<item name="android:textSize">14sp</item>
</style>
<style name="ToolbarTitleTextStyle" parent="HeaderTextStyle">
<item name="android:textSize">20sp</item>
</style>

Loading…
Cancel
Save