Add custom categories for sources, with it you could group your scanlators or hentai sources in their own categories in the Sources tab in the new categories!

This commit is contained in:
Jobobby04 2020-06-16 15:28:57 -04:00
parent d367cefe1f
commit 5ca57226fe
15 changed files with 894 additions and 1 deletions

View File

@ -262,4 +262,8 @@ object PreferenceKeys {
const val latest_tab_position = "latest_tab_position"
const val latest_tab_language_code = "latest_tab_language_code"
const val sources_tab_categories = "sources_tab_categories"
const val sources_tab_source_categories = "sources_tab_source_categories"
}

View File

@ -362,4 +362,8 @@ class PreferencesHelper(val context: Context) {
fun latestTabInFront() = flowPrefs.getBoolean(Keys.latest_tab_position, false)
fun latestTabDisplayLanguageCode() = flowPrefs.getBoolean(Keys.latest_tab_language_code, false)
fun sourcesTabCategories() = flowPrefs.getStringSet(Keys.sources_tab_categories, mutableSetOf())
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
}

View File

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
import eu.kanade.tachiyomi.util.system.toast
import exh.ui.smartsearch.SmartSearchController
import kotlinx.android.parcel.Parcelize
@ -52,7 +53,8 @@ class SourceController(bundle: Bundle? = null) :
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnBrowseClickListener,
SourceAdapter.OnLatestClickListener {
SourceAdapter.OnLatestClickListener,
ChangeSourceCategoriesDialog.Listener {
private val preferences: PreferencesHelper = Injekt.get()
@ -179,6 +181,13 @@ class SourceController(bundle: Bundle? = null) :
)
}
items.add(
Pair(
activity.getString(R.string.label_categories),
{ addToCategories(item.source) }
)
)
MaterialDialog(activity)
.title(text = item.source.name)
.listItems(
@ -222,6 +231,54 @@ class SourceController(bundle: Bundle? = null) :
preferences.latestTabSources().set(current + source.id.toString())
}
}
private fun addToCategories(source: Source) {
val categories = preferences.sourcesTabCategories().get().toList().sortedBy { it.toLowerCase() }
if (categories.isEmpty()) {
applicationContext?.toast(R.string.no_source_categories)
return
}
val preferenceSources = preferences.sourcesTabSourcesInCategories().get().toMutableList()
val sources = preferenceSources.map { it.split("|")[0] }
if (source.id.toString() in sources) {
val preferenceSources = preferenceSources.map { it.split("|") }.filter { it[0] == source.id.toString() }.map { Pair(it[0], it[1]) }.toMutableList()
val preselected = preferenceSources.map { category ->
categories.indexOf(category.second)
}.toTypedArray()
ChangeSourceCategoriesDialog(this, source, categories, preselected)
.showDialog(router)
} else {
ChangeSourceCategoriesDialog(this, source, categories, emptyArray())
.showDialog(router)
}
}
override fun updateCategoriesForSource(source: Source, categories: List<String>) {
var preferenceSources = preferences.sourcesTabSourcesInCategories().get().toMutableList()
val sources = preferenceSources.map { it.split("|")[0] }
if (source.id.toString() in sources) {
preferenceSources = preferenceSources
.map { it.split("|") }
.filter { it[0] != source.id.toString() }
.map { it[0] + "|" + it[1] }.toMutableList()
}
categories.forEach {
preferenceSources.add(source.id.toString() + "|" + it)
}
preferences.sourcesTabSourcesInCategories().set(
preferenceSources.sorted().toSet()
)
presenter.updateSources()
}
/**
* Called when browse is clicked in [SourceAdapter]
*/

View File

@ -60,6 +60,19 @@ class SourcePresenter(
val pinnedSources = mutableListOf<SourceItem>()
val pinnedCatalogues = preferences.pinnedCatalogues().get()
val categories = mutableListOf<SourceCategory>()
preferences.sourcesTabCategories().get().sortedByDescending { it.toLowerCase() }.forEach {
categories.add(SourceCategory(it))
}
val sourcesAndCategoriesCombined = preferences.sourcesTabSourcesInCategories().get()
val sourcesAndCategories = if (sourcesAndCategoriesCombined.isNotEmpty()) sourcesAndCategoriesCombined.map {
val temp = it.split("|")
Pair(temp[0], temp[1])
} else null
val sourcesInCategories = sourcesAndCategories?.map { it.first }
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
@ -77,10 +90,32 @@ class SourcePresenter(
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), controllerMode == SourceController.Mode.CATALOGUE))
}
if (sourcesInCategories != null && source.id.toString() in sourcesInCategories) {
sourcesAndCategories
.filter { SourcesAndCategory -> SourcesAndCategory.first == source.id.toString() }
.forEach { SourceAndCategory ->
categories.forEach { dataClass ->
if (dataClass.category.trim() == SourceAndCategory.second.trim()) {
dataClass.sources.add(
SourceItem(
source,
LangItem("custom|" + SourceAndCategory.second),
controllerMode == SourceController.Mode.CATALOGUE
)
)
}
}
}
}
SourceItem(source, langItem, controllerMode == SourceController.Mode.CATALOGUE)
}
}
categories.forEach {
sourceItems = it.sources.sortedBy { sourceItem -> sourceItem.source.name.toLowerCase() } + sourceItems
}
if (pinnedSources.isNotEmpty()) {
sourceItems = pinnedSources + sourceItems
}
@ -134,3 +169,5 @@ class SourcePresenter(
const val LAST_USED_KEY = "last_used"
}
}
data class SourceCategory(val category: String, var sources: MutableList<SourceItem> = mutableListOf())

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
class ChangeSourceCategoriesDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : ChangeSourceCategoriesDialog.Listener {
private var source: Source? = null
private var categories = emptyList<String>()
private var preselected = emptyArray<Int>()
constructor(
target: T,
source: Source,
categories: List<String>,
preselected: Array<Int>
) : this() {
this.source = source
this.categories = categories
this.preselected = preselected
targetController = target
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.action_move_category)
.listItemsMultiChoice(
items = categories,
initialSelection = preselected.toIntArray(),
allowEmptySelection = true
) { _, selections, _ ->
val newCategories = selections.map { categories[it] }
(targetController as? Listener)?.updateCategoriesForSource(source!!, newCategories)
}
.positiveButton(android.R.string.ok)
.negativeButton(android.R.string.cancel)
}
interface Listener {
fun updateCategoriesForSource(source: Source, categories: List<String>)
}
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.ui.category.sources
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
* Custom adapter for categories.
*
* @param controller The containing controller.
*/
class SourceCategoryAdapter(controller: SourceCategoryController) :
FlexibleAdapter<SourceCategoryItem>(null, controller, true) {
/**
* Clears the active selections from the list and the model.
*/
override fun clearSelection() {
super.clearSelection()
(0 until itemCount).forEach { getItem(it)?.isSelected = false }
}
/**
* Toggles the selection of the given position.
*
* @param position The position to toggle.
*/
override fun toggleSelection(position: Int) {
super.toggleSelection(position)
getItem(position)?.isSelected = isSelected(position)
}
}

View File

@ -0,0 +1,330 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
/**
* Controller to manage the categories for the users' library.
*/
class SourceCategoryController :
NucleusController<CategoriesControllerBinding, SourceCategoryPresenter>(),
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
SourceCategoryCreateDialog.Listener,
SourceCategoryRenameDialog.Listener,
UndoHelper.OnActionListener {
/**
* Object used to show ActionMode toolbar.
*/
private var actionMode: ActionMode? = null
/**
* Adapter containing category items.
*/
private var adapter: SourceCategoryAdapter? = null
/**
* Undo helper used for restoring a deleted category.
*/
private var undoHelper: UndoHelper? = null
/**
* Creates the presenter for this controller. Not to be manually called.
*/
override fun createPresenter() = SourceCategoryPresenter()
/**
* Returns the toolbar title to show when this controller is attached.
*/
override fun getTitle(): String? {
return resources?.getString(R.string.action_edit_categories)
}
/**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
return binding.root
}
/**
* Called after view inflation. Used to initialize the view.
*
* @param view The view of this controller.
*/
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceCategoryAdapter(this@SourceCategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
binding.recycler.adapter = adapter
adapter?.isPermanentDelete = false
binding.fab.clicks()
.onEach {
SourceCategoryCreateDialog(this@SourceCategoryController).showDialog(router, null)
}
.launchIn(scope)
binding.fab.offsetAppbarHeight(activity!!)
}
/**
* Called when the view is being destroyed. Used to release references and remove callbacks.
*
* @param view The view of this controller.
*/
override fun onDestroyView(view: View) {
// Manually call callback to delete categories if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null
actionMode = null
adapter = null
super.onDestroyView(view)
}
/**
* Called from the presenter when the categories are updated.
*
* @param categories The new list of categories to display.
*/
fun setCategories(categories: List<SourceCategoryItem>) {
actionMode?.finish()
adapter?.updateDataSet(categories)
if (categories.isNotEmpty()) {
binding.emptyView.hide()
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
}
} else {
binding.emptyView.show(R.string.information_empty_category)
}
}
/**
* Called when action mode is first created. The menu supplied will be used to generate action
* buttons for the action mode.
*
* @param mode ActionMode being created.
* @param menu Menu used to populate action buttons.
* @return true if the action mode should be created, false if entering this mode should be
* aborted.
*/
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
// Inflate menu.
mode.menuInflater.inflate(R.menu.category_selection, menu)
// Enable adapter multi selection.
adapter?.mode = SelectableAdapter.Mode.MULTI
return true
}
/**
* Called to refresh an action mode's action menu whenever it is invalidated.
*
* @param mode ActionMode being prepared.
* @param menu Menu used to populate action buttons.
* @return true if the menu or action mode was updated, false otherwise.
*/
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val adapter = adapter ?: return false
val count = adapter.selectedItemCount
mode.title = count.toString()
// Show edit button only when one item is selected
val editItem = mode.menu.findItem(R.id.action_edit)
editItem.isVisible = count == 1
return true
}
/**
* Called to report a user click on an action button.
*
* @param mode The current ActionMode.
* @param item The item that was clicked.
* @return true if this callback handled the event, false if the standard MenuItem invocation
* should continue.
*/
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val adapter = adapter ?: return false
when (item.itemId) {
R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this)
undoHelper?.start(
adapter.selectedPositions, view!!,
R.string.snack_categories_deleted, R.string.action_undo, 3000
)
mode.finish()
}
R.id.action_edit -> {
// Edit selected category
if (adapter.selectedItemCount == 1) {
val position = adapter.selectedPositions.first()
val category = adapter.getItem(position)?.category
if (category != null) {
editCategory(category)
}
}
}
else -> return false
}
return true
}
/**
* Called when an action mode is about to be exited and destroyed.
*
* @param mode The current ActionMode being destroyed.
*/
override fun onDestroyActionMode(mode: ActionMode) {
// Reset adapter to single selection
adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection()
actionMode = null
}
/**
* Called when an item in the list is clicked.
*
* @param position The position of the clicked item.
* @return true if this click should enable selection mode.
*/
override fun onItemClick(view: View, position: Int): Boolean {
// Check if action mode is initialized and selected item exist.
return if (actionMode != null && position != RecyclerView.NO_POSITION) {
toggleSelection(position)
true
} else {
false
}
}
/**
* Called when an item in the list is long clicked.
*
* @param position The position of the clicked item.
*/
override fun onItemLongClick(position: Int) {
val activity = activity as? AppCompatActivity ?: return
// Check if action mode is initialized.
if (actionMode == null) {
// Initialize action mode
actionMode = activity.startSupportActionMode(this)
}
// Set item as selected
toggleSelection(position)
}
/**
* Toggle the selection state of an item.
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
*
* @param position The position of the item to toggle.
*/
private fun toggleSelection(position: Int) {
val adapter = adapter ?: return
// Mark the position selected
adapter.toggleSelection(position)
if (adapter.selectedItemCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
/**
* Called when the undo action is clicked in the snackbar.
*
* @param action The action performed.
*/
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
adapter?.restoreDeletedItems()
undoHelper = null
}
/**
* Called when the time to restore the items expires.
*
* @param action The action performed.
* @param event The event that triggered the action
*/
override fun onActionConfirmed(action: Int, event: Int) {
val adapter = adapter ?: return
presenter.deleteCategories(adapter.deletedItems.map { it.category })
undoHelper = null
}
/**
* Show a dialog to let the user change the category name.
*
* @param category The category to be edited.
*/
private fun editCategory(category: String) {
SourceCategoryRenameDialog(this, category).showDialog(router)
}
/**
* Renames the given category with the given name.
*
* @param category The category to rename.
* @param name The new name of the category.
*/
override fun renameCategory(category: String, name: String) {
presenter.renameCategory(category, name)
}
/**
* Creates a new category with the given name.
*
* @param name The name of the new category.
*/
override fun createCategory(name: String) {
presenter.createCategory(name)
}
/**
* Called from the presenter when a category with the given name already exists.
*/
fun onCategoryExistsError() {
activity?.toast(R.string.error_category_exists)
}
/**
* Called from the presenter when a invalid category name is made
*/
fun onCategoryInvalidNameError() {
activity?.toast(R.string.invalid_category_name)
}
}

View File

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
/**
* Dialog to create a new category for the library.
*/
class SourceCategoryCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller, T : SourceCategoryCreateDialog.Listener {
/**
* Name of the new category. Value updated with each input from the user.
*/
private var currentName = ""
constructor(target: T) : this() {
targetController = target
}
/**
* Called when creating the dialog for this controller.
*
* @param savedViewState The saved state of this dialog.
* @return a new dialog instance.
*/
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.action_add_category)
.negativeButton(android.R.string.cancel)
.input(
hint = resources?.getString(R.string.name),
prefill = currentName
) { _, input ->
currentName = input.toString()
}
.positiveButton(android.R.string.ok) {
(targetController as? Listener)?.createCategory(currentName)
}
}
interface Listener {
fun createCategory(name: String)
}
}

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.view.View
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.view.gone
import kotlinx.android.synthetic.main.categories_item.reorder
import kotlinx.android.synthetic.main.categories_item.title
/**
* Holder used to display category items.
*
* @param view The view used by category items.
* @param adapter The adapter containing this holder.
*/
class SourceCategoryHolder(view: View, val adapter: SourceCategoryAdapter) : BaseFlexibleViewHolder(view, adapter) {
/**
* Binds this holder with the given category.
*
* @param category The category to bind.
*/
fun bind(category: String) {
// Set capitalized title.
title.text = category
reorder.gone()
}
}

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Category item for a recycler view.
*/
class SourceCategoryItem(val category: String) : AbstractFlexibleItem<SourceCategoryHolder>() {
/**
* Whether this item is currently selected.
*/
var isSelected = false
/**
* Returns the layout resource for this item.
*/
override fun getLayoutRes(): Int {
return R.layout.categories_item
}
/**
* Returns a new view holder for this item.
*
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceCategoryHolder {
return SourceCategoryHolder(view, adapter as SourceCategoryAdapter)
}
/**
* Binds the given view holder with this item.
*
* @param adapter The adapter of this item.
* @param holder The holder to bind.
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: SourceCategoryHolder,
position: Int,
payloads: List<Any?>?
) {
holder.bind(category)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return false
}
override fun hashCode(): Int {
return category.hashCode()
}
}

View File

@ -0,0 +1,132 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [SourceCategoryController]. Used to manage the categories of the library.
*/
class SourceCategoryPresenter(
private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<SourceCategoryController>() {
/**
* List containing categories.
*/
private var categories: List<String> = emptyList()
val preferences: PreferencesHelper = Injekt.get()
val scope = CoroutineScope(Job() + Dispatchers.Main)
/**
* Called when the presenter is created.
*
* @param savedState The saved state of this presenter.
*/
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
preferences.sourcesTabCategories().asFlow().onEach { categories ->
this.categories = categories.toList().sortedBy { it.toLowerCase() }
Observable.just(this.categories)
.map { it.map(::SourceCategoryItem) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(SourceCategoryController::setCategories)
}.launchIn(scope)
}
/**
* Creates and adds a new category to the database.
*
* @param name The name of the category to create.
*/
fun createCategory(name: String) {
// Do not allow duplicate categories.
if (categoryExists(name)) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
return
}
if (name.contains("|")) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryInvalidNameError() })
return
}
// Create category.
val newCategories = categories.toMutableList()
newCategories += name
preferences.sourcesTabCategories().set(newCategories.toSet())
}
/**
* Deletes the given categories from the database.
*
* @param categories The list of categories to delete.
*/
fun deleteCategories(categories: List<String>) {
var sources = preferences.sourcesTabSourcesInCategories().get().toList()
sources = sources.map { it.split("|") }.filterNot { it[1] in categories }.map { it[0] + "|" + it[1] }
preferences.sourcesTabSourcesInCategories().set(sources.toSet())
preferences.sourcesTabCategories().set(
this.categories.filterNot { it in categories }.toSet()
)
}
/**
* Renames a category.
*
* @param category The category to rename.
* @param name The new name of the category.
*/
fun renameCategory(categoryOld: String, categoryNew: String) {
// Do not allow duplicate categories.
if (categoryExists(categoryNew)) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
return
}
if (categoryNew.contains("|")) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryInvalidNameError() })
return
}
val newCategories = categories.filterNot { it in categoryOld }.toMutableList()
newCategories += categoryNew
var sources = preferences.sourcesTabSourcesInCategories().get().toList()
sources = sources.map { it.split("|").toMutableList() }
.map {
if (it[1] == categoryOld) {
it[1] = categoryNew
}
it[0] + "|" + it[1]
}
preferences.sourcesTabSourcesInCategories().set(sources.toSet())
preferences.sourcesTabCategories().set(newCategories.sorted().toSet())
}
/**
* Returns true if a category with the given name already exists.
*/
private fun categoryExists(name: String): Boolean {
return categories.any { it.equals(name, true) }
}
}

View File

@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
/**
* Dialog to rename an existing category of the library.
*/
class SourceCategoryRenameDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller, T : SourceCategoryRenameDialog.Listener {
private var category: String? = null
/**
* Name of the new category. Value updated with each input from the user.
*/
private var currentName = ""
constructor(target: T, category: String) : this() {
targetController = target
this.category = category
currentName = category
}
/**
* Called when creating the dialog for this controller.
*
* @param savedViewState The saved state of this dialog.
* @return a new dialog instance.
*/
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.action_rename_category)
.negativeButton(android.R.string.cancel)
.input(
hint = resources?.getString(R.string.name),
prefill = currentName
) { _, input ->
currentName = input.toString()
}
.positiveButton(android.R.string.ok) { onPositive() }
}
/**
* Called to save this Controller's state in the event that its host Activity is destroyed.
*
* @param outState The Bundle into which data should be saved
*/
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(CATEGORY_KEY, category)
super.onSaveInstanceState(outState)
}
/**
* Restores data that was saved in the [onSaveInstanceState] method.
*
* @param savedInstanceState The bundle that has data to be restored
*/
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
category = savedInstanceState.getSerializable(CATEGORY_KEY) as? String
}
/**
* Called when the positive button of the dialog is clicked.
*/
private fun onPositive() {
val target = targetController as? Listener ?: return
val category = category ?: return
target.renameCategory(category, currentName)
}
interface Listener {
fun renameCategory(category: String, name: String)
}
private companion object {
const val CATEGORY_KEY = "CategoryRenameDialog.category"
}
}

View File

@ -4,8 +4,12 @@ import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryController
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
@ -16,6 +20,21 @@ class SettingsBrowseController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.browse
preferenceCategory {
titleRes = R.string.label_sources
preference {
titleRes = R.string.action_edit_categories
val catCount = preferences.sourcesTabCategories().get().count()
summary = context.resources.getQuantityString(R.plurals.num_categories, catCount, catCount)
onClick {
router.pushController(SourceCategoryController().withFadeTransaction())
}
}
}
preferenceCategory {
titleRes = R.string.latest

View File

@ -48,6 +48,9 @@ object LocaleHelper {
* Returns Display name of a string language code
*/
fun getSourceDisplayName(lang: String?, context: Context): String {
if (lang != null && lang.contains("custom|")) {
return lang.split("|")[1]
}
return when (lang) {
"" -> context.getString(R.string.other_source)
SourcePresenter.LAST_USED_KEY -> context.getString(R.string.last_used_source)

View File

@ -123,6 +123,8 @@
<string name="too_many_watched">Too many watched sources, cannot add more then 5</string>
<string name="latest_tab_empty">You don\'t have any watched sources, go to the sources tab and long press a source to watch it</string>
<string name="pref_latest_tab_language_code">Display language code next to name</string>
<string name="no_source_categories">No source categories available</string>
<string name="invalid_category_name">Invalid category name</string>
<!-- AZ -->
<string name="az_recommends">See Recommendations</string>