diff --git a/app/build.gradle b/app/build.gradle index dd0d4e2..5041902 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -258,6 +258,7 @@ dependencies { implementation Deps.kotlin_coroutines implementation Deps.androidx_appcompat + implementation Deps.androidx_core implementation Deps.androidx_core_ktx implementation Deps.androidx_paging implementation Deps.androidx_navigation_fragment @@ -330,7 +331,9 @@ dependencies { testImplementation Deps.kotlin_coroutines_test // Must be in debug implementation - debugImplementation Deps.androidx_fragment_testing + debugImplementation(Deps.androidx_fragment_testing) { + exclude group: 'androidx.test', module : 'core' + } } // ------------------------------------------------------------------------------------------------- diff --git a/app/src/main/java/org/mozilla/reference/browser/ViewModelFactory.kt b/app/src/main/java/org/mozilla/reference/browser/ViewModelFactory.kt index 291841f..82fb916 100644 --- a/app/src/main/java/org/mozilla/reference/browser/ViewModelFactory.kt +++ b/app/src/main/java/org/mozilla/reference/browser/ViewModelFactory.kt @@ -7,6 +7,9 @@ package org.mozilla.reference.browser import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.mozilla.reference.browser.library.bookmarks.ui.BookmarkViewModel +import org.mozilla.reference.browser.library.bookmarks.ui.addfolder.AddBookmarkFolderViewModel +import org.mozilla.reference.browser.library.bookmarks.ui.edit.EditBookmarkViewModel +import org.mozilla.reference.browser.library.bookmarks.ui.selectfolder.SelectBookmarkViewModel import org.mozilla.reference.browser.library.history.ui.HistoryViewModel import java.lang.IllegalArgumentException @@ -15,17 +18,28 @@ import java.lang.IllegalArgumentException */ class ViewModelFactory(private val applicationContext: BrowserApplication) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - when (modelClass) { - HistoryViewModel::class.java -> with(applicationContext.components.useCases) { + @Suppress("UNCHECKED_CAST", "ComplexMethod") + override fun create(modelClass: Class): T { + val useCases = applicationContext.components.useCases + return when (modelClass) { + HistoryViewModel::class.java -> with(useCases) { HistoryViewModel(historyUseCases) as T } - BookmarkViewModel::class.java -> with(applicationContext.components.useCases) { + BookmarkViewModel::class.java -> with(useCases) { BookmarkViewModel(bookmarkUseCases) as T } + AddBookmarkFolderViewModel::class.java -> with(useCases) { + AddBookmarkFolderViewModel(bookmarkUseCases) as T + } + SelectBookmarkViewModel::class.java -> with(useCases) { + SelectBookmarkViewModel(bookmarkUseCases) as T + } + EditBookmarkViewModel::class.java -> with(useCases) { + EditBookmarkViewModel(bookmarkUseCases) as T + } else -> throw IllegalArgumentException("Unknown model class $modelClass") } + } companion object { private var INSTANCE: ViewModelFactory? = null diff --git a/app/src/main/java/org/mozilla/reference/browser/browser/ToolbarIntegration.kt b/app/src/main/java/org/mozilla/reference/browser/browser/ToolbarIntegration.kt index 5f31a72..5b07949 100644 --- a/app/src/main/java/org/mozilla/reference/browser/browser/ToolbarIntegration.kt +++ b/app/src/main/java/org/mozilla/reference/browser/browser/ToolbarIntegration.kt @@ -52,6 +52,7 @@ import org.mozilla.reference.browser.ext.nav import org.mozilla.reference.browser.ext.share import org.mozilla.reference.browser.settings.SettingsActivity import org.mozilla.reference.browser.settings.deletebrowsingdata.DeleteBrowsingData +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder import org.mozilla.reference.browser.view.DaisySnackbar @Suppress("TooManyFunctions") @@ -337,34 +338,36 @@ class ToolbarIntegration( } } - private suspend fun bookmarkTapped(session: Session) = withContext(Dispatchers.IO) { - val existing = + private suspend fun bookmarkTapped(session: Session) { + withContext(Dispatchers.IO) { + val existing = bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url } - if (existing != null) { - bookmarksStorage.deleteNode(existing.guid) - } else { - // Save bookmark - bookmarksStorage.addItem( - parentGuid = 0.toString(), - url = session.url, - title = session.title, - position = null - ) - - withContext(Dispatchers.Main) { - showBookmarkAddedSnackbar() + if (existing != null) { + bookmarksStorage.deleteNode(existing.guid) + } else { + // Save bookmark + val guid = bookmarksStorage.addItem( + parentGuid = 0.toString(), + url = session.url, + title = session.title, + position = null + ) + + withContext(Dispatchers.Main) { + showBookmarkAddedSnackbar(guid) + } } } } - private fun showBookmarkAddedSnackbar() { + private fun showBookmarkAddedSnackbar(guid: String) { DaisySnackbar.make( view = parentView, duration = DaisySnackbar.LENGTH_LONG ) .setText(context.getString(R.string.bookmark_added_snackbar_text)) .setAction(context.getString(R.string.bookmark_added_snackbar_edit_action)) { - openBookmarkEditFragment() + openBookmarkEditFragment(guid) } .show() } @@ -374,8 +377,9 @@ class ToolbarIntegration( context.startActivity(intent) } - private fun openBookmarkEditFragment() { - // to-do go to bookmark edit fragment + private fun openBookmarkEditFragment(guid: String) { + val direction = BrowserFragmentDirections.actionBrowserFragmentToEditBookmarkFragment(guid) + navController.nav(R.id.browserFragment, direction) } private fun openHistoryFragment() { @@ -384,7 +388,7 @@ class ToolbarIntegration( } private fun openBookmarkFragment() { - val direction = BrowserFragmentDirections.actionBrowserFragmentToBookmarkFragment() + val direction = BrowserFragmentDirections.actionBrowserFragmentToBookmarkFragment(bookmarksRootFolder) navController.nav(R.id.browserFragment, direction) } diff --git a/app/src/main/java/org/mozilla/reference/browser/ext/Fragment.kt b/app/src/main/java/org/mozilla/reference/browser/ext/Fragment.kt index d8f90d9..071856f 100644 --- a/app/src/main/java/org/mozilla/reference/browser/ext/Fragment.kt +++ b/app/src/main/java/org/mozilla/reference/browser/ext/Fragment.kt @@ -6,6 +6,7 @@ package org.mozilla.reference.browser.ext import androidx.annotation.IdRes import androidx.fragment.app.Fragment +import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.navigation.NavOptions import androidx.navigation.Navigator @@ -20,6 +21,10 @@ import org.mozilla.reference.browser.Components val Fragment.requireComponents: Components get() = requireContext().components +fun Fragment.navController(): NavController { + return NavHostFragment.findNavController(this) +} + fun Fragment.nav(@IdRes id: Int?, directions: NavDirections) { NavHostFragment.findNavController(this).nav(id, directions) } diff --git a/app/src/main/java/org/mozilla/reference/browser/freshtab/FreshTabController.kt b/app/src/main/java/org/mozilla/reference/browser/freshtab/FreshTabController.kt index c5a41df..1d6db96 100644 --- a/app/src/main/java/org/mozilla/reference/browser/freshtab/FreshTabController.kt +++ b/app/src/main/java/org/mozilla/reference/browser/freshtab/FreshTabController.kt @@ -17,6 +17,7 @@ import org.mozilla.reference.browser.ext.nav import org.mozilla.reference.browser.ext.openToBrowserAndLoad import org.mozilla.reference.browser.settings.SettingsActivity import org.mozilla.reference.browser.settings.deletebrowsingdata.DeleteBrowsingData +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder @Suppress("TooManyFunctions") interface FreshTabController { @@ -91,7 +92,7 @@ class DefaultFreshTabController( } override fun handleBookmarksClicked() { - val direction = FreshTabFragmentDirections.actionFreshTabFragmentToBookmarkFragment() + val direction = FreshTabFragmentDirections.actionFreshTabFragmentToBookmarkFragment(bookmarksRootFolder) navController.nav(R.id.freshTabFragment, direction) } diff --git a/app/src/main/java/org/mozilla/reference/browser/library/LibraryItemView.kt b/app/src/main/java/org/mozilla/reference/browser/library/LibraryItemView.kt index 02d3dd7..e4c4960 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/LibraryItemView.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/LibraryItemView.kt @@ -53,7 +53,8 @@ class LibraryItemView @JvmOverloads constructor( val urlView: TextView val actionButton: ImageButton - private val iconView: ViewSwitcher + val iconView: ViewSwitcher + private val favicon: ImageView init { diff --git a/app/src/main/java/org/mozilla/reference/browser/library/LibraryToolbar.kt b/app/src/main/java/org/mozilla/reference/browser/library/LibraryToolbar.kt index e67f1d1..5e37846 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/LibraryToolbar.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/LibraryToolbar.kt @@ -50,6 +50,10 @@ class LibraryToolbar @JvmOverloads constructor( fun searchClosed() fun searchQueryChanged(query: String) + + fun add() + + fun confirm() } var title: CharSequence @@ -107,6 +111,14 @@ class LibraryToolbar @JvmOverloads constructor( notifyObservers { openAll(newTab = true, private = true) } true } + R.id.add_folder -> { + notifyObservers { add() } + true + } + R.id.confirm -> { + notifyObservers { confirm() } + true + } else -> throw IllegalArgumentException("Invalid menu item") } } diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkFragment.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkFragment.kt index 6518b89..fb2769f 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkFragment.kt @@ -12,6 +12,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.navigation.fragment.findNavController @@ -22,7 +23,9 @@ import org.mozilla.reference.browser.BrowserDirection import org.mozilla.reference.browser.R import org.mozilla.reference.browser.ViewModelFactory import org.mozilla.reference.browser.ext.application +import org.mozilla.reference.browser.ext.nav import org.mozilla.reference.browser.ext.openToBrowserAndLoad +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder class BookmarkFragment @JvmOverloads constructor( private val initialBookmarkViewModel: BookmarkViewModel? = null @@ -33,6 +36,8 @@ class BookmarkFragment @JvmOverloads constructor( private lateinit var bookmarkView: BookmarkView private lateinit var bookmarkInteractor: BookmarkViewInteractor + private val sharedViewModel: BookmarksSharedViewModel by activityViewModels() + override fun onAttach(context: Context) { super.onAttach(context) @@ -42,7 +47,10 @@ class BookmarkFragment @JvmOverloads constructor( bookmarkInteractor = BookmarkViewInteractor( bookmarkViewModel, ::openBookmarkItem, - ::onBackPressed + ::expandBookmarkFolder, + ::onBackPressed, + ::navigateToAddFolder, + ::navigateToEditBookmark ) } @@ -54,6 +62,7 @@ class BookmarkFragment @JvmOverloads constructor( newViewMode = bookmarkViewModel.viewMode, newBookmarkNode = tree, newSelectedItems = bookmarkViewModel.selectedItems.value!!) + sharedViewModel.selectedFolder = tree }) bookmarkViewModel.selectedItems.observe(this, Observer { selectedItems -> @@ -67,6 +76,15 @@ class BookmarkFragment @JvmOverloads constructor( newSelectedItems = selectedItems, newBookmarkNode = bookmarkViewModel.tree.value) }) + + val currentGuid = BookmarkFragmentArgs.fromBundle(requireArguments()).currentRoot.ifEmpty { + bookmarksRootFolder + } + loadRootFolder(currentGuid) + } + + private fun loadRootFolder(currentGuid: String) { + bookmarkViewModel.fetchBookmarks(currentGuid) } override fun onCreateView( @@ -90,6 +108,21 @@ class BookmarkFragment @JvmOverloads constructor( } } + private fun expandBookmarkFolder(folder: BookmarkNode) { + findNavController().nav(R.id.bookmarkFragment, + BookmarkFragmentDirections.actionBookmarkFragmentSelf(folder.guid)) + } + + private fun navigateToAddFolder() { + findNavController().nav(R.id.bookmarkFragment, + BookmarkFragmentDirections.actionBookmarkFragmentToAddBookmarkFolderFragment()) + } + + fun navigateToEditBookmark(bookmarkNode: BookmarkNode) { + findNavController().nav(R.id.bookmarkFragment, + BookmarkFragmentDirections.actionBookmarkFragmentToEditBookmarkFragment(bookmarkNode.guid)) + } + override fun onBackPressed(): Boolean { if (bookmarkView.onBackPressed()) { return true diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkView.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkView.kt index fb06012..bdd74a8 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkView.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkView.kt @@ -79,6 +79,14 @@ class BookmarkView( view.bookmark_search_list.visibility = View.VISIBLE bookmarkViewModel.searchBookmarks(query) } + + override fun confirm() { + // no-op + } + + override fun add() { + interactor.navigateToAddFolder() + } }) bookmarkViewModel.bookmarkSearchList.observe(lifecycleOwner, Observer { bookmarkList -> bookmarkSearchAdapter.setData(bookmarkList) diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewInteractor.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewInteractor.kt index 3c39e25..7a909d8 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewInteractor.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewInteractor.kt @@ -7,16 +7,29 @@ package org.mozilla.reference.browser.library.bookmarks.ui import mozilla.components.concept.storage.BookmarkNode +import mozilla.components.concept.storage.BookmarkNodeType import org.mozilla.reference.browser.library.MultiSelectionInteractor +@Suppress("TooManyFunctions") class BookmarkViewInteractor( private val bookmarkViewModel: BookmarkViewModel, private val openToBrowser: (items: Set, private: Boolean) -> Unit, - private val onBackPressed: () -> Boolean + private val expandBookmarkFolder: (item: BookmarkNode) -> Unit, + private val onBackPressed: () -> Boolean, + private val navigateToAddFolder: () -> Unit, + private val navigateToEditBookmark: (item: BookmarkNode) -> Unit ) : MultiSelectionInteractor { override fun open(items: Set, newTab: Boolean, private: Boolean) { - openToBrowser.invoke(items, private) + when (items.first().type) { + BookmarkNodeType.ITEM -> { + openToBrowser.invoke(items, private) + } + BookmarkNodeType.FOLDER -> { + expandBookmarkFolder.invoke(items.first()) + } + BookmarkNodeType.SEPARATOR -> throw IllegalStateException("Cannot open separators") + } } override fun select(item: BookmarkNode) { @@ -39,6 +52,14 @@ class BookmarkViewInteractor( return false } + fun navigateToAddFolder() { + navigateToAddFolder.invoke() + } + + fun onEdit(item: BookmarkNode) { + navigateToEditBookmark.invoke(item) + } + fun onOpenInNormalTab(item: BookmarkNode) { open(setOf(item), false) } diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewModel.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewModel.kt index e1fb604..22ec34f 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewModel.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarkViewModel.kt @@ -33,10 +33,9 @@ class BookmarkViewModel(private val bookmarkUseCases: BookmarkUseCases) : ViewMo init { _selectedItems.value = mutableSetOf() _bookmarkSearchList.value = listOf() - fetchBookmarks("0") } - private fun fetchBookmarks(id: String) { + fun fetchBookmarks(id: String) { viewModelScope.launch(Dispatchers.IO) { val tree = bookmarkUseCases.getBookmarks.invoke(id) withContext(Dispatchers.Main) { diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarksSharedViewModel.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarksSharedViewModel.kt new file mode 100644 index 0000000..91fa708 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/BookmarksSharedViewModel.kt @@ -0,0 +1,20 @@ +/* + * 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.reference.browser.library.bookmarks.ui + +import androidx.lifecycle.ViewModel +import mozilla.components.concept.storage.BookmarkNode + +/** + * [ViewModel] that shares data between various bookmarks fragments. + */ +class BookmarksSharedViewModel : ViewModel() { + /** + * The currently selected bookmark root. + */ + var selectedFolder: BookmarkNode? = null +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/addfolder/AddBookmarkFolderFragment.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/addfolder/AddBookmarkFolderFragment.kt new file mode 100644 index 0000000..8e7613d --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/addfolder/AddBookmarkFolderFragment.kt @@ -0,0 +1,127 @@ +/* + * 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.reference.browser.library.bookmarks.ui.addfolder + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import kotlinx.android.synthetic.main.fragment_edit_bookmark.* +import mozilla.components.support.ktx.android.view.hideKeyboard +import mozilla.components.support.ktx.android.view.showKeyboard +import org.mozilla.reference.browser.R +import org.mozilla.reference.browser.ViewModelFactory +import org.mozilla.reference.browser.ext.application +import org.mozilla.reference.browser.ext.nav +import org.mozilla.reference.browser.ext.navController +import org.mozilla.reference.browser.library.LibraryToolbar +import org.mozilla.reference.browser.library.bookmarks.ui.BookmarksSharedViewModel +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder + +class AddBookmarkFolderFragment : Fragment(R.layout.fragment_edit_bookmark) { + + private lateinit var addBookmarkFolderViewModel: AddBookmarkFolderViewModel + + private val sharedViewModel: BookmarksSharedViewModel by activityViewModels() + + @Suppress("ComplexMethod") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + bookmark_url_label.visibility = View.GONE + bookmark_url_edit.visibility = View.GONE + bookmark_title_edit.showKeyboard() + toolbar.register(object : LibraryToolbar.Observer { + override fun confirm() { + addFolder() + } + + override fun close() { + navController().popBackStack() + } + + override fun add() { + // no-op + } + + override fun delete() { + // no-op + } + + override fun openAll(newTab: Boolean, private: Boolean) { + // no-op + } + + override fun searchOpened() { + // no-op + } + + override fun searchClosed() { + // no-op + } + + override fun searchQueryChanged(query: String) { + // no-op + } + }) + + context?.let { + toolbar.updateToolbar(R.menu.bookmarks_add_folder, it.getString(R.string.bookmark_add_folder_title)) + } + + addBookmarkFolderViewModel = ViewModelProviders.of(this, + ViewModelFactory.getInstance(view.context.application)).get(AddBookmarkFolderViewModel::class.java) + + addBookmarkFolderViewModel.tree.observe(viewLifecycleOwner, Observer { tree -> + sharedViewModel.selectedFolder = tree + bookmark_parent_folder_selector.text = sharedViewModel.selectedFolder!!.title + }) + } + + override fun onResume() { + super.onResume() + + if (sharedViewModel.selectedFolder != null) { + bookmark_parent_folder_selector.text = if (sharedViewModel.selectedFolder!!.guid == bookmarksRootFolder) { + getString(R.string.bookmarks_folder_root) + } else { + sharedViewModel.selectedFolder!!.title + } + } else { + addBookmarkFolderViewModel.fetchBookmarks(bookmarksRootFolder) + } + bookmark_parent_folder_selector.setOnClickListener { + nav( + R.id.addBookmarkFolderFragment, + AddBookmarkFolderFragmentDirections + .actionAddBookmarkFolderFragmentToSelectBookmarkFolderFragment() + ) + } + } + + override fun onPause() { + super.onPause() + bookmark_title_edit.hideKeyboard() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.bookmarks_add_folder, menu) + } + + private fun addFolder() { + if (bookmark_title_edit.text.isNullOrBlank()) { + bookmark_title_edit.error = getString(R.string.bookmark_empty_title_error) + return + } + this.view?.hideKeyboard() + val title = bookmark_title_edit.text.toString() + addBookmarkFolderViewModel.addBookmarkFolder(sharedViewModel.selectedFolder!!.guid, title) + navController().popBackStack() + } +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/addfolder/AddBookmarkFolderViewModel.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/addfolder/AddBookmarkFolderViewModel.kt new file mode 100644 index 0000000..705088b --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/addfolder/AddBookmarkFolderViewModel.kt @@ -0,0 +1,39 @@ +/* + * 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.reference.browser.library.bookmarks.ui.addfolder + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.concept.storage.BookmarkNode +import org.mozilla.reference.browser.library.bookmarks.usecases.BookmarkUseCases + +class AddBookmarkFolderViewModel(private val bookmarkUseCases: BookmarkUseCases) : ViewModel() { + + private val _tree = MutableLiveData() + val tree: LiveData = _tree + + fun fetchBookmarks(id: String) { + viewModelScope.launch(Dispatchers.IO) { + val tree = bookmarkUseCases.getBookmarks.invoke(id) + withContext(Dispatchers.Main) { + _tree.value = tree + } + } + } + + fun addBookmarkFolder(parentGuid: String, title: String) { + viewModelScope.launch(Dispatchers.IO) { + val guid = bookmarkUseCases.addFolder(parentGuid, title) + fetchBookmarks(guid) + } + } +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/edit/EditBookmarkFragment.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/edit/EditBookmarkFragment.kt new file mode 100644 index 0000000..6594c19 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/edit/EditBookmarkFragment.kt @@ -0,0 +1,144 @@ +/* + * 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.reference.browser.library.bookmarks.ui.edit + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.fragment.navArgs +import kotlinx.android.synthetic.main.fragment_edit_bookmark.* +import mozilla.components.concept.storage.BookmarkNodeType +import org.mozilla.reference.browser.R +import org.mozilla.reference.browser.ViewModelFactory +import org.mozilla.reference.browser.ext.application +import org.mozilla.reference.browser.ext.nav +import org.mozilla.reference.browser.ext.navController +import org.mozilla.reference.browser.library.LibraryToolbar +import org.mozilla.reference.browser.library.bookmarks.ui.BookmarksSharedViewModel +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder + +class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark) { + + private val args by navArgs() + + private lateinit var editBookmarkViewModel: EditBookmarkViewModel + + private val sharedViewModel: BookmarksSharedViewModel by activityViewModels() + + @Suppress("ComplexMethod") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + toolbar.register(object : LibraryToolbar.Observer { + override fun confirm() { + updateBookmark() + } + + override fun close() { + navController().popBackStack() + } + + override fun add() { + // no-op + } + + override fun delete() { + // no-op + } + + override fun openAll(newTab: Boolean, private: Boolean) { + // no-op + } + + override fun searchOpened() { + // no-op + } + + override fun searchClosed() { + // no-op + } + + override fun searchQueryChanged(query: String) { + // no-op + } + }) + + editBookmarkViewModel = ViewModelProviders.of(this, + ViewModelFactory.getInstance(view.context.application)).get(EditBookmarkViewModel::class.java) + + editBookmarkViewModel.tree.observe(viewLifecycleOwner, Observer { tree -> + when (tree?.type) { + BookmarkNodeType.FOLDER -> { + context?.let { + toolbar.updateToolbar(R.menu.bookmarks_add_folder, + it.getString(R.string.bookmark_edit_folder_title)) + } + bookmark_url_label.visibility = View.GONE + bookmark_url_edit.visibility = View.GONE + } + BookmarkNodeType.ITEM -> { + context?.let { + toolbar.updateToolbar(R.menu.bookmarks_add_folder, + it.getString(R.string.bookmark_edit_bookmark_title)) + } + } + else -> throw IllegalArgumentException() + } + tree.let { + bookmark_title_edit.setText(it.title) + bookmark_url_edit.setText(it.url) + } + + if (sharedViewModel.selectedFolder != null) { + bookmark_parent_folder_selector.text = + if (sharedViewModel.selectedFolder!!.guid == bookmarksRootFolder) { + getString(R.string.bookmarks_folder_root) + } else { + sharedViewModel.selectedFolder!!.title + } + } else { + editBookmarkViewModel.fetchParentBookmark(bookmarksRootFolder) + } + + bookmark_parent_folder_selector.setOnClickListener { + nav( + R.id.editBookmarkFragment, + EditBookmarkFragmentDirections + .actionEditBookmarkFragmentToSelectBookmarkFolderFragment() + ) + } + }) + + editBookmarkViewModel.parentTree.observe(viewLifecycleOwner, Observer { tree -> + sharedViewModel.selectedFolder = tree + bookmark_parent_folder_selector.text = sharedViewModel.selectedFolder!!.title + }) + + editBookmarkViewModel.fetchBookmarks(args.guidToEdit) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.bookmarks_add_folder, menu) + } + + private fun updateBookmark() { + val title = bookmark_title_edit.text.toString() + val url = bookmark_url_edit.text.toString() + val parentGuid = sharedViewModel.selectedFolder?.guid + ?: editBookmarkViewModel.tree.value!!.parentGuid + editBookmarkViewModel.editBookmark( + args.guidToEdit, + parentGuid, + title, + url + ) + navController().popBackStack() + } +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/edit/EditBookmarkViewModel.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/edit/EditBookmarkViewModel.kt new file mode 100644 index 0000000..0b21a38 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/edit/EditBookmarkViewModel.kt @@ -0,0 +1,60 @@ +/* + * 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.reference.browser.library.bookmarks.ui.edit + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.concept.storage.BookmarkInfo +import mozilla.components.concept.storage.BookmarkNode +import mozilla.components.concept.storage.BookmarkNodeType +import org.mozilla.reference.browser.library.bookmarks.usecases.BookmarkUseCases + +class EditBookmarkViewModel(private val bookmarkUseCases: BookmarkUseCases) : ViewModel() { + + private val _tree = MutableLiveData() + val tree: LiveData = _tree + + private val _parentTree = MutableLiveData() + val parentTree: LiveData = _parentTree + + fun fetchBookmarks(id: String) { + viewModelScope.launch(Dispatchers.IO) { + val tree = bookmarkUseCases.getBookmarks.invoke(id) + withContext(Dispatchers.Main) { + _tree.value = tree + } + } + } + + fun fetchParentBookmark(id: String) { + viewModelScope.launch(Dispatchers.IO) { + val tree = bookmarkUseCases.getBookmarks.invoke(id) + withContext(Dispatchers.Main) { + _parentTree.value = tree + } + } + } + + fun editBookmark(guid: String, parentGuid: String?, title: String, url: String) { + viewModelScope.launch(Dispatchers.IO) { + bookmarkUseCases.editBookmark( + guid, + BookmarkInfo( + parentGuid, + tree.value?.position, + title, + if (tree.value?.type == BookmarkNodeType.ITEM) url else null + ) + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkFolderAdapter.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkFolderAdapter.kt new file mode 100644 index 0000000..2dd0832 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkFolderAdapter.kt @@ -0,0 +1,141 @@ +/* + * 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.reference.browser.library.bookmarks.ui.selectfolder + +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.two_line_list_item_with_action_layout.view.* +import mozilla.components.concept.storage.BookmarkNode +import mozilla.components.concept.storage.BookmarkNodeType +import mozilla.components.support.ktx.android.util.dpToPx +import org.mozilla.reference.browser.R +import org.mozilla.reference.browser.library.LibraryItemView +import org.mozilla.reference.browser.library.bookmarks.ui.BookmarksSharedViewModel +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder + +class SelectBookmarkFolderAdapter( + private val sharedViewModel: BookmarksSharedViewModel +) : ListAdapter(DiffCallback) { + + fun updateData(tree: BookmarkNode?) { + val updatedData = tree + ?.convertToFolderDepthTree() + .orEmpty() + submitList(updatedData) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BookmarkFolderViewHolder { + val view = LibraryItemView(parent.context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + return BookmarkFolderViewHolder(view) + } + + override fun onBindViewHolder( + holder: BookmarkFolderViewHolder, + position: Int + ) { + val item = getItem(position) + + holder.bind(item, selected = (item.node == sharedViewModel.selectedFolder)) { node -> + val lastSelectedItemPosition = getSelectedItemIndex() + + sharedViewModel.toggleSelection(node) + + notifyItemChanged(position) + lastSelectedItemPosition + ?.takeIf { it != position } + ?.let { notifyItemChanged(it) } + } + } + + class BookmarkFolderViewHolder( + val view: LibraryItemView + ) : RecyclerView.ViewHolder(view), LayoutContainer { + + override val containerView get() = view + + init { + view.toggleActionButton(showActionButton = false) + view.urlView.visibility = View.GONE + } + + fun bind(folder: BookmarkNodeWithDepth, selected: Boolean, onSelect: (BookmarkNode) -> Unit) { + view.changeSelected(selected) + view.favicon.setImageDrawable( + AppCompatResources.getDrawable( + containerView.context, + R.drawable.ic_folder_icon + ) + ) + view.titleView.text = if (folder.node.guid == bookmarksRootFolder) { + view.context.getString(R.string.bookmarks_folder_root) + } else { + folder.node.title + } + view.setOnClickListener { + onSelect(folder.node) + } + val pxToIndent = dpsToIndent.dpToPx(view.context.resources.displayMetrics) + val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth + view.setPadding(padding, 0, 0, 0) + } + } + + data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?) + + private fun BookmarkNode.convertToFolderDepthTree(depth: Int = 0): List { + val newList = listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid)) + return newList + children + ?.filter { it.type == BookmarkNodeType.FOLDER } + ?.flatMap { it.convertToFolderDepthTree(depth = depth + 1) } + .orEmpty() + } + + private fun getSelectedItemIndex(): Int? { + val selectedNode = sharedViewModel.selectedFolder + val selectedNodeIndex = currentList.indexOfFirst { it.node == selectedNode } + + return selectedNodeIndex.takeIf { it != -1 } + } + + companion object { + private const val maxDepth = 10 + private const val dpsToIndent = 10 + } +} + +private object DiffCallback : + DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: SelectBookmarkFolderAdapter.BookmarkNodeWithDepth, + newItem: SelectBookmarkFolderAdapter.BookmarkNodeWithDepth + ) = oldItem.node.guid == newItem.node.guid + + override fun areContentsTheSame( + oldItem: SelectBookmarkFolderAdapter.BookmarkNodeWithDepth, + newItem: SelectBookmarkFolderAdapter.BookmarkNodeWithDepth + ) = oldItem == newItem +} + +private fun BookmarksSharedViewModel.toggleSelection(node: BookmarkNode?) { + selectedFolder = if (selectedFolder == node) null else node +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkFolderFragment.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkFolderFragment.kt new file mode 100644 index 0000000..691a532 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkFolderFragment.kt @@ -0,0 +1,98 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.reference.browser.library.bookmarks.ui.selectfolder + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.* +import org.mozilla.reference.browser.R +import org.mozilla.reference.browser.ViewModelFactory +import org.mozilla.reference.browser.ext.application +import org.mozilla.reference.browser.ext.navController +import org.mozilla.reference.browser.library.LibraryToolbar +import org.mozilla.reference.browser.library.bookmarks.ui.BookmarksSharedViewModel +import org.mozilla.reference.browser.storage.HistoryDatabase.Companion.bookmarksRootFolder + +class SelectBookmarkFolderFragment : Fragment(R.layout.fragment_select_bookmark_folder) { + + private lateinit var selectBookmarkFolderViewModel: SelectBookmarkViewModel + private lateinit var adapter: SelectBookmarkFolderAdapter + + private val sharedViewModel: BookmarksSharedViewModel by activityViewModels() + + @Suppress("ComplexMethod") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + toolbar.register(object : LibraryToolbar.Observer { + override fun confirm() { + exitFragment() + } + + override fun close() { + exitFragment() + } + + override fun add() { + // no-op + } + + override fun delete() { + // no-op + } + + override fun openAll(newTab: Boolean, private: Boolean) { + // no-op + } + + override fun searchOpened() { + // no-op + } + + override fun searchClosed() { + // no-op + } + + override fun searchQueryChanged(query: String) { + // no-op + } + }) + + context?.let { + toolbar.updateToolbar(R.menu.bookmarks_add_folder, it.getString(R.string.bookmark_select_folder_title)) + } + + selectBookmarkFolderViewModel = ViewModelProviders.of(this, + ViewModelFactory.getInstance(view.context.application)).get(SelectBookmarkViewModel::class.java) + selectBookmarkFolderViewModel.tree.observe(viewLifecycleOwner, Observer { tree -> + adapter.updateData(tree) + }) + + adapter = SelectBookmarkFolderAdapter(sharedViewModel) + bookmark_folders.adapter = adapter + } + + override fun onResume() { + super.onResume() + selectBookmarkFolderViewModel.fetchBookmarks(bookmarksRootFolder) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.bookmarks_add_folder, menu) + } + + private fun exitFragment() { + if (sharedViewModel.selectedFolder == null) { + sharedViewModel.selectedFolder = selectBookmarkFolderViewModel.tree.value + } + navController().popBackStack() + } +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkViewModel.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkViewModel.kt new file mode 100644 index 0000000..cc92a2f --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/selectfolder/SelectBookmarkViewModel.kt @@ -0,0 +1,32 @@ +/* + * 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.reference.browser.library.bookmarks.ui.selectfolder + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.concept.storage.BookmarkNode +import org.mozilla.reference.browser.library.bookmarks.usecases.BookmarkUseCases + +class SelectBookmarkViewModel(private val bookmarkUseCases: BookmarkUseCases) : ViewModel() { + + private val _tree = MutableLiveData() + val tree: LiveData = _tree + + fun fetchBookmarks(id: String) { + viewModelScope.launch(Dispatchers.IO) { + val tree = bookmarkUseCases.getBookmarks.invoke(id) + withContext(Dispatchers.Main) { + _tree.value = tree + } + } + } +} diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkFolderViewHolder.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkFolderViewHolder.kt index 6b1d538..ab54fc5 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkFolderViewHolder.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkFolderViewHolder.kt @@ -6,7 +6,11 @@ package org.mozilla.reference.browser.library.bookmarks.ui.viewholders +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import kotlinx.android.synthetic.main.two_line_list_item_with_action_layout.view.* import mozilla.components.concept.storage.BookmarkNode +import org.mozilla.reference.browser.R import org.mozilla.reference.browser.library.LibraryItemView import org.mozilla.reference.browser.library.SelectionHolder import org.mozilla.reference.browser.library.bookmarks.ui.BookmarkViewInteractor @@ -18,6 +22,16 @@ class BookmarkFolderViewHolder( ) : BookmarkNodeViewHolder(view, interactor) { override fun bind(item: BookmarkNode) { - // to-do + containerView.toggleActionButton( + showActionButton = selectionHolder.selectedItems.isEmpty() + ) + setupMenu(item) + + containerView.titleView.text = item.title + containerView.urlView.visibility = View.GONE + containerView.favicon.setImageDrawable( + AppCompatResources.getDrawable(containerView.context, R.drawable.ic_folder_icon)) + containerView.changeSelected(item in selectionHolder.selectedItems) + setSelectionListeners(item, selectionHolder) } } diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkNodeViewHolder.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkNodeViewHolder.kt index 72a09b3..f5ec6e1 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkNodeViewHolder.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/ui/viewholders/BookmarkNodeViewHolder.kt @@ -44,6 +44,7 @@ abstract class BookmarkNodeViewHolder( BookmarkItemMenu.Item.OpenInNewTab -> interactor.onOpenInNormalTab(item) BookmarkItemMenu.Item.OpenInPrivateTab -> interactor.onOpenInPrivateTab(item) BookmarkItemMenu.Item.Delete -> interactor.onDelete(item) + BookmarkItemMenu.Item.Edit -> interactor.onEdit(item) } } diff --git a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/usecases/BookmarkUseCases.kt b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/usecases/BookmarkUseCases.kt index b58d3d4..f5a4add 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/usecases/BookmarkUseCases.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/bookmarks/usecases/BookmarkUseCases.kt @@ -6,6 +6,7 @@ package org.mozilla.reference.browser.library.bookmarks.usecases +import mozilla.components.concept.storage.BookmarkInfo import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarksStorage @@ -35,8 +36,22 @@ class BookmarkUseCases(bookmarksStorage: BookmarksStorage) { } } + class AddBookmarkFolderUseCase(private val bookmarksStorage: BookmarksStorage) { + suspend operator fun invoke(parentGuid: String, title: String): String { + return bookmarksStorage.addFolder(parentGuid, title) + } + } + + class EditBookmarkFolderUseCase(private val bookmarksStorage: BookmarksStorage) { + suspend operator fun invoke(guid: String, bookmarkInfo: BookmarkInfo) { + return bookmarksStorage.updateNode(guid, bookmarkInfo) + } + } + val getBookmarks by lazy { GetBookmarksUseCase(bookmarksStorage) } val deleteBookmark by lazy { DeleteBookmarkUseCase(bookmarksStorage) } val deleteMultipleBookmarks by lazy { DeleteMultipleBookmarkUseCase(bookmarksStorage) } val searchBookmarks by lazy { SearchBookmarksUseCase(bookmarksStorage) } + val addFolder by lazy { AddBookmarkFolderUseCase(bookmarksStorage) } + val editBookmark by lazy { EditBookmarkFolderUseCase(bookmarksStorage) } } diff --git a/app/src/main/java/org/mozilla/reference/browser/library/history/ui/HistoryView.kt b/app/src/main/java/org/mozilla/reference/browser/library/history/ui/HistoryView.kt index 21d7034..18b1495 100644 --- a/app/src/main/java/org/mozilla/reference/browser/library/history/ui/HistoryView.kt +++ b/app/src/main/java/org/mozilla/reference/browser/library/history/ui/HistoryView.kt @@ -80,6 +80,14 @@ class HistoryView( view.history_search_list.visibility = View.VISIBLE historySearchAdapter.setData(historyViewModel.searchHistory(query)) } + + override fun confirm() { + // no-op + } + + override fun add() { + // no-op + } }) update(ViewMode.Normal, emptySet()) } diff --git a/app/src/main/java/org/mozilla/reference/browser/storage/HistoryDatabase.kt b/app/src/main/java/org/mozilla/reference/browser/storage/HistoryDatabase.kt index 46189a9..da720bd 100644 --- a/app/src/main/java/org/mozilla/reference/browser/storage/HistoryDatabase.kt +++ b/app/src/main/java/org/mozilla/reference/browser/storage/HistoryDatabase.kt @@ -650,7 +650,22 @@ class HistoryDatabase(context: Context) : } override suspend fun updateNode(guid: String, info: BookmarkInfo) { - // to-do + val db = dbHandler.database ?: return + val bookmarkValues = ContentValues().apply { + put(BookmarksTable.TITLE, info.title) + info.url?.let { + put(BookmarksTable.URL, it) + } + put(BookmarksTable.PARENT_ID, info.parentGuid) + } + db.beginTransaction( + transaction = { + db.update(BookmarksTable.TABLE_NAME, bookmarkValues, "${BookmarksTable.ID} = ?", arrayOf(guid)) + }, + catchBlock = { + Log.e("HistoryDatabase", "Error updating BookmarkNode with guid: $guid") + } + ) } /** @@ -818,6 +833,8 @@ class HistoryDatabase(context: Context) : db.update(UrlsTable.TABLE_NAME, domainValues, UrlsTable.ID + " = ?", arrayOf(id.toString())) } + + const val bookmarksRootFolder = 0.toString() } } diff --git a/app/src/main/java/org/mozilla/reference/browser/utils/ClearableEditText.kt b/app/src/main/java/org/mozilla/reference/browser/utils/ClearableEditText.kt new file mode 100644 index 0000000..2b42835 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/utils/ClearableEditText.kt @@ -0,0 +1,74 @@ +/* + * 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.reference.browser.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_UP +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat.SRC_IN +import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds +import org.mozilla.reference.browser.R + +/** + * An [AppCompatEditText] that shows a clear button to the user. + */ +class ClearableEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.editTextStyle +) : AppCompatEditText(context, attrs, defStyleAttr) { + + /** + * Clears the text when the clear icon is touched. + * + * Since the icon is just a compound drawable, we check the tap location + * to see if the X position of the tap is where the drawable is located. + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + if (shouldShowClearButton(length()) && event.action == ACTION_UP && event.endDrawableTouched()) { + setText("") + return true + } + return super.onTouchEvent(event) + } + + /** + * Displays a clear icon if text has been entered. + */ + override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) { + val drawable = if (shouldShowClearButton(lengthAfter)) { + AppCompatResources.getDrawable(context, R.drawable.mozac_ic_close)?.apply { + colorFilter = createBlendModeColorFilterCompat(R.color.dark_grey_5, SRC_IN) + } + } else { + null + } + putCompoundDrawablesRelativeWithIntrinsicBounds(end = drawable) + } + + /** + * Checks if the clear button should be displayed. + * + * The button should be displayed if the user has entered valid text. + * @param length Length of the text the user has entered. + */ + private fun shouldShowClearButton(length: Int) = + length > 0 && error == null + + /** + * Returns true if the location of the [MotionEvent] is on top of the end drawable. + */ + private fun MotionEvent.endDrawableTouched() = + (layoutDirection == LAYOUT_DIRECTION_LTR && rawX >= (right - compoundPaddingRight)) || + (layoutDirection == LAYOUT_DIRECTION_RTL && rawX <= (left + compoundPaddingLeft)) +} diff --git a/app/src/main/res/drawable/ic_add_folder.xml b/app/src/main/res/drawable/ic_add_folder.xml new file mode 100644 index 0000000..e543b96 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_folder.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_folder_icon.xml b/app/src/main/res/drawable/ic_folder_icon.xml new file mode 100644 index 0000000..30fb52d --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_icon.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_next_level_icon.xml b/app/src/main/res/drawable/ic_next_level_icon.xml new file mode 100644 index 0000000..3dbb0b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_next_level_icon.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_edit_bookmark.xml b/app/src/main/res/layout/fragment_edit_bookmark.xml new file mode 100644 index 0000000..25cdd91 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_bookmark.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_select_bookmark_folder.xml b/app/src/main/res/layout/fragment_select_bookmark_folder.xml new file mode 100644 index 0000000..f0f20df --- /dev/null +++ b/app/src/main/res/layout/fragment_select_bookmark_folder.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/library_toolbar.xml b/app/src/main/res/layout/library_toolbar.xml index f9754bb..c8acf12 100644 --- a/app/src/main/res/layout/library_toolbar.xml +++ b/app/src/main/res/layout/library_toolbar.xml @@ -17,7 +17,7 @@ android:id="@+id/inner_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" - tools:visibility="gone"> + tools:visibility="visible"> + android:layout_height="match_parent" + tools:visibility="gone"> - \ No newline at end of file + + + diff --git a/app/src/main/res/menu/bookmarks_add_folder.xml b/app/src/main/res/menu/bookmarks_add_folder.xml new file mode 100644 index 0000000..673430f --- /dev/null +++ b/app/src/main/res/menu/bookmarks_add_folder.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 3f4545f..acaef62 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -97,6 +97,10 @@ android:id="@+id/action_browserFragment_to_bookmarkFragment" app:destination="@id/bookmarkFragment" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/database_v10.xml b/app/src/main/res/values/database_v10.xml index fdc762f..2cba350 100644 --- a/app/src/main/res/values/database_v10.xml +++ b/app/src/main/res/values/database_v10.xml @@ -10,5 +10,5 @@ SELECT * FROM bookmarks SELECT * FROM bookmarks WHERE type=\'bookmark\' AND url=? LIMIT 1 SELECT * FROM bookmarks WHERE type=\'bookmark\' AND (url LIKE ? OR title LIKE ?) - INSERT INTO bookmarks (url, title , parent_id, type) SELECT urls.url, urls.title, 0, \'bookmark\' FROM urls WHERE favorite=1 + INSERT INTO bookmarks (url, title , parent_id, type) SELECT urls.url, urls.title, 0, \'bookmark\' FROM urls WHERE favorite=1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58b00e0..f2801c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,16 @@ Open in New Tab Open in Private Tab Delete + Bookmarks + Add Folder + Edit Folder + Edit Bookmark + Title (Required) + Url + Choose Parent Folder + Must have a title + Add Folder + Choose parent folder Search or type web address diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 86f00cb..f79c6b3 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -54,7 +54,6 @@ object Deps { const val testing_junit = "junit:junit:${Versions.junit}" const val testing_mockk = "io.mockk:mockk:${Versions.mockk}" - const val androidx_test_junit = "androidx.test.ext:junit:${Versions.AndroidX.test_ext}" const val mozilla_concept_engine = "org.mozilla.components:concept-engine:${Versions.mozilla_android_components}" const val mozilla_concept_tabstray = "org.mozilla.components:concept-tabstray:${Versions.mozilla_android_components}" @@ -129,6 +128,7 @@ object Deps { const val androidx_paging = "androidx.paging:paging-runtime-ktx:${Versions.AndroidX.paging}" const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.navigation}" const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.AndroidX.navigation}" + const val androidx_core = "androidx.core:core:${Versions.AndroidX.core}" const val androidx_core_ktx = "androidx.core:core-ktx:${Versions.AndroidX.core}" const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.AndroidX.constraintlayout}" const val androidx_preference_ktx = "androidx.preference:preference-ktx:${Versions.AndroidX.preference}" @@ -148,7 +148,7 @@ object Deps { const val espresso_web = "androidx.test.espresso:espresso-web:${Versions.AndroidX.espresso}" const val mockwebserver = "com.squareup.okhttp3:mockwebserver:${Versions.mockwebserver}" const val orchestrator = "androidx.test:orchestrator:${Versions.orchestrator}" - const val tools_test_ext_junit = "androidx.test.ext:junit:${Versions.AndroidX.core}" + const val tools_test_ext_junit = "androidx.test.ext:junit:${Versions.AndroidX.test_ext}" const val tools_test_rules = "androidx.test:rules:${Versions.tools_test_rules}" const val tools_test_runner = "androidx.test:runner:${Versions.tools_test_runner}" const val uiautomator = "androidx.test.uiautomator:uiautomator:${Versions.uiautomator}" diff --git a/freshtab/build.gradle b/freshtab/build.gradle index 6926abb..7816721 100644 --- a/freshtab/build.gradle +++ b/freshtab/build.gradle @@ -52,7 +52,7 @@ dependencies { testImplementation Deps.testing_junit testImplementation Deps.testing_mockk testImplementation Deps.kotlin_coroutines_test - testImplementation Deps.androidx_test_junit + testImplementation Deps.tools_test_ext_junit androidTestImplementation Deps.tools_test_runner androidTestImplementation Deps.espresso_core, {