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