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