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.CreateCollectionFragment
import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.collections.Tab
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.metrics.Event 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.components
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share 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.lib.Do
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
@ -606,7 +606,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope,
private fun showSaveToCollection() { private fun showSaveToCollection() {
getSessionById()?.let { 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 { val viewModel = activity?.run {
ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java) 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/. */ file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.view.ViewGroup 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change 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.UIComponent
import org.mozilla.fenix.mvi.ViewState 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 { sealed class SaveCollectionStep {
object SelectTabs : SaveCollectionStep() object SelectTabs : SaveCollectionStep()
object SelectCollection : SaveCollectionStep() object SelectCollection : SaveCollectionStep()
@ -55,7 +45,7 @@ sealed class CollectionCreationAction : Action {
data class SaveCollectionName(val tabs: List<Tab>, val name: String) : data class SaveCollectionName(val tabs: List<Tab>, val name: String) :
CollectionCreationAction() CollectionCreationAction()
data class SelectCollection(val collection: Collection) : data class SelectCollection(val collection: TabCollection) :
CollectionCreationAction() CollectionCreationAction()
} }

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

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

@ -17,11 +17,15 @@ import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
import java.util.Random
class CreateCollectionFragment : DialogFragment() { 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 private lateinit var collectionCreationComponent: CollectionCreationComponent
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -91,6 +95,8 @@ class CreateCollectionFragment : DialogFragment() {
is CollectionCreationAction.SaveCollectionName -> { is CollectionCreationAction.SaveCollectionName -> {
showSavedSnackbar(it.tabs.size) showSavedSnackbar(it.tabs.size)
dismiss() 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/. */ file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.mozilla.fenix.home.sessioncontrol.Tab
class CreateCollectionViewModel : ViewModel() { class CreateCollectionViewModel : ViewModel() {
var selectedTabs = setOf<Tab>() var selectedTabs = setOf<Tab>()

@ -14,13 +14,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class SaveCollectionListAdapter( class SaveCollectionListAdapter(
val actionEmitter: Observer<CollectionCreationAction> val actionEmitter: Observer<CollectionCreationAction>
) : RecyclerView.Adapter<CollectionViewHolder>() { ) : RecyclerView.Adapter<CollectionViewHolder>() {
private var collections: List<Collection> = listOf() private var collections: List<TabCollection> = listOf()
private lateinit var job: Job private lateinit var job: Job
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollectionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollectionViewHolder {
@ -58,7 +59,7 @@ class CollectionViewHolder(
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job get() = Dispatchers.IO + job
private var collection: Collection? = null private var collection: TabCollection? = null
private val listener = View.OnClickListener { private val listener = View.OnClickListener {
collection?.apply { collection?.apply {
@ -71,7 +72,7 @@ class CollectionViewHolder(
view.setOnClickListener(listener) view.setOnClickListener(listener)
} }
fun bind(collection: Collection) { fun bind(collection: TabCollection) {
this.collection = collection this.collection = collection
view.collection_item.text = collection.title 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.CreateCollectionFragment
import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.collections.Tab
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.allowUndo import org.mozilla.fenix.ext.allowUndo
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share 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.Mode
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent
import org.mozilla.fenix.home.sessioncontrol.SessionControlState import org.mozilla.fenix.home.sessioncontrol.SessionControlState
import org.mozilla.fenix.home.sessioncontrol.TabAction 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.lib.Do
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
@ -73,6 +75,9 @@ class HomeFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.Main + job
// TODO Remove this stub when we have the a-c version!
var storedCollections = mutableListOf<TabCollection>()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -80,12 +85,12 @@ class HomeFragment : Fragment(), CoroutineScope {
): View? { ): View? {
job = Job() job = Job()
val view = inflater.inflate(R.layout.fragment_home, container, false) val view = inflater.inflate(R.layout.fragment_home, container, false)
val mode = val mode = if ((activity as HomeActivity).browsingModeManager.isPrivate) Mode.Private else Mode.Normal
if ((activity as HomeActivity).browsingModeManager.isPrivate) Mode.Private else Mode.Normal
sessionControlComponent = SessionControlComponent( sessionControlComponent = SessionControlComponent(
view.homeLayout, view.homeLayout,
bus, bus,
SessionControlState(listOf(), mode) SessionControlState(listOf(), listOf(), mode)
) )
view.homeLayout.applyConstraintSet { view.homeLayout.applyConstraintSet {
@ -178,6 +183,7 @@ class HomeFragment : Fragment(), CoroutineScope {
.subscribe { .subscribe {
when (it) { when (it) {
is SessionControlAction.Tab -> handleTabAction(it.action) 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() { override fun onPause() {
super.onPause() super.onPause()
sessionObserver?.let { sessionObserver?.let {
@ -326,7 +370,7 @@ class HomeFragment : Fragment(), CoroutineScope {
org.mozilla.fenix.home.sessioncontrol.Tab( org.mozilla.fenix.home.sessioncontrol.Tab(
it.id, it.id,
it.url, it.url,
it.url.urlToHost(), it.url.urlToTrimmedHost(),
it.title, it.title,
selected, selected,
it.thumbnail it.thumbnail
@ -364,7 +408,7 @@ class HomeFragment : Fragment(), CoroutineScope {
org.mozilla.fenix.home.sessioncontrol.Tab( org.mozilla.fenix.home.sessioncontrol.Tab(
it.id, it.id,
it.url, it.url,
it.url.urlToHost(), it.url.urlToTrimmedHost(),
it.title, it.title,
selected, selected,
it.thumbnail it.thumbnail
@ -376,7 +420,7 @@ class HomeFragment : Fragment(), CoroutineScope {
private fun showCollectionCreationFragment(selectedTabId: String?) { private fun showCollectionCreationFragment(selectedTabId: String?) {
val tabs = requireComponents.core.sessionManager.sessions 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 { val viewModel = activity?.run {
ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java) ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java)
@ -387,11 +431,17 @@ class HomeFragment : Fragment(), CoroutineScope {
viewModel?.selectedTabs = selectedSet viewModel?.selectedTabs = selectedSet
viewModel?.saveCollectionStep = SaveCollectionStep.SelectTabs viewModel?.saveCollectionStep = SaveCollectionStep.SelectTabs
CreateCollectionFragment() CreateCollectionFragment().also {
.show( it.onCollectionSaved = {
storedCollections.add(it)
emitCollectionChange()
}
it.show(
requireActivity().supportFragmentManager, requireActivity().supportFragmentManager,
CreateCollectionFragment.createCollectionTag CreateCollectionFragment.createCollectionTag
) )
}
} }
companion object { 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.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder 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 import java.lang.IllegalStateException
sealed class AdapterItem { sealed class AdapterItem {
@ -24,6 +28,10 @@ sealed class AdapterItem {
object PrivateBrowsingDescription : AdapterItem() object PrivateBrowsingDescription : AdapterItem()
object SaveTabGroup : AdapterItem() object SaveTabGroup : AdapterItem()
object DeleteTabs : 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 val viewType: Int
get() = when (this) { get() = when (this) {
@ -33,6 +41,10 @@ sealed class AdapterItem {
SaveTabGroup -> SaveTabGroupViewHolder.LAYOUT_ID SaveTabGroup -> SaveTabGroupViewHolder.LAYOUT_ID
PrivateBrowsingDescription -> PrivateBrowsingDescriptionViewHolder.LAYOUT_ID PrivateBrowsingDescription -> PrivateBrowsingDescriptionViewHolder.LAYOUT_ID
DeleteTabs -> DeleteTabsViewHolder.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 actionEmitter
) )
DeleteTabsViewHolder.LAYOUT_ID -> DeleteTabsViewHolder(view, 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() else -> throw IllegalStateException()
} }
} }
@ -85,6 +103,13 @@ class SessionControlAdapter(
is TabViewHolder -> holder.bindSession( is TabViewHolder -> holder.bindSession(
(items[position] as AdapterItem.TabItem).tab (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( class SessionControlComponent(
private val container: ViewGroup, private val container: ViewGroup,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: SessionControlState = SessionControlState(emptyList(), Mode.Normal) override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal)
) : ) :
UIComponent<SessionControlState, SessionControlAction, SessionControlChange>( UIComponent<SessionControlState, SessionControlAction, SessionControlChange>(
bus.getManagedEmitter(SessionControlAction::class.java), bus.getManagedEmitter(SessionControlAction::class.java),
@ -26,6 +26,7 @@ class SessionControlComponent(
override val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change -> override val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) { when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs) is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode) is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
} }
@ -45,10 +46,18 @@ data class Tab(
val url: String, val url: String,
val hostname: String, val hostname: String,
val title: String, val title: String,
val selected: Boolean, val selected: Boolean? = null,
val thumbnail: Bitmap? = 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 { sealed class Mode {
object Normal : Mode() object Normal : Mode()
object Private : Mode() object Private : Mode()
@ -56,6 +65,7 @@ sealed class Mode {
data class SessionControlState( data class SessionControlState(
val tabs: List<Tab>, val tabs: List<Tab>,
val collections: List<TabCollection>,
val mode: Mode val mode: Mode
) : ViewState ) : ViewState
@ -70,15 +80,32 @@ sealed class TabAction : Action {
object PrivateBrowsingLearnMore : TabAction() 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 { sealed class SessionControlAction : Action {
data class Tab(val action: TabAction) : SessionControlAction() data class Tab(val action: TabAction) : SessionControlAction()
data class Collection(val action: CollectionAction) : SessionControlAction()
} }
fun Observer<SessionControlAction>.onNext(tabAction: TabAction) { fun Observer<SessionControlAction>.onNext(tabAction: TabAction) {
onNext(SessionControlAction.Tab(tabAction)) onNext(SessionControlAction.Tab(tabAction))
} }
fun Observer<SessionControlAction>.onNext(collectionAction: CollectionAction) {
onNext(SessionControlAction.Collection(collectionAction))
}
sealed class SessionControlChange : Change { sealed class SessionControlChange : Change {
data class TabsChange(val tabs: List<Tab>) : SessionControlChange() data class TabsChange(val tabs: List<Tab>) : SessionControlChange()
data class ModeChange(val mode: Mode) : 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 import org.mozilla.fenix.BuildConfig
// Convert HomeState into a data structure HomeAdapter understands // Convert HomeState into a data structure HomeAdapter understands
@SuppressWarnings("ComplexMethod") @SuppressWarnings("ComplexMethod", "NestedBlockDepth")
private fun SessionControlState.toAdapterList(): List<AdapterItem> { private fun SessionControlState.toAdapterList(): List<AdapterItem> {
val items = mutableListOf<AdapterItem>() val items = mutableListOf<AdapterItem>()
items.add(AdapterItem.TabHeader) items.add(AdapterItem.TabHeader)
// Populate tabs
if (tabs.isNotEmpty()) { if (tabs.isNotEmpty()) {
tabs.reversed().map(AdapterItem::TabItem).forEach { items.add(it) } tabs.reversed().map(AdapterItem::TabItem).forEach { items.add(it) }
if (mode == Mode.Private) { if (mode == Mode.Private) {
@ -36,9 +37,39 @@ private fun SessionControlState.toAdapterList(): List<AdapterItem> {
items.add(item) 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 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( class SessionControlUIView(
container: ViewGroup, container: ViewGroup,
actionEmitter: Observer<SessionControlAction>, actionEmitter: Observer<SessionControlAction>,

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer import io.reactivex.Observer
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
class SwipeToDeleteCallback( class SwipeToDeleteCallback(
@ -27,8 +28,11 @@ class SwipeToDeleteCallback(
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder is TabViewHolder) { when (viewHolder) {
actionEmitter.onNext(TabAction.Close(viewHolder.tab?.sessionId!!)) 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) super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val icon = ContextCompat.getDrawable(recyclerView.context, R.drawable.ic_delete) 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 { background?.let {
icon?.let { icon?.let {
val itemView = viewHolder.itemView val itemView = viewHolder.itemView
@ -95,7 +106,7 @@ class SwipeToDeleteCallback(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
): Int { ): Int {
return if (viewHolder is TabViewHolder) { return if (viewHolder is TabViewHolder || viewHolder is TabInCollectionViewHolder) {
super.getSwipeDirs(recyclerView, viewHolder) super.getSwipeDirs(recyclerView, viewHolder)
} else 0 } 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.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.icons.IconRequest import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.support.ktx.android.content.res.pxToDp import mozilla.components.support.ktx.android.content.res.pxToDp
@ -85,18 +84,18 @@ class TabViewHolder(
setOnClickListener { setOnClickListener {
tabMenu.menuBuilder tabMenu.menuBuilder
.build(view.context) .build(view.context)
.show(anchor = it, orientation = BrowserMenu.Orientation.DOWN) .show(anchor = it)
} }
} }
} }
fun bindSession(tab: Tab) { fun bindSession(tab: Tab) {
this.tab = tab this.tab = tab
updateText(tab) updateTabUI(tab)
updateSelected(tab.selected) updateSelected(tab.selected ?: false)
} }
fun updateText(tab: Tab) { private fun updateTabUI(tab: Tab) {
hostname.text = tab.hostname hostname.text = tab.hostname
tab_title.text = tab.title tab_title.text = tab.title
launch(Dispatchers.IO) { launch(Dispatchers.IO) {

@ -9,5 +9,5 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <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: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> </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" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<corners android:radius="8dp" /> <corners android:radius="@dimen/tab_corner_radius" />
<solid android:color="@color/photonGrey30" /> <solid android:color="@color/photonGrey30" />
</shape> </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:focusable="false"
android:textStyle="bold" android:textStyle="bold"
android:gravity="center" android:gravity="center"
android:text="@string/session_delete" android:text="@string/collection_delete"
android:textColor="?contrastText" android:textColor="?contrastText"
android:textSize="16sp" /> android:textSize="16sp" />
</FrameLayout> </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:contentDescription="@string/current_session_image"
android:paddingBottom="20dp" android:paddingBottom="20dp"
android:src="@drawable/ic_session_thumbnail_placeholder_greyscale" android:src="@drawable/ic_session_thumbnail_placeholder_greyscale"
android:tint="@color/session_placeholder_blue" android:tint="@color/collection_icon_color_blue"
android:tintMode="multiply" android:tintMode="multiply"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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:id="@+id/item_tab"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="12dp"
android:clickable="true" android:clickable="true"
android:clipToPadding="false" android:clipToPadding="false"
android:focusable="true" android:focusable="true"
@ -40,9 +40,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:ellipsize="none" android:ellipsize="none"
android:singleLine="true" android:singleLine="true"
android:textColor="?secondaryText" android:textAppearance="@style/Header12TextStyle"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/close_tab_button" app:layout_constraintEnd_toStartOf="@id/close_tab_button"
app:layout_constraintStart_toEndOf="@id/favicon_image" app:layout_constraintStart_toEndOf="@id/favicon_image"
app:layout_constraintTop_toTopOf="@id/favicon_image" /> app:layout_constraintTop_toTopOf="@id/favicon_image" />
@ -58,7 +56,7 @@
android:maxLines="2" android:maxLines="2"
android:minLines="2" android:minLines="2"
android:textColor="?primaryText" android:textColor="?primaryText"
android:textSize="15sp" android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/favicon_image" app:layout_constraintStart_toEndOf="@id/favicon_image"
app:layout_constraintEnd_toEndOf="@id/hostname" app:layout_constraintEnd_toEndOf="@id/hostname"
app:layout_constraintTop_toBottomOf="@id/hostname" app:layout_constraintTop_toBottomOf="@id/hostname"

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

@ -2,8 +2,6 @@
- License, v. 2.0. If a copy of the MPL was not distributed with this - License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources> <resources>
<!-- Home Fragment --> <!-- Home Fragment -->
<!-- Content description (not visible, for screen readers etc.): "Three dot" menu button. --> <!-- Content description (not visible, for screen readers etc.): "Three dot" menu button. -->
<string name="content_description_menu">More options</string> <string name="content_description_menu">More options</string>
@ -269,8 +267,12 @@
<string name="current_session_image">Current session image</string> <string name="current_session_image">Current session image</string>
<!-- Button to save the current set of tabs into a collection --> <!-- Button to save the current set of tabs into a collection -->
<string name="save_to_collection">Save to collection</string> <string name="save_to_collection">Save to collection</string>
<!-- Text for the button to delete a session --> <!-- Text for the menu button to delete a collection -->
<string name="session_delete">Delete session</string> <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 --> <!-- Text for the button to delete a single session -->
<string name="session_item_delete">Delete</string> <string name="session_item_delete">Delete</string>
<!-- Text to tell the user how many more tabs this session has. <!-- 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 --> <!-- Message for copying the URL via long press on the toolbar -->
<string name="url_copied">URL copied</string> <string name="url_copied">URL copied</string>
<!-- Site Permissions --> <!-- Site Permissions -->
<!-- Button label that take the user to the Android App setting --> <!-- Button label that take the user to the Android App setting -->
<string name="phone_feature_go_to_settings">Go to Settings</string> <string name="phone_feature_go_to_settings">Go to Settings</string>
<!-- Content description (not visible, for screen readers etc.): Quick settings sheet <!-- Content description (not visible, for screen readers etc.): Quick settings sheet
to give users access to site specific information / settings. For example: to give users access to site specific information / settings. For example:
Secure settings status and a button to modify site permissions --> 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 --> <!-- Summary of tracking protection preference if tracking protection is set to off -->
<string name="tracking_protection_off">Off</string> <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 --> <!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Select Tabs</string> <string name="create_collection_select_tabs">Select Tabs</string>
@ -442,7 +449,6 @@
<!-- Button to select all tabs in the "select tabs" step of the collection creator --> <!-- Button to select all tabs in the "select tabs" step of the collection creator -->
<string name="create_collection_select_all">Select All</string> <string name="create_collection_select_all">Select All</string>
<!-- Text to prompt users to select the tabs to save in the "select tabs" stepof the collection creator --> <!-- 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> <string name="create_collection_save_to_collection_empty">Select tabs to save</string>

@ -166,12 +166,34 @@
<item name="android:letterSpacing">0.03</item> <item name="android:letterSpacing">0.03</item>
</style> </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"> <style name="Header14TextStyle" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:textColor">?primaryText</item> <item name="android:textColor">?primaryText</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
</style> </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"> <style name="ToolbarTitleTextStyle" parent="HeaderTextStyle">
<item name="android:textSize">20sp</item> <item name="android:textSize">20sp</item>
</style> </style>

Loading…
Cancel
Save