1
0
Fork 0

Closes #1312, #1236, #1237, #1238, #1239: Creating, Editing, and Deleting Bookmarks and Bookmark Folders

master
Colin Lee 2019-04-04 15:40:39 -05:00
parent 8f6dca99dc
commit b54d4d1d58
32 changed files with 1183 additions and 101 deletions

View File

@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1195 - Adds telemetry for quick action sheet
- #627 - Sets engine preferred color scheme based on light/dark theme
- #904 - Added tab counter in browser toolbar
- #1312 - Added the ability to edit bookmarks
- #1236 - Added the ability to create bookmark folders
- #1237 - Added the ability to delete bookmark folders
- #1238 - Added the ability to edit bookmark folders
- #1239 - Added the ability to move bookmark folders
### Changed
- #1429 - Updated site permissions ui for MVP
### Removed

View File

@ -251,6 +251,11 @@ dependencies {
implementation Deps.rxAndroid
implementation Deps.rxKotlin
implementation Deps.rxBindings
implementation Deps.autodispose
implementation Deps.autodispose_android
implementation Deps.autodispose_android_aac
implementation Deps.anko_commons
implementation Deps.anko_sdk
implementation Deps.anko_constraintlayout
@ -316,6 +321,7 @@ dependencies {
implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_viewmodel_ktx
implementation Deps.autodispose

View File

@ -27,6 +27,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.selectfolder.SelectBookmarkFolderFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
@ -155,6 +156,8 @@ open class HomeActivity : AppCompatActivity() {
SettingsFragmentDirections.actionSettingsFragmentToBrowserFragment(sessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionBookmarkFragmentToBrowserFragment(sessionId)
BrowserDirection.FromBookmarksFolderSelect ->
SelectBookmarkFolderFragmentDirections.actionBookmarkSelectFolderFragmentToBrowserFragment(sessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionHistoryFragmentToBrowserFragment(sessionId)
}
@ -193,5 +196,5 @@ open class HomeActivity : AppCompatActivity() {
}
enum class BrowserDirection {
FromGlobal, FromHome, FromSearch, FromSettings, FromBookmarks, FromHistory
FromGlobal, FromHome, FromSearch, FromSettings, FromBookmarks, FromBookmarksFolderSelect, FromHistory
}

View File

@ -48,6 +48,7 @@ import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.BrowserMenuItemTapped.Item
@ -67,9 +68,9 @@ import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.quickactionsheet.QuickActionAction
import org.mozilla.fenix.quickactionsheet.QuickActionComponent
import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragment
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragment
@SuppressWarnings("TooManyFunctions", "LargeClass")
class BrowserFragment : Fragment(), BackHandler {
@ -323,24 +324,26 @@ class BrowserFragment : Fragment(), BackHandler {
requireComponents.analytics.metrics.track(Event.QuickActionSheetBookmarkTapped)
val session = requireComponents.core.sessionManager.selectedSession
CoroutineScope(IO).launch {
requireComponents.core.bookmarksStorage
val guid = requireComponents.core.bookmarksStorage
.addItem(BookmarkRoot.Mobile.id, session!!.url, session.title, null)
launch(Main) {
val rootView =
context?.asActivity()?.window?.decorView?.findViewById<View>(android.R.id.content)
rootView?.let { view ->
Snackbar.make(
view,
getString(R.string.bookmark_created_snackbar),
Snackbar.LENGTH_LONG
)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
ItsNotBrokenSnack(
context!!
).showSnackbar(issueNumber = "90")
}
.show()
}
context?.asActivity()?.window?.decorView
?.findViewById<View>(android.R.id.content)?.let { view ->
FenixSnackbar.make(
view as ViewGroup,
Snackbar.LENGTH_LONG
)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
Navigation.findNavController(requireActivity(), R.id.container)
.navigate(
BrowserFragmentDirections
.actionBrowserFragmentToBookmarkEditFragment(
guid
)
)
}
.setText(getString(R.string.bookmark_created_snackbar))
}!!.show()
}
}
}

View File

@ -0,0 +1,16 @@
/* 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.content.Context
import android.util.TypedValue
fun Int.getColorFromAttr(context: Context): Int {
val typedValue = TypedValue()
val typedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(this))
val color = typedArray.getColor(0, 0)
typedArray.recycle()
return color
}

View File

@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.menu.BrowserMenu
@ -22,17 +23,24 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import kotlin.coroutines.CoroutineContext
class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkAction>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var tree: List<BookmarkNode> = listOf()
private var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
private var isFirstRun = true
lateinit var job: Job
fun updateData(tree: BookmarkNode?, mode: BookmarkState.Mode) {
this.tree = tree?.children?.filterNotNull() ?: listOf()
isFirstRun = if (isFirstRun) false else {
emptyView.visibility = if (this.tree.isEmpty()) View.VISIBLE else View.GONE
false
}
this.mode = mode
notifyDataSetChanged()
}
@ -78,36 +86,10 @@ class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerVie
@SuppressWarnings("ComplexMethod")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val bookmarkItemMenu = BookmarkItemMenu(holder.itemView.context) {
when (it) {
is BookmarkItemMenu.Item.Edit -> {
actionEmitter.onNext(BookmarkAction.Edit(tree[position]))
}
is BookmarkItemMenu.Item.Select -> {
actionEmitter.onNext(BookmarkAction.Select(tree[position]))
}
is BookmarkItemMenu.Item.Copy -> {
actionEmitter.onNext(BookmarkAction.Copy(tree[position]))
}
is BookmarkItemMenu.Item.Share -> {
actionEmitter.onNext(BookmarkAction.Share(tree[position]))
}
is BookmarkItemMenu.Item.OpenInNewTab -> {
actionEmitter.onNext(BookmarkAction.OpenInNewTab(tree[position]))
}
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
actionEmitter.onNext(BookmarkAction.OpenInPrivateTab(tree[position]))
}
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(tree[position]))
}
}
}
when (holder) {
is BookmarkAdapter.BookmarkItemViewHolder -> holder.bind(tree[position], bookmarkItemMenu, mode)
is BookmarkAdapter.BookmarkFolderViewHolder -> holder.bind(tree[position], bookmarkItemMenu, mode)
is BookmarkAdapter.BookmarkSeparatorViewHolder -> holder.bind(bookmarkItemMenu)
is BookmarkAdapter.BookmarkItemViewHolder -> holder.bind(tree[position], mode)
is BookmarkAdapter.BookmarkFolderViewHolder -> holder.bind(tree[position], mode)
is BookmarkAdapter.BookmarkSeparatorViewHolder -> holder.bind(tree[position])
}
}
@ -133,12 +115,39 @@ class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerVie
bookmark_layout.isClickable = true
}
fun bind(item: BookmarkNode, bookmarkItemMenu: BookmarkItemMenu, mode: BookmarkState.Mode) {
fun bind(item: BookmarkNode, mode: BookmarkState.Mode) {
this.item = item
this.mode = mode
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
when (it) {
is BookmarkItemMenu.Item.Edit -> {
actionEmitter.onNext(BookmarkAction.Edit(item))
}
is BookmarkItemMenu.Item.Select -> {
actionEmitter.onNext(BookmarkAction.Select(item))
}
is BookmarkItemMenu.Item.Copy -> {
actionEmitter.onNext(BookmarkAction.Copy(item))
}
is BookmarkItemMenu.Item.Share -> {
actionEmitter.onNext(BookmarkAction.Share(item))
}
is BookmarkItemMenu.Item.OpenInNewTab -> {
actionEmitter.onNext(BookmarkAction.OpenInNewTab(item))
}
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
actionEmitter.onNext(BookmarkAction.OpenInPrivateTab(item))
}
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(item))
}
}
}
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView!!.context).show(
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
@ -195,12 +204,34 @@ class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerVie
bookmark_layout.isClickable = true
}
fun bind(folder: BookmarkNode, bookmarkItemMenu: BookmarkItemMenu, mode: BookmarkState.Mode) {
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView!!.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
fun bind(folder: BookmarkNode, mode: BookmarkState.Mode) {
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, folder) {
when (it) {
is BookmarkItemMenu.Item.Edit -> {
actionEmitter.onNext(BookmarkAction.Edit(folder))
}
is BookmarkItemMenu.Item.Select -> {
actionEmitter.onNext(BookmarkAction.Select(folder))
}
is BookmarkItemMenu.Item.Copy -> {
actionEmitter.onNext(BookmarkAction.Copy(folder))
}
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(folder))
}
}
}
if (enumValues<BookmarkRoot>().all { it.id != folder.guid }) {
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
} else {
bookmark_overflow.visibility = View.GONE
}
bookmark_title?.text = folder.title
bookmark_layout.setOnClickListener {
@ -222,14 +253,23 @@ class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerVie
init {
bookmark_favicon.visibility = View.GONE
bookmark_title.visibility = View.GONE
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.VISIBLE
bookmark_layout.isClickable = false
}
fun bind(bookmarkItemMenu: BookmarkItemMenu) {
fun bind(separator: BookmarkNode) {
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, separator) {
when (it) {
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(separator))
}
}
}
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView!!.context).show(
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
@ -241,6 +281,10 @@ class BookmarkAdapter(val actionEmitter: Observer<BookmarkAction>) : RecyclerVie
}
}
companion object {
private const val bookmarkOverflowExtraDips = 8
}
enum class ViewType {
ITEM, FOLDER, SEPARATOR
}

View File

@ -24,6 +24,9 @@ import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager
@ -37,10 +40,12 @@ import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import kotlin.coroutines.CoroutineContext
class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
@SuppressWarnings("TooManyFunctions")
class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserver {
private lateinit var job: Job
private lateinit var bookmarkComponent: BookmarkComponent
private lateinit var signInComponent: SignInComponent
private lateinit var currentRoot: BookmarkNode
override val coroutineContext: CoroutineContext
@ -49,6 +54,7 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
bookmarkComponent = BookmarkComponent(view.bookmark_layout, ActionBusFactory.get(this))
signInComponent = SignInComponent(view.bookmark_layout, ActionBusFactory.get(this))
return view
}
@ -61,6 +67,14 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.show()
checkIfSignedIn()
}
private fun checkIfSignedIn() {
val accountManager = requireComponents.backgroundServices.accountManager
accountManager.register(this, owner = this)
accountManager.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
}
override fun onDestroy() {
@ -94,7 +108,11 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
Navigation.findNavController(requireActivity(), R.id.container).popBackStack()
}
is BookmarkAction.Edit -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1238")
Navigation.findNavController(requireActivity(), R.id.container)
.navigate(
BookmarkFragmentDirections
.actionBookmarkFragmentToBookmarkEditFragment(it.item.guid)
)
}
is BookmarkAction.Select -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
@ -131,6 +149,16 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
}
}
}
getAutoDisposeObservable<SignInAction>()
.subscribe {
when (it) {
is SignInAction.ClickedSignIn -> {
requireComponents.services.accountsAuthFeature.beginAuthentication()
(activity as HomeActivity).openToBrowser(null, from = BrowserDirection.FromBookmarks)
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -157,10 +185,25 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler {
currentRoot = requireComponents.core.bookmarksStorage.getTree(currentGuid) as BookmarkNode
launch(Main) {
if (currentGuid != BookmarkRoot.Root.id) (activity as HomeActivity).title = currentRoot.title
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot))
}
}
}
override fun onBackPressed(): Boolean = (bookmarkComponent.uiView as BookmarkUIView).onBackPressed()
override fun onAuthenticated(account: OAuthAccount) {
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
}
override fun onError(error: Exception) {
}
override fun onLoggedOut() {
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
}
override fun onProfileUpdated(profile: Profile) {
}
}

View File

@ -7,11 +7,14 @@ package org.mozilla.fenix.library.bookmarks
import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R
class BookmarkItemMenu(
private val context: Context,
private val item: BookmarkNode,
private val onItemTapped: (BookmarkItemMenu.Item) -> Unit = {}
) {
@ -29,24 +32,36 @@ class BookmarkItemMenu(
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Edit)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_select_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Select)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_copy_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Copy)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_share_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Share)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_new_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInNewTab)
},
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_private_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInPrivateTab)
},
if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Edit)
}
} else null,
if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_select_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Select)
}
} else null,
if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_copy_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Copy)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_share_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Share)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_new_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInNewTab)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_private_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInPrivateTab)
}
} else null,
SimpleBrowserMenuItem(
context.getString(R.string.bookmark_menu_delete_button),
textColorResource = DefaultThemeManager.resolveAttribute(
@ -56,6 +71,6 @@ class BookmarkItemMenu(
) {
onItemTapped.invoke(BookmarkItemMenu.Item.Delete)
}
)
).filterNotNull()
}
}

View File

@ -6,11 +6,11 @@ package org.mozilla.fenix.library.bookmarks
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.widget.LinearLayout
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.component_bookmark.view.*
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.R
@ -29,16 +29,15 @@ class BookmarkUIView(
var canGoBack = false
override val view: RecyclerView = LayoutInflater.from(container.context)
.inflate(R.layout.component_bookmark, container, true)
.findViewById(R.id.bookmark_list)
override val view: LinearLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_bookmark, container, true) as LinearLayout
private val bookmarkAdapter = BookmarkAdapter(actionEmitter)
private val bookmarkAdapter: BookmarkAdapter
init {
view.apply {
view.bookmark_list.apply {
bookmarkAdapter = BookmarkAdapter(view.bookmarks_empty_view, actionEmitter)
adapter = bookmarkAdapter
layoutManager = LinearLayoutManager(container.context)
}
}

View File

@ -0,0 +1,12 @@
/* 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.library.bookmarks
import androidx.lifecycle.ViewModel
import mozilla.components.concept.storage.BookmarkNode
class BookmarksSharedViewModel : ViewModel() {
var selectedFolder: BookmarkNode? = null
}

View File

@ -0,0 +1,50 @@
/* 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.library.bookmarks
import android.view.ViewGroup
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
class SignInComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
override var initialState: SignInState =
SignInState(false)
) : UIComponent<SignInState, SignInAction, SignInChange>(
bus.getManagedEmitter(SignInAction::class.java),
bus.getSafeManagedObservable(SignInChange::class.java)
) {
override val reducer: Reducer<SignInState, SignInChange> = { state, change ->
when (change) {
SignInChange.SignedIn -> state.copy(signedIn = true)
SignInChange.SignedOut -> state.copy(signedIn = false)
}
}
override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
SignInUIView(container, actionEmitter, changesObservable)
init {
render(reducer)
}
}
data class SignInState(val signedIn: Boolean) : ViewState
sealed class SignInAction : Action {
object ClickedSignIn : SignInAction()
}
sealed class SignInChange : Change {
object SignedIn : SignInChange()
object SignedOut : SignInChange()
}

View File

@ -0,0 +1,36 @@
/* 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.library.bookmarks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
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 SignInUIView(
container: ViewGroup,
actionEmitter: Observer<SignInAction>,
changesObservable: Observable<SignInChange>
) : UIView<SignInState, SignInAction, SignInChange>(container, actionEmitter, changesObservable) {
override val view: Button = LayoutInflater.from(container.context)
.inflate(R.layout.component_sign_in, container, true)
.findViewById(R.id.bookmark_folders_sign_in)
init {
view.setOnClickListener {
actionEmitter.onNext(SignInAction.ClickedSignIn)
}
}
override fun updateView() = Consumer<SignInState> {
view.visibility = if (it.signedIn) View.GONE else View.VISIBLE
}
}

View File

@ -0,0 +1,104 @@
/* 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.library.bookmarks.addfolder
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_add_bookmark_folder.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import kotlin.coroutines.CoroutineContext
class AddBookmarkFolderFragment : Fragment(), CoroutineScope {
private lateinit var sharedViewModel: BookmarksSharedViewModel
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
setHasOptionsMenu(true)
sharedViewModel = activity?.run {
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
}!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_add_bookmark_folder, container, false)
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.show()
launch(IO) {
sharedViewModel.selectedFolder = sharedViewModel.selectedFolder
?: requireComponents.core.bookmarksStorage.getTree(BookmarkRoot.Mobile.id)
bookmark_add_folder_parent_selector.text = sharedViewModel.selectedFolder!!.title
bookmark_add_folder_parent_selector.setOnClickListener {
Navigation.findNavController(requireActivity(), R.id.container)
.navigate(
AddBookmarkFolderFragmentDirections
.actionBookmarkAddFolderFragmentToBookmarkSelectFolderFragment(BookmarkRoot.Root.id, true)
)
}
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.bookmarks_add_folder, menu)
menu.findItem(R.id.confirm_add_folder_button).icon.colorFilter =
PorterDuffColorFilter(R.attr.iconColor.getColorFromAttr(context!!), PorterDuff.Mode.SRC_IN)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.confirm_add_folder_button -> {
if (bookmark_add_folder_title_edit.text.isEmpty()) {
bookmark_add_folder_title_edit.error = getString(R.string.bookmark_empty_title_error)
return true
}
launch(IO) {
requireComponents.core.bookmarksStorage.addFolder(
sharedViewModel.selectedFolder!!.guid, bookmark_add_folder_title_edit.text.toString(), null
)
launch(Main) {
Navigation.findNavController(requireActivity(), R.id.container).popBackStack()
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
}

View File

@ -0,0 +1,181 @@
/* 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.library.bookmarks.edit
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import com.jakewharton.rxbinding3.widget.textChanges
import com.uber.autodispose.AutoDispose
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.fragment_edit_bookmark.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.appservices.places.UrlParseFailed
import mozilla.components.concept.storage.BookmarkInfo
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import java.lang.IllegalArgumentException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
class EditBookmarkFragment : Fragment(), CoroutineScope {
private lateinit var sharedViewModel: BookmarksSharedViewModel
private lateinit var job: Job
private lateinit var guidToEdit: String
private var bookmarkNode: BookmarkNode? = null
private var bookmarkParent: BookmarkNode? = null
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
setHasOptionsMenu(true)
sharedViewModel = activity?.run {
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
}!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_edit_bookmark, container, false)
}
override fun onResume() {
super.onResume()
(activity as? AppCompatActivity)?.supportActionBar?.show()
guidToEdit = EditBookmarkFragmentArgs.fromBundle(arguments!!).guidToEdit
launch(IO) {
bookmarkNode = requireComponents.core.bookmarksStorage.getTree(guidToEdit)
bookmarkParent = sharedViewModel.selectedFolder
?: bookmarkNode?.parentGuid?.let { requireComponents.core.bookmarksStorage.getTree(it) }
launch(Main) {
when (bookmarkNode?.type) {
BookmarkNodeType.FOLDER -> {
bookmark_url_edit.visibility = View.GONE
bookmark_url_label.visibility = View.GONE
}
BookmarkNodeType.ITEM -> {}
BookmarkNodeType.SEPARATOR -> {}
else -> throw IllegalArgumentException()
}
bookmark_name_edit.setText(bookmarkNode!!.title)
bookmark_url_edit.setText(bookmarkNode!!.url)
}
bookmarkParent?.let { node ->
launch(Main) {
bookmark_folder_selector.text = node.title
bookmark_folder_selector.setOnClickListener {
sharedViewModel.selectedFolder = null
Navigation.findNavController(requireActivity(), R.id.container).navigate(
EditBookmarkFragmentDirections
.actionBookmarkEditFragmentToBookmarkSelectFolderFragment(null)
)
}
}
}
}
updateBookmarkFromObservableInput()
}
override fun onPause() {
updateBookmarkNode(Pair(bookmark_name_edit.text, bookmark_url_edit.text))
super.onPause()
}
private fun updateBookmarkFromObservableInput() {
Observable.combineLatest(
bookmark_name_edit.textChanges().skipInitialValue(),
bookmark_url_edit.textChanges().skipInitialValue(),
BiFunction { name: CharSequence, url: CharSequence ->
Pair(name, url)
})
.filter { it.first.isNotBlank() && it.second.isNotBlank() }
.debounce(debouncePeriodInMs, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.`as`(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this@EditBookmarkFragment)))
.subscribe {
try {
bookmark_url_edit.error = null
updateBookmarkNode(it)
} catch (e: UrlParseFailed) {
bookmark_url_edit.error = getString(R.string.bookmark_invalid_url_error)
}
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.bookmarks_edit, menu)
menu.findItem(R.id.delete_bookmark_button).icon.colorFilter =
PorterDuffColorFilter(R.attr.iconColor.getColorFromAttr(context!!), PorterDuff.Mode.SRC_IN)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.delete_bookmark_button -> {
launch(IO) {
requireComponents.core.bookmarksStorage.deleteNode(guidToEdit)
launch(Main) {
Navigation.findNavController(requireActivity(), R.id.container).popBackStack()
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun updateBookmarkNode(pair: Pair<CharSequence, CharSequence>) {
launch(IO) {
requireComponents.core.bookmarksStorage.updateNode(
guidToEdit,
BookmarkInfo(
sharedViewModel.selectedFolder?.guid ?: bookmarkNode!!.parentGuid,
bookmarkNode!!.position,
pair.first.toString(),
if (bookmarkNode?.type == BookmarkNodeType.ITEM) pair.second.toString() else null
)
)
}
}
companion object {
private const val debouncePeriodInMs = 500L
}
}

View File

@ -0,0 +1,126 @@
/* 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.library.bookmarks.selectfolder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.bookmark_row.*
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.res.pxToDp
import org.mozilla.fenix.R
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedViewModel) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var tree: List<BookmarkNodeWithDepth> = listOf()
fun updateData(tree: BookmarkNode?) {
this.tree = tree!!.convertToFolderDepthTree().drop(1)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.bookmark_row, parent, false)
return when (viewType) {
BookmarkFolderViewHolder.viewType -> SelectBookmarkFolderAdapter.BookmarkFolderViewHolder(
view
)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
}
}
override fun getItemViewType(position: Int): Int {
return when (tree[position].node.type) {
BookmarkNodeType.FOLDER -> BookmarkFolderViewHolder.viewType
else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType")
}
}
override fun getItemCount(): Int = tree.size
@SuppressWarnings("ComplexMethod")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is SelectBookmarkFolderAdapter.BookmarkFolderViewHolder -> holder.bind(
tree[position],
tree[position].node == sharedViewModel.selectedFolder,
object : SelectionInterface {
override fun itemSelected(node: BookmarkNode) {
sharedViewModel.selectedFolder = node
notifyDataSetChanged()
}
})
else -> {
}
}
}
interface SelectionInterface {
fun itemSelected(node: BookmarkNode)
}
class BookmarkFolderViewHolder(
view: View,
override val containerView: View? = view
) :
RecyclerView.ViewHolder(view), LayoutContainer {
init {
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
}
fun bind(folder: BookmarkNodeWithDepth, selected: Boolean, selectionInterface: SelectionInterface) {
val backgroundTint =
if (selected) R.color.bookmark_selection_appbar_background else R.color.bookmark_favicon_background
val backgroundTintList = ContextCompat.getColorStateList(containerView!!.context, backgroundTint)
bookmark_favicon.backgroundTintList = backgroundTintList
val res = if (selected) R.drawable.mozac_ic_check else R.drawable.ic_folder_icon
bookmark_favicon.setImageResource(res)
bookmark_overflow.visibility = View.GONE
bookmark_title?.text = folder.node.title
bookmark_layout.setOnClickListener {
selectionInterface.itemSelected(folder.node)
}
val padding =
containerView.resources.pxToDp(dpsToIndent) * (if (folder.depth > maxDepth) maxDepth else folder.depth)
bookmark_layout.setPadding(padding, 0, 0, 0)
}
companion object {
const val viewType = 1
}
}
data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?)
private fun BookmarkNode?.convertToFolderDepthTree(
depth: Int = 0,
list: List<BookmarkNodeWithDepth> = listOf()
): List<BookmarkNodeWithDepth> {
return if (this != null) {
val newList = list.plus(listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid)))
newList.plus(
children?.filter { it?.type == BookmarkNodeType.FOLDER }
?.flatMap { it.convertToFolderDepthTree(depth + 1) }
?: listOf())
} else listOf()
}
companion object {
private const val maxDepth = 10
private const val dpsToIndent = 10
}
}

View File

@ -0,0 +1,157 @@
/* 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.library.bookmarks.selectfolder
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.*
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import org.mozilla.fenix.library.bookmarks.SignInAction
import org.mozilla.fenix.library.bookmarks.SignInChange
import org.mozilla.fenix.library.bookmarks.SignInComponent
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter
import kotlin.coroutines.CoroutineContext
@SuppressWarnings("TooManyFunctions")
class SelectBookmarkFolderFragment : Fragment(), CoroutineScope, AccountObserver {
private lateinit var sharedViewModel: BookmarksSharedViewModel
private lateinit var job: Job
private var folderGuid: String? = null
private var bookmarkNode: BookmarkNode? = null
private lateinit var signInComponent: SignInComponent
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
setHasOptionsMenu(true)
sharedViewModel = activity?.run {
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
}!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false)
signInComponent = SignInComponent(view.select_bookmark_layout, ActionBusFactory.get(this))
return view
}
override fun onStart() {
super.onStart()
getAutoDisposeObservable<SignInAction>()
.subscribe {
when (it) {
is SignInAction.ClickedSignIn -> {
requireComponents.services.accountsAuthFeature.beginAuthentication()
view?.let {
(activity as HomeActivity).openToBrowser(null, BrowserDirection.FromBookmarksFolderSelect)
}
}
}
}
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.show()
folderGuid = SelectBookmarkFolderFragmentArgs.fromBundle(arguments!!).folderGuid ?: BookmarkRoot.Root.id
checkIfSignedIn()
launch(IO) {
bookmarkNode = requireComponents.core.bookmarksStorage.getTree(folderGuid!!, true)
launch(Main) {
(activity as HomeActivity).title = bookmarkNode?.title ?: getString(R.string.library_bookmarks)
val adapter = SelectBookmarkFolderAdapter(sharedViewModel)
recylerView_bookmark_folders.adapter = adapter
adapter.updateData(bookmarkNode)
}
}
}
private fun checkIfSignedIn() {
val accountManager = requireComponents.backgroundServices.accountManager
accountManager.register(this, owner = this)
accountManager.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val visitedAddBookmark = SelectBookmarkFolderFragmentArgs.fromBundle(arguments!!).visitedAddBookmark
if (!visitedAddBookmark) {
inflater.inflate(R.menu.bookmarks_select_folder, menu)
menu.findItem(R.id.add_folder_button).icon.colorFilter =
PorterDuffColorFilter(R.attr.iconColor.getColorFromAttr(context!!), PorterDuff.Mode.SRC_IN)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.add_folder_button -> {
launch(Main) {
Navigation.findNavController(requireActivity(), R.id.container).navigate(
SelectBookmarkFolderFragmentDirections
.actionBookmarkSelectFolderFragmentToBookmarkAddFolderFragment()
)
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onAuthenticated(account: OAuthAccount) {
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
}
override fun onError(error: Exception) {
}
override fun onLoggedOut() {
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
}
override fun onProfileUpdated(profile: Profile) {
}
}

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bookmark_layout"
@ -23,7 +27,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="1:1"
tools:foregroundTint="@android:color/black"
android:foregroundTint="?attr/iconColor"
tools:src="@drawable/ic_folder_icon" />
<TextView
@ -34,6 +38,7 @@
android:ellipsize="end"
android:lines="1"
android:textSize="16sp"
android:textColor="?attr/bookmarksEditTextColor"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/bookmark_overflow"
app:layout_constraintHorizontal_bias="0"
@ -57,6 +62,7 @@
android:id="@+id/bookmark_separator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="68dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:importantForAccessibility="no"
@ -64,7 +70,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/bookmark_overflow"
android:background="@android:color/black"
android:background="?attr/bookmarksEditTextColor"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,9 +2,27 @@
<!-- 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.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bookmark_list"
<FrameLayout 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="match_parent" />
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookmark_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bookmarks_empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/bookmarks_empty_message"
android:visibility="gone"
android:textColor="?attr/bookmarksLabelColor"/>
</FrameLayout>

View File

@ -0,0 +1,18 @@
<?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/. -->
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bookmark_folders_sign_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sign_in_button"
android:padding="10dp"
android:layout_marginTop="32dp"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View File

@ -0,0 +1,55 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/bookmark_add_folder_title_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/bookmark_name_label"
android:textAllCaps="true"
android:textColor="?attr/bookmarksLabelColor"
android:textSize="12sp" />
<EditText
android:id="@+id/bookmark_add_folder_title_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:inputType="textAutoComplete"
android:textColor="?attr/bookmarksEditTextColor"
android:textSize="15sp"
tools:text="News" />
<TextView
android:id="@+id/bookmark_add_folder_parent_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/bookmark_folder_label"
android:textAllCaps="true"
android:textColor="?attr/bookmarksLabelColor"
android:textSize="12sp" />
<TextView
android:id="@+id/bookmark_add_folder_parent_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawableStart="@drawable/ic_folder_icon"
android:drawablePadding="10dp"
android:drawableTint="?attr/iconColor"
android:textColor="?attr/bookmarksEditTextColor"
android:textSize="16sp"
tools:targetApi="m"
tools:text="Mobile Bookmarks" />
</LinearLayout>

View File

@ -8,5 +8,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.mozilla.fenix.library.bookmarks.BookmarkFragment">
</LinearLayout>
tools:context="org.mozilla.fenix.library.bookmarks.BookmarkFragment" />

View File

@ -0,0 +1,75 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="16dp">
<TextView
android:id="@+id/bookmark_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/bookmark_name_label"
android:textColor="?attr/bookmarksLabelColor"
android:textSize="12sp"
android:textAllCaps="true"/>
<EditText
android:id="@+id/bookmark_name_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textSize="15sp"
android:textColor="?attr/bookmarksEditTextColor"
tools:text="Internet for people, not profit -- Mozilla"
android:inputType="textAutoComplete"/>
<TextView
android:id="@+id/bookmark_url_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/bookmark_url_label"
android:textColor="?attr/bookmarksLabelColor"
android:textSize="12sp"
android:textAllCaps="true"/>
<EditText
android:id="@+id/bookmark_url_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textSize="15sp"
android:textColor="?attr/bookmarksEditTextColor"
tools:text="https://www.mozilla.org/en-US/"
android:inputType="textUri"/>
<TextView
android:id="@+id/bookmark_folder_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/bookmark_folder_label"
android:textColor="?attr/bookmarksLabelColor"
android:textSize="12sp"
android:textAllCaps="true"/>
<TextView
android:id="@+id/bookmark_folder_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:textColor="?attr/bookmarksEditTextColor"
android:drawableStart="@drawable/ic_folder_icon"
android:drawablePadding="10dp"
android:drawableTint="?attr/iconColor"
tools:text="Mobile Bookmarks"
tools:targetApi="m" />
</LinearLayout>

View File

@ -0,0 +1,18 @@
<?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/select_bookmark_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recylerView_bookmark_folders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</LinearLayout>

View File

@ -7,7 +7,7 @@
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/confirm_add_folder_button"
android:icon="@drawable/ic_new"
android:icon="@drawable/mozac_ic_check"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_add_folder"
app:showAsAction="ifRoom"

View File

@ -79,6 +79,9 @@
<action
android:id="@+id/action_browserFragment_to_libraryFragment"
app:destination="@id/libraryFragment" />
<action
android:id="@+id/action_browserFragment_to_bookmarkEditFragment"
app:destination="@id/bookmarkEditFragment" />
</fragment>
<fragment
@ -115,6 +118,56 @@
<action
android:id="@+id/action_bookmarkFragment_self"
app:destination="@id/bookmarkFragment" />
<action
android:id="@+id/action_bookmarkFragment_to_bookmarkEditFragment"
app:destination="@id/bookmarkEditFragment" />
</fragment>
<fragment
android:id="@+id/bookmarkEditFragment"
android:name="org.mozilla.fenix.library.bookmarks.edit.EditBookmarkFragment"
android:label="@string/edit_bookmark_fragment_title"
tools:layout="@layout/fragment_edit_bookmark">
<argument
android:name="guidToEdit"
app:argType="string"
app:nullable="false"/>
<action
android:id="@+id/action_bookmarkEditFragment_to_bookmarkSelectFolderFragment"
app:destination="@id/bookmarkSelectFolderFragment" />
</fragment>
<fragment
android:id="@+id/bookmarkSelectFolderFragment"
android:name="org.mozilla.fenix.library.bookmarks.selectfolder.SelectBookmarkFolderFragment"
android:label="@string/bookmark_select_folder_fragment_label"
tools:layout="@layout/fragment_select_bookmark_folder">
<argument
android:name="folderGuid"
app:argType="string"
app:nullable="true"/>
<action
android:id="@+id/action_bookmarkSelectFolderFragment_self"
app:destination="@id/bookmarkSelectFolderFragment" />
<action
android:id="@+id/action_bookmarkSelectFolderFragment_to_bookmarkAddFolderFragment"
app:destination="@id/bookmarkAddFolderFragment" />
<argument
android:name="visitedAddBookmark"
app:argType="boolean"
android:defaultValue="false" />
<action
android:id="@+id/action_bookmarkSelectFolderFragment_to_browserFragment"
app:destination="@id/browserFragment" />
</fragment>
<fragment
android:id="@+id/bookmarkAddFolderFragment"
android:name="org.mozilla.fenix.library.bookmarks.addfolder.AddBookmarkFolderFragment"
android:label="@string/bookmark_add_folder_fragment_label">
<action
android:id="@+id/action_bookmarkAddFolderFragment_to_bookmarkSelectFolderFragment"
app:destination="@id/bookmarkSelectFolderFragment" />
</fragment>
<fragment

View File

@ -59,6 +59,13 @@
<!-- Library -->
<color name="library_list_item_text_color_light_mode">@color/off_white</color>
<!-- Bookmarks -->
<color name="bookmark_favicon_background">#DFDFE3</color>
<color name="bookmark_snackbar_background">#2E0EC1</color>
<color name="bookmark_selection_appbar_background">#2E0EC1</color>
<color name="bookmarks_label_normal_theme">#AFAD9C</color>
<color name="bookmarks_edit_normal_theme">@color/off_white</color>
<!-- Search Fragment -->
<color name="suggestionBackground_normal_theme">@color/accent_bright_dark_theme</color>
<color name="search_text">@color/off_white</color>

View File

@ -55,6 +55,10 @@
<attr name="historyTitleColor" format="reference" />
<attr name="historyHeader" format="reference" />
<!-- Bookmarks Fragment -->
<attr name="bookmarksLabelColor" format="reference" />
<attr name="bookmarksEditTextColor" format="reference" />
<!-- Library Fragment -->
<attr name="libraryListItemTextColor" format="reference" />
</resources>

View File

@ -15,6 +15,8 @@
<!-- Bookmarks -->
<color name="bookmark_favicon_background">#DFDFE3</color>
<color name="bookmark_snackbar_background">#2E0EC1</color>
<color name="bookmark_selection_appbar_background">#2E0EC1</color>
<!-- Specific colors for dark theme -->
<color name="background_dark_theme">#1C1B22</color>
@ -30,6 +32,8 @@
<color name="session_placeholder_purple">@color/accent_bright_dark_theme</color>
<color name="session_placeholder_pink">#FF4AA2</color>
<!-- Normal Theme -->
<color name="bookmarks_label_normal_theme">#505263</color>
<color name="bookmarks_edit_normal_theme">@color/text_color_normal_theme</color>
<color name="history_header_normal_theme">#696A6A</color>
<color name="history_title_normal_theme">@color/text_color_normal_theme</color>
<color name="history_url_normal_theme">#696A6A</color>
@ -94,6 +98,8 @@
<color name="private_browsing_top_gradient">#242251</color>
<color name="private_browsing_bottom_gradient">#393862</color>
<color name="search_pill_private_selected_background">#080639</color>
<color name="bookmarks_label_private_theme">@color/photonGrey40</color>
<color name="bookmarks_edit_private_theme">@color/off_white</color>
<color name="history_header_private_theme">@color/photonGrey40</color>
<color name="history_title_private_theme">@color/off_white</color>
<color name="history_url_private_theme">@color/photonGrey40</color>

View File

@ -285,7 +285,7 @@
<!-- Screen title for adding a bookmarks folder -->
<string name="bookmark_add_folder">Add folder</string>
<!-- Snackbar title shown after a bookmark has been created. -->
<string name="bookmark_created_snackbar">Bookmark created.</string>
<string name="bookmark_created_snackbar">Bookmark saved!</string>
<!-- Snackbar edit button shown after a bookmark has been created. -->
<string name="edit_bookmark_snackbar_action">EDIT</string>
@ -303,4 +303,15 @@
<string name="bookmark_menu_open_in_private_tab_button">Open in private tab</string>
<!-- Bookmark overflow menu delete button -->
<string name="bookmark_menu_delete_button">Delete</string>
<string name="edit_bookmark_fragment_title">Edit bookmark</string>
<string name="sign_in_button">Sign in to see synced bookmarks</string>
<string name="bookmark_url_label">URL</string>
<string name="bookmark_folder_label">FOLDER</string>
<string name="bookmark_name_label">NAME</string>
<string name="bookmark_add_folder_fragment_label">Add folder</string>
<string name="bookmark_select_folder_fragment_label">Select folder</string>
<string name="bookmark_empty_title_error">Must have a title</string>
<string name="bookmark_invalid_url_error">Invalid URL</string>
<string name="bookmarks_empty_message">No bookmarks here</string>
</resources>

View File

@ -73,6 +73,10 @@
<item name="historyURLColor">@color/history_url_normal_theme</item>
<item name="historyHeader">@color/history_header_normal_theme</item>
<!-- Bookmark fragment colors -->
<item name="bookmarksLabelColor">@color/bookmarks_label_normal_theme</item>
<item name="bookmarksEditTextColor">@color/bookmarks_edit_normal_theme</item>
<!-- Library Fragment -->
<item name="libraryListItemTextColor">@color/library_list_item_text_color_light_mode</item>
</style>
@ -138,6 +142,10 @@
<item name="historyURLColor">@color/history_url_private_theme</item>
<item name="historyHeader">@color/history_header_private_theme</item>
<!-- Bookmark fragment colors -->
<item name="bookmarksLabelColor">@color/bookmarks_label_private_theme</item>
<item name="bookmarksEditTextColor">@color/bookmarks_edit_private_theme</item>
<!-- Library Fragment -->
<item name="libraryListItemTextColor">@color/off_white</item>
</style>

View File

@ -7,6 +7,7 @@ package org.mozilla.fenix.library.bookmarks
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verifySequence
import io.reactivex.Observer
@ -27,7 +28,7 @@ internal class BookmarkAdapterTest {
setRxSchedulers()
emitter = TestObserver<BookmarkAction>()
bookmarkAdapter = spyk(
BookmarkAdapter(emitter), recordPrivateCalls = true
BookmarkAdapter(mockk(), emitter), recordPrivateCalls = true
)
every { bookmarkAdapter.notifyDataSetChanged() } just Runs
}

View File

@ -7,6 +7,7 @@ private object Versions {
const val android_gradle_plugin = "3.3.2"
const val rxAndroid = "2.1.0"
const val rxKotlin = "2.3.0"
const val rxBindings = "3.0.0-alpha2"
const val anko = "0.10.8"
const val sentry = "1.7.10"
const val leakcanary = "1.6.3"
@ -52,6 +53,7 @@ object Deps {
const val rxKotlin = "io.reactivex.rxjava2:rxkotlin:${Versions.rxKotlin}"
const val rxAndroid = "io.reactivex.rxjava2:rxandroid:${Versions.rxAndroid}"
const val rxBindings = "com.jakewharton.rxbinding3:rxbinding:${Versions.rxBindings}"
const val anko_commons = "org.jetbrains.anko:anko-commons:${Versions.anko}"
const val anko_sdk = "org.jetbrains.anko:anko-sdk25:${Versions.anko}"
@ -123,6 +125,7 @@ object Deps {
const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}"
const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}"
const val androidx_lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}"
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 = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}"