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!
|
> **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
|
||||||
|
|
|
@ -17,8 +17,9 @@ 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 300200200
|
versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -47,6 +48,10 @@ 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"
|
||||||
|
@ -80,25 +85,26 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// FireBase
|
// FireBase
|
||||||
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0')
|
googleImplementation platform('com.google.firebase:firebase-bom:33.13.0')
|
||||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0'
|
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.4.0'
|
||||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0'
|
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.4.3'
|
||||||
// Core
|
// Core
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'androidx.browser:browser:1.8.0'
|
implementation 'androidx.browser:browser:1.8.0'
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
implementation 'androidx.core:core-ktx:1.16.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
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.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.9.0"
|
implementation "androidx.work:work-runtime-ktx:2.10.1"
|
||||||
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.6.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
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 "com.anggrayudi:storage:1.5.5"
|
||||||
implementation "androidx.biometric:biometric:1.1.0"
|
implementation "androidx.biometric:biometric:1.1.0"
|
||||||
|
|
||||||
|
@ -112,7 +118,7 @@ dependencies {
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
ext.exo_version = '1.5.0'
|
ext.exo_version = '1.6.1'
|
||||||
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"
|
||||||
|
@ -123,7 +129,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.3"
|
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4"
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
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.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.2.1'
|
implementation 'androidx.paging:paging-runtime-ktx:3.3.6'
|
||||||
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'
|
||||||
|
|
||||||
|
@ -161,13 +167,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.12'
|
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||||
implementation 'com.squareup.okio:okio:3.8.0'
|
implementation 'com.squareup.okio:okio:3.9.1'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.14'
|
||||||
implementation 'org.jsoup:jsoup:1.16.1'
|
implementation 'org.jsoup:jsoup:1.18.1'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.7.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'
|
||||||
|
|
|
@ -113,21 +113,28 @@ class App : MultiDexApplication() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
scope.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)
|
||||||
}
|
}
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
scope.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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,11 @@ 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
|
||||||
|
@ -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) {
|
fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
|
||||||
|
|
|
@ -47,9 +47,9 @@ class Login : AppCompatActivity() {
|
||||||
view.evaluateJavascript(
|
view.evaluateJavascript(
|
||||||
"""
|
"""
|
||||||
(function() {
|
(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();
|
const m = []; webpackChunkdiscord_app.push([[""], {}, e => {for (let c in e.c)m.push(e.c[c])}]);
|
||||||
return wreq;
|
return m.find(n => n?.exports?.default?.getToken !== void 0)?.exports?.default?.getToken();
|
||||||
})()
|
})()
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
) { result ->
|
) { result ->
|
||||||
login(result.trim('"'))
|
login(result.trim('"'))
|
||||||
|
|
|
@ -50,8 +50,7 @@ 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(
|
return json.encodeToString(Presence.Response(
|
||||||
Presence.Response(
|
|
||||||
3,
|
3,
|
||||||
Presence(
|
Presence(
|
||||||
activities = listOf(
|
activities = listOf(
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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
|
||||||
|
@ -12,6 +13,8 @@ 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
|
||||||
|
@ -19,8 +22,10 @@ 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
|
||||||
|
@ -79,6 +84,7 @@ 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)
|
||||||
|
@ -109,6 +115,7 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -132,10 +139,12 @@ 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
|
||||||
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.mediaBottomBarContainer.setPadding(
|
||||||
rightMargin = navBarRightMargin
|
navBar.paddingLeft,
|
||||||
bottomMargin = navBarBottomMargin
|
navBar.paddingTop,
|
||||||
}
|
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 }
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
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
|
||||||
|
@ -21,6 +23,7 @@ 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()
|
||||||
|
@ -34,22 +37,34 @@ class SubtitleDownloader {
|
||||||
val responseBody = response.body.string()
|
val responseBody = response.body.string()
|
||||||
|
|
||||||
|
|
||||||
val subtitleType = when {
|
val subtitleType = getType(responseBody)
|
||||||
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(
|
||||||
|
|
|
@ -2,7 +2,6 @@ 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
|
||||||
|
@ -19,7 +18,6 @@ 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
|
||||||
|
@ -75,9 +73,7 @@ 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
|
||||||
|
@ -175,10 +171,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")
|
||||||
|
@ -1312,7 +1308,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[i]
|
speed = speeds.getOrNull(i) ?: 1f
|
||||||
curSpeed = i
|
curSpeed = i
|
||||||
playbackParameters = PlaybackParameters(speed)
|
playbackParameters = PlaybackParameters(speed)
|
||||||
exoPlayer.playbackParameters = playbackParameters
|
exoPlayer.playbackParameters = playbackParameters
|
||||||
|
@ -1362,7 +1358,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 {
|
||||||
|
@ -1384,7 +1380,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(
|
||||||
|
@ -1395,7 +1391,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(
|
||||||
|
@ -1609,29 +1605,27 @@ 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 = Uri.parse(subtitleUrl)
|
val fileUri = (subtitleUrl).toUri()
|
||||||
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_SSA
|
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||||
SubtitleType.SRT -> MimeTypes.TEXT_SSA
|
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||||
else -> MimeTypes.TEXT_SSA
|
else -> MimeTypes.TEXT_UNKNOWN
|
||||||
},
|
},
|
||||||
).setId("69")
|
).setId("69")
|
||||||
.setLanguage(subtitle.language)
|
.setLanguage(subtitle.language)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
println("sub: $sub")
|
|
||||||
} else {
|
} else {
|
||||||
val subUri = Uri.parse(subtitleUrl)
|
val subUri = subtitleUrl.toUri()
|
||||||
sub +=
|
sub +=
|
||||||
MediaItem.SubtitleConfiguration
|
MediaItem.SubtitleConfiguration
|
||||||
.Builder(subUri)
|
.Builder(subUri)
|
||||||
|
@ -1661,27 +1655,18 @@ class ExoplayerView :
|
||||||
followRedirects(true)
|
followRedirects(true)
|
||||||
followSslRedirects(true)
|
followSslRedirects(true)
|
||||||
}.build()
|
}.build()
|
||||||
val dataSourceFactory =
|
val httpDataSourceFactory =
|
||||||
DataSource.Factory {
|
OkHttpDataSource.Factory(httpClient).apply {
|
||||||
val dataSource: HttpDataSource =
|
setDefaultRequestProperties(defaultHeaders)
|
||||||
OkHttpDataSource.Factory(httpClient).createDataSource()
|
video?.file?.headers?.let {
|
||||||
defaultHeaders.forEach {
|
setDefaultRequestProperties(it)
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
|
||||||
}
|
}
|
||||||
video?.file?.headers?.forEach {
|
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
|
||||||
}
|
}
|
||||||
dataSource
|
val defaultDataSourceFactory = DefaultDataSource.Factory(this, httpDataSourceFactory)
|
||||||
}
|
|
||||||
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
|
|
||||||
cacheFactory =
|
cacheFactory =
|
||||||
CacheDataSource.Factory().apply {
|
CacheDataSource.Factory().apply {
|
||||||
setCache(VideoCache.getInstance(this@ExoplayerView))
|
setCache(VideoCache.getInstance(this@ExoplayerView))
|
||||||
if (ext.server.offline) {
|
setUpstreamDataSourceFactory(defaultDataSourceFactory)
|
||||||
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
|
|
||||||
} else {
|
|
||||||
setUpstreamDataSourceFactory(dataSourceFactory)
|
|
||||||
}
|
|
||||||
setCacheWriteDataSinkFactory(null)
|
setCacheWriteDataSinkFactory(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1943,7 +1928,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 = ""
|
||||||
|
@ -2503,7 +2488,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(Uri.parse(videoURL), "video/*")
|
shareVideo.setDataAndType(videoURL.toUri(), "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(
|
||||||
|
@ -2525,7 +2510,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 = Uri.parse(uriString)
|
intent.data = uriString.toUri()
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ 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(
|
||||||
|
@ -76,7 +77,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 =
|
||||||
|
@ -86,12 +87,20 @@ 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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ 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
|
||||||
|
@ -232,25 +233,35 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun multiDownload(n: Int) {
|
fun multiDownload(n: Int) {
|
||||||
// Get last viewed chapter
|
lifecycleScope.launch {
|
||||||
val selected = media.userProgress
|
// Get the last viewed chapter
|
||||||
|
val selected = media.userProgress ?: 0
|
||||||
val chapters = media.manga?.chapters?.values?.toList()
|
val chapters = media.manga?.chapters?.values?.toList()
|
||||||
// Filter by selected language
|
// Ensure chapters are available in the extensions
|
||||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
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
|
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||||
} ?: 0) + 1
|
} + 1).coerceAtLeast(0)
|
||||||
|
// Calculate the end value for the range of chapters to download
|
||||||
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
|
val endIndex = (progressChapterIndex + n).coerceAtMost(chapters.size)
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ 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),
|
||||||
|
|
|
@ -226,8 +226,18 @@ 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()
|
||||||
|
@ -576,7 +586,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
|
||||||
number,
|
number,
|
||||||
format!!,
|
format!!,
|
||||||
FileUrl(videoUrl, headersMap),
|
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 {
|
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)
|
||||||
|
|
|
@ -193,8 +193,7 @@ 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 =
|
val confirmPassword = view.confirmPasswordInput.text.toString()
|
||||||
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) {
|
||||||
|
@ -206,9 +205,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||||
|
|
||||||
if (canBiometricPrompt) {
|
if (canBiometricPrompt) {
|
||||||
val biometricPrompt =
|
val biometricPrompt =
|
||||||
BiometricPromptUtils.createBiometricPrompt(
|
BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
|
||||||
this@SettingsCommonActivity
|
|
||||||
) { _ ->
|
|
||||||
val token = UUID.randomUUID().toString()
|
val token = UUID.randomUUID().toString()
|
||||||
PrefManager.setVal(
|
PrefManager.setVal(
|
||||||
PrefName.BiometricToken,
|
PrefName.BiometricToken,
|
||||||
|
@ -238,14 +235,12 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||||
setOnShowListener {
|
setOnShowListener {
|
||||||
view.passwordInput.requestFocus()
|
view.passwordInput.requestFocus()
|
||||||
val canAuthenticate =
|
val canAuthenticate =
|
||||||
BiometricManager.from(applicationContext)
|
BiometricManager.from(applicationContext).canAuthenticate(
|
||||||
.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, "")
|
PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()
|
||||||
.isNotEmpty()
|
|
||||||
view.forgotPasswordCheckbox.isChecked =
|
view.forgotPasswordCheckbox.isChecked =
|
||||||
PrefManager.getVal(PrefName.OverridePassword)
|
PrefManager.getVal(PrefName.OverridePassword)
|
||||||
}
|
}
|
||||||
|
@ -319,8 +314,7 @@ 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 =
|
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||||
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))
|
||||||
|
|
|
@ -82,18 +82,9 @@ 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(
|
it.settingsTitle.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
|
||||||
R.string.subscriptions_checking_time_s,
|
PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime)
|
||||||
timeNames[i]
|
TaskScheduler.create(context, PrefManager.getVal(PrefName.UseAlarmManager)).scheduleAllTasks(context)
|
||||||
)
|
|
||||||
PrefManager.setVal(
|
|
||||||
PrefName.SubscriptionNotificationInterval,
|
|
||||||
curTime
|
|
||||||
)
|
|
||||||
TaskScheduler.create(
|
|
||||||
context,
|
|
||||||
PrefManager.getVal(PrefName.UseAlarmManager)
|
|
||||||
).scheduleAllTasks(context)
|
|
||||||
}
|
}
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
|
@ -134,8 +125,7 @@ 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 ->
|
||||||
|
|
|
@ -96,8 +96,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
|
||||||
themeSwitcher.apply {
|
themeSwitcher.apply {
|
||||||
setText(themeText)
|
setText(themeText)
|
||||||
setAdapter(
|
setAdapter(
|
||||||
ArrayAdapter(
|
ArrayAdapter(context,
|
||||||
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(
|
||||||
|
|
|
@ -52,8 +52,7 @@ class SubscriptionsBottomDialog : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
groupedSubscriptions.forEach { (parserName, mediaList) ->
|
groupedSubscriptions.forEach { (parserName, mediaList) ->
|
||||||
adapter.add(
|
adapter.add(SubscriptionSource(
|
||||||
SubscriptionSource(
|
|
||||||
parserName,
|
parserName,
|
||||||
mediaList.toMutableList(),
|
mediaList.toMutableList(),
|
||||||
adapter,
|
adapter,
|
||||||
|
|
|
@ -3,6 +3,8 @@ 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
|
||||||
|
|
||||||
|
@ -205,8 +207,14 @@ class AlertDialogBuilder(private val context: Context) {
|
||||||
onShow?.invoke()
|
onShow?.invoke()
|
||||||
}
|
}
|
||||||
dialog.window?.apply {
|
dialog.window?.apply {
|
||||||
setDimAmount(0.8f)
|
setDimAmount(0.5f)
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -48,6 +49,25 @@ 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.
|
||||||
|
|
|
@ -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
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.serialization.json.Json
|
||||||
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(
|
||||||
val url: String = "",
|
var videoUrl: String = "",
|
||||||
val quality: String = "",
|
val videoTitle: String = "",
|
||||||
var videoUrl: String? = null,
|
val resolution: Int? = null,
|
||||||
headers: Headers? = null,
|
val bitrate: Int? = null,
|
||||||
// "url", "language-label-2", "url2", "language-label-2"
|
val headers: Headers? = null,
|
||||||
|
val preferred: Boolean = false,
|
||||||
val subtitleTracks: List<Track> = emptyList(),
|
val subtitleTracks: List<Track> = emptyList(),
|
||||||
val audioTracks: 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
|
// TODO(1.6): Remove after ext lib bump
|
||||||
var headers: Headers? = headers
|
@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")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
constructor(
|
constructor(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -38,83 +108,132 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transient
|
fun copy(
|
||||||
var progressSubject: Subject<State, State>? = null
|
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) {
|
fun copy(
|
||||||
bytesDownloaded = bytesRead
|
videoUrl: String = this.videoUrl,
|
||||||
if (contentLength > totalContentLength) {
|
videoTitle: String = this.videoTitle,
|
||||||
totalContentLength = contentLength
|
resolution: Int? = this.resolution,
|
||||||
}
|
bitrate: Int? = this.bitrate,
|
||||||
val newProgress = if (totalContentLength > 0) {
|
headers: Headers? = this.headers,
|
||||||
(100 * totalBytesDownloaded / totalContentLength).toInt()
|
preferred: Boolean = this.preferred,
|
||||||
} else {
|
subtitleTracks: List<Track> = this.subtitleTracks,
|
||||||
-1
|
audioTracks: List<Track> = this.audioTracks,
|
||||||
}
|
timestamps: List<TimeStamp> = this.timestamps,
|
||||||
if (progress != newProgress) progress = newProgress
|
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 {
|
enum class State {
|
||||||
QUEUE,
|
QUEUE,
|
||||||
LOAD_VIDEO,
|
LOAD_VIDEO,
|
||||||
DOWNLOAD_IMAGE,
|
|
||||||
READY,
|
READY,
|
||||||
ERROR,
|
ERROR,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@kotlinx.serialization.Serializable
|
||||||
private fun writeObject(out: ObjectOutputStream) {
|
data class SerializableVideo(
|
||||||
out.defaultWriteObject()
|
val videoUrl: String = "",
|
||||||
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
|
val videoTitle: String = "",
|
||||||
out.writeObject(headersMap)
|
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")
|
companion object {
|
||||||
@Throws(IOException::class, ClassNotFoundException::class)
|
fun List<Video>.serialize(): String =
|
||||||
private fun readObject(input: ObjectInputStream) {
|
Json.encodeToString(
|
||||||
input.defaultReadObject()
|
this.map { vid ->
|
||||||
val headersMap = input.readObject() as? Map<String, List<String>>
|
SerializableVideo(
|
||||||
headers = headersMap?.let { map ->
|
vid.videoUrl,
|
||||||
val builder = Headers.Builder()
|
vid.videoTitle,
|
||||||
for ((key, values) in map) {
|
vid.resolution,
|
||||||
for (value in values) {
|
vid.bitrate,
|
||||||
builder.add(key, value)
|
vid.headers?.toList(),
|
||||||
}
|
vid.preferred,
|
||||||
}
|
vid.subtitleTracks,
|
||||||
builder.build()
|
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
|
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
|
||||||
|
@ -67,7 +68,7 @@ internal class ExtensionGithubApi {
|
||||||
val repos =
|
val repos =
|
||||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
|
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
|
||||||
|
|
||||||
repos.forEach {
|
repos.asyncMap {
|
||||||
val repoUrl = if (it.contains("index.min.json")) {
|
val repoUrl = if (it.contains("index.min.json")) {
|
||||||
it
|
it
|
||||||
} else {
|
} else {
|
||||||
|
@ -155,7 +156,7 @@ internal class ExtensionGithubApi {
|
||||||
val repos =
|
val repos =
|
||||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
||||||
|
|
||||||
repos.forEach {
|
repos.asyncMap {
|
||||||
val repoUrl = if (it.contains("index.min.json")) {
|
val repoUrl = if (it.contains("index.min.json")) {
|
||||||
it
|
it
|
||||||
} else {
|
} else {
|
||||||
|
@ -207,7 +208,7 @@ internal class ExtensionGithubApi {
|
||||||
val repos =
|
val repos =
|
||||||
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos).toMutableList()
|
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos).toMutableList()
|
||||||
|
|
||||||
repos.forEach {
|
repos.asyncMap {
|
||||||
val repoUrl = if (it.contains("index.min.json")) {
|
val repoUrl = if (it.contains("index.min.json")) {
|
||||||
it
|
it
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -419,13 +419,19 @@
|
||||||
</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"
|
||||||
|
@ -438,5 +444,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>
|
||||||
|
|
|
@ -353,11 +353,18 @@
|
||||||
</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|bottom"
|
android:layout_gravity="center_horizontal|top"
|
||||||
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"
|
||||||
|
@ -371,6 +378,7 @@
|
||||||
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>
|
||||||
|
|
||||||
|
|
|
@ -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="5.0"
|
android:stepSize="1.0"
|
||||||
android:value="15.0"
|
android:value="10.0"
|
||||||
android:valueFrom="5.0"
|
android:valueFrom="1.0"
|
||||||
android:valueTo="45.0"
|
android:valueTo="50.0"
|
||||||
app:labelBehavior="floating"
|
app:labelBehavior="floating"
|
||||||
app:labelStyle="@style/fontTooltip"
|
app:labelStyle="@style/fontTooltip"
|
||||||
app:thumbColor="?attr/colorSecondary"
|
app:thumbColor="?attr/colorSecondary"
|
||||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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-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"
|
||||||
|
|
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
|
#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.11.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue