manga "working" :D

This commit is contained in:
Finnley Somdahl 2023-10-20 01:44:36 -05:00
parent 57a584a820
commit 41b90e3a39
32 changed files with 1179 additions and 409 deletions

View file

@ -26,7 +26,7 @@ Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-o
| Type | Status | | Type | Status |
| ---------------- | ------- | | ---------------- | ------- |
| Anime Extensions | Working | | Anime Extensions | Working |
| Manga Extensions | Not Working | | Manga Extensions | "Working" |
| Light Novel Extensions | Not Working | | Light Novel Extensions | Not Working |

View file

@ -21,7 +21,7 @@ android {
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger()) versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "0.0.2" versionName "0.1.0"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

View file

@ -210,6 +210,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
<activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
<receiver <receiver
android:name=".subcriptions.AlarmReceiver" android:name=".subcriptions.AlarmReceiver"
@ -242,7 +251,10 @@
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
</service> </service>
<service android:name=".aniyomi.anime.util.AnimeExtensionInstallService" <service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false" />
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false" /> android:exported="false" />
</application> </application>

View file

@ -2,6 +2,7 @@ package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -17,6 +18,8 @@ import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -222,6 +225,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
} }
//ViewPager //ViewPager

View file

@ -1,6 +1,8 @@
package ani.dantotsu.aniyomi.anime.custom package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import ani.dantotsu.media.manga.MangaCache
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
@ -31,6 +33,8 @@ class AppModule(val app: Application) : InjektModule {
explicitNulls = false explicitNulls = false
} }
} }
addSingletonFactory { MangaCache() }
} }
} }

View file

@ -28,10 +28,18 @@ import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.R 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 com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class MediaDetailsViewModel : ViewModel() { class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true) val scrolledToTop = MutableLiveData(true)
@ -41,15 +49,34 @@ class MediaDetailsViewModel : ViewModel() {
} }
fun loadSelected(media: Media): Selected { fun loadSelected(media: Media): Selected {
return loadData<Selected>("${media.id}-select") ?: Selected().let { val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
it.source = if (media.isAdult) 0 else when (media.anime != null) { it.source = if (media.isAdult) "" else when (media.anime != null) {
true -> loadData("settings_def_anime_source") ?: 0 true -> loadData("settings_def_anime_source") ?: ""
else -> loadData("settings_def_manga_source") ?: 0 else -> loadData("settings_def_manga_source") ?: ""
} }
it.preferDub = loadData("settings_prefer_dub") ?: false it.preferDub = loadData("settings_prefer_dub") ?: false
it.sourceIndex = loadSelectedStringLocation(it.source)
saveSelected(media.id, it) saveSelected(media.id, it)
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 var continueMedia: Boolean? = null
@ -167,7 +194,8 @@ class MediaDetailsViewModel : ViewModel() {
val server = selected.server ?: return false val server = selected.server ?: return false
val link = ep.link ?: 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 if (!post && !it.allowsPreloading) null
else ep.sEpisode?.let { it1 -> else ep.sEpisode?.let { it1 ->
it.loadSingleVideoServer(server, link, ep.extra, it.loadSingleVideoServer(server, link, ep.extra,
@ -238,7 +266,7 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean { suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
return tryWithSuspend(true) { return tryWithSuspend(true) {
chapter.addImages( 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) if (post) mangaChapter.postValue(chapter)
true true
@ -261,7 +289,7 @@ class MediaDetailsViewModel : ViewModel() {
} }
suspend fun autoSearchNovels(media: Media) { suspend fun autoSearchNovels(media: Media) {
val source = novelSources[media.selected?.source ?: 0] val source = novelSources[loadSelectedStringLocation(media.selected?.source?:"")]
tryWithSuspend(post = true) { tryWithSuspend(post = true) {
if (source != null) { if (source != null) {
novelResponses.postValue(source.sortedSearch(media)) novelResponses.postValue(source.sortedSearch(media))

View file

@ -7,7 +7,8 @@ data class Selected(
var recyclerStyle: Int? = null, var recyclerStyle: Int? = null,
var recyclerReversed: Boolean = false, var recyclerReversed: Boolean = false,
var chip: Int = 0, var chip: Int = 0,
var source: Int = 0, var source: String = "",
var sourceIndex: Int = 0,
var preferDub: Boolean = false, var preferDub: Boolean = false,
var server: String? = null, var server: String? = null,
var video: Int = 0, var video: Int = 0,

View file

@ -57,7 +57,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
binding.searchRecyclerView.visibility = View.GONE binding.searchRecyclerView.visibility = View.GONE
binding.searchProgress.visibility = View.VISIBLE binding.searchProgress.visibility = View.VISIBLE
i = media!!.selected!!.source i = media!!.selected!!.sourceIndex
val source = if (media!!.anime != null) { val source = if (media!!.anime != null) {
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!] (if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]

View file

@ -68,7 +68,7 @@ class AnimeWatchAdapter(
} }
//Source Selection //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) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source]) binding.animeSource.setText(watchSources.names[source])
watchSources[source].apply { watchSources[source].apply {

View file

@ -130,7 +130,7 @@ class AnimeWatchFragment : Fragment() {
async { model.loadKitsuEpisodes(media) }, async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) } async { model.loadFillerEpisodes(media) }
) )
model.loadEpisodes(media, media.selected!!.source) model.loadEpisodes(media, media.selected!!.sourceIndex)
} }
loaded = true loaded = true
} else { } else {
@ -140,7 +140,7 @@ class AnimeWatchFragment : Fragment() {
} }
model.getEpisodes().observe(viewLifecycleOwner) { loadedEpisodes -> model.getEpisodes().observe(viewLifecycleOwner) { loadedEpisodes ->
if (loadedEpisodes != null) { if (loadedEpisodes != null) {
val episodes = loadedEpisodes[media.selected!!.source] val episodes = loadedEpisodes[media.selected!!.sourceIndex]
if (episodes != null) { if (episodes != null) {
episodes.forEach { (i, episode) -> episodes.forEach { (i, episode) ->
if (media.anime?.fillerEpisodes != null) { if (media.anime?.fillerEpisodes != null) {
@ -206,8 +206,8 @@ class AnimeWatchFragment : Fragment() {
media.anime?.episodes = null media.anime?.episodes = null
reload() reload()
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.watchSources?.get(selected.source)?.showUserTextListener = null model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
selected.source = i selected.sourceIndex = i
selected.server = null selected.server = null
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
media.selected = selected media.selected = selected
@ -216,11 +216,11 @@ class AnimeWatchFragment : Fragment() {
fun onDubClicked(checked: Boolean) { fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.watchSources?.get(selected.source)?.selectDub = checked model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
selected.preferDub = checked selected.preferDub = checked
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
media.selected = selected 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) { fun loadEpisodes(i: Int) {

View file

@ -817,7 +817,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources 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) { model.epChanged.observe(this) {
epChanging = !it epChanging = !it
@ -1353,7 +1353,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (media.selected!!.server != null) if (media.selected!!.server != null)
model.loadEpisodeSingleVideo(ep, selected, false) model.loadEpisodeSingleVideo(ep, selected, false)
else else
model.loadEpisodeVideos(ep, selected.source, false) model.loadEpisodeVideos(ep, selected.sourceIndex, false)
} }
} }
} }

View file

@ -135,7 +135,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadEpisodeVideos(ep, media!!.selected!!.source) model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
} }

View file

@ -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<String, ImageData>(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()
}

View file

@ -49,7 +49,7 @@ class MangaReadAdapter(
} }
//Source Selection //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) { if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source]) binding.animeSource.setText(mangaReadSources.names[source])

View file

@ -121,7 +121,7 @@ open class MangaReadFragment : Fragment() {
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.loadMangaChapters(media, media.selected!!.source) model.loadMangaChapters(media, media.selected!!.sourceIndex)
} }
loaded = true loaded = true
} else { } else {
@ -136,7 +136,7 @@ open class MangaReadFragment : Fragment() {
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters -> model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
if (loadedChapters != null) { if (loadedChapters != null) {
val chapters = loadedChapters[media.selected!!.source] val chapters = loadedChapters[media.selected!!.sourceIndex]
if (chapters != null) { if (chapters != null) {
media.manga?.chapters = chapters media.manga?.chapters = chapters
@ -177,8 +177,8 @@ open class MangaReadFragment : Fragment() {
media.manga?.chapters = null media.manga?.chapters = null
reload() reload()
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.mangaReadSources?.get(selected.source)?.showUserTextListener = null model.mangaReadSources?.get(selected.sourceIndex)?.showUserTextListener = null
selected.source = i selected.sourceIndex = i
selected.server = null selected.server = null
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
media.selected = selected media.selected = selected

View file

@ -14,15 +14,21 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.settings.CurrentReaderSettings import ani.dantotsu.settings.CurrentReaderSettings
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ani.dantotsu.media.manga.MangaCache
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class BaseImageAdapter( abstract class BaseImageAdapter(
val activity: MangaReaderActivity, val activity: MangaReaderActivity,
@ -44,10 +50,33 @@ abstract class BaseImageAdapter(
if (settings.layout != CurrentReaderSettings.Layouts.PAGED) { if (settings.layout != CurrentReaderSettings.Layouts.PAGED) {
if (settings.padding) { if (settings.padding) {
when (settings.direction) { when (settings.direction) {
CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding(0, 0, 0, 16f.px) CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding(
CurrentReaderSettings.Directions.LEFT_TO_RIGHT -> view.setPadding(0, 0, 16f.px, 0) 0,
CurrentReaderSettings.Directions.BOTTOM_TO_TOP -> view.setPadding(0, 16f.px, 0, 0) 0,
CurrentReaderSettings.Directions.RIGHT_TO_LEFT -> view.setPadding(16f.px, 0, 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 { view.updateLayoutParams {
@ -87,7 +116,7 @@ abstract class BaseImageAdapter(
abstract suspend fun loadImage(position: Int, parent: View): Boolean abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object { companion object {
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { /*suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
return tryWithSuspend { return tryWithSuspend {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap) Glide.with(this@loadBitmap)
@ -113,6 +142,43 @@ abstract class BaseImageAdapter(
.get() .get()
} }
} }
}*/
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
return tryWithSuspend {
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
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 { fun mergeBitmap(bitmap1: Bitmap, bitmap2: Bitmap, scale: Boolean = false): Bitmap {
@ -134,3 +200,7 @@ abstract class BaseImageAdapter(
} }
} }
interface ImageFetcher {
suspend fun fetchImage(page: Page): Bitmap?
}

View file

@ -30,6 +30,7 @@ import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityMangaReaderBinding import ani.dantotsu.databinding.ActivityMangaReaderBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
@ -47,12 +48,16 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
import kotlin.properties.Delegates import kotlin.properties.Delegates
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
class MangaReaderActivity : AppCompatActivity() { class MangaReaderActivity : AppCompatActivity() {
private val mangaCache = Injekt.get<MangaCache>()
private lateinit var binding: ActivityMangaReaderBinding private lateinit var binding: ActivityMangaReaderBinding
private val model: MediaDetailsViewModel by viewModels() private val model: MediaDetailsViewModel by viewModels()
private val scope = lifecycleScope private val scope = lifecycleScope
@ -106,6 +111,7 @@ class MangaReaderActivity : AppCompatActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
mangaCache.clear()
rpc?.close() rpc?.close()
super.onDestroy() super.onDestroy()
} }
@ -174,7 +180,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE 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 binding.mangaReaderTitle.text = media.userPreferredName
@ -205,6 +211,7 @@ class MangaReaderActivity : AppCompatActivity() {
//Chapter Change //Chapter Change
fun change(index: Int) { fun change(index: Int) {
mangaCache.clear()
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this) saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog") ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog")
} }
@ -258,7 +265,7 @@ class MangaReaderActivity : AppCompatActivity() {
type = RPC.Type.WATCHING type = RPC.Type.WATCHING
activityName = media.userPreferredName activityName = media.userPreferredName
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number) 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 -> media.cover?.let { cover ->
largeImage = RPC.Link(media.userPreferredName, cover) largeImage = RPC.Link(media.userPreferredName, cover)
} }
@ -691,7 +698,7 @@ class MangaReaderActivity : AppCompatActivity() {
} }
fun getTransformation(mangaImage: MangaImage): BitmapTransformation? { fun getTransformation(mangaImage: MangaImage): BitmapTransformation? {
return model.loadTransformation(mangaImage, media.selected!!.source) return model.loadTransformation(mangaImage, media.selected!!.sourceIndex)
} }
fun onImageLongClicked( fun onImageLongClicked(

View file

@ -35,7 +35,7 @@ class NovelReadAdapter(
fun search(): Boolean { fun search(): Boolean {
val query = binding.searchBarText.text.toString() 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 fragment.source = source
binding.searchBarText.clearFocus() binding.searchBarText.clearFocus()
@ -44,7 +44,7 @@ class NovelReadAdapter(
return true 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) { if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) {
binding.animeSource.setText(novelReadSources.names[source], false) binding.animeSource.setText(novelReadSources.names[source], false)
} }

View file

@ -67,7 +67,7 @@ class NovelReadFragment : Fragment() {
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter) binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter)
loaded = true loaded = true
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
search(searchQuery, sel?.source ?: 0, auto = sel?.server == null) search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null)
}, 100) }, 100)
} }
} }
@ -103,7 +103,7 @@ class NovelReadFragment : Fragment() {
fun onSourceChange(i: Int) { fun onSourceChange(i: Int) {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
selected.source = i selected.sourceIndex = i
source = i source = i
selected.server = null selected.server = null
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())

View file

@ -15,6 +15,8 @@ import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.logger 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.SEpisode
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.awaitSingle
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -39,6 +43,10 @@ import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.net.URL import java.net.URL
import java.net.URLDecoder 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 { class AniyomiAdapter {
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser { fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
@ -60,60 +68,58 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> { override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
val source = extension.sources.first() val source = extension.sources.first()
if (source is AnimeCatalogueSource) { if (source is AnimeCatalogueSource) {
var res: SEpisode? = null
try { try {
val res = source.getEpisodeList(sAnime) val res = source.getEpisodeList(sAnime)
var EpisodeList: List<Episode> = emptyList()
for (episode in res) { // Sort episodes by episode_number
println("episode: $episode") val sortedEpisodes = res.sortedBy { it.episode_number }
EpisodeList += SEpisodeToEpisode(episode)
} // Transform SEpisode objects to Episode objects
return EpisodeList
} return sortedEpisodes.map { SEpisodeToEpisode(it) }
catch (e: Exception) { } catch (e: Exception) {
println("Exception: $e") println("Exception: $e")
} }
return emptyList() return emptyList()
} }
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
} }
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> { override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
val source = extension.sources.first() val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
if (source is AnimeCatalogueSource) {
val video = source.getVideoList(sEpisode) return try {
var VideoList: List<VideoServer> = emptyList() val videos = source.getVideoList(sEpisode)
for (videoServer in video) { videos.map { VideoToVideoServer(it) }
VideoList += VideoToVideoServer(videoServer) } catch (e: Exception) {
logger("Exception occurred: ${e.message}")
emptyList()
} }
return VideoList
}
return emptyList()
} }
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? { override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
return VideoServerPassthrough(server) return VideoServerPassthrough(server)
} }
override suspend fun search(query: String): List<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first() val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
if (source is AnimeCatalogueSource) {
var res: AnimesPage? = null return try {
try { val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first() convertAnimesPageToShowResponse(res)
logger("res observable: $res") } catch (e: CloudflareBypassException) {
}
catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
//toast withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show() Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
} }
emptyList()
} catch (e: Exception) {
logger("General exception in search: $e")
emptyList()
}
}
val conv = convertAnimesPageToShowResponse(res!!)
return conv
}
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> { private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> {
return animesPage.animes.map { sAnime -> return animesPage.animes.map { sAnime ->
@ -161,6 +167,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} }
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed val extension: MangaExtension.Installed
init { init {
this.extension = extension this.extension = extension
@ -170,67 +177,127 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
override val hostUrl = extension.sources.first().name override val hostUrl = extension.sources.first().name
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> { override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
val source = extension.sources.first() val source = extension.sources.first() as? CatalogueSource ?: return emptyList()
if (source is CatalogueSource) {
try { return try {
val res = source.getChapterList(sManga) val res = source.getChapterList(sManga)
var chapterList: List<MangaChapter> = emptyList() val reversedRes = res.reversed()
for (chapter in res) { val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
chapterList += SChapterToMangaChapter(chapter)
}
logger("chapterList size: ${chapterList.size}") logger("chapterList size: ${chapterList.size}")
return chapterList logger("chapterList: ${chapterList[1].title}")
} logger("chapterList: ${chapterList[1].description}")
catch (e: Exception) { chapterList
} catch (e: Exception) {
logger("loadChapters Exception: $e") logger("loadChapters Exception: $e")
emptyList()
} }
return emptyList()
}
return emptyList() // Return an empty list if source is not a catalogueSource
} }
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> { override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val source = extension.sources.first() val source = extension.sources.first() as? HttpSource ?: return emptyList()
if (source is HttpSource) {
//try { return coroutineScope {
try {
val res = source.getPageList(sChapter) val res = source.getPageList(sChapter)
var chapterList: List<MangaImage> = emptyList() val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
for (page in res) {
println("page: $page") val deferreds = reIndexedPages.map { page ->
currContext()?.let { fetchAndProcessImage(page, source, it.contentResolver) } async(Dispatchers.IO) {
logger("new image url: ${page.imageUrl}") mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
chapterList += PageToMangaImage(page) pageToMangaImage(page)
} }
logger("image url: chapterList size: ${chapterList.size}")
return chapterList
//}
//catch (e: Exception) {
// logger("loadImages Exception: $e")
//}
return emptyList()
} }
return emptyList() // Return an empty list if source is not a CatalogueSource
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
emptyList()
} }
}
}
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch {
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)
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}")
}
}
}
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<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first() val source = extension.sources.first() as? HttpSource ?: return emptyList()
if (source is HttpSource) {
var res: MangasPage? = null return try {
try { val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
logger("res observable: $res") logger("res observable: $res")
} convertMangasPageToShowResponse(res)
catch (e: CloudflareBypassException) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show() Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
} }
emptyList()
} catch (e: Exception) {
logger("General exception in search: $e")
emptyList()
}
}
val conv = convertMangasPageToShowResponse(res!!)
return conv
}
return emptyList() // Return an empty list if source is not a CatalogueSource
}
private fun convertMangasPageToShowResponse(mangasPage: MangasPage): List<ShowResponse> { private fun convertMangasPageToShowResponse(mangasPage: MangasPage): List<ShowResponse> {
return mangasPage.mangas.map { sManga -> return mangasPage.mangas.map { sManga ->
@ -239,7 +306,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val link = sManga.url val link = sManga.url
val coverUrl = sManga.thumbnail_url ?: "" val coverUrl = sManga.thumbnail_url ?: ""
val otherNames = emptyList<String>() // Populate as needed val otherNames = emptyList<String>() // Populate as needed
val total = 20 val total = 1
val extra: Map<String, String>? = null // Populate as needed val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse // Create a new ShowResponse
@ -247,23 +314,32 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
} }
private fun PageToMangaImage(page: Page): MangaImage { private fun pageToMangaImage(page: Page): MangaImage {
//find and move any headers from page.imageUrl to headersMap var headersMap = mapOf<String, String>()
val headersMap: Map<String, String> = page.imageUrl?.split("&")?.mapNotNull { var urlWithoutHeaders = ""
val idx = it.indexOf("=") 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) { if (idx != -1) {
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8") try {
val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8") val key = URLDecoder.decode(part.substring(0, idx), "UTF-8")
val value = URLDecoder.decode(part.substring(idx + 1), "UTF-8")
Pair(key, value) Pair(key, value)
} else { } catch (e: UnsupportedEncodingException) {
null // Or some other default value null
} }
}?.toMap() ?: mapOf() } else {
val urlWithoutHeaders = page.imageUrl?.split("&")?.get(0) ?: "" null
val url = page.imageUrl ?: "" }
logger("Pageurl: $url") }.toMap()
logger("regularurl: ${page.url}") }
logger("regularurl: ${page.status}")
return MangaImage( return MangaImage(
FileUrl(url, headersMap), FileUrl(url, headersMap),
false, false,
@ -271,55 +347,81 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
) )
} }
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter { 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( return MangaChapter(
sChapter.name, number,
sChapter.url, sChapter.url,
sChapter.name, if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) {
parsedChapterTitle.third
} else {
sChapter.name
},
null, null,
sChapter sChapter
) )
} }
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
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() { class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer override val server: VideoServer
get() { get() = videoServer
return videoServer
}
override suspend fun extract(): VideoContainer { override suspend fun extract(): VideoContainer {
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) }) val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
var subList: List<Subtitle> = emptyList() val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
for(sub in videoServer.video?.subtitleTracks ?: emptyList()) {
subList += TrackToSubtitle(sub) return if (vidList.isNotEmpty()) {
} VideoContainer(vidList, subList)
if(vidList.isEmpty()) { } else {
throw Exception("No videos found") throw Exception("No videos found")
}else{
return VideoContainer(vidList, subList)
} }
} }
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video { private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
//try to find the number value from the .quality string // Find the number value from the .quality string
val regex = Regex("""\d+""") val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
val result = regex.find(aniVideo.quality)
val number = result?.value?.toInt() ?: 0 // Check for null video URL
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null") val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
val urlObj = URL(videoUrl) val urlObj = URL(videoUrl)
val path = urlObj.path val path = urlObj.path
val query = urlObj.query val query = urlObj.query
var format = getVideoType(path)
var format = when { if (format == null && query != null) {
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) {
val queryPairs: List<Pair<String, String>> = query.split("&").map { val queryPairs: List<Pair<String, String>> = query.split("&").map {
val idx = it.indexOf("=") val idx = it.indexOf("=")
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8") 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 // Assume the file is named under the "file" query parameter
val fileName = queryPairs.find { it.first == "file" }?.second ?: "" val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = when { format = getVideoType(fileName)
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
}
} }
// If the format is still undetermined, log an error or handle it appropriately // If the format is still undetermined, log an error or handle it appropriately
if (format == null) { if (format == null) {
logger("Unknown video format: $videoUrl") 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 { private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle {
return Subtitle(track.lang, track.url, type) return Subtitle(track.lang, track.url, type)
} }

View file

@ -1,5 +1,6 @@
package ani.dantotsu.parsers package ani.dantotsu.parsers
import android.graphics.Bitmap
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation

View file

@ -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<AnimeExtensionManager>()
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<AnimeExtensionsAdapter.ViewHolder>() {
private var extensions: List<AnimeExtension.Installed> = emptyList()
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
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<AllAnimeExtensionsAdapter.ViewHolder>() {
private var extensions: List<AnimeExtension.Available> = emptyList()
fun updateData(newExtensions: List<AnimeExtension.Available>, installedExtensions: List<AnimeExtension.Installed> = 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<AnimeExtension.Available> = 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
}
}
}
}
}

View file

@ -21,14 +21,21 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.databinding.ActivityExtensionsBinding import ani.dantotsu.databinding.ActivityExtensionsBinding
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.MangaFragment
import com.bumptech.glide.Glide 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 eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,7 +43,10 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import javax.inject.Inject
class ExtensionsActivity : AppCompatActivity() { class ExtensionsActivity : AppCompatActivity() {
@ -44,52 +54,10 @@ class ExtensionsActivity : AppCompatActivity() {
override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity) override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity)
} }
lateinit var binding: ActivityExtensionsBinding 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") @SuppressLint("SetTextI18n")
@ -98,35 +66,33 @@ class ExtensionsActivity : AppCompatActivity() {
binding = ActivityExtensionsBinding.inflate(layoutInflater) binding = ActivityExtensionsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
extensionsRecyclerView = findViewById(R.id.extensionsRecyclerView)
extensionsRecyclerView.layoutManager = LinearLayoutManager(this)
extensionsRecyclerView.adapter = extensionsAdapter
allextenstionsRecyclerView = findViewById(R.id.allExtensionsRecyclerView) val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
allextenstionsRecyclerView.layoutManager = LinearLayoutManager(this) val viewPager = findViewById<ViewPager2>(R.id.viewPager)
allextenstionsRecyclerView.adapter = allExtensionsAdapter
lifecycleScope.launch { viewPager.adapter = object : FragmentStateAdapter(this) {
animeExtensionManager.installedExtensionsFlow.collect { extensions -> override fun getItemCount(): Int = 2
extensionsAdapter.updateData(extensions)
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 searchView: SearchView = findViewById(R.id.searchView)
val extensionsRecyclerView: RecyclerView = findViewById(R.id.extensionsRecyclerView)
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader) val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
@ -134,17 +100,11 @@ class ExtensionsActivity : AppCompatActivity() {
} }
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
if (newText.isNullOrEmpty()) { val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
allExtensionsAdapter.filter("") // Reset the filter if (currentFragment is SearchQueryHandler) {
allextenstionsRecyclerView.visibility = View.VISIBLE currentFragment.updateContentBasedOnQuery(newText)
extensionsHeader.visibility = View.VISIBLE
extensionsRecyclerView.visibility = View.VISIBLE
} else {
allExtensionsAdapter.filter(newText)
allextenstionsRecyclerView.visibility = View.VISIBLE
extensionsRecyclerView.visibility = View.GONE
extensionsHeader.visibility = View.GONE
} }
return true return true
} }
}) })
@ -168,104 +128,8 @@ class ExtensionsActivity : AppCompatActivity() {
} }
}
private class ExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter<ExtensionsAdapter.ViewHolder>() { interface SearchQueryHandler {
fun updateContentBasedOnQuery(query: String?)
private var extensions: List<AnimeExtension.Installed> = emptyList()
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
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<AllExtensionsAdapter.ViewHolder>() {
private var extensions: List<AnimeExtension.Available> = emptyList()
fun updateData(newExtensions: List<AnimeExtension.Available>, installedExtensions: List<AnimeExtension.Installed> = 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<AnimeExtension.Available> = 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
}
}
}
}
} }

View file

@ -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<MangaExtensionManager>()
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<MangaExtensionsAdapter.ViewHolder>() {
private var extensions: List<MangaExtension.Installed> = emptyList()
fun updateData(newExtensions: List<MangaExtension.Installed>) {
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<AllMangaExtensionsAdapter.ViewHolder>() {
private var extensions: List<MangaExtension.Available> = emptyList()
fun updateData(newExtensions: List<MangaExtension.Available>, installedExtensions: List<MangaExtension.Installed> = 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<MangaExtension.Available> = 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
}
}
}
}
}

View file

@ -30,11 +30,14 @@ import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
import eu.kanade.domain.base.BasePreferences
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.random.Random import kotlin.random.Random
@ -43,6 +46,7 @@ class SettingsActivity : AppCompatActivity() {
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity) override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
} }
lateinit var binding: ActivitySettingsBinding lateinit var binding: ActivitySettingsBinding
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -88,14 +92,18 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
val animeSource = loadData<Int>("settings_def_anime_source")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0 val animeSourceName = loadData<String>("settings_def_anime_source") ?: AnimeSources.names[0]
if (MangaSources.names.isNotEmpty() && animeSource in 0 until MangaSources.names.size) { // Set the dropdown item in the UI if the name exists in the list.
binding.mangaSource.setText(MangaSources.names[animeSource], false) if (AnimeSources.names.contains(animeSourceName)) {
binding.animeSource.setText(animeSourceName, false)
} }
binding.animeSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, AnimeSources.names)) binding.animeSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, AnimeSources.names))
// Set up the item click listener for the dropdown.
binding.animeSource.setOnItemClickListener { _, _, i, _ -> 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() binding.animeSource.clearFocus()
} }
@ -114,6 +122,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
}.show() }.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.isChecked = loadData("sd_dl") ?: false
binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked -> binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { if (isChecked) {
@ -152,14 +169,22 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
saveData("settings_prefer_dub", isChecked) saveData("settings_prefer_dub", isChecked)
} }
val mangaSource = loadData<Int>("settings_def_manga_source")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0 // Load the saved manga source name from data storage.
if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) { val mangaSourceName = loadData<String>("settings_def_manga_source") ?: MangaSources.names[0]
binding.mangaSource.setText(MangaSources.names[mangaSource], false)
// 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)) binding.mangaSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, MangaSources.names))
// Set up the item click listener for the dropdown.
binding.mangaSource.setOnItemClickListener { _, _, i, _ -> 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() binding.mangaSource.clearFocus()
} }

View file

@ -14,14 +14,23 @@ import kotlinx.coroutines.withTimeoutOrNull
class SubscriptionHelper { class SubscriptionHelper {
companion object { companion object {
private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected { private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected {
return loadData<Selected>("${mediaId}-select", context) ?: Selected().let { val data = loadData<Selected>("${mediaId}-select", context) ?: Selected().let {
it.source = it.source =
if (isAdult) 0 if (isAdult) ""
else if (isAnime) loadData("settings_def_anime_source", context) ?: 0 else if (isAnime) {loadData("settings_def_anime_source", context) ?: ""}
else loadData("settings_def_manga_source", context) ?: 0 else loadData("settings_def_manga_source", context) ?: ""
it.preferDub = loadData("settings_prefer_dub", context) ?: false it.preferDub = loadData("settings_prefer_dub", context) ?: false
it 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) { private fun saveSelected(context: Context, mediaId: Int, data: Selected) {
@ -31,7 +40,9 @@ class SubscriptionHelper {
fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser { fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser {
val sources = if (isAdult) HAnimeSources else AnimeSources val sources = if (isAdult) HAnimeSources else AnimeSources
val selected = loadSelected(context, id, isAdult, true) 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 parser.selectDub = selected.preferDub
return parser return parser
} }
@ -58,7 +69,9 @@ class SubscriptionHelper {
fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser { fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser {
val sources = if (isAdult) HMangaSources else MangaSources val sources = if (isAdult) HMangaSources else MangaSources
val selected = loadSelected(context, id, isAdult, false) 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? { suspend fun getChapter(context: Context, parser: MangaParser, id: Int, isAdult: Boolean): MangaChapter? {

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -20,12 +20,12 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:cardBackgroundColor="@color/nav_bg_inv" app:cardBackgroundColor="@color/nav_bg_inv"
app:cardCornerRadius="16dp" app:cardCornerRadius="16dp"
app:cardElevation="0dp" app:cardElevation="0dp"
@ -33,8 +33,8 @@
<ImageButton <ImageButton
android:id="@+id/settingsBack" android:id="@+id/settingsBack"
android:layout_width="64dp" android:layout_width="80dp"
android:layout_height="64dp" android:layout_height="80dp"
android:background="@color/nav_bg_inv" android:background="@color/nav_bg_inv"
android:padding="16dp" android:padding="16dp"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24" app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
@ -42,13 +42,23 @@
tools:ignore="ContentDescription,SpeakableTextPresentCheck" /> tools:ignore="ContentDescription,SpeakableTextPresentCheck" />
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<SearchView <TextView
android:id="@+id/searchView" android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_gravity="center"
android:layoutDirection="rtl" android:textAlignment="center"
android:layout_gravity="center_vertical"/> android:layout_weight="1"
android:text="@string/extensions"
android:textSize="28sp" />
<ImageView
android:id="@+id/settingsLogo"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="bottom"
app:srcCompat="@drawable/anim_splash"
tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@ -57,57 +67,41 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
tools:ignore="UseCompoundDrawables"> tools:ignore="UseCompoundDrawables">
</LinearLayout>
<TextView <SearchView
android:layout_width="0dp" android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="32dp" android:layout_gravity="center_vertical"
android:layout_weight="1" android:layout_marginTop="0dp"
android:text="@string/extensions" android:layoutDirection="ltr" />
android:textSize="28sp" />
<ImageView
android:id="@+id/settingsLogo"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="bottom"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
app:srcCompat="@drawable/anim_splash"
tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>
</LinearLayout>
<LinearLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:animateLayoutChanges="true" app:tabMode="fixed"
android:clipToPadding="false" app:tabGravity="fill">
android:orientation="vertical" <com.google.android.material.tabs.TabItem
android:paddingStart="32dp" android:layout_width="wrap_content"
android:paddingEnd="32dp"> android:layout_height="wrap_content"
android:text="Anime"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Manga"/>
</com.google.android.material.tabs.TabLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.viewpager2.widget.ViewPager2
android:id="@+id/extensionsRecyclerView" android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="32dp" android:layout_weight="1"/>
android:layout_weight="1"
android:text="All Extensions"
android:textSize="28sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/allExtensionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -396,6 +396,41 @@
</ani.dantotsu.others.Xpandable> </ani.dantotsu.others.Xpandable>
<ani.dantotsu.others.Xpandable
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="64dp"
android:fontFamily="@font/poppins_bold"
android:gravity="center_vertical"
android:text="@string/extensions_settings"
android:textColor="?attr/colorSecondary"
app:drawableEndCompat="@drawable/ic_round_arrow_drop_down_24"
tools:ignore="TextContrastCheck" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/settingsForceLegacyInstall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:drawableStart="@drawable/ic_round_new_releases_24"
android:drawablePadding="16dp"
android:elegantTextHeight="true"
android:fontFamily="@font/poppins_bold"
android:minHeight="64dp"
android:text="@string/force_legacy_installer"
android:textAlignment="viewStart"
android:textColor="@color/bg_opp"
app:cornerRadius="0dp"
app:drawableTint="?attr/colorPrimary"
app:showText="false"
app:thumbTint="@color/button_switch_track" />
</ani.dantotsu.others.Xpandable>
<ani.dantotsu.others.Xpandable <ani.dantotsu.others.Xpandable
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

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

View file

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

View file

@ -31,7 +31,7 @@
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="@font/poppins_bold" android:fontFamily="@font/poppins_bold"
android:singleLine="true" android:singleLine="true"
android:text="@string/chap" android:text=""
android:textSize="14dp" android:textSize="14dp"
tools:ignore="SpUsage" /> tools:ignore="SpUsage" />
@ -39,7 +39,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold" android:fontFamily="@font/poppins_bold"
android:text="@string/colon" android:text=""
android:textSize="14dp" android:textSize="14dp"
tools:ignore="SpUsage" /> tools:ignore="SpUsage" />

View file

@ -622,5 +622,7 @@
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="view_anime">View Anime</string> <string name="view_anime">View Anime</string>
<string name="view_manga">View Manga</string> <string name="view_manga">View Manga</string>
<string name="force_legacy_installer">Force Legacy Installer</string>
<string name="extensions_settings">Extensions</string>
</resources> </resources>