From b54d4d1d58955f05856ec925c3d7018dc81d0d95 Mon Sep 17 00:00:00 2001 From: Colin Lee Date: Thu, 4 Apr 2019 15:40:39 -0500 Subject: [PATCH] Closes #1312, #1236, #1237, #1238, #1239: Creating, Editing, and Deleting Bookmarks and Bookmark Folders --- CHANGELOG.md | 5 + app/build.gradle | 6 + .../java/org/mozilla/fenix/HomeActivity.kt | 5 +- .../mozilla/fenix/browser/BrowserFragment.kt | 37 ++-- .../main/java/org/mozilla/fenix/ext/Int.kt | 16 ++ .../library/bookmarks/BookmarkAdapter.kt | 124 ++++++++---- .../library/bookmarks/BookmarkFragment.kt | 47 ++++- .../library/bookmarks/BookmarkItemMenu.kt | 53 +++-- .../fenix/library/bookmarks/BookmarkUIView.kt | 15 +- .../bookmarks/BookmarksSharedViewModel.kt | 12 ++ .../library/bookmarks/SignInComponent.kt | 50 +++++ .../fenix/library/bookmarks/SignInUIView.kt | 36 ++++ .../addfolder/AddBookmarkFolderFragment.kt | 104 ++++++++++ .../bookmarks/edit/EditBookmarkFragment.kt | 181 ++++++++++++++++++ .../SelectBookmarkFolderAdapter.kt | 126 ++++++++++++ .../SelectBookmarkFolderFragment.kt | 157 +++++++++++++++ app/src/main/res/layout/bookmark_row.xml | 14 +- .../main/res/layout/component_bookmark.xml | 28 ++- app/src/main/res/layout/component_sign_in.xml | 18 ++ .../layout/fragment_add_bookmark_folder.xml | 55 ++++++ app/src/main/res/layout/fragment_bookmark.xml | 3 +- .../res/layout/fragment_edit_bookmark.xml | 75 ++++++++ .../fragment_select_bookmark_folder.xml | 18 ++ .../main/res/menu/bookmarks_add_folder.xml | 2 +- app/src/main/res/navigation/nav_graph.xml | 53 +++++ app/src/main/res/values-night/colors.xml | 7 + app/src/main/res/values/attrs.xml | 4 + app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 13 +- app/src/main/res/values/styles.xml | 8 + .../library/bookmarks/BookmarkAdapterTest.kt | 3 +- buildSrc/src/main/java/Dependencies.kt | 3 + 32 files changed, 1183 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/ext/Int.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarksSharedViewModel.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInUIView.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/addfolder/AddBookmarkFolderFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt create mode 100644 app/src/main/res/layout/component_sign_in.xml create mode 100644 app/src/main/res/layout/fragment_add_bookmark_folder.xml create mode 100644 app/src/main/res/layout/fragment_edit_bookmark.xml create mode 100644 app/src/main/res/layout/fragment_select_bookmark_folder.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f52e3075..edf43433c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ef1148a58..89233bd45 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 009b7789b..c5b402e5d 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index ab29e1654..2dd34f841 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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(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(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() } } } diff --git a/app/src/main/java/org/mozilla/fenix/ext/Int.kt b/app/src/main/java/org/mozilla/fenix/ext/Int.kt new file mode 100644 index 000000000..756e027ca --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/Int.kt @@ -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 +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt index 6f8649da3..3014c5b70 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt @@ -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) : RecyclerView.Adapter() { +class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer) : + RecyclerView.Adapter() { private var tree: List = 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) : 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) : 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) : 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().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) : 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) : RecyclerVie } } + companion object { + private const val bookmarkOverflowExtraDips = 8 + } + enum class ViewType { ITEM, FOLDER, SEPARATOR } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index 00b1a1b78..f1f2ca95f 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -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().onNext(SignInChange.SignedIn) } + ?: getManagedEmitter().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() + .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().onNext(BookmarkChange.Change(currentRoot)) } } } override fun onBackPressed(): Boolean = (bookmarkComponent.uiView as BookmarkUIView).onBackPressed() + + override fun onAuthenticated(account: OAuthAccount) { + getManagedEmitter().onNext(SignInChange.SignedIn) + } + + override fun onError(error: Exception) { + } + + override fun onLoggedOut() { + getManagedEmitter().onNext(SignInChange.SignedOut) + } + + override fun onProfileUpdated(profile: Profile) { + } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt index 7246014ad..e584ae18d 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt @@ -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() } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt index 00fb8515f..8871fc3ee 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkUIView.kt @@ -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) } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarksSharedViewModel.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarksSharedViewModel.kt new file mode 100644 index 000000000..aa0f376af --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarksSharedViewModel.kt @@ -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 +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt new file mode 100644 index 000000000..88ab709d2 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt @@ -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( + bus.getManagedEmitter(SignInAction::class.java), + bus.getSafeManagedObservable(SignInChange::class.java) +) { + + override val reducer: Reducer = { state, change -> + when (change) { + SignInChange.SignedIn -> state.copy(signedIn = true) + SignInChange.SignedOut -> state.copy(signedIn = false) + } + } + + override fun initView(): UIView = + 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() +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInUIView.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInUIView.kt new file mode 100644 index 000000000..85e552fce --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInUIView.kt @@ -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, + changesObservable: Observable +) : UIView(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 { + view.visibility = if (it.signedIn) View.GONE else View.VISIBLE + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/addfolder/AddBookmarkFolderFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/addfolder/AddBookmarkFolderFragment.kt new file mode 100644 index 000000000..b14f1114e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/addfolder/AddBookmarkFolderFragment.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt new file mode 100644 index 000000000..111b017b3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt @@ -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) { + 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt new file mode 100644 index 000000000..7eb75fa1f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt @@ -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() { + + private var tree: List = 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 = listOf() + ): List { + 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt new file mode 100644 index 000000000..c8bd030c8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt @@ -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() + .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().onNext(SignInChange.SignedIn) } + ?: getManagedEmitter().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().onNext(SignInChange.SignedIn) + } + + override fun onError(error: Exception) { + } + + override fun onLoggedOut() { + getManagedEmitter().onNext(SignInChange.SignedOut) + } + + override fun onProfileUpdated(profile: Profile) { + } +} diff --git a/app/src/main/res/layout/bookmark_row.xml b/app/src/main/res/layout/bookmark_row.xml index cd5a6c511..ae36126f0 100644 --- a/app/src/main/res/layout/bookmark_row.xml +++ b/app/src/main/res/layout/bookmark_row.xml @@ -1,5 +1,9 @@ - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/component_bookmark.xml b/app/src/main/res/layout/component_bookmark.xml index 5aabb2212..7ba867592 100644 --- a/app/src/main/res/layout/component_bookmark.xml +++ b/app/src/main/res/layout/component_bookmark.xml @@ -2,9 +2,27 @@ - - + android:layout_height="match_parent"> + + + + + + diff --git a/app/src/main/res/layout/component_sign_in.xml b/app/src/main/res/layout/component_sign_in.xml new file mode 100644 index 000000000..e32603250 --- /dev/null +++ b/app/src/main/res/layout/component_sign_in.xml @@ -0,0 +1,18 @@ + + +