diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 363a30936..9cce21530 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt index bc1614cbc..a66dcc434 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt @@ -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, val name: String) : CollectionCreationAction() - data class SelectCollection(val collection: Collection) : + data class SelectCollection(val collection: TabCollection) : CollectionCreationAction() } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt index 6b5b87a7b..be864892b 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt @@ -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( diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt index e74db1230..815dd2487 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt @@ -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( diff --git a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt index f41f6d156..733f12d2d 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt @@ -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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt index 7addd4c97..027d5d740 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/collections/SaveToCollectionListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/SaveToCollectionListAdapter.kt index 68147d827..0b2b87cd2 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/SaveToCollectionListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/SaveToCollectionListAdapter.kt @@ -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 ) : RecyclerView.Adapter() { - private var collections: List = listOf() + private var collections: List = 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 } diff --git a/app/src/main/java/org/mozilla/fenix/ext/String.kt b/app/src/main/java/org/mozilla/fenix/ext/String.kt index 9e1d127e4..e62d535cd 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/String.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/String.kt @@ -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) { + "" + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index b588172c9..0aab62334 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -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() + 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().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 { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index dfa45d953..9276245df 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -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) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt index 38cd0c211..26219f72f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt @@ -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( 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, + 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, + val collections: List, 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.onNext(tabAction: TabAction) { onNext(SessionControlAction.Tab(tabAction)) } +fun Observer.onNext(collectionAction: CollectionAction) { + onNext(SessionControlAction.Collection(collectionAction)) +} + sealed class SessionControlChange : Change { data class TabsChange(val tabs: List) : SessionControlChange() data class ModeChange(val mode: Mode) : SessionControlChange() + data class CollectionsChange(val collections: List) : SessionControlChange() } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt index aa6d738a0..67583a332 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt @@ -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 { val items = mutableListOf() 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 { 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, + itemList: MutableList +) { + 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, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt index a8732cc86..7c220e79f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionHeaderViewHolder.kt new file mode 100644 index 000000000..024ead900 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionHeaderViewHolder.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt new file mode 100644 index 000000000..cad283092 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt @@ -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, + 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() + + 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) + } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionMessageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionMessageViewHolder.kt new file mode 100644 index 000000000..cffc4df77 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionMessageViewHolder.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt new file mode 100644 index 000000000..30933996f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt @@ -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, + 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt index 3ccdefa40..aa1ceb905 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt @@ -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) { diff --git a/app/src/main/res/drawable/ic_archive.xml b/app/src/main/res/drawable/ic_archive.xml index b51440712..170c954d7 100644 --- a/app/src/main/res/drawable/ic_archive.xml +++ b/app/src/main/res/drawable/ic_archive.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:fillColor="?primaryText" /> diff --git a/app/src/main/res/drawable/ic_chevron_down.xml b/app/src/main/res/drawable/ic_chevron_down.xml new file mode 100644 index 000000000..66ed12300 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_down.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_chevron_up.xml b/app/src/main/res/drawable/ic_chevron_up.xml new file mode 100644 index 000000000..a2593ea30 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_up.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_hollow_share.xml b/app/src/main/res/drawable/ic_hollow_share.xml new file mode 100644 index 000000000..3e4a19d03 --- /dev/null +++ b/app/src/main/res/drawable/ic_hollow_share.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_all_corners.xml b/app/src/main/res/drawable/rounded_all_corners.xml new file mode 100644 index 000000000..534bdbc5b --- /dev/null +++ b/app/src/main/res/drawable/rounded_all_corners.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_bottom_corners.xml b/app/src/main/res/drawable/rounded_bottom_corners.xml new file mode 100644 index 000000000..4ce4f140b --- /dev/null +++ b/app/src/main/res/drawable/rounded_bottom_corners.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_top_corners.xml b/app/src/main/res/drawable/rounded_top_corners.xml new file mode 100644 index 000000000..f83a5b4b9 --- /dev/null +++ b/app/src/main/res/drawable/rounded_top_corners.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/session_background.xml b/app/src/main/res/drawable/session_background.xml index 3b1d5b4d9..441bfc421 100644 --- a/app/src/main/res/drawable/session_background.xml +++ b/app/src/main/res/drawable/session_background.xml @@ -5,6 +5,6 @@ - + diff --git a/app/src/main/res/drawable/tab_in_collection_last_swipe_background.xml b/app/src/main/res/drawable/tab_in_collection_last_swipe_background.xml new file mode 100644 index 000000000..c6f1d61e4 --- /dev/null +++ b/app/src/main/res/drawable/tab_in_collection_last_swipe_background.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/drawable/tab_in_collection_swipe_background.xml b/app/src/main/res/drawable/tab_in_collection_swipe_background.xml new file mode 100644 index 000000000..5c74a6e40 --- /dev/null +++ b/app/src/main/res/drawable/tab_in_collection_swipe_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/collection_header.xml b/app/src/main/res/layout/collection_header.xml new file mode 100644 index 000000000..bc2c2b54d --- /dev/null +++ b/app/src/main/res/layout/collection_header.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/collection_home_list_row.xml b/app/src/main/res/layout/collection_home_list_row.xml new file mode 100644 index 000000000..84885eb6f --- /dev/null +++ b/app/src/main/res/layout/collection_home_list_row.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/delete_tabs_button.xml b/app/src/main/res/layout/delete_tabs_button.xml index c103d3830..af3516eea 100644 --- a/app/src/main/res/layout/delete_tabs_button.xml +++ b/app/src/main/res/layout/delete_tabs_button.xml @@ -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" /> \ No newline at end of file diff --git a/app/src/main/res/layout/no_collection_message.xml b/app/src/main/res/layout/no_collection_message.xml new file mode 100644 index 000000000..18e1a407d --- /dev/null +++ b/app/src/main/res/layout/no_collection_message.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/session_bottom_sheet.xml b/app/src/main/res/layout/session_bottom_sheet.xml index 668349852..31436f912 100644 --- a/app/src/main/res/layout/session_bottom_sheet.xml +++ b/app/src/main/res/layout/session_bottom_sheet.xml @@ -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" /> diff --git a/app/src/main/res/layout/tab_in_collection.xml b/app/src/main/res/layout/tab_in_collection.xml new file mode 100644 index 000000000..8565fbcf4 --- /dev/null +++ b/app/src/main/res/layout/tab_in_collection.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/tab_list_row.xml b/app/src/main/res/layout/tab_list_row.xml index e69862138..b58a6c746 100644 --- a/app/src/main/res/layout/tab_list_row.xml +++ b/app/src/main/res/layout/tab_list_row.xml @@ -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" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index dda31ea83..80045680c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -74,12 +74,12 @@ #DFDFE3 - - #00B3F4 - #FF8A50 - #54FFBD - #AB71FF - #FF4AA2 + + #7542E5 + #0250BB + #E31587 + #2AC3A2 + #E27F2E #B9F0FD diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 29cb4f989..d7cf9a9f8 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -65,4 +65,6 @@ pref_key_tracking_protection_settings pref_key_tracking_protection pref_key_tracking_protection_exceptions + + pref_key_collection_color diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e33f4c0c..aa24eceae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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/. --> - - More options @@ -269,8 +267,12 @@ Current session image Save to collection - - Delete session + + Delete collection + + Rename collection + + Open tabs Delete URL copied - Go to Settings - @@ -427,6 +427,13 @@ Off + + + Collections + + No collections + + Your collections will be shown here. Select Tabs @@ -442,7 +449,6 @@ Select All - Select tabs to save diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fd91c6617..0eea5501d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -166,12 +166,34 @@ 0.03 + + + + + + + +