Changing Add to/Change in library dialog to a bottom sheet

I just don't get tired of them.

This also has a button to add a new category

Also updating the snackbar when adding to library to show a "change" button to update the categories. In case you have a default category set
This commit is contained in:
Jays2Kings 2021-04-05 21:07:59 -04:00
parent 6d6766a86a
commit 0e62516777
18 changed files with 668 additions and 367 deletions

View File

@ -78,19 +78,19 @@ interface Category : Serializable {
}
companion object {
private const val DRAG_AND_DROP = 'D'
private const val ALPHA_ASC = 'a'
private const val ALPHA_DSC = 'b'
private const val UPDATED_ASC = 'c'
private const val UPDATED_DSC = 'd'
private const val UNREAD_ASC = 'e'
private const val UNREAD_DSC = 'f'
private const val LAST_READ_ASC = 'g'
private const val LAST_READ_DSC = 'h'
private const val TOTAL_ASC = 'i'
private const val TOTAL_DSC = 'j'
private const val DATE_ADDED_ASC = 'k'
private const val DATE_ADDED_DSC = 'l'
const val DRAG_AND_DROP = 'D'
const val ALPHA_ASC = 'a'
const val ALPHA_DSC = 'b'
const val UPDATED_ASC = 'c'
const val UPDATED_DSC = 'd'
const val UNREAD_ASC = 'e'
const val UNREAD_DSC = 'f'
const val LAST_READ_ASC = 'g'
const val LAST_READ_DSC = 'h'
const val TOTAL_ASC = 'i'
const val TOTAL_DSC = 'j'
const val DATE_ADDED_ASC = 'k'
const val DATE_ADDED_DSC = 'l'
fun create(name: String): Category = CategoryImpl().apply {
this.name = name

View File

@ -72,10 +72,10 @@ class CategoryPresenter(
val cat = Category.create(name)
// Set the new item in the last position.
cat.order = categories.map { it.order + 1 }.max() ?: 0
cat.order = categories.maxOf { it.order } + 1
// Insert into database.
cat.mangaSort = 'a'
cat.mangaSort = Category.ALPHA_ASC
db.insertCategory(cat).executeAsBlocking()
val cats = db.getCategories().executeAsBlocking()
val newCat = cats.find { it.name == name } ?: return false

View File

@ -1,9 +1,13 @@
package eu.kanade.tachiyomi.ui.category
import android.app.Activity
import android.app.Dialog
import android.os.Bundle
import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onShow
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import com.tfcporciuncula.flow.Preference
@ -15,8 +19,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.databinding.MangaCategoryDialogBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.util.view.visibleIf
@ -26,12 +28,12 @@ import uy.kohesive.injekt.injectLazy
class ManageCategoryDialog(bundle: Bundle? = null) :
DialogController(bundle) {
constructor(libraryController: LibraryController, category: Category) : this() {
this.libraryController = libraryController
constructor(category: Category?, updateLibrary: ((Int?) -> Unit)) : this() {
this.updateLibrary = updateLibrary
this.category = category
}
private var libraryController: LibraryController? = null
private var updateLibrary: ((Int?) -> Unit)? = null
private var category: Category? = null
private val preferences by injectLazy<PreferencesHelper>()
@ -39,28 +41,59 @@ class ManageCategoryDialog(bundle: Bundle? = null) :
lateinit var binding: MangaCategoryDialogBinding
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dialog = MaterialDialog(activity!!).apply {
title(R.string.manage_category)
customView(viewRes = R.layout.manga_category_dialog)
negativeButton(android.R.string.cancel)
positiveButton(R.string.save) { onPositiveButtonClick() }
}
val dialog = dialog(activity!!)
binding = MangaCategoryDialogBinding.bind(dialog.getCustomView())
onViewCreated()
return dialog
}
private fun onPositiveButtonClick() {
val category = category ?: return
if (category.id ?: 0 <= 0) return
fun dialog(activity: Activity): MaterialDialog {
return MaterialDialog(activity).apply {
title(if (category == null) R.string.new_category else R.string.manage_category)
customView(viewRes = R.layout.manga_category_dialog)
negativeButton(android.R.string.cancel) { dismiss() }
positiveButton(R.string.save) {
if (onPositiveButtonClick()) {
dismiss()
}
}
noAutoDismiss()
}
}
fun show(activity: Activity) {
val dialog = dialog(activity)
binding = MangaCategoryDialogBinding.bind(dialog.getCustomView())
onViewCreated()
dialog.onShow {
binding.title.requestFocus()
}
dialog.show()
}
private fun onPositiveButtonClick(): Boolean {
if (category?.id ?: 0 <= 0 && category != null) return false
val text = binding.title.text.toString()
val categoryExists = categoryExists(text)
if (text.isNotBlank() && !categoryExists && !text.equals(category.name, true)) {
val category = this.category ?: Category.create(text)
if (text.isNotBlank() && !categoryExists && !text.equals(this.category?.name ?: "", true)) {
category.name = text
db.insertCategory(category).executeAsBlocking()
libraryController?.presenter?.getLibrary()
if (this.category == null) {
val categories = db.getCategories().executeAsBlocking()
category.order = categories.maxOf { it.order } + 1
category.mangaSort = Category.ALPHA_ASC
val dbCategory = db.insertCategory(category).executeAsBlocking()
category.id = dbCategory.insertedId()?.toInt()
this.category = category
} else {
db.insertCategory(category).executeAsBlocking()
}
} else if (categoryExists) {
activity?.toast(R.string.category_with_name_exists)
binding.categoryTextLayout.error = binding.categoryTextLayout.context.getString(R.string.category_with_name_exists)
return false
} else {
binding.categoryTextLayout.error = binding.categoryTextLayout.context.getString(R.string.category_cannot_be_blank)
return false
}
if (!updatePref(preferences.downloadNewCategories(), binding.downloadNew)) {
preferences.downloadNew().set(false)
@ -73,6 +106,8 @@ class ManageCategoryDialog(bundle: Bundle? = null) :
preferences.libraryUpdateInterval().set(0)
LibraryUpdateJob.setupTask(0)
}
updateLibrary?.invoke(category.id)
return true
}
/**
@ -85,19 +120,22 @@ class ManageCategoryDialog(bundle: Bundle? = null) :
}
fun onViewCreated() {
val category = category ?: return
if (category.id ?: 0 <= 0) {
if (category?.id ?: 0 <= 0 && category != null) {
binding.title.gone()
binding.downloadNew.gone()
binding.includeGlobal.gone()
return
}
binding.editCategories.isVisible = category != null
binding.editCategories.setOnClickListener {
router.popCurrentController()
router.pushController(CategoryController().withFadeTransaction())
}
binding.title.hint = category.name
binding.title.append(category.name)
binding.title.addTextChangedListener {
binding.categoryTextLayout.error = null
}
binding.title.hint = category?.name ?: binding.editCategories.context.getString(R.string.category)
binding.title.append(category?.name ?: "")
val downloadNew = preferences.downloadNew().get()
setCheckbox(
binding.downloadNew,

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.ui.category.addtolibrary
import android.view.View
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.databinding.AddCategoryItemBinding
class AddCategoryItem(val category: Category) : AbstractItem<FastAdapter.ViewHolder<AddCategoryItem>>() {
/** defines the type defining this item. must be unique. preferably an id */
override val type: Int = R.id.category_checkbox
/** defines the layout which will be used for this item in the list */
override val layoutRes: Int = R.layout.add_category_item
override var identifier = category.id?.toLong() ?: -1L
override fun getViewHolder(v: View): FastAdapter.ViewHolder<AddCategoryItem> {
return ViewHolder(v)
}
class ViewHolder(view: View) : FastAdapter.ViewHolder<AddCategoryItem>(view) {
val binding = AddCategoryItemBinding.bind(view)
override fun bindView(item: AddCategoryItem, payloads: List<Any>) {
binding.categoryCheckbox.text = item.category.name
binding.categoryCheckbox.isChecked = item.isSelected
}
override fun unbindView(item: AddCategoryItem) {
binding.categoryCheckbox.text = null
binding.categoryCheckbox.isChecked = false
}
}
}

View File

@ -0,0 +1,215 @@
package eu.kanade.tachiyomi.ui.category.addtolibrary
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.ISelectionListener
import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.mikepenz.fastadapter.select.SelectExtension
import com.mikepenz.fastadapter.select.getSelectExtension
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.databinding.SetCategoriesSheetBinding
import eu.kanade.tachiyomi.ui.category.ManageCategoryDialog
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.expand
import eu.kanade.tachiyomi.util.view.setEdgeToEdge
import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
import java.util.Locale
import kotlin.math.max
class SetCategoriesSheet(
private val activity: Activity,
private val manga: Manga,
var categories: MutableList<Category>,
var preselected: Array<Int>,
val addingToLibrary: Boolean,
val onMangaAdded: (() -> Unit) = { }
) : BottomSheetDialog
(activity, R.style.BottomSheetDialogTheme) {
private var sheetBehavior: BottomSheetBehavior<*>
private val fastAdapter: FastAdapter<AddCategoryItem>
private val itemAdapter = ItemAdapter<AddCategoryItem>()
private val selectExtension: SelectExtension<AddCategoryItem>
private val db: DatabaseHelper by injectLazy()
private val binding = SetCategoriesSheetBinding.inflate(activity.layoutInflater)
init {
// Use activity theme for this layout
setContentView(binding.root)
sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup)
setEdgeToEdge(activity, binding.root)
binding.toolbarTitle.text = context.getString(
if (addingToLibrary) {
R.string.add_x_to
} else {
R.string.move_x_to
},
manga.mangaType(context)
)
setOnShowListener {
updateBottomButtons()
}
sheetBehavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
updateBottomButtons()
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
updateBottomButtons()
}
}
)
binding.categoryRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
sheetBehavior.isDraggable = !recyclerView.canScrollVertically(-1)
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (recyclerView.canScrollVertically(-1)) {
sheetBehavior.isDraggable = false
}
}
})
binding.titleLayout.viewTreeObserver.addOnGlobalLayoutListener {
binding.categoryRecyclerView.updateLayoutParams<ConstraintLayout.LayoutParams> {
val fullHeight = activity.window.decorView.height
val insets = activity.window.decorView.rootWindowInsets
matchConstraintMaxHeight =
fullHeight - (insets?.systemWindowInsetTop ?: 0) -
binding.titleLayout.height - binding.buttonLayout.height - 75.dpToPx
}
}
fastAdapter = FastAdapter.with(itemAdapter)
fastAdapter.setHasStableIds(true)
binding.categoryRecyclerView.layoutManager = LinearLayoutManager(context)
binding.categoryRecyclerView.adapter = fastAdapter
itemAdapter.set(categories.map(::AddCategoryItem))
itemAdapter.adapterItems.forEach { item ->
item.isSelected = preselected.any { it == item.category.id }
}
selectExtension = fastAdapter.getSelectExtension()
selectExtension.apply {
isSelectable = true
multiSelect = true
setCategoriesButtons()
selectionListener = object : ISelectionListener<AddCategoryItem> {
override fun onSelectionChanged(item: AddCategoryItem, selected: Boolean) {
setCategoriesButtons()
}
}
}
}
fun setCategoriesButtons() {
binding.addToCategoriesButton.text = context.getString(
if (addingToLibrary) {
R.string.add_to_
} else {
R.string.move_to_
},
when (selectExtension.selections.size) {
0 -> context.getString(R.string.default_category).lowercase(Locale.ROOT)
1 -> selectExtension.selectedItems.firstOrNull()?.category?.name ?: ""
else -> context.resources.getQuantityString(
R.plurals.category_plural,
selectExtension.selections.size,
selectExtension.selections.size
)
}
)
binding.categoryRecyclerView.scrollToPosition(
max(0, itemAdapter.adapterItems.indexOf(selectExtension.selectedItems.firstOrNull()))
)
}
override fun onStart() {
super.onStart()
sheetBehavior.expand()
sheetBehavior.skipCollapsed = true
updateBottomButtons()
}
fun updateBottomButtons() {
val bottomSheet = binding.root.parent as View
val bottomSheetVisibleHeight = -bottomSheet.top + (activity.window.decorView.height - bottomSheet.height)
binding.buttonLayout.translationY = bottomSheetVisibleHeight.toFloat()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
val array = context.obtainStyledAttributes(attrsArray)
val headerHeight = array.getDimensionPixelSize(0, 0)
binding.buttonLayout.updatePaddingRelative(
bottom = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom
)
binding.buttonLayout.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = headerHeight + binding.buttonLayout.paddingBottom
}
array.recycle()
binding.cancelButton.setOnClickListener { dismiss() }
binding.newCategoryButton.setOnClickListener {
ManageCategoryDialog(null) {
categories = db.getCategories().executeAsBlocking()
itemAdapter.set(categories.map(::AddCategoryItem))
itemAdapter.adapterItems.forEach { item ->
item.isSelected = it == item.category.id
}
setCategoriesButtons()
}.show(activity)
}
binding.addToCategoriesButton.setOnClickListener {
addMangaToCategories()
dismiss()
}
}
private fun addMangaToCategories() {
if (!manga.favorite) {
manga.favorite = !manga.favorite
manga.date_added = Date().time
db.insertManga(manga).executeAsBlocking()
}
val selectedCategories = selectExtension.selectedItems.map(AddCategoryItem::category)
val mc = selectedCategories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
onMangaAdded()
}
}

View File

@ -1,66 +0,0 @@
package eu.kanade.tachiyomi.ui.library
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onCancel
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.DialogController
/**
* This class is used when adding new manga to your library
*/
class AddToLibraryCategoriesDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : AddToLibraryCategoriesDialog.Listener {
private var manga: Manga? = null
private var categories = emptyList<Category>()
private var preselected = emptyArray<Int>()
private var position = 0
constructor(
target: T,
manga: Manga,
categories: List<Category>,
preselected: Array<Int>,
position: Int = 0
) : this() {
this.manga = manga
this.categories = categories
this.preselected = preselected
this.position = position
targetController = target
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!).title(R.string.add_to_library).message(R.string.add_to_categories)
.listItemsMultiChoice(
items = categories.map { it.name },
initialSelection = preselected.toIntArray(),
allowEmptySelection = true
) { _, selections, _ ->
val newCategories = selections.map { categories[it] }
(targetController as? Listener)?.updateCategoriesForManga(manga, newCategories)
}
.positiveButton(android.R.string.ok)
.negativeButton(android.R.string.cancel) {
(targetController as? Listener)?.addToLibraryCancelled(manga, position)
}
.onCancel {
(targetController as? Listener)?.addToLibraryCancelled(manga, position)
}
}
interface Listener {
fun updateCategoriesForManga(manga: Manga?, categories: List<Category>)
fun addToLibraryCancelled(manga: Manga?, position: Int)
}
}

View File

@ -1332,7 +1332,9 @@ class LibraryController(
override fun manageCategory(position: Int) {
val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return
if (!category.isDynamic) {
ManageCategoryDialog(this, category).showDialog(router)
ManageCategoryDialog(category) {
presenter.getLibrary()
}.showDialog(router)
}
}

View File

@ -68,8 +68,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.library.AddToLibraryCategoriesDialog
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity
@ -84,6 +82,8 @@ import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.source.BrowseController
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.addOrRemoveToFavorites
import eu.kanade.tachiyomi.util.moveCategories
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.ThemeUtil
import eu.kanade.tachiyomi.util.system.dpToPx
@ -116,9 +116,7 @@ class MangaDetailsController :
FlexibleAdapter.OnItemLongClickListener,
ActionMode.Callback,
MangaDetailsAdapter.MangaDetailsInterface,
FlexibleAdapter.OnItemMoveListener,
ChangeMangaCategoriesDialog.Listener,
AddToLibraryCategoriesDialog.Listener {
FlexibleAdapter.OnItemMoveListener {
constructor(
manga: Manga?,
@ -1092,7 +1090,7 @@ class MangaDetailsController :
popupView.setOnTouchListener(popup.dragToOpenListener)
}
fun makeFavPopup(popupView: View, manga: Manga, categories: List<Category>): PopupMenu {
private fun makeFavPopup(popupView: View, manga: Manga, categories: List<Category>): PopupMenu {
val popup = PopupMenu(view!!.context, popupView)
popup.menu.add(0, 1, 0, R.string.remove_from_library)
if (categories.isNotEmpty()) {
@ -1102,18 +1100,9 @@ class MangaDetailsController :
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem ->
if (menuItem.itemId == 0) {
val ids = presenter.getMangaCategoryIds()
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(
this,
listOf(manga),
categories,
preselected
).showDialog(
router
)
presenter.manga.moveCategories(presenter.db, activity!!) {
updateHeader()
}
} else {
toggleMangaFavorite()
}
@ -1123,31 +1112,24 @@ class MangaDetailsController :
}
private fun toggleMangaFavorite() {
if (presenter.toggleFavorite()) {
val categories = presenter.getCategories()
val defaultCategoryId = presenter.preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
when {
defaultCategory != null -> presenter.moveMangaToCategory(defaultCategory)
defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
presenter.moveMangaToCategory(null)
else -> {
val ids = presenter.getMangaCategoryIds()
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
AddToLibraryCategoriesDialog(
this,
presenter.manga,
categories,
preselected
).showDialog(router)
}
}
showAddedSnack()
} else {
showRemovedSnack()
val view = view ?: return
val activity = activity ?: return
snack?.dismiss()
snack = presenter.manga.addOrRemoveToFavorites(
presenter.db,
presenter.preferences,
view,
activity,
onMangaAdded = {
updateHeader()
showAddedSnack()
},
onMangaMoved = { updateHeader() },
onMangaDeleted = { presenter.confirmDeletion() }
)
if (snack?.duration == Snackbar.LENGTH_INDEFINITE) {
val favButton = getHeader()?.binding?.favoriteButton
(activity as? MainActivity)?.setUndoSnackBar(snack, favButton)
}
}
@ -1157,43 +1139,8 @@ class MangaDetailsController :
snack = view.snack(view.context.getString(R.string.added_to_library))
}
private fun showRemovedSnack() {
val view = view ?: return
snack?.dismiss()
snack = view.snack(
view.context.getString(R.string.removed_from_library),
Snackbar.LENGTH_INDEFINITE
) {
setAction(R.string.undo) {
presenter.setFavorite(true)
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (!presenter.manga.favorite) presenter.confirmDeletion()
}
}
)
}
val favButton = getHeader()?.binding?.favoriteButton
(activity as? MainActivity)?.setUndoSnackBar(snack, favButton)
}
override fun mangaPresenter(): MangaDetailsPresenter = presenter
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.moveMangaToCategories(categories)
}
override fun updateCategoriesForManga(manga: Manga?, categories: List<Category>) {
manga?.let { presenter.moveMangaToCategories(categories) }
}
override fun addToLibraryCancelled(manga: Manga?, position: Int) {
manga?.let { presenter.toggleFavorite() }
}
/**
* Copies a string to clipboard
*

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager
@ -61,7 +60,7 @@ class MangaDetailsPresenter(
val source: Source,
val preferences: PreferencesHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val chapterFilter: ChapterFilter = Injekt.get()
) : DownloadQueue.DownloadListener, LibraryServiceListener {
@ -334,7 +333,6 @@ class MangaDetailsPresenter(
}
val networkManga = nManga.await()
val mangaWasInitalized = manga.initialized
if (networkManga != null) {
manga.copyFrom(networkManga)
manga.initialized = true
@ -405,7 +403,7 @@ class MangaDetailsPresenter(
} catch (e: Exception) {
withContext(Dispatchers.Main) { controller.showError(trimException(e)) }
return@launch
} ?: listOf()
}
isLoading = false
try {
syncChaptersWithSource(db, chapters, manga, source)
@ -551,38 +549,6 @@ class MangaDetailsPresenter(
return db.getCategories().executeAsBlocking()
}
/**
* Move the given manga to the category.
*
* @param manga the manga to move.
* @param category the selected category, or null for default category.
*/
fun moveMangaToCategory(category: Category?) {
moveMangaToCategories(listOfNotNull(category))
}
/**
* Move the given manga to categories.
*
* @param manga the manga to move.
* @param categories the selected categories.
*/
fun moveMangaToCategories(categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(): Array<Int> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
fun confirmDeletion() {
coverCache.deleteFromCache(manga)
db.resetMangaInfo(manga).executeAsBlocking()

View File

@ -11,13 +11,11 @@ import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
@ -28,11 +26,11 @@ import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.library.AddToLibraryCategoriesDialog
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.source.BrowseController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.addOrRemoveToFavorites
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -60,8 +58,7 @@ open class BrowseSourceController(bundle: Bundle) :
NucleusController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
AddToLibraryCategoriesDialog.Listener {
FlexibleAdapter.EndlessScrollListener {
constructor(
source: CatalogueSource,
@ -575,73 +572,23 @@ open class BrowseSourceController(bundle: Bundle) :
*/
override fun onItemLongClick(position: Int) {
val manga = (adapter?.getItem(position) as? BrowseSourceItem?)?.manga ?: return
val view = view ?: return
val activity = activity ?: return
snack?.dismiss()
if (manga.favorite) {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
snack = binding.sourceLayout.snack(R.string.removed_from_library, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.undo) {
if (!manga.favorite) addManga(manga, position)
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (!manga.favorite) presenter.confirmDeletion(manga)
}
}
)
}
snack = manga.addOrRemoveToFavorites(
presenter.db,
preferences,
view,
activity,
onMangaAdded = {
adapter?.notifyItemChanged(position)
snack = view.snack(R.string.added_to_library)
},
onMangaMoved = { adapter?.notifyItemChanged(position) },
onMangaDeleted = { presenter.confirmDeletion(manga) }
)
if (snack?.duration == Snackbar.LENGTH_INDEFINITE) {
(activity as? MainActivity)?.setUndoSnackBar(snack)
} else {
addManga(manga, position)
snack = binding.sourceLayout.snack(R.string.added_to_library)
}
}
private fun addManga(manga: Manga, position: Int) {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
when {
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
presenter.moveMangaToCategory(manga, null)
else -> {
val ids = presenter.getMangaCategoryIds(manga)
if (ids.isNullOrEmpty()) {
presenter.moveMangaToCategory(manga, null)
}
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
AddToLibraryCategoriesDialog(this, manga, categories, preselected, position)
.showDialog(router)
}
}
}
/**
* Update manga to use selected categories.
*
* @param manga The manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForManga(manga: Manga?, categories: List<Category>) {
manga?.let { presenter.updateMangaCategories(manga, categories) }
}
/**
* Update manga to remove from favorites
*/
override fun addToLibraryCancelled(manga: Manga?, position: Int) {
manga?.let {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
}
}

View File

@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
@ -37,7 +36,6 @@ import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
/**
* Presenter of [BrowseSourceController].
@ -45,7 +43,7 @@ import java.util.Date
open class BrowseSourcePresenter(
sourceId: Long,
sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<BrowseSourceController>() {
@ -278,22 +276,6 @@ open class BrowseSourcePresenter(
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
when (manga.favorite) {
true -> manga.date_added = Date().time
false -> manga.date_added = 0
}
db.insertManga(manga).executeAsBlocking()
}
fun confirmDeletion(manga: Manga) {
coverCache.deleteFromCache(manga)
val downloadManager: DownloadManager = Injekt.get()
@ -365,56 +347,4 @@ open class BrowseSourcePresenter(
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (selectedCategories.isNotEmpty()) {
if (!manga.favorite) {
changeMangaFavorite(manga)
}
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
if (!manga.favorite) {
changeMangaFavorite(manga)
}
}
}
}

View File

@ -1,9 +1,18 @@
package eu.kanade.tachiyomi.util
import android.app.Activity
import android.view.View
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.category.addtolibrary.SetCategoriesSheet
import eu.kanade.tachiyomi.util.view.snack
import java.util.Date
fun Manga.isLocal() = source == LocalSource.ID
@ -25,3 +34,108 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty()
}
fun Manga.moveCategories(
db: DatabaseHelper,
activity: Activity,
onMangaMoved: () -> Unit
) {
val categories = db.getCategories().executeAsBlocking()
val categoriesForManga = db.getCategoriesForManga(this).executeAsBlocking()
val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray()
SetCategoriesSheet(
activity,
this,
categories.toMutableList(),
ids,
false
) {
onMangaMoved()
}.show()
}
fun Manga.addOrRemoveToFavorites(
db: DatabaseHelper,
preferences: PreferencesHelper,
view: View,
activity: Activity,
onMangaAdded: () -> Unit,
onMangaMoved: () -> Unit,
onMangaDeleted: () -> Unit
): Snackbar? {
if (!favorite) {
val categories = db.getCategories().executeAsBlocking()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
when {
defaultCategory != null -> {
favorite = true
date_added = Date().time
db.insertManga(this).executeAsBlocking()
val mc = MangaCategory.create(this, defaultCategory)
db.setMangaCategories(listOf(mc), listOf(this))
onMangaMoved()
return view.snack(activity.getString(R.string.added_to_, defaultCategory.name)) {
setAction(R.string.change) {
moveCategories(db, activity, onMangaMoved)
}
}
}
defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category
favorite = true
date_added = Date().time
db.insertManga(this).executeAsBlocking()
db.setMangaCategories(emptyList(), listOf(this))
onMangaMoved()
return if (categories.isNotEmpty()) {
view.snack(activity.getString(R.string.added_to_, activity.getString(R.string.default_value))) {
setAction(R.string.change) {
moveCategories(db, activity, onMangaMoved)
}
}
} else {
view.snack(R.string.added_to_library)
}
}
else -> {
val categoriesForManga = db.getCategoriesForManga(this).executeAsBlocking()
val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray()
SetCategoriesSheet(
activity,
this,
categories.toMutableList(),
ids,
true
) {
onMangaAdded()
}.show()
}
}
} else {
val lastAddedDate = date_added
favorite = false
date_added = 0
db.insertManga(this).executeAsBlocking()
onMangaMoved()
return view.snack(view.context.getString(R.string.removed_from_library), Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.undo) {
favorite = true
date_added = lastAddedDate
db.insertManga(this@addOrRemoveToFavorites).executeAsBlocking()
onMangaMoved()
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (!favorite) {
onMangaDeleted()
}
}
}
)
}
}
return null
}

View File

@ -0,0 +1,8 @@
<!-- drawable/plus.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</vector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.checkbox.MaterialCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/category_checkbox"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_marginStart="12dp"
android:paddingStart="12dp"
android:paddingEnd="0dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textSize="15sp"
tools:text="@string/category" />

View File

@ -50,7 +50,7 @@
android:layout_height="match_parent"
android:background="@null"
android:imeOptions="actionDone"
android:inputType="none"
android:inputType="textCapSentences"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/textColorPrimary"

View File

@ -2,17 +2,30 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<EditText
android:id="@+id/title"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/category_text_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:inputType="text"
android:maxLines="1"/>
android:layout_marginEnd="16dp"
android:hint="@string/title"
app:boxStrokeColor="@color/colorAccent"
app:endIconMode="clear_text"
app:hintEnabled="false"
app:hintTextColor="@color/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/category"
android:inputType="textCapSentences" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/download_new"

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/source_filter_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bottom_sheet_rounded_background"
app:layout_constraintVertical_chainStyle="packed"
android:backgroundTint="?android:attr/colorBackground">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/category_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/title_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/button_layout"
tools:listitem="@layout/add_category_item"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="top"
android:background="@drawable/bottom_sheet_rounded_background"
android:backgroundTint="?attr/colorSecondary"
android:elevation="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/category_recycler_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/toolbar_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/add_x_to"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
android:textColor="?actionBarTintColor"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/new_category_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Add manga to…"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/new_category_button"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
app:icon="@drawable/ic_plus_24dp"
app:iconTint="@color/colorAccent"
style="@style/Theme.Widget.Button.TextButton"
android:text="@string/new_category"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/toolbar_title"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:background="@color/divider"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_layout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="top"
android:background="@drawable/bottom_sheet_rounded_background"
android:backgroundTint="?attr/colorSecondary"
android:clickable="true"
android:elevation="0dp"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/category_recycler_view">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Theme.Widget.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/add_to_categories_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_to_categories_button"
style="@style/Theme.Widget.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginEnd="8dp"
android:text="@string/new_category"
app:iconTint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toTopOf="parent"
tools:text="Add to Default" />
<View
android:id="@+id/bottom_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/divider"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -21,6 +21,8 @@
<string name="author">Author</string>
<string name="artist">Artist</string>
<string name="description">Description</string>
<string name="move_x_to">Move %1$s to…</string>
<string name="add_x_to">Add %1$s to…</string>
<!-- Status -->
<string name="ongoing">Ongoing</string>
@ -69,6 +71,7 @@
<string name="adding_category_to_queue">Adding %1$s to update queue</string>
<string name="_already_in_queue">%1$s is already in queue</string>
<string name="create_new_category">Create new category</string>
<string name="new_category">New category</string>
<string name="category_is_empty">Category is empty</string>
<string name="category_is_hidden">Category is hidden</string>
<string name="top_category">Top category (%1$s)</string>
@ -86,12 +89,12 @@
<string name="manage_category">Manage category</string>
<string name="rename_category">Rename category</string>
<string name="move_to_categories">Move to categories</string>
<string name="add_to_categories">Choose which categories to add this to. If none are selected, this will be added to the "default" category</string>
<plurals name="category_plural">
<item quantity="one">%d category</item>
<item quantity="other">%d categories</item>
</plurals>
<string name="category_with_name_exists">A category with that name already exists!</string>
<string name="category_cannot_be_blank">Category name cannot be blank</string>
<string name="category_deleted">Category deleted</string>
<string name="long_press_category">Press and hold to edit a category</string>
<string name="jump_to_category">Jump to category</string>
@ -788,6 +791,8 @@
<!-- Miscellaneous -->
<string name="add">Add</string>
<string name="add_to_">Add to %1$s</string>
<string name="added_to_">Added to %1$s</string>
<string name="all">All</string>
<string name="alphabetically">Alphabetically</string>
<string name="always">Always</string>
@ -801,6 +806,7 @@
<string name="bug_report">Report a Bug</string>
<string name="cancel">Cancel</string>
<string name="center">Center</string>
<string name="change">Change</string>
<string name="charging">Charging</string>
<string name="clear">Clear</string>
<string name="close">Close</string>
@ -839,6 +845,7 @@
<string name="more">More</string>
<string name="move_to_bottom">Move to bottom</string>
<string name="move_to_top">Move to top</string>
<string name="move_to_">Move to %1$s</string>
<string name="moved_to_">Moved to %1$s</string>
<string name="never">Never</string>
<string name="newest">Newest</string>