offline novel

This commit is contained in:
Finnley Somdahl 2023-12-03 22:18:06 -06:00
parent 111fb16266
commit 3ded6ba87a
18 changed files with 512 additions and 212 deletions

View file

@ -48,6 +48,32 @@ class DownloadsManager(private val context: Context) {
saveDownloads()
}
fun removeMedia(title: String, type: Download.Type) {
val subDirectory = if (type == Download.Type.MANGA) {
"Manga"
} else if (type == Download.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
downloadsList.removeAll { it.title == title }
saveDownloads()
}
fun queryDownload(download: Download): Boolean {
return downloadsList.contains(download)
}

View file

@ -20,6 +20,7 @@ import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
@ -175,6 +176,7 @@ class MangaDownloaderService : Service() {
}
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
@ -221,7 +223,10 @@ class MangaDownloaderService : Service() {
}
farthest++
builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress(task.chapter, farthest * 100 / task.imageData.size)
broadcastDownloadProgress(
task.chapter,
farthest * 100 / task.imageData.size
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
@ -240,10 +245,22 @@ class MangaDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build())
saveMediaInfo(task)
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA))
downloadsManager.addDownload(
Download(
task.title,
task.chapter,
Download.Type.MANGA
)
)
broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
logger("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
broadcastDownloadFailed(task.chapter)
}
}

View file

@ -13,10 +13,12 @@ import ani.dantotsu.R
class OfflineMangaAdapter(
private val context: Context,
private val items: List<OfflineMangaModel>
private var items: List<OfflineMangaModel>,
private val searchListener: OfflineMangaSearchListener
) : BaseAdapter() {
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineMangaModel> = items
override fun getCount(): Int {
return items.size
@ -54,4 +56,22 @@ class OfflineMangaAdapter(
}
return view
}
fun onSearchQuery(query: String) {
// Implement the filtering logic here, for example:
items = if (query.isEmpty()) {
// Return the original list if the query is empty
originalItems
} else {
// Filter the list based on the query
originalItems.filter { it.title.contains(query, ignoreCase = true) }
}
notifyDataSetChanged() // Notify the adapter that the data set has changed
}
fun setItems(items: List<OfflineMangaModel>) {
this.items = items
this.originalItems = items
notifyDataSetChanged()
}
}

View file

@ -7,11 +7,14 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.widget.AutoCompleteTextView
import android.widget.GridView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
@ -41,7 +44,7 @@ import java.io.File
import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment : Fragment() {
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView
@ -79,15 +82,29 @@ class OfflineMangaFragment : Fragment() {
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onSearchQuery(s.toString())
}
})
gridView = view.findViewById(R.id.gridView)
getDownloads()
adapter = OfflineMangaAdapter(requireContext(), downloads)
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
gridView.adapter = adapter
gridView.setOnItemClickListener { parent, view, position, id ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloads.filter { it.title == item.title }.firstOrNull()
downloadManager.mangaDownloads.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
media?.let {
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
@ -99,9 +116,37 @@ class OfflineMangaFragment : Fragment() {
}
}
gridView.setOnItemLongClickListener { parent, view, position, id ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
Download.Type.MANGA
} else {
Download.Type.NOVEL
}
// Alert dialog to confirm deletion
val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.DialogTheme)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
adapter.setItems(downloads)
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
builder.show()
true
}
return view
}
override fun onSearchQuery(query: String) {
adapter.onSearchQuery(query)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight
@ -164,24 +209,42 @@ class OfflineMangaFragment : Fragment() {
}
private fun getDownloads() {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val newDownloads = mutableListOf<OfflineMangaModel>()
for (title in titles) {
downloads = listOf()
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newDownloads += offlineMangaModel
newMangaDownloads += offlineMangaModel
}
downloads = newDownloads
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val _downloads = downloadManager.novelDownloads.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
}
private fun getMedia(download: Download): Media? {
val type = if (download.type == Download.Type.MANGA) {
"Manga"
} else if (download.type == Download.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
"Dantotsu/$type/${download.title}"
)
//load media.json and convert to media class with gson
try {
return try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@ -189,19 +252,26 @@ class OfflineMangaFragment : Fragment() {
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
return gson.fromJson(mediaJson, Media::class.java)
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return null
null
}
}
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
val type = if (download.type == Download.Type.MANGA) {
"Manga"
} else if (download.type == Download.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
"Dantotsu/$type/${download.title}"
)
//load media.json and convert to media class with gson
try {
@ -215,8 +285,8 @@ class OfflineMangaFragment : Fragment() {
null
}
val title = mediaModel.nameMAL ?: "unknown"
val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else
if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing = false
val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
@ -228,3 +298,7 @@ class OfflineMangaFragment : Fragment() {
}
}
}
interface OfflineMangaSearchListener {
fun onSearchQuery(query: String)
}

View file

@ -174,7 +174,7 @@ class NovelDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
suspend fun isEpubFile(urlString: String): Boolean {
private suspend fun isEpubFile(urlString: String): Boolean {
return withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
@ -200,7 +200,12 @@ class NovelDownloaderService : Service() {
}
}
private fun isAlreadyDownloaded(urlString: String): Boolean {
return urlString.contains("file://")
}
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
@ -219,6 +224,12 @@ class NovelDownloaderService : Service() {
}
if (!isEpubFile(task.downloadLink)) {
if (isAlreadyDownloaded(task.originalLink)) {
logger("Already downloaded")
broadcastDownloadFinished(task.originalLink)
snackString("Already downloaded")
return@withContext
}
logger("Download link is not an .epub file")
broadcastDownloadFailed(task.originalLink)
snackString("Download link is not an .epub file")
@ -274,7 +285,8 @@ class NovelDownloaderService : Service() {
// Update progress at intervals
if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) {
withContext(Dispatchers.Main) {
val progress = (downloadedBytes * 100 / totalBytes).toInt()
val progress =
(downloadedBytes * 100 / totalBytes).toInt()
builder.setProgress(100, progress, false)
if (notifi) {
notificationManager.notify(
@ -287,7 +299,8 @@ class NovelDownloaderService : Service() {
}
if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) {
withContext(Dispatchers.Main) {
val progress = (downloadedBytes * 100 / totalBytes).toInt()
val progress =
(downloadedBytes * 100 / totalBytes).toInt()
logger("Download progress: $progress")
broadcastDownloadProgress(task.originalLink, progress)
}
@ -297,11 +310,14 @@ class NovelDownloaderService : Service() {
}
sink.close()
//if the file is smaller than 95% of totalBytes, it means the download was interrupted
if (file.length() < totalBytes * 0.95) {
throw IOException("Failed to download file: ${response.message}")
}
}
} catch (e: Exception) {
logger("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
logger("Exception while downloading .epub inside request: ${e.message}")
throw e
}
}
@ -313,10 +329,22 @@ class NovelDownloaderService : Service() {
}
saveMediaInfo(task)
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.NOVEL))
downloadsManager.addDownload(
Download(
task.title,
task.chapter,
Download.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
logger("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
broadcastDownloadFailed(task.originalLink)
}
}
private fun saveMediaInfo(task: DownloadTask) {

View file

@ -58,9 +58,12 @@ class MediaDetailsViewModel : ViewModel() {
it
}
if (isDownload) {
data.sourceIndex = when (media.anime != null) {
true -> AnimeSources.list.size - 1
else -> MangaSources.list.size - 1
data.sourceIndex = if (media.anime != null) {
AnimeSources.list.size - 1
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
MangaSources.list.size - 1
} else {
NovelSources.list.size - 1
}
}
return data

View file

@ -247,7 +247,8 @@ class NovelReadFragment : Fragment(),
headerAdapter.progress?.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.IO) {
if (auto || query == "") model.autoSearchNovels(media)
else model.searchNovels(query, source)
//else model.searchNovels(query, source)
else model.autoSearchNovels(media) //testing
}
searching = true
if (save) {

View file

@ -1,10 +1,12 @@
package ani.dantotsu.media.novel
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemNovelResponseBinding
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation
@ -39,6 +41,10 @@ class NovelResponseAdapter(
Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0)
.into(binding.itemEpisodeImage)
val typedValue = TypedValue()
fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true)
val color = typedValue.data
binding.itemEpisodeTitle.text = novel.name
binding.itemEpisodeFiller.text =
if (downloadedCheckCallback.downloadedCheck(novel)) {
@ -55,9 +61,7 @@ class NovelResponseAdapter(
fragment.requireContext().getColor(android.R.color.holo_green_light)
)
} else {
binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.white)
)
binding.itemEpisodeFiller.setTextColor(color)
}
binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: ""
val desc = novel.extra?.get("2")
@ -94,13 +98,21 @@ class NovelResponseAdapter(
}
binding.root.setOnLongClickListener {
val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme)
builder.setTitle("Delete ${novel.name}?")
builder.setMessage("Are you sure you want to delete ${novel.name}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadedCheckCallback.deleteDownload(novel)
deleteDownload(novel.link)
snackString("Deleted ${novel.name}")
if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) {
binding.itemEpisodeFiller.text = ""
}
notifyItemChanged(position)
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
builder.show()
true
}
}
@ -134,6 +146,8 @@ class NovelResponseAdapter(
downloadedChapters.remove(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "")
notifyItemChanged(position)
}
}
@ -143,6 +157,8 @@ class NovelResponseAdapter(
downloadedChapters.remove(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Failed")
notifyItemChanged(position)
}
}

View file

@ -31,8 +31,20 @@ abstract class NovelParser : BaseParser() {
}
suspend fun sortedSearch(mediaObj: Media): List<ShowResponse> {
val query = mediaObj.name ?: mediaObj.nameRomaji
return search(query).sortByVolume(query)
//val query = mediaObj.name ?: mediaObj.nameRomaji
//return search(query).sortByVolume(query)
val results: List<ShowResponse>
return if(mediaObj.name != null) {
val query = mediaObj.name
results = search(query).sortByVolume(query)
results.ifEmpty {
val q = mediaObj.nameRomaji
search(q).sortByVolume(q)
}
} else {
val query = mediaObj.nameRomaji
search(query).sortByVolume(query)
}
}
}

View file

@ -13,11 +13,17 @@ object NovelSources : NovelReadSources() {
suspend fun init(fromExtensions: StateFlow<List<NovelExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions)
list = createParsersFromExtensions(initialExtensions) + Lazier(
{ OfflineNovelParser() },
"Downloaded"
)
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions)
list = createParsersFromExtensions(extensions) + Lazier(
{ OfflineNovelParser() },
"Downloaded"
)
}
}

View file

@ -0,0 +1,86 @@
package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaNameAdapter
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineNovelParser: NovelParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
override val saveName: String = "Offline"
override val volumeRegex =
Regex("vol\\.? (\\d+(\\.\\d+)?)|volume (\\d+(\\.\\d+)?)", RegexOption.IGNORE_CASE)
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
//link should be a directory
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/$link"
)
val chapters = mutableListOf<Book>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = Book(
it.name,
it.absolutePath + "/cover.jpg",
null,
listOf(it.absolutePath + "/0.epub")
)
chapters.add(chapter)
}
}
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.name) }
return chapters.first()
}
return Book(
"error",
"",
null,
listOf("error")
)
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.novelDownloads.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
returnTitles.add(title)
}
}
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
//need to search the subdirectories for the ShowResponses
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/$title"
)
val names = mutableListOf<String>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
names.add(it.name)
}
}
}
val cover = currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
names.forEach {
returnList.add(ShowResponse(it, it, cover))
}
}
return returnList
}
}

View file

@ -41,6 +41,7 @@ class ExtensionsActivity : AppCompatActivity() {
val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
viewPager.offscreenPageLimit = 1
viewPager.adapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int = 6
@ -65,13 +66,24 @@ class ExtensionsActivity : AppCompatActivity() {
object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
searchView.setText("")
searchView.clearFocus()
tabLayout.clearFocus()
viewPager.updateLayoutParams<ViewGroup.LayoutParams> {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
// Do nothing
viewPager.updateLayoutParams<ViewGroup.LayoutParams> {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
tabLayout.clearFocus()
}
override fun onTabReselected(tab: TabLayout.Tab) {
viewPager.updateLayoutParams<ViewGroup.LayoutParams> {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
// Do nothing
}
}

View file

@ -11,6 +11,7 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil

View file

@ -3,6 +3,7 @@ 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
@ -58,6 +59,7 @@ class NovelExtensionsFragment : Fragment(),
lifecycleScope.launch {
viewModel.pagerFlow.collectLatest { pagingData ->
Log.d("NovelExtensionsFragment", "collectLatest")
adapter.submitData(pagingData)
}
}

View file

@ -54,7 +54,7 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
pref.isIconSpaceReserved = false
if (pref is DialogPreference) {
pref.dialogTitle = pref.title
//println("pref.dialogTitle: ${pref.dialogTitle}")
//println("pref.dialogTitle: ${pref.dialogTitle}") //TODO: could be useful for dub/sub selection
}
/*for (entry in sharedPreferences.all.entries) {
Log.d("Preferences", "Key: ${entry.key}, Value: ${entry.value}")

View file

@ -25,6 +25,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
@ -53,7 +54,13 @@ class NovelExtensionsViewModel(
}
@OptIn(ExperimentalCoroutinesApi::class)
val pagerFlow: Flow<PagingData<NovelExtension.Available>> = searchQuery.flatMapLatest { query ->
val pagerFlow: Flow<PagingData<NovelExtension.Available>> = combine(
novelExtensionManager.availableExtensionsFlow,
novelExtensionManager.installedExtensionsFlow,
searchQuery
) { available, installed, query ->
Triple(available, installed, query)
}.flatMapLatest { (available, installed, query) ->
Pager(
PagingConfig(
pageSize = 15,
@ -61,28 +68,24 @@ class NovelExtensionsViewModel(
prefetchDistance = 15
)
) {
NovelExtensionPagingSource(
novelExtensionManager.availableExtensionsFlow,
novelExtensionManager.installedExtensionsFlow,
searchQuery
).also { currentPagingSource = it }
NovelExtensionPagingSource(available, installed, query)
}.flow
}.cachedIn(viewModelScope)
}
class NovelExtensionPagingSource(
private val availableExtensionsFlow: StateFlow<List<NovelExtension.Available>>,
private val installedExtensionsFlow: StateFlow<List<NovelExtension.Installed>>,
private val searchQuery: StateFlow<String>
private val availableExtensionsFlow: List<NovelExtension.Available>,
private val installedExtensionsFlow: List<NovelExtension.Installed>,
private val searchQuery: String
) : PagingSource<Int, NovelExtension.Available>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, NovelExtension.Available> {
val position = params.key ?: 0
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
val installedExtensions = installedExtensionsFlow.map { it.pkgName }.toSet()
val availableExtensions =
availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
val query = searchQuery.first()
availableExtensionsFlow.filterNot { it.pkgName in installedExtensions }
val query = searchQuery
val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
val filteredExtensions = if (query.isEmpty()) {
availableExtensions

View file

@ -9,7 +9,7 @@
<LinearLayout
android:id="@+id/settingsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
@ -137,15 +137,7 @@
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/fragmentExtensionsContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
</FrameLayout>
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

View file

@ -50,9 +50,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:focusable="false"
android:fontFamily="@font/poppins_bold"
android:inputType="none"
android:imeOptions="actionSearch"
android:inputType="textPersonName"
android:selectAllOnFocus="true"
android:padding="8dp"
android:textSize="14sp"
tools:ignore="LabelFor,TextContrastCheck" />