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