Compare commits

..

6 commits
dev ... main

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,8 +3,6 @@ package ani.dantotsu.util
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.os.Build
import android.view.WindowManager
import android.view.View import android.view.View
import ani.dantotsu.R import ani.dantotsu.R
@ -207,14 +205,8 @@ class AlertDialogBuilder(private val context: Context) {
onShow?.invoke() onShow?.invoke()
} }
dialog.window?.apply { dialog.window?.apply {
setDimAmount(0.5f) setDimAmount(0.8f)
attributes.windowAnimations = android.R.style.Animation_Dialog 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() dialog.show()
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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