Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1fbbe3e77c | ||
![]() |
fd607d98f8 | ||
![]() |
c3f6d0ecee | ||
![]() |
5124d6a2d8 | ||
![]() |
e83a0fe7da | ||
![]() |
61a8350043 | ||
![]() |
baffbc845c | ||
![]() |
afd9f6b884 | ||
![]() |
7d0894cd92 | ||
![]() |
dec2ed7959 | ||
![]() |
e4630df3e0 | ||
![]() |
6fd3515d2c |
27 changed files with 569 additions and 262 deletions
|
@ -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!
|
||||
|
||||
<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
|
||||
By downloading, installing, or using this application, you agree to:
|
||||
- Use the application in compliance with all applicable laws
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -113,21 +113,28 @@ class App : MultiDexApplication() {
|
|||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
scope.launch {
|
||||
animeExtensionManager = Injekt.get()
|
||||
launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
scope.launch {
|
||||
mangaExtensionManager = Injekt.get()
|
||||
launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
scope.launch {
|
||||
novelExtensionManager = Injekt.get()
|
||||
launch {
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
|
|
|
@ -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 <A, B> Collection<A>.asyncMap(f: suspend (A) -> B): List<B> = 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 <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 {
|
||||
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 <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) {
|
||||
|
|
|
@ -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('"'))
|
||||
|
|
|
@ -50,8 +50,7 @@ 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(
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
activities = listOf(
|
||||
|
|
|
@ -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<ViewGroup.MarginLayoutParams> {
|
||||
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<ViewGroup.MarginLayoutParams> {
|
||||
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<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
|
|
|
@ -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,6 +23,7 @@ class SubtitleDownloader {
|
|||
suspend fun loadSubtitleType(url: String): SubtitleType =
|
||||
withContext(Dispatchers.IO) {
|
||||
return@withContext try {
|
||||
if (!url.startsWith("file")) {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val request = Request.Builder()
|
||||
|
@ -34,22 +37,34 @@ class SubtitleDownloader {
|
|||
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
|
||||
}
|
||||
} else {
|
||||
val uri = url.toUri()
|
||||
val file = uri.toFile()
|
||||
val fileBody = file.readText()
|
||||
val subtitleType = getType(fileBody)
|
||||
subtitleType
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
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
|
||||
@Deprecated("handled externally")
|
||||
suspend fun downloadSubtitle(
|
||||
|
|
|
@ -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<MediaItem.SubtitleConfiguration>().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<Boolean>(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
lifecycleScope.launch {
|
||||
// Get the last viewed chapter
|
||||
val selected = media.userProgress ?: 0
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
// Filter by selected language
|
||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||
// 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
|
||||
} ?: 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
|
||||
} + 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ object AnimeSources : WatchSources() {
|
|||
)
|
||||
isInitialized = true
|
||||
|
||||
// Update as StateFlow emits new values
|
||||
fromExtensions.collect { extensions ->
|
||||
list = sortPinnedAnimeSources(
|
||||
createParsersFromExtensions(extensions),
|
||||
|
|
|
@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
?: return emptyList())
|
||||
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -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) {
|
||||
|
@ -206,9 +205,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
|||
|
||||
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.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<String>(PrefName.DownloadsDir)
|
||||
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
launcher.registerForCallback { success ->
|
||||
if (success) {
|
||||
toast(getString(R.string.please_wait))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
@ -134,8 +125,7 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
|||
types.map { name ->
|
||||
name.replace("_", " ").lowercase().replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
|
||||
}
|
||||
}.toTypedArray(),
|
||||
} }.toTypedArray(),
|
||||
selected
|
||||
) { updatedSelected ->
|
||||
types.forEachIndexed { index, type ->
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -52,8 +52,7 @@ class SubscriptionsBottomDialog : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
groupedSubscriptions.forEach { (parserName, mediaList) ->
|
||||
adapter.add(
|
||||
SubscriptionSource(
|
||||
adapter.add(SubscriptionSource(
|
||||
parserName,
|
||||
mediaList.toMutableList(),
|
||||
adapter,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
* in the expected order; the index is ignored.
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +1,101 @@
|
|||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import rx.subjects.Subject
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
|
||||
@kotlinx.serialization.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(
|
||||
val url: String = "",
|
||||
val quality: String = "",
|
||||
var videoUrl: String? = null,
|
||||
headers: Headers? = null,
|
||||
// "url", "language-label-2", "url2", "language-label-2"
|
||||
var videoUrl: String = "",
|
||||
val videoTitle: String = "",
|
||||
val resolution: Int? = null,
|
||||
val bitrate: Int? = null,
|
||||
val headers: Headers? = null,
|
||||
val preferred: Boolean = false,
|
||||
val subtitleTracks: List<Track> = emptyList(),
|
||||
val audioTracks: List<Track> = emptyList(),
|
||||
) : Serializable, ProgressListener {
|
||||
val timestamps: List<TimeStamp> = emptyList(),
|
||||
val internalData: String = "",
|
||||
val initialized: Boolean = false,
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
val videoPageUrl: String = "",
|
||||
) {
|
||||
|
||||
@Transient
|
||||
var headers: Headers? = headers
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
@Deprecated("Use videoTitle instead", ReplaceWith("videoTitle"))
|
||||
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")
|
||||
constructor(
|
||||
url: String,
|
||||
|
@ -38,83 +108,132 @@ open class Video(
|
|||
@Transient
|
||||
@Volatile
|
||||
var status: State = State.QUEUE
|
||||
|
||||
@Transient
|
||||
private val _progressFlow = MutableStateFlow(0)
|
||||
|
||||
@Transient
|
||||
val progressFlow = _progressFlow.asStateFlow()
|
||||
var progress: Int
|
||||
get() = _progressFlow.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
|
||||
}
|
||||
|
||||
@Transient
|
||||
var progressSubject: Subject<State, State>? = null
|
||||
fun copy(
|
||||
videoUrl: String = this.videoUrl,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
bytesDownloaded = bytesRead
|
||||
if (contentLength > totalContentLength) {
|
||||
totalContentLength = contentLength
|
||||
}
|
||||
val newProgress = if (totalContentLength > 0) {
|
||||
(100 * totalBytesDownloaded / totalContentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
if (progress != newProgress) progress = newProgress
|
||||
fun copy(
|
||||
videoUrl: String = this.videoUrl,
|
||||
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,
|
||||
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 {
|
||||
QUEUE,
|
||||
LOAD_VIDEO,
|
||||
DOWNLOAD_IMAGE,
|
||||
READY,
|
||||
ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeObject(out: ObjectOutputStream) {
|
||||
out.defaultWriteObject()
|
||||
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
|
||||
out.writeObject(headersMap)
|
||||
}
|
||||
@kotlinx.serialization.Serializable
|
||||
data class SerializableVideo(
|
||||
val videoUrl: String = "",
|
||||
val videoTitle: String = "",
|
||||
val resolution: Int? = null,
|
||||
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 = "",
|
||||
) {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
private fun readObject(input: ObjectInputStream) {
|
||||
input.defaultReadObject()
|
||||
val headersMap = input.readObject() as? Map<String, List<String>>
|
||||
headers = headersMap?.let { map ->
|
||||
val builder = Headers.Builder()
|
||||
for ((key, values) in map) {
|
||||
for (value in values) {
|
||||
builder.add(key, value)
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
companion object {
|
||||
fun List<Video>.serialize(): String =
|
||||
Json.encodeToString(
|
||||
this.map { vid ->
|
||||
SerializableVideo(
|
||||
vid.videoUrl,
|
||||
vid.videoTitle,
|
||||
vid.resolution,
|
||||
vid.bitrate,
|
||||
vid.headers?.toList(),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import ani.dantotsu.asyncMap
|
||||
import ani.dantotsu.parsers.novel.AvailableNovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtension
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
|
@ -67,7 +68,7 @@ internal class ExtensionGithubApi {
|
|||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
repos.asyncMap {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
|
@ -155,7 +156,7 @@ internal class ExtensionGithubApi {
|
|||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
repos.asyncMap {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
|
@ -207,7 +208,7 @@ internal class ExtensionGithubApi {
|
|||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
repos.asyncMap {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
|
|
|
@ -419,13 +419,19 @@
|
|||
</LinearLayout>
|
||||
|
||||
</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
|
||||
android:id="@+id/mediaBottomBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_weight="0"
|
||||
android:background="?attr/colorSurface"
|
||||
android:padding="0dp"
|
||||
app:abb_animationInterpolator="@anim/over_shoot"
|
||||
|
@ -438,5 +444,6 @@
|
|||
app:itemTextAppearanceActive="@style/NavBarText"
|
||||
app:itemTextAppearanceInactive="@style/NavBarText"
|
||||
app:itemTextColor="@color/tab_layout_icon" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -353,11 +353,18 @@
|
|||
</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
|
||||
android:id="@+id/mediaBottomBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_gravity="center_horizontal|top"
|
||||
android:background="?attr/colorSurface"
|
||||
android:padding="0dp"
|
||||
app:abb_animationInterpolator="@anim/over_shoot"
|
||||
|
@ -371,6 +378,7 @@
|
|||
app:itemTextAppearanceActive="@style/NavBarText"
|
||||
app:itemTextAppearanceInactive="@style/NavBarText"
|
||||
app:itemTextColor="@color/tab_layout_icon" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -1102,10 +1102,10 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:stepSize="5.0"
|
||||
android:value="15.0"
|
||||
android:valueFrom="5.0"
|
||||
android:valueTo="45.0"
|
||||
android:stepSize="1.0"
|
||||
android:value="10.0"
|
||||
android:valueFrom="1.0"
|
||||
android:valueTo="50.0"
|
||||
app:labelBehavior="floating"
|
||||
app:labelStyle="@style/fontTooltip"
|
||||
app:thumbColor="?attr/colorSecondary"
|
||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.9.0'
|
||||
classpath 'com.android.tools.build:gradle:8.7.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version"
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
#Wed Aug 30 19:57:04 IST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue