[fenix] Closes https://github.com/mozilla-mobile/fenix/issues/1312, https://github.com/mozilla-mobile/fenix/issues/1236, https://github.com/mozilla-mobile/fenix/issues/1237, https://github.com/mozilla-mobile/fenix/issues/1238, https://github.com/mozilla-mobile/fenix/issues/1239: Creating, Editing, and Deleting Bookmarks and Bookmark Folders
parent
aec21e5cc2
commit
c7b4a69ee7
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.bookmarks
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.mozilla.fenix.mvi.Action
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.Change
|
||||
import org.mozilla.fenix.mvi.Reducer
|
||||
import org.mozilla.fenix.mvi.UIComponent
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
import org.mozilla.fenix.mvi.ViewState
|
||||
|
||||
class SignInComponent(
|
||||
private val container: ViewGroup,
|
||||
bus: ActionBusFactory,
|
||||
override var initialState: SignInState =
|
||||
SignInState(false)
|
||||
) : UIComponent<SignInState, SignInAction, SignInChange>(
|
||||
bus.getManagedEmitter(SignInAction::class.java),
|
||||
bus.getSafeManagedObservable(SignInChange::class.java)
|
||||
) {
|
||||
|
||||
override val reducer: Reducer<SignInState, SignInChange> = { state, change ->
|
||||
when (change) {
|
||||
SignInChange.SignedIn -> state.copy(signedIn = true)
|
||||
SignInChange.SignedOut -> state.copy(signedIn = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
|
||||
SignInUIView(container, actionEmitter, changesObservable)
|
||||
|
||||
init {
|
||||
render(reducer)
|
||||
}
|
||||
}
|
||||
|
||||
data class SignInState(val signedIn: Boolean) : ViewState
|
||||
|
||||
sealed class SignInAction : Action {
|
||||
object ClickedSignIn : SignInAction()
|
||||
}
|
||||
|
||||
sealed class SignInChange : Change {
|
||||
object SignedIn : SignInChange()
|
||||
object SignedOut : SignInChange()
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.bookmarks
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.functions.Consumer
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class SignInUIView(
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<SignInAction>,
|
||||
changesObservable: Observable<SignInChange>
|
||||
) : UIView<SignInState, SignInAction, SignInChange>(container, actionEmitter, changesObservable) {
|
||||
|
||||
override val view: Button = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_sign_in, container, true)
|
||||
.findViewById(R.id.bookmark_folders_sign_in)
|
||||
|
||||
init {
|
||||
view.setOnClickListener {
|
||||
actionEmitter.onNext(SignInAction.ClickedSignIn)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateView() = Consumer<SignInState> {
|
||||
view.visibility = if (it.signedIn) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.bookmarks.edit
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import com.uber.autodispose.AutoDispose
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.fragment_edit_bookmark.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.appservices.places.UrlParseFailed
|
||||
import mozilla.components.concept.storage.BookmarkInfo
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.getColorFromAttr
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class EditBookmarkFragment : Fragment(), CoroutineScope {
|
||||
|
||||
private lateinit var sharedViewModel: BookmarksSharedViewModel
|
||||
private lateinit var job: Job
|
||||
private lateinit var guidToEdit: String
|
||||
private var bookmarkNode: BookmarkNode? = null
|
||||
private var bookmarkParent: BookmarkNode? = null
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
job = Job()
|
||||
setHasOptionsMenu(true)
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
|
||||
}!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_edit_bookmark, container, false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.show()
|
||||
|
||||
guidToEdit = EditBookmarkFragmentArgs.fromBundle(arguments!!).guidToEdit
|
||||
launch(IO) {
|
||||
bookmarkNode = requireComponents.core.bookmarksStorage.getTree(guidToEdit)
|
||||
bookmarkParent = sharedViewModel.selectedFolder
|
||||
?: bookmarkNode?.parentGuid?.let { requireComponents.core.bookmarksStorage.getTree(it) }
|
||||
|
||||
launch(Main) {
|
||||
when (bookmarkNode?.type) {
|
||||
BookmarkNodeType.FOLDER -> {
|
||||
bookmark_url_edit.visibility = View.GONE
|
||||
bookmark_url_label.visibility = View.GONE
|
||||
}
|
||||
BookmarkNodeType.ITEM -> {}
|
||||
BookmarkNodeType.SEPARATOR -> {}
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
bookmark_name_edit.setText(bookmarkNode!!.title)
|
||||
bookmark_url_edit.setText(bookmarkNode!!.url)
|
||||
}
|
||||
|
||||
bookmarkParent?.let { node ->
|
||||
launch(Main) {
|
||||
bookmark_folder_selector.text = node.title
|
||||
bookmark_folder_selector.setOnClickListener {
|
||||
sharedViewModel.selectedFolder = null
|
||||
Navigation.findNavController(requireActivity(), R.id.container).navigate(
|
||||
EditBookmarkFragmentDirections
|
||||
.actionBookmarkEditFragmentToBookmarkSelectFolderFragment(null)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateBookmarkFromObservableInput()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
updateBookmarkNode(Pair(bookmark_name_edit.text, bookmark_url_edit.text))
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun updateBookmarkFromObservableInput() {
|
||||
Observable.combineLatest(
|
||||
bookmark_name_edit.textChanges().skipInitialValue(),
|
||||
bookmark_url_edit.textChanges().skipInitialValue(),
|
||||
BiFunction { name: CharSequence, url: CharSequence ->
|
||||
Pair(name, url)
|
||||
})
|
||||
.filter { it.first.isNotBlank() && it.second.isNotBlank() }
|
||||
.debounce(debouncePeriodInMs, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.`as`(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this@EditBookmarkFragment)))
|
||||
.subscribe {
|
||||
try {
|
||||
bookmark_url_edit.error = null
|
||||
updateBookmarkNode(it)
|
||||
} catch (e: UrlParseFailed) {
|
||||
bookmark_url_edit.error = getString(R.string.bookmark_invalid_url_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.bookmarks_edit, menu)
|
||||
menu.findItem(R.id.delete_bookmark_button).icon.colorFilter =
|
||||
PorterDuffColorFilter(R.attr.iconColor.getColorFromAttr(context!!), PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.delete_bookmark_button -> {
|
||||
launch(IO) {
|
||||
requireComponents.core.bookmarksStorage.deleteNode(guidToEdit)
|
||||
launch(Main) {
|
||||
Navigation.findNavController(requireActivity(), R.id.container).popBackStack()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBookmarkNode(pair: Pair<CharSequence, CharSequence>) {
|
||||
launch(IO) {
|
||||
requireComponents.core.bookmarksStorage.updateNode(
|
||||
guidToEdit,
|
||||
BookmarkInfo(
|
||||
sharedViewModel.selectedFolder?.guid ?: bookmarkNode!!.parentGuid,
|
||||
bookmarkNode!!.position,
|
||||
pair.first.toString(),
|
||||
if (bookmarkNode?.type == BookmarkNodeType.ITEM) pair.second.toString() else null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val debouncePeriodInMs = 500L
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.bookmarks.selectfolder
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.bookmark_row.*
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import mozilla.components.support.ktx.android.content.res.pxToDp
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||
|
||||
class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedViewModel) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var tree: List<BookmarkNodeWithDepth> = listOf()
|
||||
|
||||
fun updateData(tree: BookmarkNode?) {
|
||||
this.tree = tree!!.convertToFolderDepthTree().drop(1)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.bookmark_row, parent, false)
|
||||
|
||||
return when (viewType) {
|
||||
BookmarkFolderViewHolder.viewType -> SelectBookmarkFolderAdapter.BookmarkFolderViewHolder(
|
||||
view
|
||||
)
|
||||
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (tree[position].node.type) {
|
||||
BookmarkNodeType.FOLDER -> BookmarkFolderViewHolder.viewType
|
||||
else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = tree.size
|
||||
|
||||
@SuppressWarnings("ComplexMethod")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
|
||||
when (holder) {
|
||||
is SelectBookmarkFolderAdapter.BookmarkFolderViewHolder -> holder.bind(
|
||||
tree[position],
|
||||
tree[position].node == sharedViewModel.selectedFolder,
|
||||
object : SelectionInterface {
|
||||
override fun itemSelected(node: BookmarkNode) {
|
||||
sharedViewModel.selectedFolder = node
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
})
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectionInterface {
|
||||
fun itemSelected(node: BookmarkNode)
|
||||
}
|
||||
|
||||
class BookmarkFolderViewHolder(
|
||||
view: View,
|
||||
override val containerView: View? = view
|
||||
) :
|
||||
RecyclerView.ViewHolder(view), LayoutContainer {
|
||||
|
||||
init {
|
||||
bookmark_favicon.visibility = View.VISIBLE
|
||||
bookmark_title.visibility = View.VISIBLE
|
||||
bookmark_separator.visibility = View.GONE
|
||||
bookmark_layout.isClickable = true
|
||||
}
|
||||
|
||||
fun bind(folder: BookmarkNodeWithDepth, selected: Boolean, selectionInterface: SelectionInterface) {
|
||||
val backgroundTint =
|
||||
if (selected) R.color.bookmark_selection_appbar_background else R.color.bookmark_favicon_background
|
||||
val backgroundTintList = ContextCompat.getColorStateList(containerView!!.context, backgroundTint)
|
||||
bookmark_favicon.backgroundTintList = backgroundTintList
|
||||
val res = if (selected) R.drawable.mozac_ic_check else R.drawable.ic_folder_icon
|
||||
bookmark_favicon.setImageResource(res)
|
||||
bookmark_overflow.visibility = View.GONE
|
||||
bookmark_title?.text = folder.node.title
|
||||
bookmark_layout.setOnClickListener {
|
||||
selectionInterface.itemSelected(folder.node)
|
||||
}
|
||||
val padding =
|
||||
containerView.resources.pxToDp(dpsToIndent) * (if (folder.depth > maxDepth) maxDepth else folder.depth)
|
||||
bookmark_layout.setPadding(padding, 0, 0, 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val viewType = 1
|
||||
}
|
||||
}
|
||||
|
||||
data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?)
|
||||
|
||||
private fun BookmarkNode?.convertToFolderDepthTree(
|
||||
depth: Int = 0,
|
||||
list: List<BookmarkNodeWithDepth> = listOf()
|
||||
): List<BookmarkNodeWithDepth> {
|
||||
return if (this != null) {
|
||||
val newList = list.plus(listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid)))
|
||||
newList.plus(
|
||||
children?.filter { it?.type == BookmarkNodeType.FOLDER }
|
||||
?.flatMap { it.convertToFolderDepthTree(depth + 1) }
|
||||
?: listOf())
|
||||
} else listOf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val maxDepth = 10
|
||||
private const val dpsToIndent = 10
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.bookmarks.selectfolder
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.*
|
||||
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.getColorFromAttr
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||
import org.mozilla.fenix.library.bookmarks.SignInAction
|
||||
import org.mozilla.fenix.library.bookmarks.SignInChange
|
||||
import org.mozilla.fenix.library.bookmarks.SignInComponent
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class SelectBookmarkFolderFragment : Fragment(), CoroutineScope, AccountObserver {
|
||||
|
||||
private lateinit var sharedViewModel: BookmarksSharedViewModel
|
||||
private lateinit var job: Job
|
||||
private var folderGuid: String? = null
|
||||
private var bookmarkNode: BookmarkNode? = null
|
||||
|
||||
private lateinit var signInComponent: SignInComponent
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
job = Job()
|
||||
setHasOptionsMenu(true)
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
|
||||
}!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false)
|
||||
signInComponent = SignInComponent(view.select_bookmark_layout, ActionBusFactory.get(this))
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
getAutoDisposeObservable<SignInAction>()
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is SignInAction.ClickedSignIn -> {
|
||||
requireComponents.services.accountsAuthFeature.beginAuthentication()
|
||||
view?.let {
|
||||
(activity as HomeActivity).openToBrowser(null, BrowserDirection.FromBookmarksFolderSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(activity as AppCompatActivity).supportActionBar?.show()
|
||||
|
||||
folderGuid = SelectBookmarkFolderFragmentArgs.fromBundle(arguments!!).folderGuid ?: BookmarkRoot.Root.id
|
||||
checkIfSignedIn()
|
||||
|
||||
launch(IO) {
|
||||
bookmarkNode = requireComponents.core.bookmarksStorage.getTree(folderGuid!!, true)
|
||||
launch(Main) {
|
||||
(activity as HomeActivity).title = bookmarkNode?.title ?: getString(R.string.library_bookmarks)
|
||||
val adapter = SelectBookmarkFolderAdapter(sharedViewModel)
|
||||
recylerView_bookmark_folders.adapter = adapter
|
||||
adapter.updateData(bookmarkNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfSignedIn() {
|
||||
val accountManager = requireComponents.backgroundServices.accountManager
|
||||
accountManager.register(this, owner = this)
|
||||
accountManager.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
|
||||
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
val visitedAddBookmark = SelectBookmarkFolderFragmentArgs.fromBundle(arguments!!).visitedAddBookmark
|
||||
if (!visitedAddBookmark) {
|
||||
inflater.inflate(R.menu.bookmarks_select_folder, menu)
|
||||
menu.findItem(R.id.add_folder_button).icon.colorFilter =
|
||||
PorterDuffColorFilter(R.attr.iconColor.getColorFromAttr(context!!), PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.add_folder_button -> {
|
||||
launch(Main) {
|
||||
Navigation.findNavController(requireActivity(), R.id.container).navigate(
|
||||
SelectBookmarkFolderFragmentDirections
|
||||
.actionBookmarkSelectFolderFragmentToBookmarkAddFolderFragment()
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticated(account: OAuthAccount) {
|
||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
|
||||
}
|
||||
|
||||
override fun onError(error: Exception) {
|
||||
}
|
||||
|
||||
override fun onLoggedOut() {
|
||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
||||
}
|
||||
|
||||
override fun onProfileUpdated(profile: Profile) {
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<Button
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/bookmark_folders_sign_in"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sign_in_button"
|
||||
android:padding="10dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_add_folder_title_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/bookmark_name_label"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?attr/bookmarksLabelColor"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/bookmark_add_folder_title_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:inputType="textAutoComplete"
|
||||
android:textColor="?attr/bookmarksEditTextColor"
|
||||
android:textSize="15sp"
|
||||
tools:text="News" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_add_folder_parent_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bookmark_folder_label"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?attr/bookmarksLabelColor"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_add_folder_parent_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:drawableStart="@drawable/ic_folder_icon"
|
||||
android:drawablePadding="10dp"
|
||||
android:drawableTint="?attr/iconColor"
|
||||
android:textColor="?attr/bookmarksEditTextColor"
|
||||
android:textSize="16sp"
|
||||
tools:targetApi="m"
|
||||
tools:text="Mobile Bookmarks" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/bookmark_name_label"
|
||||
android:textColor="?attr/bookmarksLabelColor"
|
||||
android:textSize="12sp"
|
||||
android:textAllCaps="true"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/bookmark_name_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textSize="15sp"
|
||||
android:textColor="?attr/bookmarksEditTextColor"
|
||||
tools:text="Internet for people, not profit -- Mozilla"
|
||||
android:inputType="textAutoComplete"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_url_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bookmark_url_label"
|
||||
android:textColor="?attr/bookmarksLabelColor"
|
||||
android:textSize="12sp"
|
||||
android:textAllCaps="true"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/bookmark_url_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textSize="15sp"
|
||||
android:textColor="?attr/bookmarksEditTextColor"
|
||||
tools:text="https://www.mozilla.org/en-US/"
|
||||
android:inputType="textUri"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_folder_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bookmark_folder_label"
|
||||
android:textColor="?attr/bookmarksLabelColor"
|
||||
android:textSize="12sp"
|
||||
android:textAllCaps="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_folder_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/bookmarksEditTextColor"
|
||||
android:drawableStart="@drawable/ic_folder_icon"
|
||||
android:drawablePadding="10dp"
|
||||
android:drawableTint="?attr/iconColor"
|
||||
tools:text="Mobile Bookmarks"
|
||||
tools:targetApi="m" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/select_bookmark_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recylerView_bookmark_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
</LinearLayout>
|
Loading…
Reference in New Issue