Compare commits

..

6 commits
dev ... main

Author SHA1 Message Date
rebel onion
a93b4f5b11 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2025-05-14 21:40:08 -05:00
rebel onion
69c44b7d20 chore: formatting changes 2025-05-14 21:40:06 -05:00
Rishvaish
a684aac0b1
To install multiple mangas (#582)
users can enter the value required to install as there is an EditText field instead of the Text View
2025-04-02 10:40:39 +05:30
Daniele Santoru
6c49839f87
Fixed missing manga pages when downloading (#586) 2025-04-02 10:39:33 +05:30
rebel onion
7053a7b4b2
Update README.md 2025-01-16 20:27:24 -06:00
rebel onion
1c156053d0
Merge pull request #565 from rebelonion/dev
Dev
2025-01-16 00:15:34 -06:00
27 changed files with 262 additions and 569 deletions

View file

@ -14,6 +14,8 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>
## Terms of Use
By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws

View file

@ -17,9 +17,8 @@ android {
applicationId "ani.dantotsu"
minSdk 21
targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.2.2"
versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
versionCode 300200200
signingConfig signingConfigs.debug
}
@ -48,10 +47,6 @@ android {
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null
isDefault true
debuggable true
jniDebuggable true
minifyEnabled false
shrinkResources false
}
debug {
applicationIdSuffix ".beta"
@ -85,26 +80,25 @@ android {
dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:33.13.0')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.4.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.4.3'
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0'
// Core
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.fragment:fragment-ktx:1.8.6'
implementation 'androidx.activity:activity-ktx:1.10.1'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.work:work-runtime-ktx:2.10.1"
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.13.0'
implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
@ -118,7 +112,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.6.1'
ext.exo_version = '1.5.0'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@ -129,7 +123,7 @@ dependencies {
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0"
// Media3 extension
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4"
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
// UI
implementation 'com.google.android.material:material:1.12.0'
@ -138,7 +132,7 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.3.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
@ -167,13 +161,13 @@ dependencies {
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1'
implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.9.1'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.14'
implementation 'org.jsoup:jsoup:1.18.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.7.3'
implementation 'com.squareup.okio:okio:3.8.0'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
implementation 'org.jsoup:jsoup:1.16.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'

View file

@ -113,28 +113,21 @@ class App : MultiDexApplication() {
}
}
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get()
launch {
animeExtensionManager.findAvailableExtensions()
}
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
scope.launch {
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager = Injekt.get()
launch {
mangaExtensionManager.findAvailableExtensions()
}
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
scope.launch {
CoroutineScope(Dispatchers.IO).launch {
novelExtensionManager = Injekt.get()
launch {
novelExtensionManager.findAvailableExtensions()
}
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}

View file

@ -11,11 +11,7 @@ import com.lagradost.nicehttp.addGenericDns
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
@ -89,42 +85,12 @@ object Mapper : ResponseParser {
}
}
/**
* Performs parallel processing of collection items without blocking threads.
* Each operation runs in its own coroutine on the specified dispatcher.
*
* @param dispatcher The CoroutineDispatcher to use for parallel operations (defaults to IO)
* @param f The suspend function to apply to each item
* @return List of results in the same order as the original collection
*/
suspend fun <A, B> Collection<A>.asyncMap(
dispatcher: CoroutineDispatcher = Dispatchers.IO,
f: suspend (A) -> B
): List<B> = coroutineScope {
map { item ->
async(dispatcher) {
f(item)
}
}.awaitAll()
fun <A, B> Collection<A>.asyncMap(f: suspend (A) -> B): List<B> = runBlocking {
map { async { f(it) } }.map { it.await() }
}
/**
* Performs parallel processing of collection items without blocking threads,
* filtering out null results.
*
* @param dispatcher The CoroutineDispatcher to use for parallel operations (defaults to IO)
* @param f The suspend function to apply to each item
* @return List of non-null results in the same order as the original collection
*/
suspend fun <A, B> Collection<A>.asyncMapNotNull(
dispatcher: CoroutineDispatcher = Dispatchers.IO,
f: suspend (A) -> B?
): List<B> = coroutineScope {
map { item ->
async(dispatcher) {
f(item)
}
}.mapNotNull { it.await() }
fun <A, B> Collection<A>.asyncMapNotNull(f: suspend (A) -> B?): List<B> = runBlocking {
map { async { f(it) } }.mapNotNull { it.await() }
}
fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {

View file

@ -47,8 +47,8 @@ class Login : AppCompatActivity() {
view.evaluateJavascript(
"""
(function() {
const m = []; webpackChunkdiscord_app.push([[""], {}, e => {for (let c in e.c)m.push(e.c[c])}]);
return m.find(n => n?.exports?.default?.getToken !== void 0)?.exports?.default?.getToken();
const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken();
return wreq;
})()
""".trimIndent()
) { result ->

View file

@ -50,7 +50,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
return json.encodeToString(Presence.Response(
return json.encodeToString(
Presence.Response(
3,
Presence(
activities = listOf(

View file

@ -4,7 +4,6 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.GestureDetector
@ -13,8 +12,6 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@ -22,10 +19,8 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
@ -84,7 +79,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
val id = intent.getIntExtra("mediaId", -1)
@ -115,7 +109,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
// Ui init
initActivity(this)
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
@ -139,12 +132,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
binding.mediaBottomBarContainer.setPadding(
navBar.paddingLeft,
navBar.paddingTop,
navBar.paddingRight + navBarRightMargin,
navBar.paddingBottom + navBarBottomMargin
)
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
}
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }

View file

@ -1,8 +1,6 @@
package ani.dantotsu.media
import android.content.Context
import androidx.core.net.toFile
import androidx.core.net.toUri
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
@ -23,7 +21,6 @@ class SubtitleDownloader {
suspend fun loadSubtitleType(url: String): SubtitleType =
withContext(Dispatchers.IO) {
return@withContext try {
if (!url.startsWith("file")) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get<NetworkHelper>()
val request = Request.Builder()
@ -37,34 +34,22 @@ class SubtitleDownloader {
val responseBody = response.body.string()
val subtitleType = getType(responseBody)
val subtitleType = when {
responseBody.contains("[Script Info]") -> SubtitleType.ASS
responseBody.contains("WEBVTT") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
subtitleType
} else {
SubtitleType.UNKNOWN
}
} else {
val uri = url.toUri()
val file = uri.toFile()
val fileBody = file.readText()
val subtitleType = getType(fileBody)
subtitleType
}
} catch (e: Exception) {
Logger.log(e)
SubtitleType.UNKNOWN
}
}
private fun getType(content: String): SubtitleType {
return when {
content.contains("[Script Info]") -> SubtitleType.ASS
content.contains("WEBVTT") -> SubtitleType.VTT
content.contains("SRT") -> SubtitleType.SRT
else -> SubtitleType.UNKNOWN
}
}
//actually downloads lol
@Deprecated("handled externally")
suspend fun downloadSubtitle(

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media.anime
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.app.PictureInPictureParams
import android.app.PictureInPictureUiState
@ -18,6 +19,7 @@ import android.media.AudioManager.AUDIOFOCUS_GAIN
import android.media.AudioManager.AUDIOFOCUS_LOSS
import android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
import android.media.AudioManager.STREAM_MUSIC
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
@ -73,7 +75,9 @@ import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl
@ -171,10 +175,10 @@ import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import androidx.core.net.toUri
@UnstableApi
@SuppressLint("ClickableViewAccessibility")
@ -1308,7 +1312,7 @@ class ExoplayerView :
setTitle(R.string.speed)
singleChoiceItems(speedsName, curSpeed) { i ->
PrefManager.setCustomVal("${media.id}_speed", i)
speed = speeds.getOrNull(i) ?: 1f
speed = speeds[i]
curSpeed = i
playbackParameters = PlaybackParameters(speed)
exoPlayer.playbackParameters = playbackParameters
@ -1358,7 +1362,7 @@ class ExoplayerView :
val showProgressDialog =
if (PrefManager.getVal(PrefName.AskIndividualPlayer)) {
PrefManager.getCustomVal(
"${media.id}_progressDialog",
"${media.id}_ProgressDialog",
true,
)
} else {
@ -1380,7 +1384,7 @@ class ExoplayerView :
setCancelable(false)
setPosButton(R.string.yes) {
PrefManager.setCustomVal(
"${media.id}_progressDialog",
"${media.id}_ProgressDialog",
false,
)
PrefManager.setCustomVal(
@ -1391,7 +1395,7 @@ class ExoplayerView :
}
setNegButton(R.string.no) {
PrefManager.setCustomVal(
"${media.id}_progressDialog",
"${media.id}_ProgressDialog",
false,
)
PrefManager.setCustomVal(
@ -1605,27 +1609,29 @@ class ExoplayerView :
emptyList<MediaItem.SubtitleConfiguration>().toMutableList()
ext.subtitles.forEach { subtitle ->
val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle.file.url
// var localFile: String? = null
if (subtitle.type == SubtitleType.UNKNOWN) {
runBlocking {
val type = SubtitleDownloader.loadSubtitleType(subtitleUrl)
val fileUri = (subtitleUrl).toUri()
val fileUri = Uri.parse(subtitleUrl)
sub +=
MediaItem.SubtitleConfiguration
.Builder(fileUri)
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setMimeType(
when (type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.VTT -> MimeTypes.TEXT_SSA
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
else -> MimeTypes.TEXT_UNKNOWN
SubtitleType.SRT -> MimeTypes.TEXT_SSA
else -> MimeTypes.TEXT_SSA
},
).setId("69")
.setLanguage(subtitle.language)
.build()
}
println("sub: $sub")
} else {
val subUri = subtitleUrl.toUri()
val subUri = Uri.parse(subtitleUrl)
sub +=
MediaItem.SubtitleConfiguration
.Builder(subUri)
@ -1655,18 +1661,27 @@ class ExoplayerView :
followRedirects(true)
followSslRedirects(true)
}.build()
val httpDataSourceFactory =
OkHttpDataSource.Factory(httpClient).apply {
setDefaultRequestProperties(defaultHeaders)
video?.file?.headers?.let {
setDefaultRequestProperties(it)
val dataSourceFactory =
DataSource.Factory {
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(httpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
video?.file?.headers?.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
val defaultDataSourceFactory = DefaultDataSource.Factory(this, httpDataSourceFactory)
dataSource
}
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
cacheFactory =
CacheDataSource.Factory().apply {
setCache(VideoCache.getInstance(this@ExoplayerView))
setUpstreamDataSourceFactory(defaultDataSourceFactory)
if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
setUpstreamDataSourceFactory(dataSourceFactory)
}
setCacheWriteDataSinkFactory(null)
}
@ -1928,7 +1943,7 @@ class ExoplayerView :
if (PrefManager.getVal<Boolean>(PrefName.TextviewSubtitles)) {
exoSubtitleView.visibility = View.GONE
customSubtitleView.visibility = View.VISIBLE
val newCues = cueGroup.cues.map { it.text.toString() ?: "" }
val newCues = cueGroup.cues.map { it.text.toString() }
if (newCues.isEmpty()) {
customSubtitleView.text = ""
@ -2488,7 +2503,7 @@ class ExoplayerView :
val videoURL = video?.file?.url ?: return
val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle!!.file.url
val shareVideo = Intent(Intent.ACTION_VIEW)
shareVideo.setDataAndType(videoURL.toUri(), "video/*")
shareVideo.setDataAndType(Uri.parse(videoURL), "video/*")
shareVideo.setPackage("com.instantbits.cast.webvideo")
if (subtitle != null) shareVideo.putExtra("subtitle", subtitleUrl)
shareVideo.putExtra(
@ -2510,7 +2525,7 @@ class ExoplayerView :
} catch (ex: ActivityNotFoundException) {
val intent = Intent(Intent.ACTION_VIEW)
val uriString = "market://details?id=com.instantbits.cast.webvideo"
intent.data = uriString.toUri()
intent.data = Uri.parse(uriString)
startActivity(intent)
}
}

View file

@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
data class ImageData(
@ -77,7 +76,7 @@ fun saveImage(
uri?.let {
contentResolver.openOutputStream(it)?.use { os ->
bitmap.compress(format, quality, os)
} ?: throw FileNotFoundException("Failed to open output stream for URI: $uri")
}
}
} else {
val directory =
@ -87,20 +86,12 @@ fun saveImage(
}
val file = File(directory, filename)
// Check if the file already exists
if (file.exists()) {
println("File already exists: ${file.absolutePath}")
return
}
FileOutputStream(file).use { outputStream ->
bitmap.compress(format, quality, outputStream)
}
}
} catch (e: FileNotFoundException) {
println("File not found: ${e.message}")
} catch (e: Exception) {
// Handle exception here
println("Exception while saving image: ${e.message}")
}
}

View file

@ -66,7 +66,6 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
@ -233,35 +232,25 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun multiDownload(n: Int) {
lifecycleScope.launch {
// Get the last viewed chapter
val selected = media.userProgress ?: 0
// Get last viewed chapter
val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList()
// Ensure chapters are available in the extensions
if (chapters.isNullOrEmpty() || n < 1) return@launch
// Find the index of the last viewed chapter
val progressChapterIndex = (chapters.indexOfFirst {
// Filter by selected language
val progressChapterIndex = (chapters?.indexOfFirst {
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} + 1).coerceAtLeast(0)
// Calculate the end value for the range of chapters to download
val endIndex = (progressChapterIndex + n).coerceAtMost(chapters.size)
// Get the list of chapters to download
} ?: 0) + 1
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
// Calculate the end index
val endIndex = minOf(progressChapterIndex + n, chapters.size)
// Make sure there are enough chapters
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
// Trigger the download for each chapter sequentially
for (chapter in chaptersToDownload) {
try {
downloadChapterSequentially(chapter)
} catch (e: Exception) {
Toast.makeText(requireContext(), "Failed to download chapter: ${chapter.title}", Toast.LENGTH_SHORT).show()
}
}
Toast.makeText(requireContext(), "All downloads completed!", Toast.LENGTH_SHORT).show()
}
}
private suspend fun downloadChapterSequentially(chapter: MangaChapter) {
withContext(Dispatchers.IO) {
onMangaChapterDownloadClick(chapter)
delay(2000) // A 2-second download
}
}

View file

@ -26,6 +26,7 @@ object AnimeSources : WatchSources() {
)
isInitialized = true
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = sortPinnedAnimeSources(
createParsersFromExtensions(extensions),

View file

@ -226,18 +226,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
?: return emptyList())
return try {
// TODO(1.6): Remove else block when dropping support for ext lib <1.6
if ((source as AnimeHttpSource).javaClass.declaredMethods.any { it.name == "getHosterList" }){
val hosters = source.getHosterList(sEpisode)
val allVideos = hosters.flatMap { hoster ->
val videos = source.getVideoList(hoster)
videos.map { it.copy(videoTitle = "${hoster.hosterName} - ${it.videoTitle}") }
}
allVideos.map { videoToVideoServer(it) }
} else {
val videos = source.getVideoList(sEpisode)
videos.map { videoToVideoServer(it) }
}
} catch (e: Exception) {
Logger.log("Exception occurred: ${e.message}")
emptyList()
@ -586,7 +576,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
number,
format!!,
FileUrl(videoUrl, headersMap),
null
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
)
}
@ -646,6 +636,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
}
private fun trackToSubtitle(track: Track): Subtitle {
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
var type: SubtitleType?
runBlocking {
type = findSubtitleType(track.url)

View file

@ -193,7 +193,8 @@ class SettingsCommonActivity : AppCompatActivity() {
PrefManager.setVal(PrefName.OverridePassword, true)
}
val password = view.passwordInput.text.toString()
val confirmPassword = view.confirmPasswordInput.text.toString()
val confirmPassword =
view.confirmPasswordInput.text.toString()
if (password == confirmPassword && password.isNotEmpty()) {
PrefManager.setVal(PrefName.AppPassword, password)
if (view.biometricCheckbox.isChecked) {
@ -205,7 +206,9 @@ class SettingsCommonActivity : AppCompatActivity() {
if (canBiometricPrompt) {
val biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
BiometricPromptUtils.createBiometricPrompt(
this@SettingsCommonActivity
) { _ ->
val token = UUID.randomUUID().toString()
PrefManager.setVal(
PrefName.BiometricToken,
@ -235,12 +238,14 @@ class SettingsCommonActivity : AppCompatActivity() {
setOnShowListener {
view.passwordInput.requestFocus()
val canAuthenticate =
BiometricManager.from(applicationContext).canAuthenticate(
BiometricManager.from(applicationContext)
.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK,
) == BiometricManager.BIOMETRIC_SUCCESS
view.biometricCheckbox.isVisible = canAuthenticate
view.biometricCheckbox.isChecked =
PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()
PrefManager.getVal(PrefName.BiometricToken, "")
.isNotEmpty()
view.forgotPasswordCheckbox.isChecked =
PrefManager.getVal(PrefName.OverridePassword)
}
@ -314,7 +319,8 @@ class SettingsCommonActivity : AppCompatActivity() {
setTitle(R.string.change_download_location)
setMessage(R.string.download_location_msg)
setPosButton(R.string.ok) {
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
val oldUri =
PrefManager.getVal<String>(PrefName.DownloadsDir)
launcher.registerForCallback { success ->
if (success) {
toast(getString(R.string.please_wait))

View file

@ -82,9 +82,18 @@ class SettingsNotificationActivity : AppCompatActivity() {
setTitle(R.string.subscriptions_checking_time)
singleChoiceItems(timeNames, curTime) { i ->
curTime = i
it.settingsTitle.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime)
TaskScheduler.create(context, PrefManager.getVal(PrefName.UseAlarmManager)).scheduleAllTasks(context)
it.settingsTitle.text = getString(
R.string.subscriptions_checking_time_s,
timeNames[i]
)
PrefManager.setVal(
PrefName.SubscriptionNotificationInterval,
curTime
)
TaskScheduler.create(
context,
PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(context)
}
show()
}
@ -125,7 +134,8 @@ class SettingsNotificationActivity : AppCompatActivity() {
types.map { name ->
name.replace("_", " ").lowercase().replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
} }.toTypedArray(),
}
}.toTypedArray(),
selected
) { updatedSelected ->
types.forEachIndexed { index, type ->

View file

@ -96,7 +96,8 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
themeSwitcher.apply {
setText(themeText)
setAdapter(
ArrayAdapter(context,
ArrayAdapter(
context,
R.layout.item_dropdown,
ThemeManager.Companion.Theme.entries.map {
it.theme.substring(

View file

@ -52,7 +52,8 @@ class SubscriptionsBottomDialog : BottomSheetDialogFragment() {
}
groupedSubscriptions.forEach { (parserName, mediaList) ->
adapter.add(SubscriptionSource(
adapter.add(
SubscriptionSource(
parserName,
mediaList.toMutableList(),
adapter,

View file

@ -3,8 +3,6 @@ package ani.dantotsu.util
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.os.Build
import android.view.WindowManager
import android.view.View
import ani.dantotsu.R
@ -207,14 +205,8 @@ class AlertDialogBuilder(private val context: Context) {
onShow?.invoke()
}
dialog.window?.apply {
setDimAmount(0.5f)
setDimAmount(0.8f)
attributes.windowAnimations = android.R.style.Animation_Dialog
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val params = attributes
params.flags = params.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
params.setBlurBehindRadius(20)
attributes = params
}
}
dialog.show()
}

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.Hoster
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
@ -49,25 +48,6 @@ interface AnimeSource {
return fetchEpisodeList(anime).awaitSingle()
}
/**
* Get the list of hoster for an episode. The first hoster in the list should
* be the preferred hoster.
*
* @since extensions-lib 16
* @param episode the episode.
* @return the hosters for the episode.
*/
suspend fun getHosterList(episode: SEpisode): List<Hoster> = throw IllegalStateException("Not used")
/**
* Get the list of videos for a hoster.
*
* @since extensions-lib 16
* @param hoster the hoster.
* @return the videos for the hoster.
*/
suspend fun getVideoList(hoster: Hoster): List<Video> = throw IllegalStateException("Not used")
/**
* Get the list of videos a episode has. Pages should be returned
* in the expected order; the index is ignored.

View file

@ -1,81 +0,0 @@
package eu.kanade.tachiyomi.animesource.model
import eu.kanade.tachiyomi.animesource.model.SerializableVideo.Companion.serialize
import eu.kanade.tachiyomi.animesource.model.SerializableVideo.Companion.toVideoList
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
open class Hoster(
val hosterUrl: String = "",
val hosterName: String = "",
val videoList: List<Video>? = null,
val internalData: String = "",
) {
@Transient
@Volatile
var status: State = State.IDLE
enum class State {
IDLE,
LOADING,
READY,
ERROR,
}
fun copy(
hosterUrl: String = this.hosterUrl,
hosterName: String = this.hosterName,
videoList: List<Video>? = this.videoList,
internalData: String = this.internalData,
): Hoster {
return Hoster(hosterUrl, hosterName, videoList, internalData)
}
companion object {
const val NO_HOSTER_LIST = "no_hoster_list"
fun List<Video>.toHosterList(): List<Hoster> {
return listOf(
Hoster(
hosterUrl = "",
hosterName = NO_HOSTER_LIST,
videoList = this,
),
)
}
}
}
@Serializable
data class SerializableHoster(
val hosterUrl: String = "",
val hosterName: String = "",
val videoList: String? = null,
val internalData: String = "",
) {
companion object {
fun List<Hoster>.serialize(): String =
Json.encodeToString(
this.map { host ->
SerializableHoster(
host.hosterUrl,
host.hosterName,
host.videoList?.serialize(),
host.internalData,
)
},
)
fun String.toHosterList(): List<Hoster> =
Json.decodeFromString<List<SerializableHoster>>(this)
.map { sHost ->
Hoster(
sHost.hosterUrl,
sHost.hosterName,
sHost.videoList?.toVideoList(),
sHost.internalData,
)
}
}
}

View file

@ -1,101 +1,31 @@
package eu.kanade.tachiyomi.animesource.model
import android.net.Uri
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.Headers
import rx.subjects.Subject
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
@kotlinx.serialization.Serializable
data class Track(val url: String, val lang: String) : Serializable
@kotlinx.serialization.Serializable
enum class ChapterType {
Opening,
Ending,
Recap,
MixedOp,
Other,
}
@kotlinx.serialization.Serializable
data class TimeStamp(
val start: Double,
val end: Double,
val name: String,
val type: ChapterType = ChapterType.Other,
)
open class Video(
var videoUrl: String = "",
val videoTitle: String = "",
val resolution: Int? = null,
val bitrate: Int? = null,
val headers: Headers? = null,
val preferred: Boolean = false,
val url: String = "",
val quality: String = "",
var videoUrl: String? = null,
headers: Headers? = null,
// "url", "language-label-2", "url2", "language-label-2"
val subtitleTracks: List<Track> = emptyList(),
val audioTracks: List<Track> = emptyList(),
val timestamps: List<TimeStamp> = emptyList(),
val internalData: String = "",
val initialized: Boolean = false,
// TODO(1.6): Remove after ext lib bump
val videoPageUrl: String = "",
) {
) : Serializable, ProgressListener {
// TODO(1.6): Remove after ext lib bump
@Deprecated("Use videoTitle instead", ReplaceWith("videoTitle"))
val quality: String
get() = videoTitle
@Transient
var headers: Headers? = headers
// TODO(1.6): Remove after ext lib bump
@Deprecated("Use videoPageUrl instead", ReplaceWith("videoPageUrl"))
val url: String
get() = videoPageUrl
// TODO(1.6): Remove after ext lib bump
constructor(
url: String,
quality: String,
videoUrl: String?,
headers: Headers? = null,
subtitleTracks: List<Track> = emptyList(),
audioTracks: List<Track> = emptyList(),
) : this(
videoPageUrl = url,
videoTitle = quality,
videoUrl = videoUrl ?: "null",
headers = headers,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
)
// TODO(1.6): Remove after ext lib bump
constructor(
videoUrl: String = "",
videoTitle: String = "",
resolution: Int? = null,
bitrate: Int? = null,
headers: Headers? = null,
preferred: Boolean = false,
subtitleTracks: List<Track> = emptyList(),
audioTracks: List<Track> = emptyList(),
timestamps: List<TimeStamp> = emptyList(),
internalData: String = "",
) : this(
videoUrl = videoUrl,
videoTitle = videoTitle,
resolution = resolution,
bitrate = bitrate,
headers = headers,
preferred = preferred,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
timestamps = timestamps,
internalData = internalData,
videoPageUrl = "",
)
// TODO(1.6): Remove after ext lib bump
@Suppress("UNUSED_PARAMETER")
constructor(
url: String,
@ -108,132 +38,83 @@ open class Video(
@Transient
@Volatile
var status: State = State.QUEUE
@Transient
private val _progressFlow = MutableStateFlow(0)
@Transient
val progressFlow = _progressFlow.asStateFlow()
var progress: Int
get() = _progressFlow.value
set(value) {
_progressFlow.value = value
}
@Transient
@Volatile
var totalBytesDownloaded: Long = 0L
@Transient
@Volatile
var totalContentLength: Long = 0L
@Transient
@Volatile
var bytesDownloaded: Long = 0L
set(value) {
totalBytesDownloaded += if (value < field) {
value
} else {
value - field
}
field = value
}
fun copy(
videoUrl: String = this.videoUrl,
videoTitle: String = this.videoTitle,
resolution: Int? = this.resolution,
bitrate: Int? = this.bitrate,
headers: Headers? = this.headers,
preferred: Boolean = this.preferred,
subtitleTracks: List<Track> = this.subtitleTracks,
audioTracks: List<Track> = this.audioTracks,
timestamps: List<TimeStamp> = this.timestamps,
internalData: String = this.internalData,
): Video {
return Video(
videoUrl = videoUrl,
videoTitle = videoTitle,
resolution = resolution,
bitrate = bitrate,
headers = headers,
preferred = preferred,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
timestamps = timestamps,
internalData = internalData,
)
}
@Transient
var progressSubject: Subject<State, State>? = null
fun copy(
videoUrl: String = this.videoUrl,
videoTitle: String = this.videoTitle,
resolution: Int? = this.resolution,
bitrate: Int? = this.bitrate,
headers: Headers? = this.headers,
preferred: Boolean = this.preferred,
subtitleTracks: List<Track> = this.subtitleTracks,
audioTracks: List<Track> = this.audioTracks,
timestamps: List<TimeStamp> = this.timestamps,
internalData: String = this.internalData,
initialized: Boolean = this.initialized,
videoPageUrl: String = this.videoPageUrl,
): Video {
return Video(
videoUrl = videoUrl,
videoTitle = videoTitle,
resolution = resolution,
bitrate = bitrate,
headers = headers,
preferred = preferred,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
timestamps = timestamps,
internalData = internalData,
initialized = initialized,
videoPageUrl = videoPageUrl,
)
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
bytesDownloaded = bytesRead
if (contentLength > totalContentLength) {
totalContentLength = contentLength
}
val newProgress = if (totalContentLength > 0) {
(100 * totalBytesDownloaded / totalContentLength).toInt()
} else {
-1
}
if (progress != newProgress) progress = newProgress
}
enum class State {
QUEUE,
LOAD_VIDEO,
DOWNLOAD_IMAGE,
READY,
ERROR,
}
@Throws(IOException::class)
private fun writeObject(out: ObjectOutputStream) {
out.defaultWriteObject()
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
out.writeObject(headersMap)
}
@kotlinx.serialization.Serializable
data class SerializableVideo(
val videoUrl: String = "",
val videoTitle: String = "",
val resolution: Int? = null,
val bitrate: Int? = null,
val headers: List<Pair<String, String>>? = null,
val preferred: Boolean = false,
val subtitleTracks: List<Track> = emptyList(),
val audioTracks: List<Track> = emptyList(),
val timestamps: List<TimeStamp> = emptyList(),
val internalData: String = "",
val initialized: Boolean = false,
// TODO(1.6): Remove after ext lib bump
val videoPageUrl: String = "",
) {
companion object {
fun List<Video>.serialize(): String =
Json.encodeToString(
this.map { vid ->
SerializableVideo(
vid.videoUrl,
vid.videoTitle,
vid.resolution,
vid.bitrate,
vid.headers?.toList(),
vid.preferred,
vid.subtitleTracks,
vid.audioTracks,
vid.timestamps,
vid.internalData,
vid.initialized,
vid.videoPageUrl,
)
},
)
fun String.toVideoList(): List<Video> =
Json.decodeFromString<List<SerializableVideo>>(this)
.map { sVid ->
Video(
sVid.videoUrl,
sVid.videoTitle,
sVid.resolution,
sVid.bitrate,
sVid.headers
?.flatMap { it.toList() }
?.let { Headers.headersOf(*it.toTypedArray()) },
sVid.preferred,
sVid.subtitleTracks,
sVid.audioTracks,
sVid.timestamps,
sVid.internalData,
sVid.initialized,
sVid.videoPageUrl,
)
@Suppress("UNCHECKED_CAST")
@Throws(IOException::class, ClassNotFoundException::class)
private fun readObject(input: ObjectInputStream) {
input.defaultReadObject()
val headersMap = input.readObject() as? Map<String, List<String>>
headers = headersMap?.let { map ->
val builder = Headers.Builder()
for ((key, values) in map) {
for (value in values) {
builder.add(key, value)
}
}
builder.build()
}
}
}

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.api
import ani.dantotsu.asyncMap
import ani.dantotsu.parsers.novel.AvailableNovelSources
import ani.dantotsu.parsers.novel.NovelExtension
import ani.dantotsu.settings.saving.PrefManager
@ -68,7 +67,7 @@ internal class ExtensionGithubApi {
val repos =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
repos.asyncMap {
repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {
@ -156,7 +155,7 @@ internal class ExtensionGithubApi {
val repos =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
repos.asyncMap {
repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {
@ -208,7 +207,7 @@ internal class ExtensionGithubApi {
val repos =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos).toMutableList()
repos.asyncMap {
repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {

View file

@ -419,19 +419,13 @@
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<FrameLayout
android:id="@+id/mediaBottomBarContainer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0"
android:background="?attr/colorSurface"
android:layout_gravity="center_horizontal|bottom">
<nl.joery.animatedbottombar.AnimatedBottomBar
android:id="@+id/mediaBottomBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal|bottom"
android:layout_weight="0"
android:background="?attr/colorSurface"
android:padding="0dp"
app:abb_animationInterpolator="@anim/over_shoot"
@ -444,6 +438,5 @@
app:itemTextAppearanceActive="@style/NavBarText"
app:itemTextAppearanceInactive="@style/NavBarText"
app:itemTextColor="@color/tab_layout_icon" />
</FrameLayout>
</LinearLayout>

View file

@ -353,18 +353,11 @@
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/mediaBottomBarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:layout_gravity="center_horizontal|bottom">
<nl.joery.animatedbottombar.AnimatedBottomBar
android:id="@+id/mediaBottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
android:layout_gravity="center_horizontal|bottom"
android:background="?attr/colorSurface"
android:padding="0dp"
app:abb_animationInterpolator="@anim/over_shoot"
@ -378,7 +371,6 @@
app:itemTextAppearanceActive="@style/NavBarText"
app:itemTextAppearanceInactive="@style/NavBarText"
app:itemTextColor="@color/tab_layout_icon" />
</FrameLayout>
</LinearLayout>

View file

@ -1102,10 +1102,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:stepSize="1.0"
android:value="10.0"
android:valueFrom="1.0"
android:valueTo="50.0"
android:stepSize="5.0"
android:value="15.0"
android:valueFrom="5.0"
android:valueTo="45.0"
app:labelBehavior="floating"
app:labelStyle="@style/fontTooltip"
app:thumbColor="?attr/colorSecondary"

View file

@ -12,7 +12,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.3'
classpath 'com.android.tools.build:gradle:8.9.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version"

View file

@ -1,6 +1,6 @@
#Wed Aug 30 19:57:04 IST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists