diff --git a/app/build.gradle b/app/build.gradle index b8b31b2f..d27a4fbc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,7 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.github.VipulOG:ebook-reader:0.1.6' + implementation 'androidx.paging:paging-runtime-ktx:3.2.1' // string matching implementation 'me.xdrop:fuzzywuzzy:1.4.0' diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index ab44679f..488f3e59 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -64,6 +64,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nl.joery.animatedbottombar.AnimatedBottomBar +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.Serializable @@ -74,9 +76,8 @@ class MainActivity : AppCompatActivity() { private var load = false private var uiSettings = UserInterfaceSettings() - private val animeExtensionManager: AnimeExtensionManager by injectLazy() - private val mangaExtensionManager: MangaExtensionManager by injectLazy() - + private val animeExtensionManager: AnimeExtensionManager = Injekt.get() + private val mangaExtensionManager: MangaExtensionManager = Injekt.get() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index 2d948e8d..c74e42bf 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -1205,7 +1205,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { else -> MimeTypes.TEXT_SSA } ) - .setId("2") + .setId("69") .build() } println("sub: $sub") @@ -1221,7 +1221,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { else -> MimeTypes.TEXT_UNKNOWN } ) - .setId("2") + .setId("69") .build() } } @@ -1302,6 +1302,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { ) .setMaxVideoSize(1, 1) //.setOverrideForType( + // TrackSelectionOverride(trackSelector, 2)) ) if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) { @@ -1352,6 +1353,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { } playerView.player = exoPlayer + try { mediaSession = MediaSession.Builder(this, exoPlayer).build() } catch (e: Exception) { @@ -1572,6 +1574,27 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { } override fun onTracksChanged(tracks: Tracks) { + tracks.groups.forEach { + println("Track__: $it") + println("Track__: ${it.length}") + println("Track__: ${it.isSelected}") + println("Track__: ${it.type}") + println("Track__: ${it.mediaTrackGroup.id}") + if (it.type == 3 && it.mediaTrackGroup.id == "1:"){ + playerView.player?.trackSelectionParameters = + playerView.player?.trackSelectionParameters?.buildUpon() + ?.setOverrideForType( + TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)) + ?.build()!! + }else if(it.type == 3){ + playerView.player?.trackSelectionParameters = + playerView.player?.trackSelectionParameters?.buildUpon() + ?.addOverride( + TrackSelectionOverride(it.mediaTrackGroup, listOf())) + ?.build()!! + } + } + println("Track: ${tracks.groups.size}") if (tracks.groups.size <= 2) exoQuality.visibility = View.GONE else { exoQuality.visibility = View.VISIBLE diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt index 54c48bbd..d3c63e2e 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt @@ -156,10 +156,6 @@ abstract class BaseImageAdapter( .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) } else { - println("bitmap from cache") - println(link.url) - println(mangaCache.get(link.url)) - println("cache size: ${mangaCache.size()}") mangaCache.get(link.url)?.let { imageData -> val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap) it.load(bitmap) diff --git a/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt b/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt index ed16d154..e46308a7 100644 --- a/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt +++ b/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt @@ -11,6 +11,7 @@ import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.FileUrl import ani.dantotsu.R import ani.dantotsu.databinding.BottomSheetImageBinding +import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap_old import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap import ani.dantotsu.openLinkInBrowser @@ -78,7 +79,11 @@ class ImageViewDialog : BottomSheetDialogFragment() { val binding = _binding ?: return@launch var bitmap = requireContext().loadBitmap_old(image, trans1 ?: listOf()) - val bitmap2 = if (image2 != null) requireContext().loadBitmap_old(image2, trans2 ?: listOf()) else null + var bitmap2 = if (image2 != null) requireContext().loadBitmap_old(image2, trans2 ?: listOf()) else null + if (bitmap == null) { + bitmap = requireContext().loadBitmap(image, trans1 ?: listOf()) + bitmap2 = if (image2 != null) requireContext().loadBitmap(image2, trans2 ?: listOf()) else null + } bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index 02b9f075..b14d243a 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -57,17 +57,17 @@ abstract class BaseParser { setUserText("Searching : ${mediaObj.mainName()}") val results = search(mediaObj.mainName()) val sortedResults = if (results.isNotEmpty()) { - results.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.mainName()) } + results.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.mainName().lowercase()) } } else { emptyList() } response = sortedResults.firstOrNull() - if (response == null || FuzzySearch.ratio(response.name, mediaObj.mainName()) < 100) { + if (response == null || FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase()) < 100) { setUserText("Searching : ${mediaObj.nameRomaji}") val romajiResults = search(mediaObj.nameRomaji) val sortedRomajiResults = if (romajiResults.isNotEmpty()) { - romajiResults.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.nameRomaji) } + romajiResults.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.nameRomaji.lowercase()) } } else { emptyList() } @@ -78,10 +78,10 @@ abstract class BaseParser { logger("No exact match found in results. Using closest match from RomajiResults.") closestRomaji } else { - val romajiRatio = FuzzySearch.ratio(closestRomaji?.name ?: "", mediaObj.nameRomaji) - val mainNameRatio = FuzzySearch.ratio(response.name, mediaObj.mainName()) - logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name}") - logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name ?: "None"}") + val romajiRatio = FuzzySearch.ratio(closestRomaji?.name?.lowercase() ?: "", mediaObj.nameRomaji.lowercase()) + val mainNameRatio = FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase()) + logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name.lowercase()}") + logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name?.lowercase() ?: "None"}") if (romajiRatio > mainNameRatio) { logger("RomajiResults has a closer match. Replacing response.") diff --git a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt index 3016efd1..28e1482b 100644 --- a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt @@ -2,110 +2,79 @@ package ani.dantotsu.settings import android.app.NotificationManager import android.content.Context -import android.graphics.drawable.Drawable import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.getSystemService import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding -import ani.dantotsu.loadData -import com.bumptech.glide.Glide +import ani.dantotsu.settings.paging.AnimeExtensionAdapter +import ani.dantotsu.settings.paging.AnimeExtensionsViewModel +import ani.dantotsu.settings.paging.AnimeExtensionsViewModelFactory +import ani.dantotsu.settings.paging.OnAnimeInstallClickListener import com.google.firebase.crashlytics.FirebaseCrashlytics import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class AnimeExtensionsFragment : Fragment(), - SearchQueryHandler { + SearchQueryHandler, OnAnimeInstallClickListener { private var _binding: FragmentAnimeExtensionsBinding? = null private val binding get() = _binding!! - val skipIcons = loadData("skip_extension_icons") ?: false + private val viewModel: AnimeExtensionsViewModel by viewModels { + AnimeExtensionsViewModelFactory(animeExtensionManager) + } - private lateinit var extensionsRecyclerView: RecyclerView - private lateinit var allextenstionsRecyclerView: RecyclerView - private val animeExtensionManager: AnimeExtensionManager = Injekt.get() - private val extensionsAdapter = AnimeExtensionsAdapter({ pkg -> - if (isAdded) { // Check if the fragment is currently added to its activity - val context = requireContext() // Store context in a variable - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + private val adapter by lazy { + AnimeExtensionAdapter(this) + } - if (pkg.hasUpdate) { - animeExtensionManager.updateExtension(pkg) - .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread - .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle("Updating extension") - .setContentText("Step: $installStep") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - FirebaseCrashlytics.getInstance().recordException(error) - Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle("Update failed: ${error.message}") - .setContentText("Error: ${error.message}") - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) - .setContentTitle("Update complete") - .setContentText("The extension has been successfully updated.") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - } - ) - } else { - animeExtensionManager.uninstallExtension(pkg.pkgName) + private val animeExtensionManager: AnimeExtensionManager = Injekt.get() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false) + + binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = true + binding.allAnimeExtensionsRecyclerView.adapter = adapter + binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context) + (binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false + + lifecycleScope.launch { + viewModel.pagerFlow.collectLatest { + adapter.submitData(it) } } - }, skipIcons) - private val allExtensionsAdapter = AllAnimeExtensionsAdapter(lifecycleScope, { pkgName -> + return binding.root + } + + override fun updateContentBasedOnQuery(query: String?) { + viewModel.setSearchQuery(query ?: "") + } + + override fun onInstallClick(pkg: AnimeExtension.Available) { val context = requireContext() if (isAdded) { val notificationManager = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Start the installation process - animeExtensionManager.installExtension(pkgName) + animeExtensionManager.installExtension(pkg) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { installStep -> @@ -136,63 +105,15 @@ class AnimeExtensionsFragment : Fragment(), context, Notifications.CHANNEL_DOWNLOADER_PROGRESS ) - .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setSmallIcon(R.drawable.ic_round_download_24) .setContentTitle("Installation complete") .setContentText("The extension has been successfully installed.") .setPriority(NotificationCompat.PRIORITY_LOW) notificationManager.notify(1, builder.build()) + viewModel.invalidatePager() } ) } - }, skipIcons) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false) - - extensionsRecyclerView = binding.animeExtensionsRecyclerView - extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) - extensionsRecyclerView.adapter = extensionsAdapter - - allextenstionsRecyclerView = binding.allAnimeExtensionsRecyclerView - allextenstionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) - allextenstionsRecyclerView.adapter = allExtensionsAdapter - - lifecycleScope.launch { - animeExtensionManager.installedExtensionsFlow.collect { extensions -> - extensionsAdapter.updateData(extensions) - } - } - lifecycleScope.launch { - combine( - animeExtensionManager.availableExtensionsFlow, - animeExtensionManager.installedExtensionsFlow - ) { availableExtensions, installedExtensions -> - // Pair of available and installed extensions - Pair(availableExtensions, installedExtensions) - }.collect { pair -> - val (availableExtensions, installedExtensions) = pair - - allExtensionsAdapter.updateData(availableExtensions, installedExtensions) - } - } - val extensionsRecyclerView: RecyclerView = binding.animeExtensionsRecyclerView - return binding.root - } - - override fun updateContentBasedOnQuery(query: String?) { - if (query.isNullOrEmpty()) { - allExtensionsAdapter.filter("") // Reset the filter - allextenstionsRecyclerView.visibility = View.VISIBLE - extensionsRecyclerView.visibility = View.VISIBLE - } else { - allExtensionsAdapter.filter(query) - allextenstionsRecyclerView.visibility = View.VISIBLE - extensionsRecyclerView.visibility = View.GONE - } } override fun onDestroyView() { @@ -200,158 +121,4 @@ class AnimeExtensionsFragment : Fragment(), } - private class AnimeExtensionsAdapter( - private val onUninstallClicked: (AnimeExtension.Installed) -> Unit, - skipIcons: Boolean - ) : ListAdapter( - DIFF_CALLBACK_INSTALLED - ) { - - val skipIcons = skipIcons - - fun updateData(newExtensions: List) { - submitList(newExtensions) // Use submitList instead of manual list handling - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_extension, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val extension = getItem(position) // Use getItem() from ListAdapter - holder.extensionNameTextView.text = extension.name - if (!skipIcons) { - holder.extensionIconImageView.setImageDrawable(extension.icon) - } - if (extension.hasUpdate) { - holder.closeTextView.text = "Update" - holder.closeTextView.setTextColor( - ContextCompat.getColor( - holder.itemView.context, - R.color.warning - ) - ) - } else { - holder.closeTextView.text = "Uninstall" - } - holder.closeTextView.setOnClickListener { - onUninstallClicked(extension) - } - } - - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) - val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) - val closeTextView: TextView = view.findViewById(R.id.closeTextView) - } - - companion object { - val DIFF_CALLBACK_INSTALLED = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: AnimeExtension.Installed, - newItem: AnimeExtension.Installed - ): Boolean { - return oldItem.pkgName == newItem.pkgName - } - - override fun areContentsTheSame( - oldItem: AnimeExtension.Installed, - newItem: AnimeExtension.Installed - ): Boolean { - return oldItem == newItem - } - } - } - } - - - private class AllAnimeExtensionsAdapter( - private val coroutineScope: CoroutineScope, - private val onButtonClicked: (AnimeExtension.Available) -> Unit, - skipIcons: Boolean - ) : ListAdapter( - DIFF_CALLBACK_AVAILABLE - ) { - val skipIcons = skipIcons - - fun updateData( - newExtensions: List, - installedExtensions: List = emptyList() - ) { - coroutineScope.launch(Dispatchers.Default) { - val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() - val filteredExtensions = newExtensions.filter { it.pkgName !in installedPkgNames } - - // Switch back to main thread to update UI - withContext(Dispatchers.Main) { - submitList(filteredExtensions) - } - } - } - - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): AllAnimeExtensionsAdapter.ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_extension_all, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val extension = getItem(position) - - holder.extensionNameTextView.text = extension.name - - if (!skipIcons) { - Glide.with(holder.itemView.context) - .load(extension.iconUrl) - .into(holder.extensionIconImageView) - } - - holder.closeTextView.text = "Install" - holder.closeTextView.setOnClickListener { - onButtonClicked(extension) - } - } - - fun filter(query: String) { - val filteredExtensions = if (query.isEmpty()) { - currentList - } else { - currentList.filter { it.name.contains(query, ignoreCase = true) } - } - submitList(filteredExtensions) - } - - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) - val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) - val closeTextView: TextView = view.findViewById(R.id.closeTextView) - } - - companion object { - val DIFF_CALLBACK_AVAILABLE = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: AnimeExtension.Available, - newItem: AnimeExtension.Available - ): Boolean { - return oldItem.pkgName == newItem.pkgName - } - - override fun areContentsTheSame( - oldItem: AnimeExtension.Available, - newItem: AnimeExtension.Available - ): Boolean { - return oldItem == newItem - } - } - } - } - } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index c319de5a..71a5f43b 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -70,12 +70,14 @@ class ExtensionsActivity : AppCompatActivity() { val viewPager = findViewById(R.id.viewPager) viewPager.adapter = object : FragmentStateAdapter(this) { - override fun getItemCount(): Int = 2 + override fun getItemCount(): Int = 4 override fun createFragment(position: Int): Fragment { return when (position) { - 0 -> AnimeExtensionsFragment() - 1 -> MangaExtensionsFragment() + 0 -> InstalledAnimeExtensionsFragment() + 1 -> AnimeExtensionsFragment() + 2 -> InstalledMangaExtensionsFragment() + 3 -> MangaExtensionsFragment() else -> AnimeExtensionsFragment() } } @@ -83,8 +85,10 @@ class ExtensionsActivity : AppCompatActivity() { TabLayoutMediator(tabLayout, viewPager) { tab, position -> tab.text = when (position) { - 0 -> "Anime" // Your tab title - 1 -> "Manga" // Your tab title + 0 -> "Installed Anime" + 1 -> "Available Anime" + 2 -> "Installed Manga" + 3 -> "Available Manga" else -> null } }.attach() diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt new file mode 100644 index 00000000..9294bd3c --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt @@ -0,0 +1,184 @@ +package ani.dantotsu.settings + +import android.app.NotificationManager +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R +import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding +import ani.dantotsu.loadData +import com.google.firebase.crashlytics.FirebaseCrashlytics +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension +import kotlinx.coroutines.launch +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class InstalledAnimeExtensionsFragment : Fragment() { + private var _binding: FragmentAnimeExtensionsBinding? = null + private val binding get() = _binding!! + private lateinit var extensionsRecyclerView: RecyclerView + val skipIcons = loadData("skip_extension_icons") ?: false + private val animeExtensionManager: AnimeExtensionManager = Injekt.get() + private val extensionsAdapter = AnimeExtensionsAdapter({ pkg -> + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() // Store context in a variable + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + + if (pkg.hasUpdate) { + animeExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + } + ) + } else { + animeExtensionManager.uninstallExtension(pkg.pkgName) + } + } + }, skipIcons) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false) + + extensionsRecyclerView = binding.allAnimeExtensionsRecyclerView + extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + extensionsRecyclerView.adapter = extensionsAdapter + + + lifecycleScope.launch { + animeExtensionManager.installedExtensionsFlow.collect { extensions -> + extensionsAdapter.updateData(extensions) + } + } + val extensionsRecyclerView: RecyclerView = binding.allAnimeExtensionsRecyclerView + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView();_binding = null + } + + + private class AnimeExtensionsAdapter( + private val onUninstallClicked: (AnimeExtension.Installed) -> Unit, + skipIcons: Boolean + ) : ListAdapter( + DIFF_CALLBACK_INSTALLED + ) { + + val skipIcons = skipIcons + + fun updateData(newExtensions: List) { + submitList(newExtensions) // Use submitList instead of manual list handling + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_extension, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val extension = getItem(position) // Use getItem() from ListAdapter + holder.extensionNameTextView.text = extension.name + if (!skipIcons) { + holder.extensionIconImageView.setImageDrawable(extension.icon) + } + if (extension.hasUpdate) { + holder.closeTextView.text = "Update" + holder.closeTextView.setTextColor( + ContextCompat.getColor( + holder.itemView.context, + R.color.warning + ) + ) + } else { + holder.closeTextView.text = "Uninstall" + } + holder.closeTextView.setOnClickListener { + onUninstallClicked(extension) + } + } + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) + val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) + val closeTextView: TextView = view.findViewById(R.id.closeTextView) + } + + companion object { + val DIFF_CALLBACK_INSTALLED = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: AnimeExtension.Installed, + newItem: AnimeExtension.Installed + ): Boolean { + return oldItem.pkgName == newItem.pkgName + } + + override fun areContentsTheSame( + oldItem: AnimeExtension.Installed, + newItem: AnimeExtension.Installed + ): Boolean { + return oldItem == newItem + } + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt new file mode 100644 index 00000000..42dd191e --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -0,0 +1,185 @@ +package ani.dantotsu.settings + + +import android.app.NotificationManager +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R +import ani.dantotsu.databinding.FragmentMangaExtensionsBinding +import ani.dantotsu.loadData +import com.google.firebase.crashlytics.FirebaseCrashlytics +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager +import eu.kanade.tachiyomi.extension.manga.model.MangaExtension +import kotlinx.coroutines.launch +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class InstalledMangaExtensionsFragment : Fragment() { + private var _binding: FragmentMangaExtensionsBinding? = null + private val binding get() = _binding!! + private lateinit var extensionsRecyclerView: RecyclerView + val skipIcons = loadData("skip_extension_icons") ?: false + private val mangaExtensionManager: MangaExtensionManager = Injekt.get() + private val extensionsAdapter = MangaExtensionsAdapter({ pkg -> + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() // Store context in a variable + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + + if (pkg.hasUpdate) { + mangaExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + } + ) + } else { + mangaExtensionManager.uninstallExtension(pkg.pkgName) + } + } + }, skipIcons) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false) + + extensionsRecyclerView = binding.allMangaExtensionsRecyclerView + extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + extensionsRecyclerView.adapter = extensionsAdapter + + + lifecycleScope.launch { + mangaExtensionManager.installedExtensionsFlow.collect { extensions -> + extensionsAdapter.updateData(extensions) + } + } + val extensionsRecyclerView: RecyclerView = binding.allMangaExtensionsRecyclerView + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView();_binding = null + } + + + private class MangaExtensionsAdapter( + private val onUninstallClicked: (MangaExtension.Installed) -> Unit, + skipIcons: Boolean + ) : ListAdapter( + DIFF_CALLBACK_INSTALLED + ) { + + val skipIcons = skipIcons + + fun updateData(newExtensions: List) { + submitList(newExtensions) // Use submitList instead of manual list handling + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_extension, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val extension = getItem(position) // Use getItem() from ListAdapter + holder.extensionNameTextView.text = extension.name + if (!skipIcons) { + holder.extensionIconImageView.setImageDrawable(extension.icon) + } + if (extension.hasUpdate) { + holder.closeTextView.text = "Update" + holder.closeTextView.setTextColor( + ContextCompat.getColor( + holder.itemView.context, + R.color.warning + ) + ) + } else { + holder.closeTextView.text = "Uninstall" + } + holder.closeTextView.setOnClickListener { + onUninstallClicked(extension) + } + } + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) + val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) + val closeTextView: TextView = view.findViewById(R.id.closeTextView) + } + + companion object { + val DIFF_CALLBACK_INSTALLED = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: MangaExtension.Installed, + newItem: MangaExtension.Installed + ): Boolean { + return oldItem.pkgName == newItem.pkgName + } + + override fun areContentsTheSame( + oldItem: MangaExtension.Installed, + newItem: MangaExtension.Installed + ): Boolean { + return oldItem == newItem + } + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt index 1058e021..0cc0d359 100644 --- a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt @@ -2,150 +2,46 @@ package ani.dantotsu.settings import android.app.NotificationManager import android.content.Context -import android.graphics.drawable.Drawable import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R -import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.databinding.FragmentMangaExtensionsBinding -import ani.dantotsu.loadData -import com.bumptech.glide.Glide import com.google.firebase.crashlytics.FirebaseCrashlytics import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.model.MangaExtension -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import ani.dantotsu.settings.paging.MangaExtensionAdapter +import ani.dantotsu.settings.paging.MangaExtensionsViewModel +import ani.dantotsu.settings.paging.MangaExtensionsViewModelFactory +import ani.dantotsu.settings.paging.OnMangaInstallClickListener +import kotlinx.coroutines.flow.collectLatest class MangaExtensionsFragment : Fragment(), - SearchQueryHandler { + SearchQueryHandler, OnMangaInstallClickListener { private var _binding: FragmentMangaExtensionsBinding? = null private val binding get() = _binding!! - val skipIcons = loadData("skip_extension_icons") ?: false + private val viewModel: MangaExtensionsViewModel by viewModels { + MangaExtensionsViewModelFactory(mangaExtensionManager) + } - private lateinit var extensionsRecyclerView: RecyclerView - private lateinit var allextenstionsRecyclerView: RecyclerView - private val mangaExtensionManager: MangaExtensionManager = Injekt.get() - private val extensionsAdapter = MangaExtensionsAdapter({ pkg -> - if (isAdded) { // Check if the fragment is currently added to its activity - val context = requireContext() // Store context in a variable - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + private val adapter by lazy { + MangaExtensionAdapter(this) + } - if (pkg.hasUpdate) { - mangaExtensionManager.updateExtension(pkg) - .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread - .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle("Updating extension") - .setContentText("Step: $installStep") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - FirebaseCrashlytics.getInstance().recordException(error) - Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle("Update failed") - .setContentText("Error: ${error.message}") - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) - .setContentTitle("Update complete") - .setContentText("The extension has been successfully updated.") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - } - ) - } else { - mangaExtensionManager.uninstallExtension(pkg.pkgName) - } - } - }, skipIcons) + private val mangaExtensionManager: MangaExtensionManager = Injekt.get() - private val allExtensionsAdapter = - AllMangaExtensionsAdapter(lifecycleScope, { pkgName -> - if (isAdded) { // Check if the fragment is currently added to its activity - val context = requireContext() - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - // Start the installation process - mangaExtensionManager.installExtension(pkgName) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle("Installing extension") - .setContentText("Step: $installStep") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - FirebaseCrashlytics.getInstance().recordException(error) - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle("Installation failed: ${error.message}") - .setContentText("Error: ${error.message}") - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) - .setContentTitle("Installation complete") - .setContentText("The extension has been successfully installed.") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - } - ) - } - }, skipIcons) override fun onCreateView( inflater: LayoutInflater, @@ -154,44 +50,70 @@ class MangaExtensionsFragment : Fragment(), ): View { _binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false) - extensionsRecyclerView = binding.mangaExtensionsRecyclerView - extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) - extensionsRecyclerView.adapter = extensionsAdapter - - allextenstionsRecyclerView = binding.allMangaExtensionsRecyclerView - allextenstionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) - allextenstionsRecyclerView.adapter = allExtensionsAdapter + binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = true + binding.allMangaExtensionsRecyclerView.adapter = adapter + binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context) + (binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false lifecycleScope.launch { - mangaExtensionManager.installedExtensionsFlow.collect { extensions -> - extensionsAdapter.updateData(extensions) + viewModel.pagerFlow.collectLatest { + adapter.submitData(it) } } - lifecycleScope.launch { - combine( - mangaExtensionManager.availableExtensionsFlow, - mangaExtensionManager.installedExtensionsFlow - ) { availableExtensions, installedExtensions -> - // Pair of available and installed extensions - Pair(availableExtensions, installedExtensions) - }.collect { pair -> - val (availableExtensions, installedExtensions) = pair - allExtensionsAdapter.updateData(availableExtensions, installedExtensions) - } - } - val extensionsRecyclerView: RecyclerView = binding.mangaExtensionsRecyclerView + return binding.root } override fun updateContentBasedOnQuery(query: String?) { - if (query.isNullOrEmpty()) { - allExtensionsAdapter.filter("") // Reset the filter - allextenstionsRecyclerView.visibility = View.VISIBLE - extensionsRecyclerView.visibility = View.VISIBLE - } else { - allExtensionsAdapter.filter(query) - allextenstionsRecyclerView.visibility = View.VISIBLE - extensionsRecyclerView.visibility = View.GONE + viewModel.setSearchQuery(query ?: "") + } + + override fun onInstallClick(pkg: MangaExtension.Available) { + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Start the installation process + mangaExtensionManager.installExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Installing extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Installation failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_download_24) + .setContentTitle("Installation complete") + .setContentText("The extension has been successfully installed.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + viewModel.invalidatePager() + } + ) } } @@ -199,165 +121,6 @@ class MangaExtensionsFragment : Fragment(), super.onDestroyView();_binding = null } - private class MangaExtensionsAdapter( - private val onUninstallClicked: (MangaExtension.Installed) -> Unit, - skipIcons: Boolean - ) : ListAdapter( - DIFF_CALLBACK_INSTALLED - ) { - val skipIcons = skipIcons - - // Use submitList to update data - fun updateData(newExtensions: List) { - submitList(newExtensions) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_extension, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val extension = getItem(position) // Use getItem from ListAdapter - - holder.extensionNameTextView.text = extension.name - if (!skipIcons) { - holder.extensionIconImageView.setImageDrawable(extension.icon) - } - - if (extension.hasUpdate) { - holder.closeTextView.text = "Update" - holder.closeTextView.setTextColor( - ContextCompat.getColor( - holder.itemView.context, - R.color.warning - ) - ) - } else { - holder.closeTextView.text = "Uninstall" - } - - holder.closeTextView.setOnClickListener { - onUninstallClicked(extension) - } - } - - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) - val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) - val closeTextView: TextView = view.findViewById(R.id.closeTextView) - } - - companion object { - val DIFF_CALLBACK_INSTALLED = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: MangaExtension.Installed, - newItem: MangaExtension.Installed - ): Boolean { - return oldItem.pkgName == newItem.pkgName - } - - override fun areContentsTheSame( - oldItem: MangaExtension.Installed, - newItem: MangaExtension.Installed - ): Boolean { - return oldItem == newItem - } - } - } - } - - - private class AllMangaExtensionsAdapter( - private val coroutineScope: CoroutineScope, - private val onButtonClicked: (MangaExtension.Available) -> Unit, - skipIcons: Boolean - ) : ListAdapter( - DIFF_CALLBACK_AVAILABLE - ) { - init { - setHasStableIds(true) - } - - - val skipIcons = skipIcons - - // Use submitList to update the data - fun updateData( - newExtensions: List, - installedExtensions: List = emptyList() - ) { - coroutineScope.launch(Dispatchers.Default) { - val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() - val filteredExtensions = newExtensions.filter { it.pkgName !in installedPkgNames } - - // Switch back to main thread to update UI - withContext(Dispatchers.Main) { - submitList(filteredExtensions) - } - } - } - - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_extension_all, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val extension = getItem(position) // Use getItem from ListAdapter - - holder.extensionNameTextView.text = extension.name - if (!skipIcons) { - Glide.with(holder.itemView.context) - .load(extension.iconUrl) - .into(holder.extensionIconImageView) - } - - holder.closeTextView.text = "Install" - holder.closeTextView.setOnClickListener { - onButtonClicked(extension) - } - } - - // Filtering function - fun filter(query: String) { - val filteredExtensions = if (query.isEmpty()) { - currentList - } else { - currentList.filter { it.name.contains(query, ignoreCase = true) } - } - submitList(filteredExtensions) - } - - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) - val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) - val closeTextView: TextView = view.findViewById(R.id.closeTextView) - } - - companion object { - val DIFF_CALLBACK_AVAILABLE = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: MangaExtension.Available, - newItem: MangaExtension.Available - ): Boolean { - return oldItem.pkgName == newItem.pkgName - } - - override fun areContentsTheSame( - oldItem: MangaExtension.Available, - newItem: MangaExtension.Available - ): Boolean { - return oldItem == newItem - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt new file mode 100644 index 00000000..653d85d6 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt @@ -0,0 +1,162 @@ +package ani.dantotsu.settings.paging + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.cachedIn +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.databinding.ItemExtensionAllBinding +import ani.dantotsu.loadData +import com.bumptech.glide.Glide +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest + + +class AnimeExtensionsViewModelFactory( + private val animeExtensionManager: AnimeExtensionManager +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return AnimeExtensionsViewModel(animeExtensionManager) as T + } +} + + +class AnimeExtensionsViewModel( + private val animeExtensionManager: AnimeExtensionManager +) : ViewModel() { + private val searchQuery = MutableStateFlow("") + private var currentPagingSource: AnimeExtensionPagingSource? = null + fun setSearchQuery(query: String) { + searchQuery.value = query + } + fun invalidatePager() { + currentPagingSource?.invalidate() + } + @OptIn(ExperimentalCoroutinesApi::class) + val pagerFlow: Flow> = searchQuery.flatMapLatest { query -> + Pager( + PagingConfig( + pageSize = 15, + initialLoadSize = 15, + prefetchDistance = 15 + ) + ) { + AnimeExtensionPagingSource( + animeExtensionManager.availableExtensionsFlow, + animeExtensionManager.installedExtensionsFlow, + searchQuery + ).also { currentPagingSource = it } + }.flow + }.cachedIn(viewModelScope) +} + +class AnimeExtensionPagingSource( + private val availableExtensionsFlow: StateFlow>, + private val installedExtensionsFlow: StateFlow>, + private val searchQuery: StateFlow +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val position = params.key ?: 0 + val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() + val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } + val query = searchQuery.first() + val filteredExtensions = if (query.isEmpty()) { + availableExtensions + } else { + availableExtensions.filter { it.name.contains(query, ignoreCase = true) } + } + + return try { + val sublist = filteredExtensions.subList( + fromIndex = position, + toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size) + ) + LoadResult.Page( + data = sublist, + prevKey = if (position == 0) null else position - params.loadSize, + nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return null + } +} + +class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListener) : + PagingDataAdapter( + DIFF_CALLBACK + ) { + + private val skipIcons = loadData("skip_extension_icons") ?: false + + companion object { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AnimeExtension.Available, newItem: AnimeExtension.Available): Boolean { + // Your logic here + return oldItem.pkgName == newItem.pkgName + } + + override fun areContentsTheSame(oldItem: AnimeExtension.Available, newItem: AnimeExtension.Available): Boolean { + // Your logic here + return oldItem == newItem + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeExtensionViewHolder { + val binding = ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return AnimeExtensionViewHolder(binding) + } + + override fun onBindViewHolder(holder: AnimeExtensionViewHolder, position: Int) { + val extension = getItem(position) + if (extension != null) { + if (!skipIcons) { + Glide.with(holder.itemView.context) + .load(extension.iconUrl) + .into(holder.extensionIconImageView) + } + holder.bind(extension) + } + } + + inner class AnimeExtensionViewHolder(private val binding: ItemExtensionAllBinding) : RecyclerView.ViewHolder(binding.root) { + init { + binding.closeTextView.setOnClickListener { + val extension = getItem(bindingAdapterPosition) + if (extension != null) { + clickListener.onInstallClick(extension) + } + } + } + val extensionIconImageView: ImageView = binding.extensionIconImageView + fun bind(extension: AnimeExtension.Available) { + binding.extensionNameTextView.text = extension.name + } + } +} + +interface OnAnimeInstallClickListener { + fun onInstallClick(pkg: AnimeExtension.Available) +} diff --git a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt new file mode 100644 index 00000000..511bd501 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt @@ -0,0 +1,166 @@ +package ani.dantotsu.settings.paging + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.cachedIn +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.databinding.ItemExtensionAllBinding +import ani.dantotsu.loadData +import com.bumptech.glide.Glide +import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager +import eu.kanade.tachiyomi.extension.manga.model.MangaExtension +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import java.lang.Math.min + +class MangaExtensionsViewModelFactory( + private val mangaExtensionManager: MangaExtensionManager +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return MangaExtensionsViewModel(mangaExtensionManager) as T + } +} + +class MangaExtensionsViewModel( + private val mangaExtensionManager: MangaExtensionManager +) : ViewModel() { + private val searchQuery = MutableStateFlow("") + private var currentPagingSource: MangaExtensionPagingSource? = null + + fun setSearchQuery(query: String) { + searchQuery.value = query + } + + fun invalidatePager() { + currentPagingSource?.invalidate() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val pagerFlow: Flow> = searchQuery.flatMapLatest { query -> + Pager( + PagingConfig( + pageSize = 15, + initialLoadSize = 15, + prefetchDistance = 15 + ) + ) { + MangaExtensionPagingSource( + mangaExtensionManager.availableExtensionsFlow, + mangaExtensionManager.installedExtensionsFlow, + searchQuery + ).also { currentPagingSource = it } + }.flow + }.cachedIn(viewModelScope) +} + + +class MangaExtensionPagingSource( + private val availableExtensionsFlow: StateFlow>, + private val installedExtensionsFlow: StateFlow>, + private val searchQuery: StateFlow +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val position = params.key ?: 0 + val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() + val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } + val query = searchQuery.first() + val filteredExtensions = if (query.isEmpty()) { + availableExtensions + } else { + availableExtensions.filter { it.name.contains(query, ignoreCase = true) } + } + + return try { + val sublist = filteredExtensions.subList( + fromIndex = position, + toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size) + ) + LoadResult.Page( + data = sublist, + prevKey = if (position == 0) null else position - params.loadSize, + nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return null + } +} + +class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListener) : + PagingDataAdapter( + DIFF_CALLBACK + ) { + + private val skipIcons = loadData("skip_extension_icons") ?: false + + companion object { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean { + // Your logic here + return oldItem.pkgName == newItem.pkgName + } + + override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean { + // Your logic here + return oldItem == newItem + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaExtensionViewHolder { + val binding = ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return MangaExtensionViewHolder(binding) + } + + override fun onBindViewHolder(holder: MangaExtensionViewHolder, position: Int) { + val extension = getItem(position) + if (extension != null) { + if (!skipIcons) { + Glide.with(holder.itemView.context) + .load(extension.iconUrl) + .into(holder.extensionIconImageView) + } + holder.bind(extension) + } + } + + inner class MangaExtensionViewHolder(private val binding: ItemExtensionAllBinding) : RecyclerView.ViewHolder(binding.root) { + init { + binding.closeTextView.setOnClickListener { + val extension = getItem(bindingAdapterPosition) + if (extension != null) { + clickListener.onInstallClick(extension) + } + } + } + val extensionIconImageView: ImageView = binding.extensionIconImageView + fun bind(extension: MangaExtension.Available) { + binding.extensionNameTextView.text = extension.name + } + } +} + +interface OnMangaInstallClickListener { + fun onInstallClick(pkg: MangaExtension.Available) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt index 5f04c1b5..fcb9603c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt @@ -11,7 +11,7 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable -data class Track(val url: String, val lang: String) +data class Track(val url: String, val lang: String) : Serializable open class Video( val url: String = "", diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt index fc555129..3dbdd8e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt @@ -108,8 +108,11 @@ internal class AnimeExtensionInstaller(private val context: Context) { // Get the current download status .map { downloadManager.query(query).use { cursor -> - cursor.moveToFirst() - cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + if (cursor.moveToFirst()) { + cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + } else { + DownloadManager.STATUS_FAILED + } } } // Ignore duplicate results diff --git a/app/src/main/res/layout/activity_manga_reader.xml b/app/src/main/res/layout/activity_manga_reader.xml index c84d80ab..8bd8e543 100644 --- a/app/src/main/res/layout/activity_manga_reader.xml +++ b/app/src/main/res/layout/activity_manga_reader.xml @@ -55,7 +55,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - app:cardBackgroundColor="#000000" + app:cardBackgroundColor="?attr/colorSurface" app:cardCornerRadius="16dp" app:contentPadding="8dp" app:strokeColor="?attr/colorSecondary" @@ -70,7 +70,7 @@ android:layout_marginEnd="8dp" android:fontFamily="@font/poppins_bold" android:text="@string/app_name" - android:textColor="?android:colorBackground" + android:textColor="?attr/colorOnSurface" android:textSize="16sp" /> @@ -93,7 +93,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" - app:cardBackgroundColor="#000000" + app:cardBackgroundColor="?attr/colorSurface" app:cardCornerRadius="16dp" app:contentPadding="8dp" app:strokeColor="?attr/colorSecondary" @@ -108,7 +108,7 @@ android:layout_marginEnd="8dp" android:fontFamily="@font/poppins_bold" android:text="@string/app_name" - android:textColor="?android:colorBackground" + android:textColor="?attr/colorOnSurface" android:textSize="16sp" /> @@ -268,7 +268,11 @@ android:layout_marginEnd="48dp" android:fontFamily="@font/poppins" android:singleLine="false" - android:textColor="?android:colorBackground" + android:textColor="@color/bg_white" + android:shadowColor="#000" + android:shadowDx="1" + android:shadowDy="1" + android:shadowRadius="1" android:textSize="12sp" tools:ignore="TextContrastCheck" tools:text="@string/popular_anime" /> diff --git a/app/src/main/res/layout/fragment_anime_extensions.xml b/app/src/main/res/layout/fragment_anime_extensions.xml index f10aebea..50c236c6 100644 --- a/app/src/main/res/layout/fragment_anime_extensions.xml +++ b/app/src/main/res/layout/fragment_anime_extensions.xml @@ -1,34 +1,15 @@ - + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingStart="32dp" + android:paddingEnd="32dp"> - + android:layout_height="0dp" + android:layout_weight="1" /> - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_manga_extensions.xml b/app/src/main/res/layout/fragment_manga_extensions.xml index 427da4ba..fb3a9d74 100644 --- a/app/src/main/res/layout/fragment_manga_extensions.xml +++ b/app/src/main/res/layout/fragment_manga_extensions.xml @@ -1,35 +1,15 @@ - + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingStart="32dp" + android:paddingEnd="32dp"> - + android:layout_height="0dp" + android:layout_weight="1" /> - - - - - - - - - +