manga "working" :D
This commit is contained in:
parent
57a584a820
commit
41b90e3a39
32 changed files with 1179 additions and 409 deletions
|
@ -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 |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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!!]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
64
app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
Normal file
64
app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
Normal 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()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()) {
|
|
||||||
throw Exception("No videos found")
|
|
||||||
} else {
|
} else {
|
||||||
return VideoContainer(vidList, subList)
|
throw Exception("No videos found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>() {
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SearchQueryHandler {
|
||||||
|
fun updateContentBasedOnQuery(query: String?)
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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>
|
|
|
@ -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"
|
||||||
|
|
34
app/src/main/res/layout/fragment_anime_extensions.xml
Normal file
34
app/src/main/res/layout/fragment_anime_extensions.xml
Normal 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>
|
35
app/src/main/res/layout/fragment_manga_extensions.xml
Normal file
35
app/src/main/res/layout/fragment_manga_extensions.xml
Normal 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>
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue