subtitle, image, and extension page fixes

This commit is contained in:
Finnley Somdahl 2023-10-26 00:40:57 -05:00
parent 866bd3b3a9
commit 20bea76e6c
18 changed files with 895 additions and 670 deletions

View file

@ -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'

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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.")

View file

@ -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 lateinit var extensionsRecyclerView: RecyclerView
private lateinit var allextenstionsRecyclerView: RecyclerView
private val animeExtensionManager: AnimeExtensionManager = Injekt.get<AnimeExtensionManager>()
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())
private val viewModel: AnimeExtensionsViewModel by viewModels {
AnimeExtensionsViewModelFactory(animeExtensionManager)
}
)
} else {
animeExtensionManager.uninstallExtension(pkg.pkgName)
private val adapter by lazy {
AnimeExtensionAdapter(this)
}
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<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
val skipIcons = skipIcons
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
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<AnimeExtension.Installed>() {
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<AnimeExtension.Available, AllAnimeExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_AVAILABLE
) {
val skipIcons = skipIcons
fun updateData(
newExtensions: List<AnimeExtension.Available>,
installedExtensions: List<AnimeExtension.Installed> = 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<AnimeExtension.Available>() {
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
}
}
}
}
}

View file

@ -70,12 +70,14 @@ class ExtensionsActivity : AppCompatActivity() {
val viewPager = findViewById<ViewPager2>(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()

View file

@ -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<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
val skipIcons = skipIcons
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
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<AnimeExtension.Installed>() {
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
}
}
}
}
}

View file

@ -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<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
val skipIcons = skipIcons
fun updateData(newExtensions: List<MangaExtension.Installed>) {
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<MangaExtension.Installed>() {
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
}
}
}
}
}

View file

@ -2,111 +2,80 @@ 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 lateinit var extensionsRecyclerView: RecyclerView
private lateinit var allextenstionsRecyclerView: RecyclerView
private val mangaExtensionManager: MangaExtensionManager = Injekt.get<MangaExtensionManager>()
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")
.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())
private val viewModel: MangaExtensionsViewModel by viewModels {
MangaExtensionsViewModelFactory(mangaExtensionManager)
}
)
} else {
mangaExtensionManager.uninstallExtension(pkg.pkgName)
private val adapter by lazy {
MangaExtensionAdapter(this)
}
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = true
binding.allMangaExtensionsRecyclerView.adapter = adapter
binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false
lifecycleScope.launch {
viewModel.pagerFlow.collectLatest {
adapter.submitData(it)
}
}
}, skipIcons)
private val allExtensionsAdapter =
AllMangaExtensionsAdapter(lifecycleScope, { pkgName ->
return binding.root
}
override fun updateContentBasedOnQuery(query: String?) {
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(pkgName)
mangaExtensionManager.installExtension(pkg)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ installStep ->
@ -137,227 +106,21 @@ class MangaExtensionsFragment : 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 = 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
lifecycleScope.launch {
mangaExtensionManager.installedExtensionsFlow.collect { extensions ->
extensionsAdapter.updateData(extensions)
}
}
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
}
}
override fun onDestroyView() {
super.onDestroyView();_binding = null
}
private class MangaExtensionsAdapter(
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
skipIcons: Boolean
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
val skipIcons = skipIcons
// Use submitList to update data
fun updateData(newExtensions: List<MangaExtension.Installed>) {
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<MangaExtension.Installed>() {
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<MangaExtension.Available, AllMangaExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_AVAILABLE
) {
init {
setHasStableIds(true)
}
val skipIcons = skipIcons
// Use submitList to update the data
fun updateData(
newExtensions: List<MangaExtension.Available>,
installedExtensions: List<MangaExtension.Installed> = 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<MangaExtension.Available>() {
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
}
}
}
}
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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<PagingData<AnimeExtension.Available>> = 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<List<AnimeExtension.Available>>,
private val installedExtensionsFlow: StateFlow<List<AnimeExtension.Installed>>,
private val searchQuery: StateFlow<String>
) : PagingSource<Int, AnimeExtension.Available>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AnimeExtension.Available> {
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, AnimeExtension.Available>): Int? {
return null
}
}
class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListener) :
PagingDataAdapter<AnimeExtension.Available, AnimeExtensionAdapter.AnimeExtensionViewHolder>(
DIFF_CALLBACK
) {
private val skipIcons = loadData("skip_extension_icons") ?: false
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<AnimeExtension.Available>() {
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)
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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<PagingData<MangaExtension.Available>> = 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<List<MangaExtension.Available>>,
private val installedExtensionsFlow: StateFlow<List<MangaExtension.Installed>>,
private val searchQuery: StateFlow<String>
) : PagingSource<Int, MangaExtension.Available>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MangaExtension.Available> {
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, MangaExtension.Available>): Int? {
return null
}
}
class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListener) :
PagingDataAdapter<MangaExtension.Available, MangaExtensionAdapter.MangaExtensionViewHolder>(
DIFF_CALLBACK
) {
private val skipIcons = loadData("skip_extension_icons") ?: false
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MangaExtension.Available>() {
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)
}

View file

@ -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 = "",

View file

@ -108,8 +108,11 @@ internal class AnimeExtensionInstaller(private val context: Context) {
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
if (cursor.moveToFirst()) {
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
} else {
DownloadManager.STATUS_FAILED
}
}
}
// Ignore duplicate results

View file

@ -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" />
</com.google.android.material.card.MaterialCardView>
@ -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" />
</com.google.android.material.card.MaterialCardView>
@ -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" />

View file

@ -1,34 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="32dp"
android:paddingEnd="32dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/animeExtensionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="All Extensions"
android:textSize="28sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/allAnimeExtensionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,35 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="32dp"
android:paddingEnd="32dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mangaExtensionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="All Extensions"
android:textSize="28sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/allMangaExtensionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>