Fixes #351 Create home screen component for multitasking
parent
77883c0f30
commit
f09dc2453f
|
@ -6,6 +6,7 @@ apply plugin: 'com.android.application'
|
|||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply from: "$project.rootDir/automation/gradle/versionCode.gradle"
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
|
|
@ -17,7 +17,6 @@ import mozilla.components.concept.engine.EngineView
|
|||
import mozilla.components.feature.intent.IntentProcessor
|
||||
import mozilla.components.support.base.feature.BackHandler
|
||||
import mozilla.components.support.utils.SafeIntent
|
||||
import org.mozilla.fenix.browser.BrowserFragment
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
open class HomeActivity : AppCompatActivity() {
|
||||
|
@ -71,10 +70,8 @@ open class HomeActivity : AppCompatActivity() {
|
|||
private fun openToBrowser() {
|
||||
val sessionId = SafeIntent(intent).getStringExtra(IntentProcessor.ACTIVE_SESSION_ID)
|
||||
val host = supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
|
||||
|
||||
host.navController.navigate(R.id.action_global_browser, Bundle().apply {
|
||||
putString(BrowserFragment.SESSION_ID, sessionId)
|
||||
})
|
||||
val directions = NavGraphDirections.actionGlobalBrowser(sessionId)
|
||||
host.navController.navigate(directions)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -55,17 +55,19 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
|
||||
private val customTabsToolbarFeature = ViewBoundFeatureWrapper<CustomTabsToolbarFeature>()
|
||||
private val toolbarIntegration = ViewBoundFeatureWrapper<ToolbarIntegration>()
|
||||
var sessionId: String? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
sessionId = BrowserFragmentArgs.fromBundle(arguments!!).sessionId
|
||||
val view = inflater.inflate(R.layout.fragment_browser, container, false)
|
||||
|
||||
toolbarComponent = ToolbarComponent(
|
||||
view.browserLayout,
|
||||
ActionBusFactory.get(this),
|
||||
ActionBusFactory.get(this), sessionId,
|
||||
SearchState("", isEditing = false)
|
||||
)
|
||||
|
||||
|
@ -98,7 +100,7 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
.subscribe {
|
||||
when (it) {
|
||||
is SearchAction.ToolbarTapped -> Navigation.findNavController(toolbar)
|
||||
.navigate(R.id.action_browserFragment_to_searchFragment, null, null)
|
||||
.navigate(BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(null))
|
||||
is SearchAction.ToolbarMenuItemTapped -> handleToolbarItemInteraction(it)
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +111,6 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
|
||||
(activity as AppCompatActivity).supportActionBar?.hide()
|
||||
|
||||
val sessionId = arguments?.getString(SESSION_ID)
|
||||
|
||||
val sessionManager = requireComponents.core.sessionManager
|
||||
|
||||
contextMenuFeature.set(
|
||||
|
@ -182,8 +182,6 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
if (sessionFeature.onBackPressed()) return true
|
||||
if (customTabsToolbarFeature.onBackPressed()) return true
|
||||
|
||||
// We'll want to improve this when we add multitasking
|
||||
requireComponents.core.sessionManager.remove()
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -211,9 +209,9 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
is ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke()
|
||||
is ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke()
|
||||
is ToolbarMenu.Item.Settings -> Navigation.findNavController(toolbar)
|
||||
.navigate(R.id.action_browserFragment_to_settingsFragment, null, null)
|
||||
.navigate(BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment())
|
||||
is ToolbarMenu.Item.Library -> Navigation.findNavController(toolbar)
|
||||
.navigate(R.id.action_browserFragment_to_libraryFragment, null, null)
|
||||
.navigate(BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment())
|
||||
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(action.item.isChecked)
|
||||
is ToolbarMenu.Item.Share -> requireComponents.core.sessionManager
|
||||
.selectedSession?.url?.apply { requireContext().share(this) }
|
||||
|
@ -231,7 +229,6 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val SESSION_ID = "session_id"
|
||||
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
|
||||
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
|
||||
private const val TOOLBAR_HEIGHT = 56f
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* 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.ext
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.TouchDelegate
|
||||
import android.view.View
|
||||
|
||||
fun View?.increaseTapArea(extraDps: Int) {
|
||||
this!!.post {
|
||||
val touchRect = Rect()
|
||||
getHitRect(touchRect)
|
||||
touchRect.top -= extraDps
|
||||
touchRect.left -= extraDps
|
||||
touchRect.right += extraDps
|
||||
touchRect.bottom += extraDps
|
||||
(parent as View).touchDelegate = TouchDelegate(touchRect, this)
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package org.mozilla.fenix.home
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Bundle
|
||||
|
@ -16,28 +17,42 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.navigation.Navigation
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.view.*
|
||||
import kotlinx.android.synthetic.main.tab_list_header.view.*
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
import org.mozilla.fenix.ThemeManager
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.home.sessions.SessionsComponent
|
||||
import org.mozilla.fenix.home.sessions.layoutComponents
|
||||
import org.mozilla.fenix.home.tabs.TabsAction
|
||||
import org.mozilla.fenix.home.tabs.TabsChange
|
||||
import org.mozilla.fenix.home.tabs.TabsComponent
|
||||
import org.mozilla.fenix.home.tabs.TabsState
|
||||
import org.mozilla.fenix.isPrivate
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||
import org.mozilla.fenix.mvi.getSafeManagedObservable
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
private val bus = ActionBusFactory.get(this)
|
||||
private var sessionObserver: SessionManager.Observer? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_home, container, false)
|
||||
SessionsComponent(view.homeLayout, ActionBusFactory.get(this))
|
||||
TabsComponent(view.homeLayout, bus, TabsState(requireComponents.core.sessionManager.sessions))
|
||||
SessionsComponent(view.homeLayout, bus)
|
||||
ActionBusFactory.get(this).logMergedObservables()
|
||||
return view
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
@ -45,20 +60,41 @@ class HomeFragment : Fragment() {
|
|||
|
||||
layoutComponents(view.homeLayout)
|
||||
|
||||
getSafeManagedObservable<TabsAction>()
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is TabsAction.Select -> {
|
||||
requireComponents.core.sessionManager.select(it.session)
|
||||
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(it.session.id)
|
||||
Navigation.findNavController(view).navigate(directions)
|
||||
}
|
||||
is TabsAction.Close -> {
|
||||
requireComponents.core.sessionManager.remove(it.session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext()).let {
|
||||
BitmapDrawable(resources, it.icon)
|
||||
}
|
||||
|
||||
// Temporary so we can easily test settings
|
||||
view.menuButton.setOnClickListener {
|
||||
Navigation.findNavController(it).navigate(R.id.action_homeFragment_to_settingsFragment, null, null)
|
||||
val directions = HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
|
||||
Navigation.findNavController(it).navigate(directions)
|
||||
}
|
||||
|
||||
view.toolbar.setCompoundDrawablesWithIntrinsicBounds(searchIcon, null, null, null)
|
||||
val roundToInt = (toolbarPaddingDp * Resources.getSystem().displayMetrics.density).roundToInt()
|
||||
view.toolbar.compoundDrawablePadding = roundToInt
|
||||
view.toolbar.setOnClickListener { it ->
|
||||
Navigation.findNavController(it).navigate(R.id.action_homeFragment_to_searchFragment, null, null)
|
||||
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(null)
|
||||
Navigation.findNavController(it).navigate(directions)
|
||||
}
|
||||
view.add_tab_button.increaseTapArea(addTabButtonIncreaseDps)
|
||||
view.add_tab_button.setOnClickListener {
|
||||
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(null)
|
||||
Navigation.findNavController(it).navigate(directions)
|
||||
}
|
||||
|
||||
// There is currently an issue with visibility changes in ConstraintLayout 2.0.0-alpha3
|
||||
|
@ -85,6 +121,7 @@ class HomeFragment : Fragment() {
|
|||
firstKeyTrigger.conditionallyFire(progress)
|
||||
secondKeyTrigger.conditionallyFire(progress)
|
||||
}
|
||||
|
||||
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { }
|
||||
})
|
||||
|
||||
|
@ -148,7 +185,56 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sessionObserver = subscribeToSessions()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sessionObserver?.let {
|
||||
requireComponents.core.sessionManager.unregister(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToSessions(): SessionManager.Observer {
|
||||
val observer = object : SessionManager.Observer {
|
||||
override fun onSessionAdded(session: Session) {
|
||||
super.onSessionAdded(session)
|
||||
getManagedEmitter<TabsChange>().onNext(
|
||||
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
|
||||
}
|
||||
|
||||
override fun onSessionRemoved(session: Session) {
|
||||
super.onSessionRemoved(session)
|
||||
getManagedEmitter<TabsChange>().onNext(
|
||||
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
|
||||
}
|
||||
|
||||
override fun onSessionSelected(session: Session) {
|
||||
super.onSessionSelected(session)
|
||||
getManagedEmitter<TabsChange>().onNext(
|
||||
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
|
||||
}
|
||||
|
||||
override fun onSessionsRestored() {
|
||||
super.onSessionsRestored()
|
||||
getManagedEmitter<TabsChange>().onNext(
|
||||
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
|
||||
}
|
||||
|
||||
override fun onAllSessionsRemoved() {
|
||||
super.onAllSessionsRemoved()
|
||||
getManagedEmitter<TabsChange>().onNext(
|
||||
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
|
||||
}
|
||||
}
|
||||
requireComponents.core.sessionManager.register(observer)
|
||||
return observer
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val addTabButtonIncreaseDps = 8
|
||||
const val toolbarPaddingDp = 12f
|
||||
const val firstKeyTriggerFrame = 55
|
||||
const val secondKeyTriggerFrame = 90
|
||||
|
|
|
@ -2,27 +2,41 @@
|
|||
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.sessions
|
||||
package org.mozilla.fenix.home
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import kotlinx.android.synthetic.main.component_sessions.*
|
||||
import kotlinx.android.synthetic.main.component_tabs.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.tab_list_header.*
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.START
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP
|
||||
import org.jetbrains.anko.constraint.layout.applyConstraintSet
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
|
||||
fun HomeFragment.layoutComponents(layout: ConstraintLayout) {
|
||||
layout.applyConstraintSet {
|
||||
tabs_header {
|
||||
connect(
|
||||
TOP to BOTTOM of homeDivider,
|
||||
START to START of tabs_list,
|
||||
END to END of PARENT_ID
|
||||
)
|
||||
}
|
||||
tabs_list {
|
||||
connect(
|
||||
TOP to BOTTOM of tabs_header,
|
||||
START to START of PARENT_ID,
|
||||
END to END of PARENT_ID
|
||||
)
|
||||
}
|
||||
session_list {
|
||||
connect(
|
||||
BOTTOM to BOTTOM of PARENT_ID,
|
||||
TOP to BOTTOM of tabs_list,
|
||||
START to START of PARENT_ID,
|
||||
END to END of PARENT_ID,
|
||||
TOP to BOTTOM of toolbar_wrapper
|
||||
END to END of PARENT_ID
|
||||
)
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ class SessionsComponent(
|
|||
|
||||
override val reducer: (SessionsState, SessionsChange) -> SessionsState = { state, change ->
|
||||
when (change) {
|
||||
is SessionsChange.SessionsChanged -> state // copy state with changes here
|
||||
is SessionsChange.Changed -> state // copy state with changes here
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,5 +42,5 @@ sealed class SessionsAction : Action {
|
|||
}
|
||||
|
||||
sealed class SessionsChange : Change {
|
||||
object SessionsChanged : SessionsChange()
|
||||
object Changed : SessionsChange()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/* 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.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observer
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||
import mozilla.components.browser.session.Session
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
|
||||
class TabsAdapter(private val actionEmitter: Observer<TabsAction>) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
var sessions = listOf<Session>()
|
||||
set(value) {
|
||||
val diffResult = DiffUtil.calculateDiff(TabsDiffCallback(field, value), true)
|
||||
field = value
|
||||
diffResult.dispatchUpdatesTo(this@TabsAdapter)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return TabViewHolder(view, actionEmitter)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int) = TabViewHolder.LAYOUT_ID
|
||||
|
||||
override fun getItemCount(): Int = sessions.size
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is TabViewHolder -> {
|
||||
holder.bindSession(sessions[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TabViewHolder(
|
||||
view: View,
|
||||
actionEmitter: Observer<TabsAction>,
|
||||
override val containerView: View? = view
|
||||
) :
|
||||
RecyclerView.ViewHolder(view), LayoutContainer {
|
||||
|
||||
var session: Session? = null
|
||||
|
||||
init {
|
||||
item_tab.setOnClickListener {
|
||||
actionEmitter.onNext(TabsAction.Select(session!!))
|
||||
}
|
||||
|
||||
close_tab_button?.run {
|
||||
increaseTapArea(closeButtonIncreaseDps)
|
||||
setOnClickListener {
|
||||
actionEmitter.onNext(TabsAction.Close(session!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bindSession(session: Session) {
|
||||
this.session = session
|
||||
text_url.text = session.url
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val closeButtonIncreaseDps = 12
|
||||
const val LAYOUT_ID = R.layout.tab_list_row
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TabsDiffCallback(
|
||||
private val oldList: List<Session>,
|
||||
private val newList: List<Session>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition].id == newList[newItemPosition].id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||
val oldSession = oldList[oldItemPosition]
|
||||
val newSession = newList[newItemPosition]
|
||||
val diffBundle = Bundle()
|
||||
if (oldSession.url != newSession.url) {
|
||||
diffBundle.putString("url", newSession.url)
|
||||
}
|
||||
return if (diffBundle.size() == 0) null else diffBundle
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/* 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.tabs
|
||||
|
||||
import android.view.ViewGroup
|
||||
import mozilla.components.browser.session.Session
|
||||
import org.mozilla.fenix.mvi.Action
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.Change
|
||||
import org.mozilla.fenix.mvi.UIComponent
|
||||
import org.mozilla.fenix.mvi.ViewState
|
||||
|
||||
class TabsComponent(
|
||||
private val container: ViewGroup,
|
||||
bus: ActionBusFactory,
|
||||
override var initialState: TabsState = TabsState(listOf())
|
||||
) :
|
||||
UIComponent<TabsState, TabsAction, TabsChange>(
|
||||
bus.getManagedEmitter(TabsAction::class.java),
|
||||
bus.getSafeManagedObservable(TabsChange::class.java)
|
||||
) {
|
||||
|
||||
override val reducer: (TabsState, TabsChange) -> TabsState = { state, change ->
|
||||
when (change) {
|
||||
is TabsChange.Changed -> state.copy(sessions = change.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initView() = TabsUIView(container, actionEmitter, changesObservable)
|
||||
|
||||
init {
|
||||
render(reducer)
|
||||
}
|
||||
}
|
||||
|
||||
data class TabsState(val sessions: List<Session>) : ViewState
|
||||
|
||||
sealed class TabsAction : Action {
|
||||
data class Select(val session: Session) : TabsAction()
|
||||
data class Close(val session: Session) : TabsAction()
|
||||
}
|
||||
|
||||
sealed class TabsChange : Change {
|
||||
data class Changed(val sessions: List<Session>) : TabsChange()
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* 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.tabs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.functions.Consumer
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class TabsUIView(
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<TabsAction>,
|
||||
changesObservable: Observable<TabsChange>
|
||||
) :
|
||||
UIView<TabsState, TabsAction, TabsChange>(container, actionEmitter, changesObservable) {
|
||||
|
||||
private val header: ConstraintLayout = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.tab_list_header, container, true)
|
||||
.findViewById(R.id.tabs_header)
|
||||
|
||||
override val view: RecyclerView = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabs, container, true)
|
||||
.findViewById(R.id.tabs_list)
|
||||
|
||||
private val tabsAdapter = TabsAdapter(actionEmitter)
|
||||
|
||||
init {
|
||||
view.apply {
|
||||
layoutManager = LinearLayoutManager(container.context)
|
||||
adapter = tabsAdapter
|
||||
itemAnimator = DefaultItemAnimator()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateView() = Consumer<TabsState> {
|
||||
tabsAdapter.sessions = it.sessions
|
||||
header.visibility = if (it.sessions.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
|
@ -15,10 +15,12 @@ import androidx.navigation.Navigation
|
|||
import kotlinx.android.synthetic.main.fragment_search.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||
import org.mozilla.fenix.mvi.getSafeManagedObservable
|
||||
import org.mozilla.fenix.search.awesomebar.AwesomeBarAction
|
||||
import org.mozilla.fenix.search.awesomebar.AwesomeBarChange
|
||||
import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent
|
||||
import org.mozilla.fenix.search.awesomebar.AwesomeBarState
|
||||
import org.mozilla.fenix.search.toolbar.SearchAction
|
||||
import org.mozilla.fenix.search.toolbar.SearchState
|
||||
import org.mozilla.fenix.search.toolbar.ToolbarComponent
|
||||
|
@ -33,13 +35,18 @@ class SearchFragment : Fragment() {
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val sessionId = SearchFragmentArgs.fromBundle(arguments!!).sessionId
|
||||
val view = inflater.inflate(R.layout.fragment_search, container, false)
|
||||
toolbarComponent = ToolbarComponent(
|
||||
view.toolbar_wrapper,
|
||||
ActionBusFactory.get(this),
|
||||
sessionId,
|
||||
SearchState("", isEditing = true)
|
||||
)
|
||||
awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this))
|
||||
awesomeBarComponent = AwesomeBarComponent(
|
||||
view.search_layout, ActionBusFactory.get(this),
|
||||
AwesomeBarState("", sessionId == null)
|
||||
)
|
||||
ActionBusFactory.get(this).logMergedObservables()
|
||||
return view
|
||||
}
|
||||
|
@ -61,8 +68,7 @@ class SearchFragment : Fragment() {
|
|||
when (it) {
|
||||
is SearchAction.UrlCommitted -> transitionToBrowser()
|
||||
is SearchAction.TextChanged -> {
|
||||
ActionBusFactory.get(this)
|
||||
.emit(AwesomeBarChange::class.java, AwesomeBarChange.UpdateQuery(it.query))
|
||||
getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange.UpdateQuery(it.query))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +82,8 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun transitionToBrowser() {
|
||||
Navigation.findNavController(view!!.search_layout)
|
||||
.navigate(R.id.action_searchFragment_to_browserFragment, null, null)
|
||||
val sessionId = SearchFragmentArgs.fromBundle(arguments!!).sessionId
|
||||
val directions = SearchFragmentDirections.actionSearchFragmentToBrowserFragment(sessionId)
|
||||
Navigation.findNavController(view!!.search_layout).navigate(directions)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import org.mozilla.fenix.mvi.Reducer
|
|||
import org.mozilla.fenix.mvi.UIComponent
|
||||
import org.mozilla.fenix.mvi.ViewState
|
||||
|
||||
data class AwesomeBarState(val query: String) : ViewState
|
||||
data class AwesomeBarState(val query: String, val useNewTab: Boolean = false) : ViewState
|
||||
|
||||
sealed class AwesomeBarAction : Action {
|
||||
object ItemSelected : AwesomeBarAction()
|
||||
|
@ -35,7 +35,7 @@ class AwesomeBarComponent(
|
|||
}
|
||||
}
|
||||
|
||||
override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
|
||||
override fun initView() = AwesomeBarUIView(initialState.useNewTab, container, actionEmitter, changesObservable)
|
||||
|
||||
init {
|
||||
render(reducer)
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.mozilla.fenix.ext.components
|
|||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class AwesomeBarUIView(
|
||||
useNewTab: Boolean,
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<AwesomeBarAction>,
|
||||
changesObservable: Observable<AwesomeBarChange>
|
||||
|
@ -32,7 +33,11 @@ class AwesomeBarUIView(
|
|||
with(container.context) {
|
||||
view.addProviders(ClipboardSuggestionProvider(
|
||||
this,
|
||||
components.useCases.sessionUseCases.loadUrl,
|
||||
if (useNewTab) {
|
||||
components.useCases.tabsUseCases.addTab
|
||||
} else {
|
||||
components.useCases.sessionUseCases.loadUrl
|
||||
},
|
||||
getDrawable(R.drawable.ic_link).toBitmap(),
|
||||
getString(R.string.awesomebar_clipboard_title)
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.mozilla.fenix.mvi.ViewState
|
|||
class ToolbarComponent(
|
||||
private val container: ViewGroup,
|
||||
bus: ActionBusFactory,
|
||||
private val sessionId: String?,
|
||||
override var initialState: SearchState = SearchState("", false)
|
||||
) :
|
||||
UIComponent<SearchState, SearchAction, SearchChange>(
|
||||
|
@ -33,7 +34,7 @@ class ToolbarComponent(
|
|||
}
|
||||
}
|
||||
|
||||
override fun initView() = ToolbarUIView(container, actionEmitter, changesObservable)
|
||||
override fun initView() = ToolbarUIView(sessionId, container, actionEmitter, changesObservable)
|
||||
init {
|
||||
render(reducer)
|
||||
applyTheme()
|
||||
|
|
|
@ -16,6 +16,7 @@ import mozilla.components.feature.toolbar.ToolbarFeature
|
|||
import org.mozilla.fenix.DefaultThemeManager
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||
import org.mozilla.fenix.ext.application
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
|
@ -38,7 +39,8 @@ class ToolbarIntegration(
|
|||
context.getString(R.string.browser_home_button),
|
||||
visible = { sessionManager.runWithSession(sessionId) { it.isCustomTabSession().not() } }
|
||||
) {
|
||||
Navigation.findNavController(toolbar).navigate(R.id.action_browserFragment_to_homeFragment)
|
||||
Navigation.findNavController(toolbar)
|
||||
.navigate(BrowserFragmentDirections.actionBrowserFragmentToHomeFragment())
|
||||
}
|
||||
|
||||
toolbar.addBrowserAction(home)
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.mozilla.fenix.ext.components
|
|||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class ToolbarUIView(
|
||||
sessionId: String?,
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<SearchAction>,
|
||||
changesObservable: Observable<SearchChange>
|
||||
|
@ -51,11 +52,12 @@ class ToolbarUIView(
|
|||
|
||||
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
|
||||
override fun onTextChanged(text: String) {
|
||||
url = text
|
||||
actionEmitter.onNext(SearchAction.TextChanged(text))
|
||||
}
|
||||
|
||||
override fun onStopEditing() {
|
||||
actionEmitter.onNext(SearchAction.UrlCommitted("foo"))
|
||||
actionEmitter.onNext(SearchAction.UrlCommitted(url))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -71,7 +73,7 @@ class ToolbarUIView(
|
|||
ShippedDomainsProvider().also { it.initialize(this) },
|
||||
components.core.historyStorage,
|
||||
components.core.sessionManager,
|
||||
components.core.sessionManager.selectedSession?.id
|
||||
sessionId ?: components.core.sessionManager.selectedSession?.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -1,12 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/session_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"/>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/tabs_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"/>
|
|
@ -74,4 +74,16 @@
|
|||
android:textSize="14sp" />
|
||||
</org.mozilla.fenix.home.SearchView>
|
||||
|
||||
<View
|
||||
android:id="@+id/homeDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/photonGrey30"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginEnd="40dp"/>
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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/tabs_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
<TextView
|
||||
android:id="@+id/header_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tabs_header_title"
|
||||
android:textSize="24sp"
|
||||
android:textColor="@android:color/black"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
<ImageView
|
||||
android:id="@+id/add_tab_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_add_black_24dp"
|
||||
android:baselineAlignBottom="true"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/header_text"
|
||||
app:layout_constraintEnd_toStartOf="@id/tabs_overflow_button"/>
|
||||
<ImageView
|
||||
android:id="@+id/tabs_overflow_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_menu"
|
||||
android:baselineAlignBottom="true"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/header_text"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:padding="10dp"
|
||||
android:elevation="5dp"
|
||||
app:cardCornerRadius="10dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/item_tab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="10dp">
|
||||
|
||||
<ImageView android:id="@+id/favicon_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_link"
|
||||
android:tint="@android:color/black"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:contentDescription="@string/favicon_content_description"/>
|
||||
|
||||
<TextView android:id="@+id/text_url"
|
||||
android:textSize="18sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/favicon_image"
|
||||
app:layout_constraintEnd_toStartOf="@id/close_tab_button"/>
|
||||
|
||||
<ImageView android:id="@+id/close_tab_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_close_black_24dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:contentDescription="@string/close_tab"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_url"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<ImageView android:layout_width="0dp"
|
||||
android:layout_height="100dp"
|
||||
android:src="@color/photonBlue40"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_url"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="TODO"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -37,6 +37,7 @@
|
|||
android:id="@+id/action_searchFragment_to_browserFragment"
|
||||
app:destination="@id/browserFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<argument android:name="session_id" app:argType="string" app:nullable="true"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -125,4 +125,7 @@
|
|||
<string name="library_title">Library</string>
|
||||
<!-- Settings Page Title -->
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="tabs_header_title">Current Session</string>
|
||||
<string name="close_tab">Close tab</string>
|
||||
<string name="favicon_content_description">Favorite icon</string>
|
||||
</resources>
|
||||
|
|
|
@ -8,6 +8,7 @@ buildscript {
|
|||
dependencies {
|
||||
classpath Deps.tools_androidgradle
|
||||
classpath Deps.tools_kotlingradle
|
||||
classpath Deps.androidx_safeargs
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
|
@ -18,6 +18,7 @@ private object Versions {
|
|||
const val androidx_annotation = "1.0.1"
|
||||
const val androidx_lifecycle = "2.0.0"
|
||||
const val androidx_fragment = "1.1.0-alpha04"
|
||||
const val androidx_safeargs = "1.0.0-beta01"
|
||||
|
||||
const val mozilla_android_components = "0.42.0-SNAPSHOT"
|
||||
|
||||
|
@ -99,6 +100,7 @@ object Deps {
|
|||
const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}"
|
||||
const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}"
|
||||
const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}"
|
||||
const val androidx_safeargs = "android.arch.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_safeargs}"
|
||||
const val android_arch_navigation = "android.arch.navigation:navigation-fragment:${Versions.android_arch_navigation}"
|
||||
const val android_arch_navigation_ui = "android.arch.navigation:navigation-ui:${Versions.android_arch_navigation}"
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#Wed Feb 06 16:31:29 CST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
|
Loading…
Reference in New Issue