diff --git a/README.md b/README.md
index 76bece4b..a1b1d673 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,6 @@ 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!
-
-
## Terms of Use
By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws
diff --git a/app/build.gradle b/app/build.gradle
index f7aab7ca..535d8922 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,8 +18,8 @@ android {
minSdk 21
targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger())
- versionName "3.2.1"
- versionCode 300200100
+ versionName "3.2.2"
+ versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
signingConfig signingConfigs.debug
}
@@ -48,6 +48,10 @@ 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"
@@ -81,25 +85,26 @@ android {
dependencies {
// FireBase
- 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'
+ 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'
// Core
- implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.browser:browser:1.8.0'
- implementation 'androidx.core:core-ktx:1.13.1'
- implementation 'androidx.fragment:fragment-ktx:1.6.2'
+ 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.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.9.0"
+ implementation "androidx.work:work-runtime-ktx:2.10.1"
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.6.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
implementation 'androidx.preference:preference-ktx:1.2.1'
- implementation 'androidx.webkit:webkit:1.11.0'
+ implementation 'androidx.webkit:webkit:1.13.0'
implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
@@ -113,7 +118,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
- ext.exo_version = '1.5.0'
+ ext.exo_version = '1.6.1'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@@ -124,7 +129,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.3"
+ implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4"
// UI
implementation 'com.google.android.material:material:1.12.0'
@@ -133,7 +138,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.2.1'
+ implementation 'androidx.paging:paging-runtime-ktx:3.3.6'
implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
@@ -162,13 +167,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.12'
- implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
+ 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:okhttp-dnsoverhttps'
- 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.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.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt
index 057c6d22..0b5751e0 100644
--- a/app/src/main/java/ani/dantotsu/App.kt
+++ b/app/src/main/java/ani/dantotsu/App.kt
@@ -113,21 +113,28 @@ class App : MultiDexApplication() {
}
}
- CoroutineScope(Dispatchers.IO).launch {
+ val scope = CoroutineScope(Dispatchers.IO)
+ scope.launch {
animeExtensionManager = Injekt.get()
- animeExtensionManager.findAvailableExtensions()
+ launch {
+ animeExtensionManager.findAvailableExtensions()
+ }
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
- CoroutineScope(Dispatchers.IO).launch {
+ scope.launch {
mangaExtensionManager = Injekt.get()
- mangaExtensionManager.findAvailableExtensions()
+ launch {
+ mangaExtensionManager.findAvailableExtensions()
+ }
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
- CoroutineScope(Dispatchers.IO).launch {
+ scope.launch {
novelExtensionManager = Injekt.get()
- novelExtensionManager.findAvailableExtensions()
+ launch {
+ novelExtensionManager.findAvailableExtensions()
+ }
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
diff --git a/app/src/main/java/ani/dantotsu/Network.kt b/app/src/main/java/ani/dantotsu/Network.kt
index ce536e8d..f9d8b7f2 100644
--- a/app/src/main/java/ani/dantotsu/Network.kt
+++ b/app/src/main/java/ani/dantotsu/Network.kt
@@ -11,7 +11,11 @@ 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
@@ -85,12 +89,42 @@ object Mapper : ResponseParser {
}
}
-fun Collection.asyncMap(f: suspend (A) -> B): List = runBlocking {
- map { async { f(it) } }.map { it.await() }
+/**
+ * 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 Collection.asyncMap(
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ f: suspend (A) -> B
+): List = coroutineScope {
+ map { item ->
+ async(dispatcher) {
+ f(item)
+ }
+ }.awaitAll()
}
-fun Collection.asyncMapNotNull(f: suspend (A) -> B?): List = runBlocking {
- map { async { f(it) } }.mapNotNull { 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 Collection.asyncMapNotNull(
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ f: suspend (A) -> B?
+): List = coroutineScope {
+ map { item ->
+ async(dispatcher) {
+ f(item)
+ }
+ }.mapNotNull { it.await() }
}
fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
diff --git a/app/src/main/java/ani/dantotsu/connections/discord/Login.kt b/app/src/main/java/ani/dantotsu/connections/discord/Login.kt
index 513da55b..f33c03e9 100644
--- a/app/src/main/java/ani/dantotsu/connections/discord/Login.kt
+++ b/app/src/main/java/ani/dantotsu/connections/discord/Login.kt
@@ -47,9 +47,9 @@ class Login : AppCompatActivity() {
view.evaluateJavascript(
"""
(function() {
- 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;
- })()
+ 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();
+})()
""".trimIndent()
) { result ->
login(result.trim('"'))
diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
index 58e7de38..2ea82108 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
@@ -232,12 +232,18 @@ class MangaDownloaderService : Service() {
image.page,
image.source
)
+ if (bitmap == null) {
+ snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
+ }
retryCount++
}
- if (bitmap != null) {
- saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
+ if (bitmap == null) {
+ outputDir.deleteRecursively(this@MangaDownloaderService, false)
+ throw Exception("${task.chapter} - Unable to download all pages after $retryCount attempts. Try again.")
}
+
+ saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
farthest++
builder.setProgress(task.imageData.size, farthest, false)
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
index bcb21c8c..1e1d2e19 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
@@ -4,6 +4,7 @@ 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
@@ -12,6 +13,8 @@ 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
@@ -19,8 +22,10 @@ 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
@@ -79,6 +84,7 @@ 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)
@@ -109,6 +115,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
// Ui init
initActivity(this)
+
binding.mediaViewPager.updateLayoutParams {
bottomMargin = navBarHeight
}
@@ -132,10 +139,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
- navBar.updateLayoutParams {
- rightMargin = navBarRightMargin
- bottomMargin = navBarBottomMargin
- }
+ binding.mediaBottomBarContainer.setPadding(
+ navBar.paddingLeft,
+ navBar.paddingTop,
+ navBar.paddingRight + navBarRightMargin,
+ navBar.paddingBottom + navBarBottomMargin
+ )
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams { topMargin += statusBarHeight }
diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
index 5c66f6a2..7fa32de9 100644
--- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
+++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
@@ -1,6 +1,8 @@
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
@@ -21,28 +23,32 @@ class SubtitleDownloader {
suspend fun loadSubtitleType(url: String): SubtitleType =
withContext(Dispatchers.IO) {
return@withContext try {
- // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
- val networkHelper = Injekt.get()
- val request = Request.Builder()
- .url(url)
- .build()
+ if (!url.startsWith("file")) {
+ // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
+ val networkHelper = Injekt.get()
+ val request = Request.Builder()
+ .url(url)
+ .build()
- val response = networkHelper.client.newCall(request).execute()
+ val response = networkHelper.client.newCall(request).execute()
- // Check if response is successful
- if (response.isSuccessful) {
- val responseBody = response.body.string()
+ // Check if response is successful
+ if (response.isSuccessful) {
+ val responseBody = response.body.string()
- val subtitleType = when {
- responseBody.contains("[Script Info]") -> SubtitleType.ASS
- responseBody.contains("WEBVTT") -> SubtitleType.VTT
- else -> SubtitleType.SRT
+ val subtitleType = getType(responseBody)
+
+ subtitleType
+ } else {
+ SubtitleType.UNKNOWN
}
-
- subtitleType
} else {
- SubtitleType.UNKNOWN
+ val uri = url.toUri()
+ val file = uri.toFile()
+ val fileBody = file.readText()
+ val subtitleType = getType(fileBody)
+ subtitleType
}
} catch (e: Exception) {
Logger.log(e)
@@ -50,6 +56,15 @@ class SubtitleDownloader {
}
}
+ 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(
diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
index 465949a4..75332806 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
@@ -2,7 +2,6 @@ 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
@@ -19,7 +18,6 @@ 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
@@ -75,9 +73,7 @@ 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
@@ -175,10 +171,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")
@@ -427,7 +423,8 @@ class ExoplayerView :
false -> 0f
}
- val textElevation = PrefManager.getVal(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels
+ val textElevation =
+ PrefManager.getVal(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels
textView.translationY = -textElevation
}
@@ -606,9 +603,9 @@ class ExoplayerView :
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
pipEnabled =
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) &&
- PrefManager.getVal(
- PrefName.Pip,
- )
+ PrefManager.getVal(
+ PrefName.Pip,
+ )
if (pipEnabled) {
exoPip.visibility = View.VISIBLE
exoPip.setOnClickListener {
@@ -1044,7 +1041,8 @@ class ExoplayerView :
}
}
- override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(false, event) else handleController()
+ override fun onSingleClick(event: MotionEvent) =
+ if (isSeeking) doubleTap(false, event) else handleController()
},
)
val rewindArea = playerView.findViewById(R.id.exo_rewind_area)
@@ -1079,7 +1077,8 @@ class ExoplayerView :
}
}
- override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(true, event) else handleController()
+ override fun onSingleClick(event: MotionEvent) =
+ if (isSeeking) doubleTap(true, event) else handleController()
},
)
val forwardArea = playerView.findViewById(R.id.exo_forward_area)
@@ -1309,7 +1308,7 @@ class ExoplayerView :
setTitle(R.string.speed)
singleChoiceItems(speedsName, curSpeed) { i ->
PrefManager.setCustomVal("${media.id}_speed", i)
- speed = speeds[i]
+ speed = speeds.getOrNull(i) ?: 1f
curSpeed = i
playbackParameters = PlaybackParameters(speed)
exoPlayer.playbackParameters = playbackParameters
@@ -1359,7 +1358,7 @@ class ExoplayerView :
val showProgressDialog =
if (PrefManager.getVal(PrefName.AskIndividualPlayer)) {
PrefManager.getCustomVal(
- "${media.id}_ProgressDialog",
+ "${media.id}_progressDialog",
true,
)
} else {
@@ -1381,7 +1380,7 @@ class ExoplayerView :
setCancelable(false)
setPosButton(R.string.yes) {
PrefManager.setCustomVal(
- "${media.id}_ProgressDialog",
+ "${media.id}_progressDialog",
false,
)
PrefManager.setCustomVal(
@@ -1392,7 +1391,7 @@ class ExoplayerView :
}
setNegButton(R.string.no) {
PrefManager.setCustomVal(
- "${media.id}_ProgressDialog",
+ "${media.id}_progressDialog",
false,
)
PrefManager.setCustomVal(
@@ -1449,7 +1448,8 @@ class ExoplayerView :
else -> mutableListOf()
}
val startTimestamp = Calendar.getInstance()
- val durationInSeconds = if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440
+ val durationInSeconds =
+ if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440
val endTimestamp =
Calendar.getInstance().apply {
@@ -1502,12 +1502,12 @@ class ExoplayerView :
@Suppress("UNCHECKED_CAST")
val list =
(
- PrefManager.getNullableCustomVal(
- "continueAnimeList",
- listOf(),
- List::class.java,
- ) as List
- ).toMutableList()
+ PrefManager.getNullableCustomVal(
+ "continueAnimeList",
+ listOf(),
+ List::class.java,
+ ) as List
+ ).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueAnimeList", list)
@@ -1567,7 +1567,11 @@ class ExoplayerView :
subtitle = intent.getSerialized("subtitle")
?: when (
val subLang: String? =
- PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)
+ PrefManager.getNullableCustomVal(
+ "subLang_${media.id}",
+ null,
+ String::class.java
+ )
) {
null -> {
when (episode.selectedSubtitle) {
@@ -1575,8 +1579,12 @@ class ExoplayerView :
-1 ->
ext.subtitles.find {
it.language.contains(lang, ignoreCase = true) ||
- it.language.contains(getLanguageCode(lang), ignoreCase = true)
+ it.language.contains(
+ getLanguageCode(lang),
+ ignoreCase = true
+ )
}
+
else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!)
}
}
@@ -1597,29 +1605,27 @@ class ExoplayerView :
emptyList().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 = Uri.parse(subtitleUrl)
+ val fileUri = (subtitleUrl).toUri()
sub +=
MediaItem.SubtitleConfiguration
.Builder(fileUri)
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setMimeType(
when (type) {
- SubtitleType.VTT -> MimeTypes.TEXT_SSA
+ SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
- SubtitleType.SRT -> MimeTypes.TEXT_SSA
- else -> MimeTypes.TEXT_SSA
+ SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
+ else -> MimeTypes.TEXT_UNKNOWN
},
).setId("69")
.setLanguage(subtitle.language)
.build()
}
- println("sub: $sub")
} else {
- val subUri = Uri.parse(subtitleUrl)
+ val subUri = subtitleUrl.toUri()
sub +=
MediaItem.SubtitleConfiguration
.Builder(subUri)
@@ -1649,26 +1655,18 @@ class ExoplayerView :
followRedirects(true)
followSslRedirects(true)
}.build()
- val dataSourceFactory =
- DataSource.Factory {
- val dataSource: HttpDataSource = OkHttpDataSource.Factory(httpClient).createDataSource()
- defaultHeaders.forEach {
- dataSource.setRequestProperty(it.key, it.value)
+ val httpDataSourceFactory =
+ OkHttpDataSource.Factory(httpClient).apply {
+ setDefaultRequestProperties(defaultHeaders)
+ video?.file?.headers?.let {
+ setDefaultRequestProperties(it)
}
- video?.file?.headers?.forEach {
- dataSource.setRequestProperty(it.key, it.value)
- }
- dataSource
}
- val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
+ val defaultDataSourceFactory = DefaultDataSource.Factory(this, httpDataSourceFactory)
cacheFactory =
CacheDataSource.Factory().apply {
setCache(VideoCache.getInstance(this@ExoplayerView))
- if (ext.server.offline) {
- setUpstreamDataSourceFactory(dafuckDataSourceFactory)
- } else {
- setUpstreamDataSourceFactory(dataSourceFactory)
- }
+ setUpstreamDataSourceFactory(defaultDataSourceFactory)
setCacheWriteDataSinkFactory(null)
}
@@ -1717,16 +1715,18 @@ class ExoplayerView :
val docFile =
directory.listFiles().firstOrNull {
it.name?.endsWith(".mp4") == true ||
- it.name?.endsWith(".mkv") == true ||
- it.name?.endsWith(
- ".${Injekt
- .get()
- .extension
- ?.extension
- ?.getFileExtension()
- ?.first ?: "ts"}",
- ) ==
- true
+ it.name?.endsWith(".mkv") == true ||
+ it.name?.endsWith(
+ ".${
+ Injekt
+ .get()
+ .extension
+ ?.extension
+ ?.getFileExtension()
+ ?.first ?: "ts"
+ }",
+ ) ==
+ true
}
if (docFile != null) {
val uri = docFile.uri
@@ -1840,30 +1840,30 @@ class ExoplayerView :
"%02d:%02d:%02d",
TimeUnit.MILLISECONDS.toHours(playbackPosition),
TimeUnit.MILLISECONDS.toMinutes(playbackPosition) -
- TimeUnit.HOURS.toMinutes(
- TimeUnit.MILLISECONDS.toHours(
- playbackPosition,
+ TimeUnit.HOURS.toMinutes(
+ TimeUnit.MILLISECONDS.toHours(
+ playbackPosition,
+ ),
),
- ),
TimeUnit.MILLISECONDS.toSeconds(playbackPosition) -
- TimeUnit.MINUTES.toSeconds(
- TimeUnit.MILLISECONDS.toMinutes(
- playbackPosition,
+ TimeUnit.MINUTES.toSeconds(
+ TimeUnit.MILLISECONDS.toMinutes(
+ playbackPosition,
+ ),
),
- ),
)
- customAlertDialog().apply {
- setTitle(getString(R.string.continue_from, time))
- setCancelable(false)
- setPosButton(getString(R.string.yes)) {
- buildExoplayer()
- }
- setNegButton(getString(R.string.no)) {
- playbackPosition = 0L
- buildExoplayer()
- }
- show()
+ customAlertDialog().apply {
+ setTitle(getString(R.string.continue_from, time))
+ setCancelable(false)
+ setPosButton(getString(R.string.yes)) {
+ buildExoplayer()
}
+ setNegButton(getString(R.string.no)) {
+ playbackPosition = 0L
+ buildExoplayer()
+ }
+ show()
+ }
} else {
buildExoplayer()
}
@@ -1940,7 +1940,9 @@ class ExoplayerView :
val currentPosition = exoPlayer.currentPosition
- if ((lastSubtitle?.length ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)) {
+ if ((lastSubtitle?.length
+ ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)
+ ) {
activeSubtitles.clear()
}
@@ -2187,7 +2189,7 @@ class ExoplayerView :
currentTimeStamp =
model.timeStamps.value?.find { timestamp ->
timestamp.interval.startTime < playerCurrentTime &&
- playerCurrentTime < (timestamp.interval.endTime - 1)
+ playerCurrentTime < (timestamp.interval.endTime - 1)
}
val new = currentTimeStamp
@@ -2213,7 +2215,8 @@ class ExoplayerView :
override fun onTick(millisUntilFinished: Long) {
if (new == null) {
skipTimeButton.visibility = View.GONE
- exoSkip.isVisible = PrefManager.getVal(PrefName.SkipTime) > 0
+ exoSkip.isVisible =
+ PrefManager.getVal(PrefName.SkipTime) > 0
disappeared = false
functionstarted = false
cancelTimer()
@@ -2222,7 +2225,8 @@ class ExoplayerView :
override fun onFinish() {
skipTimeButton.visibility = View.GONE
- exoSkip.isVisible = PrefManager.getVal(PrefName.SkipTime) > 0
+ exoSkip.isVisible =
+ PrefManager.getVal(PrefName.SkipTime) > 0
disappeared = true
functionstarted = false
cancelTimer()
@@ -2310,7 +2314,7 @@ class ExoplayerView :
tracks.groups.forEach {
println(
"Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" +
- "Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}",
+ "Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}",
)
when (it.type) {
TRACK_TYPE_AUDIO -> {
@@ -2365,7 +2369,7 @@ class ExoplayerView :
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
- -> {
+ -> {
toast("Source Exception : ${error.message}")
isPlayerPlaying = true
sourceClick()
@@ -2403,9 +2407,9 @@ class ExoplayerView :
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
val episodeEnd =
exoPlayer.currentPosition / episodeLength >
- PrefManager.getVal(
- PrefName.WatchPercentage,
- )
+ PrefManager.getVal(
+ PrefName.WatchPercentage,
+ )
val episode0 = currentEpisodeIndex == 0 && PrefManager.getVal(PrefName.ChapterZeroPlayer)
if (!incognito && (episodeEnd || episode0) && Anilist.userid != null
) {
@@ -2484,7 +2488,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(Uri.parse(videoURL), "video/*")
+ shareVideo.setDataAndType(videoURL.toUri(), "video/*")
shareVideo.setPackage("com.instantbits.cast.webvideo")
if (subtitle != null) shareVideo.putExtra("subtitle", subtitleUrl)
shareVideo.putExtra(
@@ -2506,7 +2510,7 @@ class ExoplayerView :
} catch (ex: ActivityNotFoundException) {
val intent = Intent(Intent.ACTION_VIEW)
val uriString = "market://details?id=com.instantbits.cast.webvideo"
- intent.data = Uri.parse(uriString)
+ intent.data = uriString.toUri()
startActivity(intent)
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
index 359c64a5..f47f416f 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
@@ -16,6 +16,7 @@ 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(
@@ -76,7 +77,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 =
@@ -86,12 +87,20 @@ 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}")
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt
index a76d44a4..ddf98974 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt
@@ -7,9 +7,11 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckBox
+import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.NumberPicker
+import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity
@@ -265,19 +267,22 @@ class MangaReadAdapter(
}
// Multi download
- downloadNo.text = "0"
+ //downloadNo.text = "0"
mediaDownloadTop.setOnClickListener {
- // Alert dialog asking for the number of chapters to download
fragment.requireContext().customAlertDialog().apply {
setTitle("Multi Chapter Downloader")
setMessage("Enter the number of chapters to download")
- val input = NumberPicker(currContext())
- input.minValue = 1
- input.maxValue = 20
- input.value = 1
+ val input = View.inflate(currContext(), R.layout.dialog_layout, null)
+ val editText = input.findViewById(R.id.downloadNo)
setCustomView(input)
setPosButton(R.string.ok) {
- downloadNo.text = "${input.value}"
+ val value = editText.text.toString().toIntOrNull()
+ if (value != null && value > 0) {
+ downloadNo.setText(value.toString(), TextView.BufferType.EDITABLE)
+ fragment.multiDownload(value)
+ } else {
+ toast("Please enter a valid number")
+ }
}
setNegButton(R.string.cancel)
show()
@@ -382,8 +387,9 @@ class MangaReadAdapter(
setCustomView(root)
setPosButton("OK") {
if (run) fragment.onIconPressed(style, reversed)
- if (downloadNo.text != "0") {
- fragment.multiDownload(downloadNo.text.toString().toInt())
+ val value = downloadNo.text.toString().toIntOrNull()
+ if (value != null && value > 0) {
+ fragment.multiDownload(value)
}
if (refresh) fragment.loadChapters(source, true)
}
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
index 6b7eb6bc..2ad28a75 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
@@ -66,6 +66,7 @@ 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
@@ -232,25 +233,35 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun multiDownload(n: Int) {
- // Get last viewed chapter
- val selected = media.userProgress
- val chapters = media.manga?.chapters?.values?.toList()
- // Filter by selected language
- val progressChapterIndex = (chapters?.indexOfFirst {
- MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
- } ?: 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)
-
-
- for (chapter in chaptersToDownload) {
+ lifecycleScope.launch {
+ // Get the last viewed chapter
+ val selected = media.userProgress ?: 0
+ 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 {
+ 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
+ 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
}
}
@@ -474,7 +485,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
scanlator = chapter.scanlator ?: "Unknown",
imageData = images,
sourceMedia = media,
- retries = 2,
+ retries = 25,
simultaneousDownloads = 2
)
diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
index f343d70b..f534ce6b 100644
--- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
@@ -26,7 +26,6 @@ object AnimeSources : WatchSources() {
)
isInitialized = true
- // Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = sortPinnedAnimeSources(
createParsersFromExtensions(extensions),
diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
index adfd857e..0f2a438a 100644
--- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
@@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
?: return emptyList())
return try {
- val videos = source.getVideoList(sEpisode)
- videos.map { videoToVideoServer(it) }
+ // 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()
@@ -576,7 +586,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
number,
format!!,
FileUrl(videoUrl, headersMap),
- if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
+ null
)
}
@@ -636,7 +646,6 @@ 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)
diff --git a/app/src/main/java/ani/dantotsu/util/AlertDialogBuilder.kt b/app/src/main/java/ani/dantotsu/util/AlertDialogBuilder.kt
index 74683ab1..036dd1b4 100644
--- a/app/src/main/java/ani/dantotsu/util/AlertDialogBuilder.kt
+++ b/app/src/main/java/ani/dantotsu/util/AlertDialogBuilder.kt
@@ -3,6 +3,8 @@ 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
@@ -205,8 +207,14 @@ class AlertDialogBuilder(private val context: Context) {
onShow?.invoke()
}
dialog.window?.apply {
- setDimAmount(0.8f)
+ setDimAmount(0.5f)
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()
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt
index 171b07b2..4b54360a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt
@@ -1,5 +1,6 @@
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
@@ -48,6 +49,25 @@ 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 = 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