diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index f8104db1e..9f9a60948 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -17,6 +17,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER @@ -154,7 +155,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { databaseHelper.inTransaction { // Get manga from database - val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */ + val mangas = getFavoriteManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY --> */ + getMergedManga() /* SY <-- */ val extensions: MutableSet = mutableSetOf() @@ -652,7 +653,15 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { // Store the inserted id in the mergedMangaReference if (!found) { // Let the db assign the id - val mergedManga = (if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga) ?: return@forEach + var mergedManga = if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga + if (mergedManga == null) { + mergedManga = Manga.create(MERGED_SOURCE_ID).apply { + url = mergedMangaReference.mergeUrl + title = context.getString(R.string.refresh_merge) + } + mergedManga.id = databaseHelper.insertManga(mergedManga).executeAsBlocking().insertedId() + } + val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach lastMergeManga = mergedManga diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index 21fc9c001..cb71754de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS +import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION @@ -250,6 +251,8 @@ class BackupRestoreService : Service() { // SY --> json.get(SAVEDSEARCHES)?.let { restoreSavedSearches(it) } + + json.get(MERGEDMANGAREFERENCES)?.let { restoreMergedMangaReferences(it) } // SY <-- // Store source mapping for error messages @@ -296,7 +299,7 @@ class BackupRestoreService : Service() { } restoreProgress += 1 - showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories)) + showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.merged_references)) } // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 03c1bbd56..f163c4aaf 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -316,4 +316,6 @@ object PreferenceKeys { const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders" const val biometricTimeRanges = "biometric_time_ranges" + + const val sortTagsForLibrary = "sort_tags_for_library" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index e6bddddf0..6c04c515a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -356,7 +356,7 @@ class PreferencesHelper(val context: Context) { fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false) - fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4) + fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 10) fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true) @@ -392,7 +392,7 @@ class PreferencesHelper(val context: Context) { fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL) - fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, false) + fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, true) fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false) @@ -421,4 +421,6 @@ class PreferencesHelper(val context: Context) { fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false) fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf()) + + fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagAdapter.kt new file mode 100644 index 000000000..921432270 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagAdapter.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.category.genre + +import eu.davidea.flexibleadapter.FlexibleAdapter + +/** + * Custom adapter for categories. + * + * @param controller The containing controller. + */ +class SortTagAdapter(controller: SortTagController) : + FlexibleAdapter(null, controller, true) { + + /** + * Listener called when an item of the list is released. + */ + val onItemReleaseListener: OnItemReleaseListener = controller + + /** + * 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) + } + + interface OnItemReleaseListener { + /** + * Called when an item of the list is released. + */ + fun onItemReleased(position: Int) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt new file mode 100644 index 000000000..a361be990 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt @@ -0,0 +1,323 @@ +package eu.kanade.tachiyomi.ui.category.genre + +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.floatingactionbutton.ExtendedFloatingActionButton +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.FabController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import kotlinx.android.synthetic.main.main_activity.root_coordinator +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 SortTagController : + NucleusController(), + FabController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + SortTagAdapter.OnItemReleaseListener, + SortTagCreateDialog.Listener, + UndoHelper.OnActionListener { + + /** + * Object used to show ActionMode toolbar. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing category items. + */ + private var adapter: SortTagAdapter? = null + + private var actionFab: ExtendedFloatingActionButton? = null + private var actionFabScrollListener: RecyclerView.OnScrollListener? = 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() = SortTagPresenter() + + /** + * Returns the toolbar title to show when this controller is attached. + */ + override fun getTitle(): String? { + return resources?.getString(R.string.action_edit_tags) + } + + /** + * 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 = SortTagAdapter(this@SortTagController) + binding.recycler.layoutManager = LinearLayoutManager(view.context) + binding.recycler.setHasFixedSize(true) + binding.recycler.adapter = adapter + adapter?.isPermanentDelete = false + + actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) + } + + override fun configureFab(fab: ExtendedFloatingActionButton) { + actionFab = fab + fab.setText(R.string.action_add) + fab.setIconResource(R.drawable.ic_add_24dp) + fab.clicks() + .onEach { + SortTagCreateDialog(this@SortTagController).showDialog(router, null) + } + .launchIn(scope) + } + + override fun cleanupFab(fab: ExtendedFloatingActionButton) { + actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } + actionFab = null + } + + /** + * 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) { + 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_tags) + } + } + + /** + * 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 + mode.menu.findItem(R.id.action_edit).isVisible = false + 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, + activity!!.root_coordinator, + R.string.snack_tags_deleted, + R.string.action_undo, + 3000 + ) + mode.finish() + } + 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 an item is released from a drag. + * + * @param position The position of the released item. + */ + override fun onItemReleased(position: Int) { + val adapter = adapter ?: return + val tags = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.tag } + presenter.reorderTags(tags) + } + + /** + * Called when the undo action is clicked in the snackbar. + * + * @param action The action performed. + */ + override fun onActionCanceled(action: Int, positions: MutableList?) { + 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.deleteTags(adapter.deletedItems.map { it.tag }) + undoHelper = null + } + + /** + * 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 onTagExistsError() { + activity?.toast(R.string.error_tag_exists) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagCreateDialog.kt new file mode 100644 index 000000000..f1d9689d4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagCreateDialog.kt @@ -0,0 +1,51 @@ +package eu.kanade.tachiyomi.ui.category.genre + +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 SortTagCreateDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SortTagCreateDialog.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) + .message(R.string.action_add_tags_message) + .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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagHolder.kt new file mode 100644 index 000000000..2e19cb616 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagHolder.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.ui.category.genre + +import android.view.View +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +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 SortTagHolder(view: View, val adapter: SortTagAdapter) : BaseFlexibleViewHolder(view, adapter) { + /** + * Binds this holder with the given category. + * + * @param tag The tag to bind. + */ + fun bind(tag: String) { + // Set capitalized title. + title.text = tag + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagItem.kt new file mode 100644 index 000000000..858b6218f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagItem.kt @@ -0,0 +1,69 @@ +package eu.kanade.tachiyomi.ui.category.genre + +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 SortTagItem(val tag: String) : AbstractFlexibleItem() { + + /** + * 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>): SortTagHolder { + return SortTagHolder(view, adapter as SortTagAdapter) + } + + /** + * 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>, + holder: SortTagHolder, + position: Int, + payloads: List? + ) { + holder.bind(tag) + } + + /** + * Returns true if this item is draggable. + */ + override fun isDraggable(): Boolean { + return true + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return false + } + + override fun hashCode(): Int { + return tag.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt new file mode 100644 index 000000000..38cdbf422 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt @@ -0,0 +1,91 @@ +package eu.kanade.tachiyomi.ui.category.genre + +import android.os.Bundle +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.minusAssign +import eu.kanade.tachiyomi.data.preference.plusAssign +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 [SortTagController]. Used to manage the categories of the library. + */ +class SortTagPresenter : BasePresenter() { + + /** + * List containing categories. + */ + private var tags: List = 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.sortTagsForLibrary().asFlow().onEach { tags -> + this.tags = tags.toList() + + Observable.just(this.tags) + .map { it.map(::SortTagItem) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(SortTagController::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 (tagExists(name)) { + Observable.just(Unit).subscribeFirst({ view, _ -> view.onTagExistsError() }) + return + } + + preferences.sortTagsForLibrary() += name + } + + /** + * Deletes the given categories from the database. + * + * @param categories The list of categories to delete. + */ + fun deleteTags(categories: List) { + categories.forEach { + preferences.sortTagsForLibrary() -= it + } + } + + /** + * Reorders the given categories in the database. + * + * @param categories The list of categories to reorder. + */ + fun reorderTags(categories: List) { + preferences.sortTagsForLibrary().set(categories.toSet()) + } + + /** + * Returns true if a category with the given name already exists. + */ + private fun tagExists(name: String): Boolean { + return tags.any { it.equals(name, true) } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index d8adac4d2..e166e643e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -290,6 +290,10 @@ class LibraryPresenter( db.getLatestChapterManga().executeAsBlocking().associate { it.id!! to counter++ } } + val listOfTags by lazy { + preferences.sortTagsForLibrary().get().toList().map { ("(, |^)$it").toRegex(RegexOption.IGNORE_CASE) } + } + val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> when (sortingMode) { LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true) @@ -318,6 +322,11 @@ class LibraryPresenter( LibrarySort.DRAG_AND_DROP -> { 0 } + LibrarySort.TAG_LIST -> { + val manga1IndexOfTag = listOfTags.indexOfFirst { i1.manga.genre?.let { tagString -> it.containsMatchIn(tagString) } ?: false } + val manga2IndexOfTag = listOfTags.indexOfFirst { i2.manga.genre?.let { tagString -> it.containsMatchIn(tagString) } ?: false } + manga1IndexOfTag.compareTo(manga2IndexOfTag) + } // SY <-- else -> throw Exception("Unknown sorting mode") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index 90497507a..5da4b5361 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -172,11 +172,12 @@ class LibrarySettingsSheet( private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this) // SY --> private val dragAndDrop = Item.MultiSort(R.string.action_sort_drag_and_drop, this) + private val tagList = Item.MultiSort(R.string.tag_sorting, this) // SY <-- override val header = null override val items = - listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, dateAdded /* SY --> */, dragAndDrop /* SY <-- */) + listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, dateAdded /* SY --> */, dragAndDrop) + if (preferences.sortTagsForLibrary().get().isNotEmpty()) listOf(tagList) else emptyList() /* SY <-- */ override val footer = null override fun initModels() { @@ -203,6 +204,8 @@ class LibrarySettingsSheet( if (sorting == LibrarySort.DATE_ADDED) order else Item.MultiSort.SORT_NONE // SY --> dragAndDrop.state = if (sorting == LibrarySort.DRAG_AND_DROP) order else Item.MultiSort.SORT_NONE + tagList.state = + if (sorting == LibrarySort.TAG_LIST) order else Item.MultiSort.SORT_NONE // SY <-- } @@ -241,6 +244,7 @@ class LibrarySettingsSheet( dateAdded -> LibrarySort.DATE_ADDED // SY --> dragAndDrop -> LibrarySort.DRAG_AND_DROP + tagList -> LibrarySort.TAG_LIST // SY <-- else -> throw Exception("Unknown sorting") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt index 0c203fe40..f4cfe06ec 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -11,6 +11,7 @@ object LibrarySort { const val DATE_ADDED = 8 // SY --> const val DRAG_AND_DROP = 7 + const val TAG_LIST = 9 // SY <-- @Deprecated("Removed in favor of searching by source") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index 855f2cb10..c57eccc21 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -38,7 +38,7 @@ class SettingsBrowseController : SettingsController() { key = Keys.useNewSourceNavigation titleRes = R.string.pref_source_navigation summaryRes = R.string.pref_source_navigation_summery - defaultValue = false + defaultValue = true } switchPreference { key = Keys.allowLocalSourceHiddenFolders diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt index 7cdd7577d..165f45e1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt @@ -257,6 +257,15 @@ class SettingsLibraryController : SettingsController() { } } + preferenceCategory { + titleRes = R.string.pref_sorting_settings + preference { + titleRes = R.string.pref_tag_sorting + val count = preferences.sortTagsForLibrary().get().size + summary = resources!!.getQuantityString(R.plurals.pref_tag_sorting_desc, count, count) + } + } + // SY --> if (preferences.skipPreMigration().get() || preferences.migrationSources().get() .isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 9bd0999e2..d88d6e74c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -162,30 +162,26 @@ class SettingsReaderController : SettingsController() { key = Keys.eh_preload_size titleRes = R.string.reader_preload_amount entryValues = arrayOf( - "1", - "2", - "3", "4", "6", "8", "10", "12", "14", - "16" + "16", + "20", ) entriesRes = arrayOf( - R.string.reader_preload_amount_1_page, - R.string.reader_preload_amount_2_pages, - R.string.reader_preload_amount_3_pages, R.string.reader_preload_amount_4_pages, R.string.reader_preload_amount_6_pages, R.string.reader_preload_amount_8_pages, R.string.reader_preload_amount_10_pages, R.string.reader_preload_amount_12_pages, R.string.reader_preload_amount_14_pages, - R.string.reader_preload_amount_16_pages + R.string.reader_preload_amount_16_pages, + R.string.reader_preload_amount_20_pages ) - defaultValue = "4" + defaultValue = "10" summaryRes = R.string.reader_preload_amount_summary } listPreference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt index 3065b1a30..1f09e2a36 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -45,7 +45,7 @@ abstract class LoginDialogPreference( return dialog } - fun onViewCreated(view: View) { + /* SY --> */ open /* SY <-- */ fun onViewCreated(view: View) { v = view.apply { if (usernameLabelRes != null) { username_label.hint = context.getString(usernameLabelRes) diff --git a/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt b/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt index 586f7286a..626601b36 100644 --- a/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt +++ b/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt @@ -3,6 +3,7 @@ package exh.widget.preference import android.app.Dialog import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import eu.kanade.tachiyomi.R @@ -15,7 +16,9 @@ import exh.md.utils.MdUtil import kotlinx.android.synthetic.main.pref_account_login.view.login import kotlinx.android.synthetic.main.pref_account_login.view.password import kotlinx.android.synthetic.main.pref_account_login.view.username -import kotlinx.android.synthetic.main.pref_site_login_two_factor_auth.view.* +import kotlinx.android.synthetic.main.pref_site_login_two_factor_auth.view.two_factor_check +import kotlinx.android.synthetic.main.pref_site_login_two_factor_auth.view.two_factor_edit +import kotlinx.android.synthetic.main.pref_site_login_two_factor_auth.view.two_factor_holder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -50,6 +53,15 @@ class MangadexLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle return dialog } + override fun onViewCreated(view: View) { + super.onViewCreated(view) + v?.apply { + two_factor_check?.setOnCheckedChangeListener { _, isChecked -> + two_factor_holder.isVisible = isChecked + } + } + } + override fun setCredentialsOnView(view: View) = with(view) { username.setText(service.getUsername()) password.setText(service.getPassword()) diff --git a/app/src/main/res/values-pt-rBR/strings_sy.xml b/app/src/main/res/values-pt-rBR/strings_sy.xml index e7c50e47d..9945448de 100644 --- a/app/src/main/res/values-pt-rBR/strings_sy.xml +++ b/app/src/main/res/values-pt-rBR/strings_sy.xml @@ -194,9 +194,6 @@ Ignorar fila ao tentar novamente Normalmente, tocar no botão Tentar novamente, aguardará até terminar de baixar a última página antes de baixar novamente a página com falha. Ativar isso forçará a baixar a página falhada assim que tocar em Tentar novamente. Quantidade de pré-carregamento do leitor - 1 Página - 2 Páginas - 3 Páginas 4 Páginas 6 Páginas 8 Páginas @@ -204,6 +201,8 @@ 12 Páginas 14 Páginas 16 Páginas + 18 Páginas + 20 Páginas Quantidade de páginas a serem pré-carregadas enquanto lê. Valores mais altos resultarão em uma experiência de leitura mais suave, ao custo do uso do cache mais alto, é recomendável aumentar a quantidade de cache que você aloca ao usar valores maiores Tamanho de cache do leitor Quantidade de imagens para salvar no dispositivo enquanto lê. Valores mais altos resultarão em uma experiência de leitura mais suave, com o custo do uso de espaço maior em disco diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 8d3c85d52..0b9bfafa4 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -166,6 +166,7 @@ Put the recommendations button in the overflow menu instead of on the manga page + Sorting Settings Use last saved pre-migration preferences and sources to mass migrate Library settings sheet More library display settings @@ -209,9 +210,6 @@ Skip queue on retry Normally, pressing the retry button on a failed download will wait until the downloader has finished downloading the last page before beginning to re-download the failed page. Enabling this will force the downloader to begin re-downloading the failed page as soon as you press the retry button. Reader Preload amount - 1 Page - 2 Pages - 3 Pages 4 Pages 6 Pages 8 Pages @@ -219,6 +217,8 @@ 12 Pages 14 Pages 16 Pages + 18 Pages + 20 Pages The amount of pages to preload when reading. Higher values will result in a smoother reading experience, at the cost of higher cache usage, it is recommended to increase the amount of cache you allocate when using larger values Reader cache size The amount of images to save on device while reading. Higher values will result in a smoother reading experience, at the cost of higher disk space usage @@ -284,6 +284,22 @@ Too many watched sources, cannot add more then 5 You don\'t have any watched sources, go to the sources tab and long press a source to watch it + + Tag sorting tags + Tag sorting + + No tags in sorting list. This adds a option in the library to sort by a priority based tag list, which means manga will be sorted in a way to prioritise the ones with the tags you want + %1$d tag in sorting list. This adds a option in the library to sort by a priority based tag list, which means manga will be sorted in a way to prioritise the ones with the tags you want + %1$d tags in sorting list. This adds a option in the library to sort by a priority based tag list, which means manga will be sorted in a way to prioritise the ones with the tags you want + + Tag sorting list + Add tag + Read this! Tags must be exact, there are no partial matches, you cannot do netorare to filter out female:netorare or similar!\nThe style for namespace tags is "female: sole female" without quotes!\nAdding multiple variants of the same tag is supported, so feel free to do "tag: netorare" for NHentai and "female: netorare" for E-Hentai! + Edit tags + You have no tags. Tap the plus button to create one for sorting your library by tags + This tag exists! + Tags deleted + Redundant This extension is redundant and will not be used inside this version of Tachiyomi. @@ -513,6 +529,7 @@ Toggling this will disable or enable chapter downloads for this merged manga Merged references invalid Toggle chapter updates error + Merged references Could not find manga to toggle chapter updates Toggle download chapters error Could not find manga to toggle chapter downloads @@ -520,6 +537,7 @@ Dedupe mode: Info manga: Toggle dedupe + Refresh to get proper info Unfollowed @@ -537,4 +555,5 @@ MangaDex follows Random + \ No newline at end of file