diff --git a/README.md b/README.md index 8d85fcff..75d1dc16 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-o | Type | Status | | ---------------- | ------- | | Anime Extensions | Working | -| Manga Extensions | Not Working | +| Manga Extensions | "Working" | | Light Novel Extensions | Not Working | diff --git a/app/build.gradle b/app/build.gradle index 6e4f3192..9dca6e60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { minSdk 23 targetSdk 34 versionCode ((System.currentTimeMillis() / 60000).toInteger()) - versionName "0.0.2" + versionName "0.1.0" signingConfig signingConfigs.debug } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d87bab8f..c530597a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -210,6 +210,15 @@ + + + - + + diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index e81b05d2..f8dde199 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -2,6 +2,7 @@ package ani.dantotsu import android.animation.ObjectAnimator import android.content.Intent +import android.content.pm.PackageManager import android.graphics.drawable.Animatable import android.net.Uri import android.os.Build @@ -17,6 +18,8 @@ import androidx.activity.addCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.animation.doOnEnd +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.view.doOnAttach import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment @@ -222,6 +225,7 @@ class MainActivity : AppCompatActivity() { } } } + } //ViewPager diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt index bff74eeb..172a6ef1 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt @@ -1,6 +1,8 @@ package ani.dantotsu.aniyomi.anime.custom + import android.app.Application +import ani.dantotsu.media.manga.MangaCache import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import tachiyomi.core.preference.PreferenceStore import eu.kanade.domain.base.BasePreferences @@ -31,6 +33,8 @@ class AppModule(val app: Application) : InjektModule { explicitNulls = false } } + + addSingletonFactory { MangaCache() } } } diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index ea00c624..2381562a 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -28,10 +28,18 @@ import ani.dantotsu.snackString import ani.dantotsu.tryWithSuspend import ani.dantotsu.currContext import ani.dantotsu.R +import ani.dantotsu.parsers.AnimeSources +import ani.dantotsu.parsers.AniyomiAdapter +import ani.dantotsu.parsers.DynamicMangaParser +import ani.dantotsu.parsers.HAnimeSources +import ani.dantotsu.parsers.HMangaSources +import ani.dantotsu.parsers.MangaSources import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class MediaDetailsViewModel : ViewModel() { val scrolledToTop = MutableLiveData(true) @@ -41,15 +49,34 @@ class MediaDetailsViewModel : ViewModel() { } fun loadSelected(media: Media): Selected { - return loadData("${media.id}-select") ?: Selected().let { - it.source = if (media.isAdult) 0 else when (media.anime != null) { - true -> loadData("settings_def_anime_source") ?: 0 - else -> loadData("settings_def_manga_source") ?: 0 + val data = loadData("${media.id}-select") ?: Selected().let { + it.source = if (media.isAdult) "" else when (media.anime != null) { + true -> loadData("settings_def_anime_source") ?: "" + else -> loadData("settings_def_manga_source") ?: "" } it.preferDub = loadData("settings_prefer_dub") ?: false + it.sourceIndex = loadSelectedStringLocation(it.source) saveSelected(media.id, it) it } + if (media.anime != null) { + val sources = if (media.isAdult) HAnimeSources else AnimeSources + data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } + } else { + val sources = if (media.isAdult) HMangaSources else MangaSources + data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } + } + if (data.sourceIndex == -1) { + data.sourceIndex = 0 + } + return data + } + + fun loadSelectedStringLocation(sourceName: String): Int { + //find the location of the source in the list + var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0 + if (location == -1) {location = 0} + return location } var continueMedia: Boolean? = null @@ -167,7 +194,8 @@ class MediaDetailsViewModel : ViewModel() { val server = selected.server ?: return false val link = ep.link ?: return false - ep.extractors = mutableListOf(watchSources?.get(selected.source)?.let { + ep.extractors = mutableListOf(watchSources?.get(loadSelectedStringLocation(selected.source))?.let { + selected.sourceIndex = loadSelectedStringLocation(selected.source) if (!post && !it.allowsPreloading) null else ep.sEpisode?.let { it1 -> it.loadSingleVideoServer(server, link, ep.extra, @@ -238,7 +266,7 @@ class MediaDetailsViewModel : ViewModel() { suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean { return tryWithSuspend(true) { chapter.addImages( - mangaReadSources?.get(selected.source)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false + mangaReadSources?.get(loadSelectedStringLocation(selected.source))?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false ) if (post) mangaChapter.postValue(chapter) true @@ -261,7 +289,7 @@ class MediaDetailsViewModel : ViewModel() { } suspend fun autoSearchNovels(media: Media) { - val source = novelSources[media.selected?.source ?: 0] + val source = novelSources[loadSelectedStringLocation(media.selected?.source?:"")] tryWithSuspend(post = true) { if (source != null) { novelResponses.postValue(source.sortedSearch(media)) diff --git a/app/src/main/java/ani/dantotsu/media/Selected.kt b/app/src/main/java/ani/dantotsu/media/Selected.kt index 9eb8f44f..3849c272 100644 --- a/app/src/main/java/ani/dantotsu/media/Selected.kt +++ b/app/src/main/java/ani/dantotsu/media/Selected.kt @@ -7,7 +7,8 @@ data class Selected( var recyclerStyle: Int? = null, var recyclerReversed: Boolean = false, var chip: Int = 0, - var source: Int = 0, + var source: String = "", + var sourceIndex: Int = 0, var preferDub: Boolean = false, var server: String? = null, var video: Int = 0, diff --git a/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt index 163eb9c5..46d8454a 100644 --- a/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt @@ -57,7 +57,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() { binding.searchRecyclerView.visibility = View.GONE binding.searchProgress.visibility = View.VISIBLE - i = media!!.selected!!.source + i = media!!.selected!!.sourceIndex val source = if (media!!.anime != null) { (if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!] diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt index c9fc7c1f..4b214418 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -68,7 +68,7 @@ class AnimeWatchAdapter( } //Source Selection - val source = media.selected!!.source.let { if (it >= watchSources.names.size) 0 else it } + val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { binding.animeSource.setText(watchSources.names[source]) watchSources[source].apply { diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt index 9ed57e17..fc3893c2 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -130,7 +130,7 @@ class AnimeWatchFragment : Fragment() { async { model.loadKitsuEpisodes(media) }, async { model.loadFillerEpisodes(media) } ) - model.loadEpisodes(media, media.selected!!.source) + model.loadEpisodes(media, media.selected!!.sourceIndex) } loaded = true } else { @@ -140,7 +140,7 @@ class AnimeWatchFragment : Fragment() { } model.getEpisodes().observe(viewLifecycleOwner) { loadedEpisodes -> if (loadedEpisodes != null) { - val episodes = loadedEpisodes[media.selected!!.source] + val episodes = loadedEpisodes[media.selected!!.sourceIndex] if (episodes != null) { episodes.forEach { (i, episode) -> if (media.anime?.fillerEpisodes != null) { @@ -206,8 +206,8 @@ class AnimeWatchFragment : Fragment() { media.anime?.episodes = null reload() val selected = model.loadSelected(media) - model.watchSources?.get(selected.source)?.showUserTextListener = null - selected.source = i + model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null + selected.sourceIndex = i selected.server = null model.saveSelected(media.id, selected, requireActivity()) media.selected = selected @@ -216,11 +216,11 @@ class AnimeWatchFragment : Fragment() { fun onDubClicked(checked: Boolean) { val selected = model.loadSelected(media) - model.watchSources?.get(selected.source)?.selectDub = checked + model.watchSources?.get(selected.sourceIndex)?.selectDub = checked selected.preferDub = checked model.saveSelected(media.id, selected, requireActivity()) media.selected = selected - lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.source) } + lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } } fun loadEpisodes(i: Int) { 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 0fa803ce..10430b78 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -817,7 +817,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { } model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources - serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.source) ?: model.watchSources!!.names[0] + serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.sourceIndex) ?: model.watchSources!!.names[0] model.epChanged.observe(this) { epChanging = !it @@ -1353,7 +1353,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { if (media.selected!!.server != null) model.loadEpisodeSingleVideo(ep, selected, false) else - model.loadEpisodeVideos(ep, selected.source, false) + model.loadEpisodeVideos(ep, selected.sourceIndex, false) } } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index 536e66bc..d2903885 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -135,7 +135,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } scope.launch(Dispatchers.IO) { - model.loadEpisodeVideos(ep, media!!.selected!!.source) + model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex) withContext(Dispatchers.Main){ binding.selectorProgressBar.visibility = View.GONE } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt new file mode 100644 index 00000000..210e7b07 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt @@ -0,0 +1,64 @@ +package ani.dantotsu.media.manga + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.LruCache +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +data class ImageData( + val page: Page, + val source: HttpSource, +){ + suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource): Bitmap? { + return withContext(Dispatchers.IO) { + try { + // Fetch the image + val response = httpSource.getImage(page) + + // Convert the Response to an InputStream + val inputStream = response.body?.byteStream() + + // Convert InputStream to Bitmap + val bitmap = BitmapFactory.decodeStream(inputStream) + + inputStream?.close() + + return@withContext bitmap + } catch (e: Exception) { + // Handle any exceptions + println("An error occurred: ${e.message}") + return@withContext null + } + } + } +} + +class MangaCache() { + private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt() + private val cache = LruCache(maxMemory) + + @Synchronized + fun put(key: String, imageDate: ImageData) { + cache.put(key, imageDate) + } + + @Synchronized + fun get(key: String): ImageData? = cache.get(key) + + @Synchronized + fun remove(key: String) { + cache.remove(key) + } + + @Synchronized + fun clear() { + cache.evictAll() + } + + fun size(): Int = cache.size() + + +} diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt index 1cfa438f..1c119835 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -49,7 +49,7 @@ class MangaReadAdapter( } //Source Selection - val source = media.selected!!.source.let { if (it >= mangaReadSources.names.size) 0 else it } + val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { binding.animeSource.setText(mangaReadSources.names[source]) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index 26eaeea4..c8a83cca 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -121,7 +121,7 @@ open class MangaReadFragment : Fragment() { binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) lifecycleScope.launch(Dispatchers.IO) { - model.loadMangaChapters(media, media.selected!!.source) + model.loadMangaChapters(media, media.selected!!.sourceIndex) } loaded = true } else { @@ -136,7 +136,7 @@ open class MangaReadFragment : Fragment() { model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters -> if (loadedChapters != null) { - val chapters = loadedChapters[media.selected!!.source] + val chapters = loadedChapters[media.selected!!.sourceIndex] if (chapters != null) { media.manga?.chapters = chapters @@ -177,8 +177,8 @@ open class MangaReadFragment : Fragment() { media.manga?.chapters = null reload() val selected = model.loadSelected(media) - model.mangaReadSources?.get(selected.source)?.showUserTextListener = null - selected.source = i + model.mangaReadSources?.get(selected.sourceIndex)?.showUserTextListener = null + selected.sourceIndex = i selected.server = null model.saveSelected(media.id, selected, requireActivity()) media.selected = selected 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 9bfe3c1d..a9a41547 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 @@ -14,15 +14,21 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.* import ani.dantotsu.media.manga.MangaChapter +import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.settings.CurrentReaderSettings import com.alexvasilkov.gestures.views.GestureFrameLayout import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import ani.dantotsu.media.manga.MangaCache +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get abstract class BaseImageAdapter( val activity: MangaReaderActivity, @@ -44,10 +50,33 @@ abstract class BaseImageAdapter( if (settings.layout != CurrentReaderSettings.Layouts.PAGED) { if (settings.padding) { when (settings.direction) { - CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding(0, 0, 0, 16f.px) - CurrentReaderSettings.Directions.LEFT_TO_RIGHT -> view.setPadding(0, 0, 16f.px, 0) - CurrentReaderSettings.Directions.BOTTOM_TO_TOP -> view.setPadding(0, 16f.px, 0, 0) - CurrentReaderSettings.Directions.RIGHT_TO_LEFT -> view.setPadding(16f.px, 0, 0, 0) + CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding( + 0, + 0, + 0, + 16f.px + ) + + CurrentReaderSettings.Directions.LEFT_TO_RIGHT -> view.setPadding( + 0, + 0, + 16f.px, + 0 + ) + + CurrentReaderSettings.Directions.BOTTOM_TO_TOP -> view.setPadding( + 0, + 16f.px, + 0, + 0 + ) + + CurrentReaderSettings.Directions.RIGHT_TO_LEFT -> view.setPadding( + 16f.px, + 0, + 0, + 0 + ) } } view.updateLayoutParams { @@ -87,7 +116,7 @@ abstract class BaseImageAdapter( abstract suspend fun loadImage(position: Int, parent: View): Boolean companion object { - suspend fun Context.loadBitmap(link: FileUrl, transforms: List): Bitmap? { + /*suspend fun Context.loadBitmap(link: FileUrl, transforms: List): Bitmap? { return tryWithSuspend { withContext(Dispatchers.IO) { Glide.with(this@loadBitmap) @@ -113,6 +142,43 @@ abstract class BaseImageAdapter( .get() } } + }*/ + + suspend fun Context.loadBitmap(link: FileUrl, transforms: List): Bitmap? { + return tryWithSuspend { + val mangaCache = uy.kohesive.injekt.Injekt.get() + withContext(Dispatchers.IO) { + Glide.with(this@loadBitmap) + .asBitmap() + .let { + if (link.url.startsWith("file://")) { + it.load(link.url) + .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) + it.load(bitmap) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + } + } + } + ?.let { + if (transforms.isNotEmpty()) { + it.transform(*transforms.toTypedArray()) + } else { + it + } + } + ?.submit() + ?.get() + } + } } fun mergeBitmap(bitmap1: Bitmap, bitmap2: Bitmap, scale: Boolean = false): Bitmap { @@ -133,4 +199,8 @@ abstract class BaseImageAdapter( } } +} + +interface ImageFetcher { + suspend fun fetchImage(page: Page): Bitmap? } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index 4b95d8b5..484fce01 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -30,6 +30,7 @@ import ani.dantotsu.connections.updateProgress import ani.dantotsu.databinding.ActivityMangaReaderBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsViewModel +import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.getSerialized @@ -47,12 +48,16 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.* import kotlin.math.min import kotlin.properties.Delegates @SuppressLint("SetTextI18n") class MangaReaderActivity : AppCompatActivity() { + private val mangaCache = Injekt.get() + private lateinit var binding: ActivityMangaReaderBinding private val model: MediaDetailsViewModel by viewModels() private val scope = lifecycleScope @@ -106,6 +111,7 @@ class MangaReaderActivity : AppCompatActivity() { } override fun onDestroy() { + mangaCache.clear() rpc?.close() super.onDestroy() } @@ -174,7 +180,7 @@ class MangaReaderActivity : AppCompatActivity() { model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE - binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.source] + binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex] binding.mangaReaderTitle.text = media.userPreferredName @@ -205,6 +211,7 @@ class MangaReaderActivity : AppCompatActivity() { //Chapter Change fun change(index: Int) { + mangaCache.clear() saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this) ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog") } @@ -258,7 +265,7 @@ class MangaReaderActivity : AppCompatActivity() { type = RPC.Type.WATCHING activityName = media.userPreferredName details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number) - state = "Chapter : ${chap.number}/${media.manga?.totalChapters ?: "??"}" + state = "${chap.number}/${media.manga?.totalChapters ?: "??"}" media.cover?.let { cover -> largeImage = RPC.Link(media.userPreferredName, cover) } @@ -691,7 +698,7 @@ class MangaReaderActivity : AppCompatActivity() { } fun getTransformation(mangaImage: MangaImage): BitmapTransformation? { - return model.loadTransformation(mangaImage, media.selected!!.source) + return model.loadTransformation(mangaImage, media.selected!!.sourceIndex) } fun onImageLongClicked( diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt index 3c9ef53d..dd7a79a1 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt @@ -35,7 +35,7 @@ class NovelReadAdapter( fun search(): Boolean { val query = binding.searchBarText.text.toString() - val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it } + val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } fragment.source = source binding.searchBarText.clearFocus() @@ -44,7 +44,7 @@ class NovelReadAdapter( return true } - val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it } + val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) { binding.animeSource.setText(novelReadSources.names[source], false) } diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt index 03f78ff7..e6c62b24 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt @@ -67,7 +67,7 @@ class NovelReadFragment : Fragment() { binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter) loaded = true Handler(Looper.getMainLooper()).postDelayed({ - search(searchQuery, sel?.source ?: 0, auto = sel?.server == null) + search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null) }, 100) } } @@ -103,7 +103,7 @@ class NovelReadFragment : Fragment() { fun onSourceChange(i: Int) { val selected = model.loadSelected(media) - selected.source = i + selected.sourceIndex = i source = i selected.server = null model.saveSelected(media.id, selected, requireActivity()) diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index a93607c6..6bfb01ac 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -15,6 +15,8 @@ import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import ani.dantotsu.currContext import ani.dantotsu.logger +import ani.dantotsu.media.manga.ImageData +import ani.dantotsu.media.manga.MangaCache import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimesPage @@ -29,9 +31,11 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.util.lang.awaitSingle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File @@ -39,6 +43,10 @@ import java.io.FileOutputStream import java.io.OutputStream import java.net.URL import java.net.URLDecoder +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.* +import java.io.UnsupportedEncodingException +import java.util.regex.Pattern class AniyomiAdapter { fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser { @@ -60,61 +68,59 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { override suspend fun loadEpisodes(animeLink: String, extra: Map?, sAnime: SAnime): List { val source = extension.sources.first() if (source is AnimeCatalogueSource) { - var res: SEpisode? = null try { val res = source.getEpisodeList(sAnime) - var EpisodeList: List = emptyList() - for (episode in res) { - println("episode: $episode") - EpisodeList += SEpisodeToEpisode(episode) - } - return EpisodeList - } - catch (e: Exception) { + + // Sort episodes by episode_number + val sortedEpisodes = res.sortedBy { it.episode_number } + + // Transform SEpisode objects to Episode objects + + return sortedEpisodes.map { SEpisodeToEpisode(it) } + } catch (e: Exception) { println("Exception: $e") } return emptyList() } return emptyList() // Return an empty list if source is not an AnimeCatalogueSource } + override suspend fun loadVideoServers(episodeLink: String, extra: Map?, sEpisode: SEpisode): List { - val source = extension.sources.first() - if (source is AnimeCatalogueSource) { - val video = source.getVideoList(sEpisode) - var VideoList: List = emptyList() - for (videoServer in video) { - VideoList += VideoToVideoServer(videoServer) - } - return VideoList + val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList() + + return try { + val videos = source.getVideoList(sEpisode) + videos.map { VideoToVideoServer(it) } + } catch (e: Exception) { + logger("Exception occurred: ${e.message}") + emptyList() } - return emptyList() } + override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? { return VideoServerPassthrough(server) } override suspend fun search(query: String): List { - val source = extension.sources.first() - if (source is AnimeCatalogueSource) { + val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList() - var res: AnimesPage? = null - try { - res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first() - logger("res observable: $res") - } - catch (e: CloudflareBypassException) { - logger("Exception in search: $e") - //toast + return try { + val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first() + convertAnimesPageToShowResponse(res) + } catch (e: CloudflareBypassException) { + logger("Exception in search: $e") + withContext(Dispatchers.Main) { Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show() } - - val conv = convertAnimesPageToShowResponse(res!!) - return conv + emptyList() + } catch (e: Exception) { + logger("General exception in search: $e") + emptyList() } - return emptyList() // Return an empty list if source is not an AnimeCatalogueSource } + private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List { return animesPage.animes.map { sAnime -> // Extract required fields from sAnime @@ -161,6 +167,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { } class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { + val mangaCache = Injekt.get() val extension: MangaExtension.Installed init { this.extension = extension @@ -170,68 +177,128 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { override val hostUrl = extension.sources.first().name override suspend fun loadChapters(mangaLink: String, extra: Map?, sManga: SManga): List { - val source = extension.sources.first() - if (source is CatalogueSource) { - try { - val res = source.getChapterList(sManga) - var chapterList: List = emptyList() - for (chapter in res) { - chapterList += SChapterToMangaChapter(chapter) - } - logger("chapterList size: ${chapterList.size}") - return chapterList - } - catch (e: Exception) { - logger("loadChapters Exception: $e") - } - return emptyList() + val source = extension.sources.first() as? CatalogueSource ?: return emptyList() + + return try { + val res = source.getChapterList(sManga) + val reversedRes = res.reversed() + val chapterList = reversedRes.map { SChapterToMangaChapter(it) } + logger("chapterList size: ${chapterList.size}") + logger("chapterList: ${chapterList[1].title}") + logger("chapterList: ${chapterList[1].description}") + chapterList + } catch (e: Exception) { + logger("loadChapters Exception: $e") + emptyList() } - return emptyList() // Return an empty list if source is not a catalogueSource } + override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List { - val source = extension.sources.first() - if (source is HttpSource) { - //try { + val source = extension.sources.first() as? HttpSource ?: return emptyList() + + return coroutineScope { + try { val res = source.getPageList(sChapter) - var chapterList: List = emptyList() - for (page in res) { - println("page: $page") - currContext()?.let { fetchAndProcessImage(page, source, it.contentResolver) } - logger("new image url: ${page.imageUrl}") - chapterList += PageToMangaImage(page) + val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) } + + val deferreds = reIndexedPages.map { page -> + async(Dispatchers.IO) { + mangaCache.put(page.imageUrl ?: "", ImageData(page, source)) + pageToMangaImage(page) + } } - logger("image url: chapterList size: ${chapterList.size}") - return chapterList - //} - //catch (e: Exception) { - // logger("loadImages Exception: $e") - //} - return emptyList() + + deferreds.awaitAll() + } catch (e: Exception) { + logger("loadImages Exception: $e") + emptyList() + } } - return emptyList() // Return an empty list if source is not a CatalogueSource } - - override suspend fun search(query: String): List { - val source = extension.sources.first() - if (source is HttpSource) { - var res: MangasPage? = null + + + fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) { + CoroutineScope(Dispatchers.IO).launch { try { - res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first() - logger("res observable: $res") + // Fetch the image + val response = httpSource.getImage(page) + + // Convert the Response to an InputStream + val inputStream = response.body?.byteStream() + + // Convert InputStream to Bitmap + val bitmap = BitmapFactory.decodeStream(inputStream) + + withContext(Dispatchers.IO) { + // Save the Bitmap using MediaStore API + saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100) + } + + inputStream?.close() + } catch (e: Exception) { + // Handle any exceptions + println("An error occurred: ${e.message}") } - catch (e: CloudflareBypassException) { - logger("Exception in search: $e") + } + } + + fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") + put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime") + } + + val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + uri?.let { + contentResolver.openOutputStream(it)?.use { os -> + bitmap.compress(format, quality, os) + } + } + } else { + val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime") + if (!directory.exists()) { + directory.mkdirs() + } + + val file = File(directory, filename) + FileOutputStream(file).use { outputStream -> + bitmap.compress(format, quality, outputStream) + } + } + } catch (e: Exception) { + // Handle exception here + println("Exception while saving image: ${e.message}") + } + } + + + + override suspend fun search(query: String): List { + val source = extension.sources.first() as? HttpSource ?: return emptyList() + + return try { + val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first() + logger("res observable: $res") + convertMangasPageToShowResponse(res) + } catch (e: CloudflareBypassException) { + logger("Exception in search: $e") + withContext(Dispatchers.Main) { Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show() } - - val conv = convertMangasPageToShowResponse(res!!) - return conv + emptyList() + } catch (e: Exception) { + logger("General exception in search: $e") + emptyList() } - return emptyList() // Return an empty list if source is not a CatalogueSource } + private fun convertMangasPageToShowResponse(mangasPage: MangasPage): List { return mangasPage.mangas.map { sManga -> // Extract required fields from sManga @@ -239,7 +306,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { val link = sManga.url val coverUrl = sManga.thumbnail_url ?: "" val otherNames = emptyList() // Populate as needed - val total = 20 + val total = 1 val extra: Map? = null // Populate as needed // Create a new ShowResponse @@ -247,23 +314,32 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { } } - private fun PageToMangaImage(page: Page): MangaImage { - //find and move any headers from page.imageUrl to headersMap - val headersMap: Map = page.imageUrl?.split("&")?.mapNotNull { - val idx = it.indexOf("=") - if (idx != -1) { - val key = URLDecoder.decode(it.substring(0, idx), "UTF-8") - val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8") - Pair(key, value) - } else { - null // Or some other default value - } - }?.toMap() ?: mapOf() - val urlWithoutHeaders = page.imageUrl?.split("&")?.get(0) ?: "" - val url = page.imageUrl ?: "" - logger("Pageurl: $url") - logger("regularurl: ${page.url}") - logger("regularurl: ${page.status}") + private fun pageToMangaImage(page: Page): MangaImage { + var headersMap = mapOf() + var urlWithoutHeaders = "" + var url = "" + + page.imageUrl?.let { + val splitUrl = it.split("&") + urlWithoutHeaders = splitUrl.getOrNull(0) ?: "" + url = it + + headersMap = splitUrl.mapNotNull { part -> + val idx = part.indexOf("=") + if (idx != -1) { + try { + val key = URLDecoder.decode(part.substring(0, idx), "UTF-8") + val value = URLDecoder.decode(part.substring(idx + 1), "UTF-8") + Pair(key, value) + } catch (e: UnsupportedEncodingException) { + null + } + } else { + null + } + }.toMap() + } + return MangaImage( FileUrl(url, headersMap), false, @@ -271,55 +347,81 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { ) } + private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter { + val parsedChapterTitle = parseChapterTitle(sChapter.name) + val number = if (sChapter.chapter_number.toInt() != -1){ + sChapter.chapter_number.toString() + } else if(parsedChapterTitle.first != null || parsedChapterTitle.second != null){ + (parsedChapterTitle.first ?: "") + "." + (parsedChapterTitle.second ?: "") + }else{ + sChapter.name + } return MangaChapter( - sChapter.name, + number, sChapter.url, - sChapter.name, + if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) { + parsedChapterTitle.third + } else { + sChapter.name + }, null, sChapter ) } + fun parseChapterTitle(title: String): Triple { + val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE) + val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE) + + val volumeMatcher = volumePattern.matcher(title) + val chapterMatcher = chapterPattern.matcher(title) + + val volumeNumber = if (volumeMatcher.find()) volumeMatcher.group(1) else null + val chapterNumber = if (chapterMatcher.find()) chapterMatcher.group(1) else null + + var remainingTitle = title + if (volumeNumber != null) { + remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString() + } + if (chapterNumber != null) { + remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString() + } + + return Triple(volumeNumber, chapterNumber, remainingTitle.trim()) + } + } class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { override val server: VideoServer - get() { - return videoServer - } + get() = videoServer override suspend fun extract(): VideoContainer { val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) }) - var subList: List = emptyList() - for(sub in videoServer.video?.subtitleTracks ?: emptyList()) { - subList += TrackToSubtitle(sub) - } - if(vidList.isEmpty()) { + val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList() + + return if (vidList.isNotEmpty()) { + VideoContainer(vidList, subList) + } else { throw Exception("No videos found") - }else{ - return VideoContainer(vidList, subList) } } private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video { - //try to find the number value from the .quality string - val regex = Regex("""\d+""") - val result = regex.find(aniVideo.quality) - val number = result?.value?.toInt() ?: 0 + // Find the number value from the .quality string + val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0 + + // Check for null video URL val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null") + val urlObj = URL(videoUrl) val path = urlObj.path val query = urlObj.query + var format = getVideoType(path) - var format = when { - path.endsWith(".mp4", ignoreCase = true) || videoUrl.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER - path.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8 - path.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH - else -> null - } - if (format == null) { + if (format == null && query != null) { val queryPairs: List> = query.split("&").map { val idx = it.indexOf("=") val key = URLDecoder.decode(it.substring(0, idx), "UTF-8") @@ -330,13 +432,9 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { // Assume the file is named under the "file" query parameter val fileName = queryPairs.find { it.first == "file" }?.second ?: "" - format = when { - fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER - fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8 - fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH - else -> null - } + format = getVideoType(fileName) } + // If the format is still undetermined, log an error or handle it appropriately if (format == null) { logger("Unknown video format: $videoUrl") @@ -353,6 +451,15 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { ) } + private fun getVideoType(fileName: String): VideoType? { + return when { + fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER + fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8 + fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH + else -> null + } + } + private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle { return Subtitle(track.lang, track.url, type) } diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt index 81de230b..492d1088 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt @@ -1,5 +1,6 @@ package ani.dantotsu.parsers +import android.graphics.Bitmap import ani.dantotsu.FileUrl import ani.dantotsu.media.Media import com.bumptech.glide.load.resource.bitmap.BitmapTransformation diff --git a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt new file mode 100644 index 00000000..8db88476 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt @@ -0,0 +1,236 @@ +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.getSystemService +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R +import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding +import com.bumptech.glide.Glide +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.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 { + private var _binding: FragmentAnimeExtensionsBinding? = null + private val binding get() = _binding!! + + private lateinit var extensionsRecyclerView: RecyclerView + private lateinit var allextenstionsRecyclerView: RecyclerView + private val animeExtensionManager: AnimeExtensionManager = Injekt.get() + private val extensionsAdapter = AnimeExtensionsAdapter { pkgName -> + animeExtensionManager.uninstallExtension(pkgName) + } + private val allExtensionsAdapter = AllAnimeExtensionsAdapter(lifecycleScope) { pkgName -> + + val notificationManager = + requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Start the installation process + animeExtensionManager.installExtension(pkgName) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + requireContext(), + 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 -> + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Installation failed") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + requireContext(), + 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()) + } + ) + } + 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 + println("asdf: ${allExtensionsAdapter.getItemCount()}") + } else { + allExtensionsAdapter.filter(query) + allextenstionsRecyclerView.visibility = View.VISIBLE + extensionsRecyclerView.visibility = View.GONE + } + } + + override fun onDestroyView() { + super.onDestroyView();_binding = null + } + + + private class AnimeExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter() { + + private var extensions: List = emptyList() + + fun updateData(newExtensions: List) { + extensions = newExtensions + notifyDataSetChanged() + } + + 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 = extensions[position] + holder.extensionNameTextView.text = extension.name + holder.extensionIconImageView.setImageDrawable(extension.icon) + holder.closeTextView.text = "Uninstall" + holder.closeTextView.setOnClickListener { + onUninstallClicked(extension.pkgName) + } + } + + override fun getItemCount(): Int = extensions.size + + 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) + } + } + + private class AllAnimeExtensionsAdapter(private val coroutineScope: CoroutineScope, + private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter() { + private var extensions: List = emptyList() + + fun updateData(newExtensions: List, installedExtensions: List = emptyList()) { + val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() + extensions = newExtensions.filter { it.pkgName !in installedPkgNames } + filteredExtensions = extensions + notifyDataSetChanged() + } + + 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 = filteredExtensions[position] + holder.extensionNameTextView.text = extension.name + coroutineScope.launch { + val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) + holder.extensionIconImageView.setImageDrawable(drawable) + } + holder.closeTextView.text = "Install" + holder.closeTextView.setOnClickListener { + onButtonClicked(extension) + } + } + + override fun getItemCount(): Int = filteredExtensions.size + + private var filteredExtensions: List = emptyList() + + fun filter(query: String) { + filteredExtensions = if (query.isEmpty()) { + extensions + } else { + extensions.filter { it.name.contains(query, ignoreCase = true) } + } + notifyDataSetChanged() + } + + 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) + } + + suspend fun urlToDrawable(context: Context, url: String): Drawable? { + return withContext(Dispatchers.IO) { + try { + return@withContext Glide.with(context) + .load(url) + .submit() + .get() + } catch (e: Exception) { + e.printStackTrace() + return@withContext null + } + } + } + } +} \ 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 8f4a8184..9bd7f041 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -21,14 +21,21 @@ import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams +import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import ani.dantotsu.databinding.ActivityExtensionsBinding +import ani.dantotsu.home.AnimeFragment +import ani.dantotsu.home.MangaFragment import com.bumptech.glide.Glide +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator import eu.kanade.tachiyomi.data.notification.Notifications import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,7 +43,10 @@ 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 uy.kohesive.injekt.injectLazy +import javax.inject.Inject class ExtensionsActivity : AppCompatActivity() { @@ -44,52 +54,10 @@ class ExtensionsActivity : AppCompatActivity() { override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity) } lateinit var binding: ActivityExtensionsBinding - private lateinit var extensionsRecyclerView: RecyclerView - private lateinit var allextenstionsRecyclerView: RecyclerView - private val animeExtensionManager: AnimeExtensionManager by injectLazy() - private val extensionsAdapter = ExtensionsAdapter { pkgName -> - animeExtensionManager.uninstallExtension(pkgName) - } - private val allExtensionsAdapter = AllExtensionsAdapter(lifecycleScope) { pkgName -> - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - // Start the installation process - animeExtensionManager.installExtension(pkgName) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { installStep -> - val builder = NotificationCompat.Builder(this, - 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 -> - val builder = NotificationCompat.Builder(this, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle("Installation failed") - .setContentText("Error: ${error.message}") - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - }, - { - val builder = NotificationCompat.Builder(this, - 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()) - } - ) - } + + @SuppressLint("SetTextI18n") @@ -98,35 +66,33 @@ class ExtensionsActivity : AppCompatActivity() { binding = ActivityExtensionsBinding.inflate(layoutInflater) setContentView(binding.root) - extensionsRecyclerView = findViewById(R.id.extensionsRecyclerView) - extensionsRecyclerView.layoutManager = LinearLayoutManager(this) - extensionsRecyclerView.adapter = extensionsAdapter - allextenstionsRecyclerView = findViewById(R.id.allExtensionsRecyclerView) - allextenstionsRecyclerView.layoutManager = LinearLayoutManager(this) - allextenstionsRecyclerView.adapter = allExtensionsAdapter + val tabLayout = findViewById(R.id.tabLayout) + val viewPager = findViewById(R.id.viewPager) - lifecycleScope.launch { - animeExtensionManager.installedExtensionsFlow.collect { extensions -> - extensionsAdapter.updateData(extensions) + viewPager.adapter = object : FragmentStateAdapter(this) { + override fun getItemCount(): Int = 2 + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> AnimeExtensionsFragment() + 1 -> MangaExtensionsFragment() + else -> AnimeExtensionsFragment() + } } } - 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) + + TabLayoutMediator(tabLayout, viewPager) { tab, position -> + tab.text = when (position) { + 0 -> "Anime" // Your tab title + 1 -> "Manga" // Your tab title + else -> null } - } + }.attach() val searchView: SearchView = findViewById(R.id.searchView) - val extensionsRecyclerView: RecyclerView = findViewById(R.id.extensionsRecyclerView) + val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader) searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { @@ -134,17 +100,11 @@ class ExtensionsActivity : AppCompatActivity() { } override fun onQueryTextChange(newText: String?): Boolean { - if (newText.isNullOrEmpty()) { - allExtensionsAdapter.filter("") // Reset the filter - allextenstionsRecyclerView.visibility = View.VISIBLE - extensionsHeader.visibility = View.VISIBLE - extensionsRecyclerView.visibility = View.VISIBLE - } else { - allExtensionsAdapter.filter(newText) - allextenstionsRecyclerView.visibility = View.VISIBLE - extensionsRecyclerView.visibility = View.GONE - extensionsHeader.visibility = View.GONE + val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}") + if (currentFragment is SearchQueryHandler) { + currentFragment.updateContentBasedOnQuery(newText) } + return true } }) @@ -168,104 +128,8 @@ class ExtensionsActivity : AppCompatActivity() { } +} - - private class ExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter() { - - private var extensions: List = emptyList() - - fun updateData(newExtensions: List) { - extensions = newExtensions - notifyDataSetChanged() - } - - 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 = extensions[position] - holder.extensionNameTextView.text = extension.name - holder.extensionIconImageView.setImageDrawable(extension.icon) - holder.closeTextView.text = "Uninstall" - holder.closeTextView.setOnClickListener { - onUninstallClicked(extension.pkgName) - } - } - - override fun getItemCount(): Int = extensions.size - - 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) - } - } - - private class AllExtensionsAdapter(private val coroutineScope: CoroutineScope, - private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter() { - private var extensions: List = emptyList() - - fun updateData(newExtensions: List, installedExtensions: List = emptyList()) { - val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() - extensions = newExtensions.filter { it.pkgName !in installedPkgNames } - filteredExtensions = extensions - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AllExtensionsAdapter.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 = filteredExtensions[position] - holder.extensionNameTextView.text = extension.name - coroutineScope.launch { - val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) - holder.extensionIconImageView.setImageDrawable(drawable) - } - holder.closeTextView.text = "Install" - holder.closeTextView.setOnClickListener { - onButtonClicked(extension) - } - } - - override fun getItemCount(): Int = filteredExtensions.size - - private var filteredExtensions: List = emptyList() - - fun filter(query: String) { - filteredExtensions = if (query.isEmpty()) { - extensions - } else { - extensions.filter { it.name.contains(query, ignoreCase = true) } - } - notifyDataSetChanged() - } - - 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) - } - - suspend fun urlToDrawable(context: Context, url: String): Drawable? { - return withContext(Dispatchers.IO) { - try { - return@withContext Glide.with(context) - .load(url) - .submit() - .get() - } catch (e: Exception) { - e.printStackTrace() - return@withContext null - } - } - } - } - +interface SearchQueryHandler { + fun updateContentBasedOnQuery(query: String?) } \ 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 new file mode 100644 index 00000000..595fbe1f --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt @@ -0,0 +1,234 @@ +package ani.dantotsu.settings + +import android.app.NotificationManager +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Bundle +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.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R +import ani.dantotsu.databinding.FragmentMangaBinding +import ani.dantotsu.databinding.FragmentMangaExtensionsBinding +import com.bumptech.glide.Glide +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 + +class MangaExtensionsFragment : Fragment(), SearchQueryHandler { + private var _binding: FragmentMangaExtensionsBinding? = null + private val binding get() = _binding!! + + private lateinit var extensionsRecyclerView: RecyclerView + private lateinit var allextenstionsRecyclerView: RecyclerView + private val mangaExtensionManager:MangaExtensionManager = Injekt.get() + private val extensionsAdapter = MangaExtensionsAdapter { pkgName -> + mangaExtensionManager.uninstallExtension(pkgName) + } + private val allExtensionsAdapter = + AllMangaExtensionsAdapter(lifecycleScope) { pkgName -> + + val notificationManager = + requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Start the installation process + mangaExtensionManager.installExtension(pkgName) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + requireContext(), + 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 -> + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Installation failed") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + requireContext(), + 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()) + } + ) + } + 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: (String) -> Unit) : RecyclerView.Adapter() { + + private var extensions: List = emptyList() + + fun updateData(newExtensions: List) { + extensions = newExtensions + notifyDataSetChanged() + } + + 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 = extensions[position] + holder.extensionNameTextView.text = extension.name + holder.extensionIconImageView.setImageDrawable(extension.icon) + holder.closeTextView.text = "Uninstall" + holder.closeTextView.setOnClickListener { + onUninstallClicked(extension.pkgName) + } + } + + override fun getItemCount(): Int = extensions.size + + 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) + } + } + + private class AllMangaExtensionsAdapter(private val coroutineScope: CoroutineScope, + private val onButtonClicked: (MangaExtension.Available) -> Unit) : RecyclerView.Adapter() { + private var extensions: List = emptyList() + + fun updateData(newExtensions: List, installedExtensions: List = emptyList()) { + val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() + extensions = newExtensions.filter { it.pkgName !in installedPkgNames } + filteredExtensions = extensions + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AllMangaExtensionsAdapter.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 = filteredExtensions[position] + holder.extensionNameTextView.text = extension.name + coroutineScope.launch { + val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) + holder.extensionIconImageView.setImageDrawable(drawable) + } + holder.closeTextView.text = "Install" + holder.closeTextView.setOnClickListener { + onButtonClicked(extension) + } + } + + override fun getItemCount(): Int = filteredExtensions.size + + private var filteredExtensions: List = emptyList() + + fun filter(query: String) { + filteredExtensions = if (query.isEmpty()) { + extensions + } else { + extensions.filter { it.name.contains(query, ignoreCase = true) } + } + notifyDataSetChanged() + } + + 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) + } + + suspend fun urlToDrawable(context: Context, url: String): Drawable? { + return withContext(Dispatchers.IO) { + try { + return@withContext Glide.with(context) + .load(url) + .submit() + .get() + } catch (e: Exception) { + e.printStackTrace() + return@withContext null + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index 2e244652..68a663da 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -30,11 +30,14 @@ import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes +import eu.kanade.domain.base.BasePreferences import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import kotlin.random.Random @@ -43,6 +46,7 @@ class SettingsActivity : AppCompatActivity() { override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity) } lateinit var binding: ActivitySettingsBinding + private val extensionInstaller = Injekt.get().extensionInstaller() @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { @@ -88,14 +92,18 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) onBackPressedDispatcher.onBackPressed() } - val animeSource = loadData("settings_def_anime_source")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0 - if (MangaSources.names.isNotEmpty() && animeSource in 0 until MangaSources.names.size) { - binding.mangaSource.setText(MangaSources.names[animeSource], false) + val animeSourceName = loadData("settings_def_anime_source") ?: AnimeSources.names[0] + // Set the dropdown item in the UI if the name exists in the list. + if (AnimeSources.names.contains(animeSourceName)) { + binding.animeSource.setText(animeSourceName, false) } binding.animeSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, AnimeSources.names)) + // Set up the item click listener for the dropdown. binding.animeSource.setOnItemClickListener { _, _, i, _ -> - saveData("settings_def_anime_source", i) + val selectedName = AnimeSources.names[i] + // Save the string name of the selected item. + saveData("settings_def_anime_source", selectedName) binding.animeSource.clearFocus() } @@ -114,6 +122,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) }.show() } + binding.settingsForceLegacyInstall.isChecked = extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY + binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + extensionInstaller.set(BasePreferences.ExtensionInstaller.LEGACY) + }else{ + extensionInstaller.set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) + } + } + binding.settingsDownloadInSd.isChecked = loadData("sd_dl") ?: false binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { @@ -152,14 +169,22 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) saveData("settings_prefer_dub", isChecked) } - val mangaSource = loadData("settings_def_manga_source")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0 - if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) { - binding.mangaSource.setText(MangaSources.names[mangaSource], false) + // Load the saved manga source name from data storage. + val mangaSourceName = loadData("settings_def_manga_source") ?: MangaSources.names[0] + + // Set the dropdown item in the UI if the name exists in the list. + if (MangaSources.names.contains(mangaSourceName)) { + binding.mangaSource.setText(mangaSourceName, false) } + // Set up the dropdown adapter. binding.mangaSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, MangaSources.names)) + + // Set up the item click listener for the dropdown. binding.mangaSource.setOnItemClickListener { _, _, i, _ -> - saveData("settings_def_manga_source", i) + val selectedName = MangaSources.names[i] + // Save the string name of the selected item. + saveData("settings_def_manga_source", selectedName) binding.mangaSource.clearFocus() } diff --git a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt index 038f6598..0ed1b8e7 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt @@ -14,14 +14,23 @@ import kotlinx.coroutines.withTimeoutOrNull class SubscriptionHelper { companion object { private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected { - return loadData("${mediaId}-select", context) ?: Selected().let { + val data = loadData("${mediaId}-select", context) ?: Selected().let { it.source = - if (isAdult) 0 - else if (isAnime) loadData("settings_def_anime_source", context) ?: 0 - else loadData("settings_def_manga_source", context) ?: 0 + if (isAdult) "" + else if (isAnime) {loadData("settings_def_anime_source", context) ?: ""} + else loadData("settings_def_manga_source", context) ?: "" it.preferDub = loadData("settings_prefer_dub", context) ?: false it } + if (isAnime){ + val sources = if (isAdult) HAnimeSources else AnimeSources + data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } + }else{ + val sources = if (isAdult) HMangaSources else MangaSources + data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } + } + if (data.sourceIndex == -1) {data.sourceIndex = 0} + return data } private fun saveSelected(context: Context, mediaId: Int, data: Selected) { @@ -31,7 +40,9 @@ class SubscriptionHelper { fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser { val sources = if (isAdult) HAnimeSources else AnimeSources val selected = loadSelected(context, id, isAdult, true) - val parser = sources[selected.source] + var location = sources.list.indexOfFirst { it.name == selected.source } + if (location == -1) {location = 0} + val parser = sources[location] parser.selectDub = selected.preferDub return parser } @@ -58,7 +69,9 @@ class SubscriptionHelper { fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser { val sources = if (isAdult) HMangaSources else MangaSources val selected = loadSelected(context, id, isAdult, false) - return sources[selected.source] + var location = sources.list.indexOfFirst { it.name == selected.source } + if (location == -1) {location = 0} + return sources[location] } suspend fun getChapter(context: Context, parser: MangaParser, id: Int, isAdult: Boolean): MangaChapter? { diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index 790fdf63..9471cff1 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -1,5 +1,5 @@ - - + android:layout_gravity="center" + android:textAlignment="center" + android:layout_weight="1" + android:text="@string/extensions" + android:textSize="28sp" /> + + + - - - - - + - + android:layout_height="wrap_content" + app:tabMode="fixed" + app:tabGravity="fill"> + + + - + - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 651bc6a6..ef411f3c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -396,6 +396,41 @@ + + + + + + + + + + + + + + + + + + + + + \ 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 new file mode 100644 index 00000000..427da4ba --- /dev/null +++ b/app/src/main/res/layout/fragment_manga_extensions.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_chapter_list.xml b/app/src/main/res/layout/item_chapter_list.xml index 244c00ef..fc97efb6 100644 --- a/app/src/main/res/layout/item_chapter_list.xml +++ b/app/src/main/res/layout/item_chapter_list.xml @@ -31,7 +31,7 @@ android:ellipsize="end" android:fontFamily="@font/poppins_bold" android:singleLine="true" - android:text="@string/chap" + android:text="" android:textSize="14dp" tools:ignore="SpUsage" /> @@ -39,7 +39,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="@font/poppins_bold" - android:text="@string/colon" + android:text="" android:textSize="14dp" tools:ignore="SpUsage" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de0af621..a30d721f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -622,5 +622,7 @@ Warning View Anime View Manga + Force Legacy Installer + Extensions