diff --git a/README.md b/README.md
index 6b713a2a..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 0604d87e..535d8922 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -17,8 +17,9 @@ android {
applicationId "ani.dantotsu"
minSdk 21
targetSdk 35
+ versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.2.2"
- versionCode 300200200
+ versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
signingConfig signingConfigs.debug
}
@@ -47,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"
@@ -80,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"
@@ -112,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"
@@ -123,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'
@@ -132,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'
@@ -161,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/connections/discord/RPC.kt b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
index eec99b96..20bafb39 100644
--- a/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
+++ b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
@@ -50,9 +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(
- 3,
+ return json.encodeToString(Presence.Response(
+ 3,
Presence(
activities = listOf(
Activity(
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 4de053dd..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")
@@ -1312,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
@@ -1362,7 +1358,7 @@ class ExoplayerView :
val showProgressDialog =
if (PrefManager.getVal(PrefName.AskIndividualPlayer)) {
PrefManager.getCustomVal(
- "${media.id}_ProgressDialog",
+ "${media.id}_progressDialog",
true,
)
} else {
@@ -1384,7 +1380,7 @@ class ExoplayerView :
setCancelable(false)
setPosButton(R.string.yes) {
PrefManager.setCustomVal(
- "${media.id}_ProgressDialog",
+ "${media.id}_progressDialog",
false,
)
PrefManager.setCustomVal(
@@ -1395,7 +1391,7 @@ class ExoplayerView :
}
setNegButton(R.string.no) {
PrefManager.setCustomVal(
- "${media.id}_ProgressDialog",
+ "${media.id}_progressDialog",
false,
)
PrefManager.setCustomVal(
@@ -1609,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)
@@ -1661,27 +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)
}
@@ -1943,7 +1928,7 @@ class ExoplayerView :
if (PrefManager.getVal(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 = ""
@@ -2503,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(
@@ -2525,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/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
index 63ef7408..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
}
}
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/settings/SettingsCommonActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt
index f0bf91d0..38230a86 100644
--- a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt
@@ -193,8 +193,7 @@ 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) {
@@ -202,13 +201,11 @@ class SettingsCommonActivity : AppCompatActivity() {
BiometricManager
.from(applicationContext)
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ==
- BiometricManager.BIOMETRIC_SUCCESS
+ BiometricManager.BIOMETRIC_SUCCESS
if (canBiometricPrompt) {
val biometricPrompt =
- BiometricPromptUtils.createBiometricPrompt(
- this@SettingsCommonActivity
- ) { _ ->
+ BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
val token = UUID.randomUUID().toString()
PrefManager.setVal(
PrefName.BiometricToken,
@@ -238,14 +235,12 @@ class SettingsCommonActivity : AppCompatActivity() {
setOnShowListener {
view.passwordInput.requestFocus()
val canAuthenticate =
- BiometricManager.from(applicationContext)
- .canAuthenticate(
- BiometricManager.Authenticators.BIOMETRIC_WEAK,
- ) == BiometricManager.BIOMETRIC_SUCCESS
+ 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)
}
@@ -319,8 +314,7 @@ class SettingsCommonActivity : AppCompatActivity() {
setTitle(R.string.change_download_location)
setMessage(R.string.download_location_msg)
setPosButton(R.string.ok) {
- val oldUri =
- PrefManager.getVal(PrefName.DownloadsDir)
+ val oldUri = PrefManager.getVal(PrefName.DownloadsDir)
launcher.registerForCallback { success ->
if (success) {
toast(getString(R.string.please_wait))
diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt
index f1225ed4..c5ca440a 100644
--- a/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt
@@ -82,18 +82,9 @@ 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()
}
@@ -129,22 +120,21 @@ class SettingsNotificationActivity : AppCompatActivity() {
.toMutableSet()
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
context.customAlertDialog().apply {
- setTitle(R.string.anilist_notification_filters)
- multiChoiceItems(
+ setTitle(R.string.anilist_notification_filters)
+ multiChoiceItems(
types.map { name ->
name.replace("_", " ").lowercase().replaceFirstChar {
- if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
- }
- }.toTypedArray(),
+ if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
+ } }.toTypedArray(),
selected
) { updatedSelected ->
- types.forEachIndexed { index, type ->
- if (updatedSelected[index]) {
- filteredTypes.add(type)
- } else {
- filteredTypes.remove(type)
- }
+ types.forEachIndexed { index, type ->
+ if (updatedSelected[index]) {
+ filteredTypes.add(type)
+ } else {
+ filteredTypes.remove(type)
}
+ }
PrefManager.setVal(PrefName.AnilistFilteredTypes, filteredTypes)
}
show()
@@ -162,8 +152,8 @@ class SettingsNotificationActivity : AppCompatActivity() {
icon = R.drawable.ic_round_notifications_none_24,
onClick = {
context.customAlertDialog().apply {
- setTitle(R.string.subscriptions_checking_time)
- singleChoiceItems(
+ setTitle(R.string.subscriptions_checking_time)
+ singleChoiceItems(
aItems.toTypedArray(),
PrefManager.getVal(PrefName.AnilistNotificationInterval)
) { i ->
@@ -191,11 +181,11 @@ class SettingsNotificationActivity : AppCompatActivity() {
icon = R.drawable.ic_round_notifications_none_24,
onClick = {
context.customAlertDialog().apply {
- setTitle(R.string.subscriptions_checking_time)
- singleChoiceItems(
+ setTitle(R.string.subscriptions_checking_time)
+ singleChoiceItems(
cItems.toTypedArray(),
PrefManager.getVal(PrefName.CommentNotificationInterval)
- ) { i ->
+ ) { i ->
PrefManager.setVal(PrefName.CommentNotificationInterval, i)
it.settingsTitle.text =
getString(
@@ -235,9 +225,9 @@ class SettingsNotificationActivity : AppCompatActivity() {
switch = { isChecked, view ->
if (isChecked) {
context.customAlertDialog().apply {
- setTitle(R.string.use_alarm_manager)
- setMessage(R.string.use_alarm_manager_confirm)
- setPosButton(R.string.use) {
+ setTitle(R.string.use_alarm_manager)
+ setMessage(R.string.use_alarm_manager_confirm)
+ setPosButton(R.string.use) {
PrefManager.setVal(PrefName.UseAlarmManager, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt
index 648c26bd..8133640a 100644
--- a/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt
@@ -96,8 +96,7 @@ 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(
diff --git a/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt b/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt
index 188da9d3..9fa7552e 100644
--- a/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt
@@ -52,15 +52,14 @@ class SubscriptionsBottomDialog : BottomSheetDialogFragment() {
}
groupedSubscriptions.forEach { (parserName, mediaList) ->
- adapter.add(
- SubscriptionSource(
- parserName,
- mediaList.toMutableList(),
- adapter,
- getParserIcon(parserName)
- ) { group ->
- adapter.remove(group)
- })
+ adapter.add(SubscriptionSource(
+ parserName,
+ mediaList.toMutableList(),
+ adapter,
+ getParserIcon(parserName)
+ ) { group ->
+ adapter.remove(group)
+ })
}
}
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