diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt index c3969d54..ff170326 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt @@ -3,18 +3,24 @@ package ani.dantotsu.parsers import android.content.Context import ani.dantotsu.Lazier import ani.dantotsu.lazyList -import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefManager.asLiveString +import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet +import ani.dantotsu.settings.saving.PrefName import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch object AnimeSources : WatchSources() { override var list: List> = emptyList() - var pinnedAnimeSources: Set = emptySet() + var pinnedAnimeSources: List = emptyList() suspend fun init(fromExtensions: StateFlow>, context: Context) { - pinnedAnimeSources = PrefManager.getVal(PrefName.PinnedAnimeSources) + pinnedAnimeSources = PrefManager.getNullableVal>(PrefName.AnimeSourcesOrder, null) + ?: emptyList() // Initialize with the first value from StateFlow val initialExtensions = fromExtensions.first() @@ -53,14 +59,17 @@ object AnimeSources : WatchSources() { private fun sortPinnedAnimeSources( Sources: List>, - pinnedAnimeSources: Set + pinnedAnimeSources: List ): List> { - //find the pinned sources - val pinnedSources = Sources.filter { pinnedAnimeSources.contains(it.name) } + val pinnedSourcesMap = Sources.filter { pinnedAnimeSources.contains(it.name) } + .associateBy { it.name } + val orderedPinnedSources = pinnedAnimeSources.mapNotNull { name -> + pinnedSourcesMap[name] + } //find the unpinned sources val unpinnedSources = Sources.filter { !pinnedAnimeSources.contains(it.name) } //put the pinned sources at the top of the list - return pinnedSources + unpinnedSources + return orderedPinnedSources + unpinnedSources } } diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt index 44b21482..b416d3a9 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt @@ -10,10 +10,11 @@ import kotlinx.coroutines.flow.first object MangaSources : MangaReadSources() { override var list: List> = emptyList() - var pinnedMangaSources: Set = emptySet() + var pinnedMangaSources: List = emptyList() suspend fun init(fromExtensions: StateFlow>) { - pinnedMangaSources = PrefManager.getVal(PrefName.PinnedMangaSources) + pinnedMangaSources = PrefManager.getNullableVal>(PrefName.MangaSourcesOrder, null) + ?: emptyList() // Initialize with the first value from StateFlow val initialExtensions = fromExtensions.first() @@ -52,14 +53,17 @@ object MangaSources : MangaReadSources() { private fun sortPinnedMangaSources( Sources: List>, - pinnedMangaSources: Set + pinnedMangaSources: List ): List> { - //find the pinned sources - val pinnedSources = Sources.filter { pinnedMangaSources.contains(it.name) } + val pinnedSourcesMap = Sources.filter { pinnedMangaSources.contains(it.name) } + .associateBy { it.name } + val orderedPinnedSources = pinnedMangaSources.mapNotNull { name -> + pinnedSourcesMap[name] + } //find the unpinned sources val unpinnedSources = Sources.filter { !pinnedMangaSources.contains(it.name) } //put the pinned sources at the top of the list - return pinnedSources + unpinnedSources + return orderedPinnedSources + unpinnedSources } } diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt index 5bdce222..d49a6212 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt @@ -6,7 +6,9 @@ import android.app.NotificationManager import android.content.Context import android.os.Bundle import android.util.Log +import android.view.GestureDetector import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -17,16 +19,19 @@ import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding +import ani.dantotsu.logger import ani.dantotsu.others.LanguageMapper +import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment -import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.snackString import com.google.android.material.tabs.TabLayout import com.google.android.material.textfield.TextInputLayout @@ -39,8 +44,10 @@ import kotlinx.coroutines.launch import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Collections import java.util.Locale + class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { @@ -174,6 +181,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { }, skipIcons ) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -185,16 +193,53 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) extensionsRecyclerView.adapter = extensionsAdapter + val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + extensionsAdapter.onMove(viewHolder.adapterPosition, target.adapterPosition) + return true + } + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.elevation = 8f + viewHolder?.itemView?.translationZ = 8f + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.elevation = 0f + viewHolder.itemView.translationZ = 0f + } + } + ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(extensionsRecyclerView) + lifecycleScope.launch { animeExtensionManager.installedExtensionsFlow.collect { extensions -> - extensionsAdapter.updateData(extensions) + extensionsAdapter.updateData(sortToAnimeSourcesList(extensions)) } } val extensionsRecyclerView: RecyclerView = binding.allAnimeExtensionsRecyclerView return binding.root } + + private fun sortToAnimeSourcesList(inpt: List): List { + val sourcesMap = inpt.associateBy { it.name } + val orderedSources = AnimeSources.pinnedAnimeSources.mapNotNull { name -> + sourcesMap[name] + } + return orderedSources + inpt.filter { !AnimeSources.pinnedAnimeSources.contains(it.name) } + } + override fun onDestroyView() { super.onDestroyView();_binding = null } @@ -211,8 +256,21 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { DIFF_CALLBACK_INSTALLED ) { + private val data: MutableList = mutableListOf() + fun updateData(newExtensions: List) { - submitList(newExtensions) // Use submitList instead of manual list handling + submitList(newExtensions) + data.clear() + data.addAll(newExtensions) + } + + fun onMove(fromPosition: Int, toPosition: Int) { + Collections.swap(data, fromPosition, toPosition) + val map = data.map { it.name }.toList() + PrefManager.setVal(PrefName.AnimeSourcesOrder, map) + AnimeSources.pinnedAnimeSources = map + AnimeSources.performReorderAnimeSources() + notifyItemMoved(fromPosition, toPosition) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -221,7 +279,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { return ViewHolder(view) } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = getItem(position) // Use getItem() from ListAdapter val nsfw = if (extension.isNsfw) "(18+)" else "" @@ -242,7 +300,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { holder.settingsImageView.setOnClickListener { onSettingsClicked(extension) } - holder.card.setOnLongClickListener { + holder.closeTextView.setOnLongClickListener { onUninstallClicked(extension, true) true } diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt index 4f19c60f..ede77485 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -8,6 +8,7 @@ import android.content.Context import android.os.Bundle import android.util.Log import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -18,6 +19,7 @@ import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -25,6 +27,7 @@ import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.databinding.FragmentMangaExtensionsBinding import ani.dantotsu.others.LanguageMapper +import ani.dantotsu.parsers.MangaSources import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefManager @@ -33,6 +36,7 @@ import com.google.android.material.tabs.TabLayout import com.google.android.material.textfield.TextInputLayout import com.google.firebase.crashlytics.FirebaseCrashlytics import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.source.ConfigurableSource @@ -40,6 +44,7 @@ import kotlinx.coroutines.launch import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Collections import java.util.Locale class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { @@ -184,16 +189,52 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) extensionsRecyclerView.adapter = extensionsAdapter + val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + extensionsAdapter.onMove(viewHolder.adapterPosition, target.adapterPosition) + return true + } + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.elevation = 8f + viewHolder?.itemView?.translationZ = 8f + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.elevation = 0f + viewHolder.itemView.translationZ = 0f + } + } + ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(extensionsRecyclerView) + lifecycleScope.launch { mangaExtensionManager.installedExtensionsFlow.collect { extensions -> - extensionsAdapter.updateData(extensions) + extensionsAdapter.updateData(sortToMangaSourcesList(extensions)) } } val extensionsRecyclerView: RecyclerView = binding.allMangaExtensionsRecyclerView return binding.root } + private fun sortToMangaSourcesList(inpt: List): List { + val sourcesMap = inpt.associateBy { it.name } + val orderedSources = MangaSources.pinnedMangaSources.mapNotNull { name -> + sourcesMap[name] + } + return orderedSources + inpt.filter { !MangaSources.pinnedMangaSources.contains(it.name) } + } + override fun onDestroyView() { super.onDestroyView();_binding = null } @@ -205,15 +246,17 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { private class MangaExtensionsAdapter( private val onSettingsClicked: (MangaExtension.Installed) -> Unit, private val onUninstallClicked: (MangaExtension.Installed, Boolean) -> Unit, - skipIcons: Boolean + val skipIcons: Boolean ) : ListAdapter( DIFF_CALLBACK_INSTALLED ) { - val skipIcons = skipIcons + private val data: MutableList = mutableListOf() fun updateData(newExtensions: List) { - submitList(newExtensions) // Use submitList instead of manual list handling + submitList(newExtensions) + data.clear() + data.addAll(newExtensions) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -222,7 +265,16 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { return ViewHolder(view) } - @SuppressLint("SetTextI18n") + fun onMove(fromPosition: Int, toPosition: Int) { + Collections.swap(data, fromPosition, toPosition) + val map = data.map { it.name }.toList() + PrefManager.setVal(PrefName.MangaSourcesOrder, map) + MangaSources.pinnedMangaSources = map + MangaSources.performReorderMangaSources() + notifyItemMoved(fromPosition, toPosition) + } + + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = getItem(position) // Use getItem() from ListAdapter val nsfw = if (extension.isNsfw) "(18+)" else "" @@ -243,11 +295,6 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { holder.settingsImageView.setOnClickListener { onSettingsClicked(extension) } - - holder.card.setOnLongClickListener { - onUninstallClicked(extension, true) - true - } } fun filter(query: String, currentList: List) { diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index 3b4acc1a..20ed17ca 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -212,37 +212,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene .show(this, tag) } - binding.settingsPinnedAnimeSources.setOnClickListener { - val animeSourcesWithoutDownloadsSource = - AnimeSources.list.filter { it.name != "Downloaded" } - val names = animeSourcesWithoutDownloadsSource.map { it.name } - val pinnedSourcesBoolean = - animeSourcesWithoutDownloadsSource.map { it.name in AnimeSources.pinnedAnimeSources } - val pinnedSourcesOriginal: Set = PrefManager.getVal(PrefName.PinnedAnimeSources) - val pinnedSources = pinnedSourcesOriginal.toMutableSet() - val alertDialog = AlertDialog.Builder(this, R.style.MyPopup) - .setTitle("Pinned Anime Sources") - .setMultiChoiceItems( - names.toTypedArray(), - pinnedSourcesBoolean.toBooleanArray() - ) { _, which, isChecked -> - if (isChecked) { - pinnedSources.add(AnimeSources.names[which]) - } else { - pinnedSources.remove(AnimeSources.names[which]) - } - } - .setPositiveButton("OK") { dialog, _ -> - PrefManager.setVal(PrefName.PinnedAnimeSources, pinnedSources) - AnimeSources.pinnedAnimeSources = pinnedSources - AnimeSources.performReorderAnimeSources() - dialog.dismiss() - } - .create() - alertDialog.show() - alertDialog.window?.setDimAmount(0.8f) - } - binding.settingsPlayer.setOnClickListener { startActivity(Intent(this, PlayerSettingsActivity::class.java)) } @@ -472,37 +441,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene PrefManager.setVal(PrefName.SettingsPreferDub, isChecked) } - binding.settingsPinnedMangaSources.setOnClickListener { - val mangaSourcesWithoutDownloadsSource = - MangaSources.list.filter { it.name != "Downloaded" } - val names = mangaSourcesWithoutDownloadsSource.map { it.name } - val pinnedSourcesBoolean = - mangaSourcesWithoutDownloadsSource.map { it.name in MangaSources.pinnedMangaSources } - val pinnedSourcesOriginal: Set = PrefManager.getVal(PrefName.PinnedMangaSources) - val pinnedSources = pinnedSourcesOriginal.toMutableSet() - val alertDialog = AlertDialog.Builder(this, R.style.MyPopup) - .setTitle("Pinned Manga Sources") - .setMultiChoiceItems( - names.toTypedArray(), - pinnedSourcesBoolean.toBooleanArray() - ) { _, which, isChecked -> - if (isChecked) { - pinnedSources.add(MangaSources.names[which]) - } else { - pinnedSources.remove(MangaSources.names[which]) - } - } - .setPositiveButton("OK") { dialog, _ -> - PrefManager.setVal(PrefName.PinnedMangaSources, pinnedSources) - MangaSources.pinnedMangaSources = pinnedSources - MangaSources.performReorderMangaSources() - dialog.dismiss() - } - .create() - alertDialog.show() - alertDialog.window?.setDimAmount(0.8f) - } - binding.settingsReader.setOnClickListener { startActivity(Intent(this, ReaderSettingsActivity::class.java)) } diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 03593cd4..e746d3c2 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -21,9 +21,9 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files VerboseLogging(Pref(Location.General, Boolean::class, false)), DohProvider(Pref(Location.General, Int::class, 0)), DefaultUserAgent(Pref(Location.General, String::class, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")), - PinnedAnimeSources(Pref(Location.General, Set::class, setOf())), + AnimeSourcesOrder(Pref(Location.General, List::class, listOf())), AnimeSearchHistory(Pref(Location.General, Set::class, setOf())), - PinnedMangaSources(Pref(Location.General, Set::class, setOf())), + MangaSourcesOrder(Pref(Location.General, List::class, listOf())), MangaSearchHistory(Pref(Location.General, Set::class, setOf())), //User Interface diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 1ec4b195..5d148b42 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1101,37 +1101,6 @@ app:iconSize="24dp" app:iconTint="?attr/colorPrimary" /> - - -