1
0
Fork 0

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
master
Sawyer Blatz 2019-05-06 11:20:19 -07:00 committed by GitHub
parent 282ad31345
commit 7d577e5953
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 924 additions and 66 deletions

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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(

View File

@ -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(

View File

@ -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)
}
}
}

View File

@ -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>()

View File

@ -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
}

View File

@ -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) {
""
}
}

View File

@ -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 {

View File

@ -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)
}
}
}
}

View File

@ -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()
}

View File

@ -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>,

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)
}
)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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) {

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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>

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>