Compare commits

...

15 Commits
v3.2.0 ... dev

Author SHA1 Message Date
Ankit Grai
c3f6d0ecee fix : dialog key names (#605) 2025-05-22 12:51:18 +05:30
aayush262
5124d6a2d8 fix: discord login 2025-05-22 12:50:06 +05:30
Ankit Grai
e83a0fe7da fix : long press progress dialog reset not working (#603) 2025-05-19 17:11:24 +05:30
rebel onion
61a8350043 fix: avoid waiting on network for local exts 2025-05-15 01:47:06 -05:00
rebel onion
baffbc845c fix: help bounds check /w custom speeds 2025-05-15 01:16:23 -05:00
rebel onion
afd9f6b884 fix: subtitles not showing 2025-05-15 01:13:47 -05:00
rebel onion
7d0894cd92 chore: bump extension interface 2025-05-14 22:35:50 -05:00
Rishvaish
dec2ed7959 hope for the best
* Update README.md

* To install multiple mangas

users can enter the value required to install as there is an EditText field instead of the Text View

* Issues

1)Installation of many mangas at same time now made to one to increase the installation efficiency
2)Installation order from the latest progresses chapter to the limit index
3)Tried to resolve the app crash bug

* Issues

1)Installation of many mangas at same time now made to one to increase the installation efficiency
2)Installation order from the latest progresses chapter to the limit index
3)Tried to resolve the app crash bug

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2025-04-23 14:58:42 +05:30
aayush262
e4630df3e0 move stuff to dev (#587)
* Update README.md

* Fixed missing manga pages when downloading (#586)

* To install multiple mangas (#582)

users can enter the value required to install as there is an EditText field instead of the Text View

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
Co-authored-by: Daniele Santoru <30676094+danyev3@users.noreply.github.com>
Co-authored-by: Rishvaish <68911202+rishabpuranika@users.noreply.github.com>
2025-04-02 10:52:58 +05:30
Sadwhy
6fd3515d2c stuff (#567)
* Add blur to dialog for devices that support it

* More adjustable seek time

* Bump exo player
2025-01-17 13:03:01 +05:30
rebel onion
6fa2f11db2 Merge branch 'main' into dev 2025-01-16 00:15:21 -06:00
rebel onion
a5babea27c chore: version bump 2025-01-16 00:14:25 -06:00
rebelonion
8a9b8cca7e fix: Serializable 2025-01-13 14:23:02 -06:00
rebel onion
7479f5f43b Update stable.md 2025-01-09 19:58:00 -06:00
Sadwhy
3ac9307329 Use custom alert builder for all dialogs [skip ci] 2025-01-09 18:04:22 +05:30
27 changed files with 1986 additions and 1499 deletions

View File

@@ -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=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
## Terms of Use ## Terms of Use
By downloading, installing, or using this application, you agree to: By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws - Use the application in compliance with all applicable laws

View File

@@ -18,8 +18,8 @@ android {
minSdk 21 minSdk 21
targetSdk 35 targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger()) versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.2.0" versionName "3.2.1"
versionCode 300200000 versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@@ -48,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"
@@ -81,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"
@@ -113,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"
@@ -124,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'
@@ -133,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'
@@ -162,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'

View File

@@ -1,4 +1,4 @@
`<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">

View File

@@ -113,21 +113,28 @@ class App : MultiDexApplication() {
} }
} }
CoroutineScope(Dispatchers.IO).launch { val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
animeExtensionManager = Injekt.get() animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions() launch {
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()
mangaExtensionManager.findAvailableExtensions() launch {
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()
novelExtensionManager.findAvailableExtensions() launch {
novelExtensionManager.findAvailableExtensions()
}
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow) NovelSources.init(novelExtensionManager.installedExtensionsFlow)
} }

View File

@@ -253,7 +253,7 @@ data class MediaStreamingEpisode(
// The site location of the streaming episode // The site location of the streaming episode
@SerialName("site") var site: String?, @SerialName("site") var site: String?,
) ) : java.io.Serializable
@Serializable @Serializable
data class MediaCoverImage( data class MediaCoverImage(

View File

@@ -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('"'))

View File

@@ -232,12 +232,18 @@ class MangaDownloaderService : Service() {
image.page, image.page,
image.source image.source
) )
if (bitmap == null) {
snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
}
retryCount++ retryCount++
} }
if (bitmap != null) { if (bitmap == null) {
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap) outputDir.deleteRecursively(this@MangaDownloaderService, false)
throw Exception("${task.chapter} - Unable to download all pages after $retryCount attempts. Try again.")
} }
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
farthest++ farthest++
builder.setProgress(task.imageData.size, farthest, false) builder.setProgress(task.imageData.size, farthest, false)

View File

@@ -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> { navBar.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 }

View File

@@ -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,28 +23,32 @@ 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 {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it if (!url.startsWith("file")) {
val networkHelper = Injekt.get<NetworkHelper>() // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val request = Request.Builder() val networkHelper = Injekt.get<NetworkHelper>()
.url(url) val request = Request.Builder()
.build() .url(url)
.build()
val response = networkHelper.client.newCall(request).execute() val response = networkHelper.client.newCall(request).execute()
// Check if response is successful // Check if response is successful
if (response.isSuccessful) { if (response.isSuccessful) {
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 subtitleType
else -> SubtitleType.SRT } else {
SubtitleType.UNKNOWN
} }
subtitleType
} else { } else {
SubtitleType.UNKNOWN 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)
@@ -50,6 +56,15 @@ class SubtitleDownloader {
} }
} }
private fun getType(content: String): SubtitleType {
return when {
content.contains("[Script Info]") -> SubtitleType.ASS
content.contains("WEBVTT") -> SubtitleType.VTT
content.contains("SRT") -> SubtitleType.SRT
else -> SubtitleType.UNKNOWN
}
}
//actually downloads lol //actually downloads lol
@Deprecated("handled externally") @Deprecated("handled externally")
suspend fun downloadSubtitle( suspend fun downloadSubtitle(

File diff suppressed because it is too large Load Diff

View File

@@ -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}")
} }
} }

View File

@@ -7,9 +7,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
@@ -265,19 +267,22 @@ class MangaReadAdapter(
} }
// Multi download // Multi download
downloadNo.text = "0" //downloadNo.text = "0"
mediaDownloadTop.setOnClickListener { mediaDownloadTop.setOnClickListener {
// Alert dialog asking for the number of chapters to download
fragment.requireContext().customAlertDialog().apply { fragment.requireContext().customAlertDialog().apply {
setTitle("Multi Chapter Downloader") setTitle("Multi Chapter Downloader")
setMessage("Enter the number of chapters to download") setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext()) val input = View.inflate(currContext(), R.layout.dialog_layout, null)
input.minValue = 1 val editText = input.findViewById<EditText>(R.id.downloadNo)
input.maxValue = 20
input.value = 1
setCustomView(input) setCustomView(input)
setPosButton(R.string.ok) { setPosButton(R.string.ok) {
downloadNo.text = "${input.value}" val value = editText.text.toString().toIntOrNull()
if (value != null && value > 0) {
downloadNo.setText(value.toString(), TextView.BufferType.EDITABLE)
fragment.multiDownload(value)
} else {
toast("Please enter a valid number")
}
} }
setNegButton(R.string.cancel) setNegButton(R.string.cancel)
show() show()
@@ -382,8 +387,9 @@ class MangaReadAdapter(
setCustomView(root) setCustomView(root)
setPosButton("OK") { setPosButton("OK") {
if (run) fragment.onIconPressed(style, reversed) if (run) fragment.onIconPressed(style, reversed)
if (downloadNo.text != "0") { val value = downloadNo.text.toString().toIntOrNull()
fragment.multiDownload(downloadNo.text.toString().toInt()) if (value != null && value > 0) {
fragment.multiDownload(value)
} }
if (refresh) fragment.loadChapters(source, true) if (refresh) fragment.loadChapters(source, true)
} }

View File

@@ -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 chapters = media.manga?.chapters?.values?.toList() val selected = media.userProgress ?: 0
// Filter by selected language val chapters = media.manga?.chapters?.values?.toList()
val progressChapterIndex = (chapters?.indexOfFirst { // Ensure chapters are available in the extensions
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected if (chapters.isNullOrEmpty() || n < 1) return@launch
} ?: 0) + 1 // Find the index of the last viewed chapter
val progressChapterIndex = (chapters.indexOfFirst {
if (progressChapterIndex < 0 || n < 1 || chapters == null) return MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} + 1).coerceAtLeast(0)
// Calculate the end index // Calculate the end value for the range of chapters to download
val endIndex = minOf(progressChapterIndex + n, chapters.size) val endIndex = (progressChapterIndex + n).coerceAtMost(chapters.size)
// Get the list of chapters to download
// 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) {
try {
for (chapter in chaptersToDownload) { 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
} }
} }
@@ -474,7 +485,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
scanlator = chapter.scanlator ?: "Unknown", scanlator = chapter.scanlator ?: "Unknown",
imageData = images, imageData = images,
sourceMedia = media, sourceMedia = media,
retries = 2, retries = 25,
simultaneousDownloads = 2 simultaneousDownloads = 2
) )

View File

@@ -24,11 +24,11 @@ class CrashActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
initActivity(this) initActivity(this)
binding = ActivityCrashBinding.inflate(layoutInflater)
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE WindowManager.LayoutParams.FLAG_SECURE
) )
binding = ActivityCrashBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight

View File

@@ -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),

View File

@@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
?: return emptyList()) ?: return emptyList())
return try { return try {
val videos = source.getVideoList(sEpisode) // TODO(1.6): Remove else block when dropping support for ext lib <1.6
videos.map { videoToVideoServer(it) } 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) { } 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)

View File

@@ -19,6 +19,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.customAlertDialog
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -57,23 +58,16 @@ class SettingsAnimeActivity : AppCompatActivity() {
desc = getString(R.string.purge_anime_downloads_desc), desc = getString(R.string.purge_anime_downloads_desc),
icon = R.drawable.ic_round_delete_24, icon = R.drawable.ic_round_delete_24,
onClick = { onClick = {
val dialog = AlertDialog.Builder(context, R.style.MyPopup) context.customAlertDialog().apply {
.setTitle(R.string.purge_anime_downloads) setTitle(R.string.purge_anime_downloads)
.setMessage( setMessage(R.string.purge_confirm, getString(R.string.anime))
getString( setPosButton(R.string.yes, onClick = {
R.string.purge_confirm,
getString(R.string.anime)
)
)
.setPositiveButton(R.string.yes) { dialog, _ ->
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.purgeDownloads(MediaType.ANIME) downloadsManager.purgeDownloads(MediaType.ANIME)
dialog.dismiss() })
}.setNegativeButton(R.string.no) { dialog, _ -> setNegButton(R.string.no)
dialog.dismiss() show()
}.create() }
dialog.window?.setDimAmount(0.8f)
dialog.show()
} }
), ),
@@ -143,4 +137,4 @@ class SettingsAnimeActivity : AppCompatActivity() {
} }
} }
} }

View File

@@ -45,7 +45,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.UUID import java.util.UUID
class SettingsCommonActivity : AppCompatActivity() { class SettingsCommonActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsCommonBinding private lateinit var binding: ActivitySettingsCommonBinding
private lateinit var launcher: LauncherWrapper private lateinit var launcher: LauncherWrapper
@@ -62,23 +61,27 @@ class SettingsCommonActivity : AppCompatActivity() {
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) { if (uri != null) {
try { try {
val jsonString = contentResolver.openInputStream(uri)?.readBytes() val jsonString =
?: throw Exception("Error reading file") contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name = DocumentFile.fromSingleUri(this, uri)?.name ?: "settings" val name = DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not // .sani is encrypted, .ani is not
if (name.endsWith(".sani")) { if (name.endsWith(".sani")) {
passwordAlertDialog(false) { password -> passwordAlertDialog(false) { password ->
if (password != null) { if (password != null) {
val salt = jsonString.copyOfRange(0, 16) val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size) val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try { val decryptedJson =
PreferenceKeystore.decryptWithPassword( try {
password, encrypted, salt PreferenceKeystore.decryptWithPassword(
) password,
} catch (e: Exception) { encrypted,
toast(getString(R.string.incorrect_password)) salt,
return@passwordAlertDialog )
} } catch (e: Exception) {
toast(getString(R.string.incorrect_password))
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) restartApp() if (PreferencePackager.unpack(decryptedJson)) restartApp()
} else { } else {
toast(getString(R.string.password_cannot_be_empty)) toast(getString(R.string.password_cannot_be_empty))
@@ -100,7 +103,6 @@ class SettingsCommonActivity : AppCompatActivity() {
launcher = LauncherWrapper(this, contract) launcher = LauncherWrapper(this, contract)
binding.apply { binding.apply {
settingsCommonLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> { settingsCommonLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
bottomMargin = navBarHeight bottomMargin = navBarHeight
@@ -108,27 +110,30 @@ class SettingsCommonActivity : AppCompatActivity() {
commonSettingsBack.setOnClickListener { commonSettingsBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
val exDns = listOf( val exDns =
"None", listOf(
"Cloudflare", "None",
"Google", "Cloudflare",
"AdGuard", "Google",
"Quad9", "AdGuard",
"AliDNS", "Quad9",
"DNSPod", "AliDNS",
"360", "DNSPod",
"Quad101", "360",
"Mullvad", "Quad101",
"Controld", "Mullvad",
"Njalla", "Controld",
"Shecan", "Njalla",
"Libre" "Shecan",
) "Libre",
)
settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)]) settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)])
settingsExtensionDns.setAdapter( settingsExtensionDns.setAdapter(
ArrayAdapter( ArrayAdapter(
context, R.layout.item_dropdown, exDns context,
) R.layout.item_dropdown,
exDns,
),
) )
settingsExtensionDns.setOnItemClickListener { _, _, i, _ -> settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
PrefManager.setVal(PrefName.DohProvider, i) PrefManager.setVal(PrefName.DohProvider, i)
@@ -136,283 +141,281 @@ class SettingsCommonActivity : AppCompatActivity() {
restartApp() restartApp()
} }
settingsRecyclerView.adapter = SettingsAdapter( settingsRecyclerView.adapter =
arrayListOf( SettingsAdapter(
Settings( arrayListOf(
type = 1, Settings(
name = getString(R.string.ui_settings), type = 1,
desc = getString(R.string.ui_settings_desc), name = getString(R.string.ui_settings),
icon = R.drawable.ic_round_auto_awesome_24, desc = getString(R.string.ui_settings_desc),
onClick = { icon = R.drawable.ic_round_auto_awesome_24,
startActivity( onClick = {
Intent( startActivity(
context, Intent(
UserInterfaceSettingsActivity::class.java context,
UserInterfaceSettingsActivity::class.java,
),
) )
) },
}, isActivity = true,
isActivity = true ),
), Settings(
Settings( type = 1,
type = 2, name = getString(R.string.download_manager_select),
name = getString(R.string.open_animanga_directly), desc = getString(R.string.download_manager_select_desc),
desc = getString(R.string.open_animanga_directly_info), icon = R.drawable.ic_download_24,
icon = R.drawable.ic_round_search_24, onClick = {
isChecked = PrefManager.getVal(PrefName.AniMangaSearchDirect), val managers = arrayOf("Default", "1DM", "ADM")
switch = { isChecked, _ -> customAlertDialog().apply {
PrefManager.setVal(PrefName.AniMangaSearchDirect, isChecked) setTitle(getString(R.string.download_manager))
} singleChoiceItems(
), managers,
Settings( PrefManager.getVal(PrefName.DownloadManager),
type = 1, ) { count ->
name = getString(R.string.download_manager_select), PrefManager.setVal(PrefName.DownloadManager, count)
desc = getString(R.string.download_manager_select_desc),
icon = R.drawable.ic_download_24,
onClick = {
val managers = arrayOf("Default", "1DM", "ADM")
customAlertDialog().apply {
setTitle(getString(R.string.download_manager))
singleChoiceItems(
managers,
PrefManager.getVal(PrefName.DownloadManager)
) { count ->
PrefManager.setVal(PrefName.DownloadManager, count)
}
show()
}
}
),
Settings(
type = 1,
name = getString(R.string.app_lock),
desc = getString(R.string.app_lock_desc),
icon = R.drawable.ic_round_lock_open_24,
onClick = {
customAlertDialog().apply {
val view = DialogSetPasswordBinding.inflate(layoutInflater)
setTitle(R.string.app_lock)
setCustomView(view.root)
setPosButton(R.string.ok) {
if (view.forgotPasswordCheckbox.isChecked) {
PrefManager.setVal(PrefName.OverridePassword, true)
} }
val password = view.passwordInput.text.toString() show()
val confirmPassword = view.confirmPasswordInput.text.toString() }
if (password == confirmPassword && password.isNotEmpty()) { },
PrefManager.setVal(PrefName.AppPassword, password) ),
if (view.biometricCheckbox.isChecked) { Settings(
val canBiometricPrompt = type = 1,
BiometricManager.from(applicationContext) name = getString(R.string.app_lock),
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS desc = getString(R.string.app_lock_desc),
icon = R.drawable.ic_round_lock_open_24,
if (canBiometricPrompt) { onClick = {
val biometricPrompt = customAlertDialog().apply {
BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ -> val view = DialogSetPasswordBinding.inflate(layoutInflater)
val token = UUID.randomUUID().toString() setTitle(R.string.app_lock)
PrefManager.setVal( setCustomView(view.root)
PrefName.BiometricToken, setPosButton(R.string.ok) {
token if (view.forgotPasswordCheckbox.isChecked) {
) PrefManager.setVal(PrefName.OverridePassword, true)
toast(R.string.success)
}
val promptInfo =
BiometricPromptUtils.createPromptInfo(this@SettingsCommonActivity)
biometricPrompt.authenticate(promptInfo)
}
} else {
PrefManager.setVal(PrefName.BiometricToken, "")
toast(R.string.success)
} }
} else { val password = view.passwordInput.text.toString()
toast(R.string.password_mismatch) val confirmPassword = view.confirmPasswordInput.text.toString()
} if (password == confirmPassword && password.isNotEmpty()) {
} PrefManager.setVal(PrefName.AppPassword, password)
setNegButton(R.string.cancel) if (view.biometricCheckbox.isChecked) {
setNeutralButton(R.string.remove) { val canBiometricPrompt =
PrefManager.setVal(PrefName.AppPassword, "") BiometricManager
PrefManager.setVal(PrefName.BiometricToken, "") .from(applicationContext)
PrefManager.setVal(PrefName.OverridePassword, false) .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ==
toast(R.string.success) BiometricManager.BIOMETRIC_SUCCESS
}
setOnShowListener {
view.passwordInput.requestFocus()
val canAuthenticate =
BiometricManager.from(applicationContext).canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK
) == BiometricManager.BIOMETRIC_SUCCESS
view.biometricCheckbox.isVisible = canAuthenticate
view.biometricCheckbox.isChecked =
PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()
view.forgotPasswordCheckbox.isChecked =
PrefManager.getVal(PrefName.OverridePassword)
}
show()
}
}
), if (canBiometricPrompt) {
Settings( val biometricPrompt =
type = 1, BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
name = getString(R.string.backup_restore), val token = UUID.randomUUID().toString()
desc = getString(R.string.backup_restore_desc), PrefManager.setVal(
icon = R.drawable.backup_restore, PrefName.BiometricToken,
onClick = { token,
StoragePermissions.downloadsPermission(context) )
val selectedArray = mutableListOf(false) toast(R.string.success)
val filteredLocations = Location.entries.filter { it.exportable } }
selectedArray.addAll(List(filteredLocations.size - 1) { false }) val promptInfo =
val dialog = AlertDialog.Builder(context, R.style.MyPopup) BiometricPromptUtils.createPromptInfo(this@SettingsCommonActivity)
.setTitle(R.string.backup_restore).setMultiChoiceItems( biometricPrompt.authenticate(promptInfo)
filteredLocations.map { it.name }.toTypedArray(), }
selectedArray.toBooleanArray()
) { _, which, isChecked ->
selectedArray[which] = isChecked
}.setPositiveButton(R.string.button_restore) { dialog, _ ->
openDocumentLauncher.launch(arrayOf("*/*"))
dialog.dismiss()
}.setNegativeButton(R.string.button_backup) { dialog, _ ->
if (!selectedArray.contains(true)) {
toast(R.string.no_location_selected)
return@setNegativeButton
}
dialog.dismiss()
val selected =
filteredLocations.filterIndexed { index, _ -> selectedArray[index] }
if (selected.contains(Location.Protected)) {
passwordAlertDialog(true) { password ->
if (password != null) {
savePrefsToDownloads(
"DantotsuSettings",
PrefManager.exportAllPrefs(selected),
context,
password
)
} else { } else {
toast(R.string.password_cannot_be_empty) PrefManager.setVal(PrefName.BiometricToken, "")
toast(R.string.success)
} }
} else {
toast(R.string.password_mismatch)
} }
} else {
savePrefsToDownloads(
"DantotsuSettings",
PrefManager.exportAllPrefs(selected),
context,
null
)
} }
}.setNeutralButton(R.string.cancel) { dialog, _ -> setNegButton(R.string.cancel)
dialog.dismiss() setNeutralButton(R.string.remove) {
}.create() PrefManager.setVal(PrefName.AppPassword, "")
dialog.window?.setDimAmount(0.8f) PrefManager.setVal(PrefName.BiometricToken, "")
dialog.show() PrefManager.setVal(PrefName.OverridePassword, false)
}, toast(R.string.success)
), }
Settings( setOnShowListener {
type = 1, view.passwordInput.requestFocus()
name = getString(R.string.change_download_location), val canAuthenticate =
desc = getString(R.string.change_download_location_desc), BiometricManager.from(applicationContext).canAuthenticate(
icon = R.drawable.ic_round_source_24, BiometricManager.Authenticators.BIOMETRIC_WEAK,
onClick = { ) == BiometricManager.BIOMETRIC_SUCCESS
context.customAlertDialog().apply { view.biometricCheckbox.isVisible = canAuthenticate
setTitle(R.string.change_download_location) view.biometricCheckbox.isChecked =
setMessage(R.string.download_location_msg) PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()
setPosButton(R.string.ok) { view.forgotPasswordCheckbox.isChecked =
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir) PrefManager.getVal(PrefName.OverridePassword)
launcher.registerForCallback { success -> }
if (success) { show()
toast(getString(R.string.please_wait)) }
val newUri = },
PrefManager.getVal<String>(PrefName.DownloadsDir) ),
GlobalScope.launch(Dispatchers.IO) { Settings(
Injekt.get<DownloadsManager>().moveDownloadsDir( type = 1,
context, Uri.parse(oldUri), Uri.parse(newUri) name = getString(R.string.backup_restore),
) { finished, message -> desc = getString(R.string.backup_restore_desc),
if (finished) { icon = R.drawable.backup_restore,
toast(getString(R.string.success)) onClick = {
} else { StoragePermissions.downloadsPermission(context)
toast(message) val filteredLocations = Location.entries.filter { it.exportable }
} val selectedArray = BooleanArray(filteredLocations.size) { false }
context.customAlertDialog().apply {
setTitle(R.string.backup_restore)
multiChoiceItems(
filteredLocations.map { it.name }.toTypedArray(),
selectedArray,
) { updatedSelection ->
for (i in updatedSelection.indices) {
selectedArray[i] = updatedSelection[i]
}
}
setPosButton(R.string.button_restore) {
openDocumentLauncher.launch(arrayOf("*/*"))
}
setNegButton(R.string.button_backup) {
if (!selectedArray.contains(true)) {
toast(R.string.no_location_selected)
return@setNegButton
}
val selected =
filteredLocations.filterIndexed { index, _ -> selectedArray[index] }
if (selected.contains(Location.Protected)) {
passwordAlertDialog(true) { password ->
if (password != null) {
savePrefsToDownloads(
"DantotsuSettings",
PrefManager.exportAllPrefs(selected),
context,
password,
)
} else {
toast(R.string.password_cannot_be_empty)
} }
} }
} else { } else {
toast(getString(R.string.error)) savePrefsToDownloads(
"DantotsuSettings",
PrefManager.exportAllPrefs(selected),
context,
null,
)
} }
} }
launcher.launch() setNeutralButton(R.string.cancel) {}
show()
} }
setNegButton(R.string.cancel) },
show() ),
} Settings(
} type = 1,
), name = getString(R.string.change_download_location),
Settings( desc = getString(R.string.change_download_location_desc),
type = 2, icon = R.drawable.ic_round_source_24,
name = getString(R.string.always_continue_content), onClick = {
desc = getString(R.string.always_continue_content_desc), context.customAlertDialog().apply {
icon = R.drawable.ic_round_delete_24, setTitle(R.string.change_download_location)
isChecked = PrefManager.getVal(PrefName.ContinueMedia), setMessage(R.string.download_location_msg)
switch = { isChecked, _ -> setPosButton(R.string.ok) {
PrefManager.setVal(PrefName.ContinueMedia, isChecked) val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
} launcher.registerForCallback { success ->
), if (success) {
Settings( toast(getString(R.string.please_wait))
type = 2, val newUri =
name = getString(R.string.hide_private), PrefManager.getVal<String>(PrefName.DownloadsDir)
desc = getString(R.string.hide_private_desc), GlobalScope.launch(Dispatchers.IO) {
icon = R.drawable.ic_round_remove_red_eye_24, Injekt.get<DownloadsManager>().moveDownloadsDir(
isChecked = PrefManager.getVal(PrefName.HidePrivate), context,
switch = { isChecked, _ -> Uri.parse(oldUri),
PrefManager.setVal(PrefName.HidePrivate, isChecked) Uri.parse(newUri),
restartApp() ) { finished, message ->
} if (finished) {
), toast(getString(R.string.success))
Settings( } else {
type = 2, toast(message)
name = getString(R.string.search_source_list), }
desc = getString(R.string.search_source_list_desc), }
icon = R.drawable.ic_round_search_sources_24, }
isChecked = PrefManager.getVal(PrefName.SearchSources), } else {
switch = { isChecked, _ -> toast(getString(R.string.error))
PrefManager.setVal(PrefName.SearchSources, isChecked) }
} }
), launcher.launch()
Settings( }
type = 2, setNegButton(R.string.cancel)
name = getString(R.string.recentlyListOnly), show()
desc = getString(R.string.recentlyListOnly_desc), }
icon = R.drawable.ic_round_new_releases_24, },
isChecked = PrefManager.getVal(PrefName.RecentlyListOnly), ),
switch = { isChecked, _ -> Settings(
PrefManager.setVal(PrefName.RecentlyListOnly, isChecked) type = 2,
} name = getString(R.string.always_continue_content),
), desc = getString(R.string.always_continue_content_desc),
Settings( icon = R.drawable.ic_round_delete_24,
type = 2, isChecked = PrefManager.getVal(PrefName.ContinueMedia),
name = getString(R.string.adult_only_content), switch = { isChecked, _ ->
desc = getString(R.string.adult_only_content_desc), PrefManager.setVal(PrefName.ContinueMedia, isChecked)
icon = R.drawable.ic_round_nsfw_24, },
isChecked = PrefManager.getVal(PrefName.AdultOnly), ),
switch = { isChecked, _ -> Settings(
PrefManager.setVal(PrefName.AdultOnly, isChecked) type = 2,
restartApp() name = getString(R.string.hide_private),
}, desc = getString(R.string.hide_private_desc),
isVisible = Anilist.adult icon = R.drawable.ic_round_remove_red_eye_24,
isChecked = PrefManager.getVal(PrefName.HidePrivate),
switch = { isChecked, _ ->
PrefManager.setVal(PrefName.HidePrivate, isChecked)
restartApp()
},
),
Settings(
type = 2,
name = getString(R.string.search_source_list),
desc = getString(R.string.search_source_list_desc),
icon = R.drawable.ic_round_search_sources_24,
isChecked = PrefManager.getVal(PrefName.SearchSources),
switch = { isChecked, _ ->
PrefManager.setVal(PrefName.SearchSources, isChecked)
},
),
Settings(
type = 2,
name = getString(R.string.recentlyListOnly),
desc = getString(R.string.recentlyListOnly_desc),
icon = R.drawable.ic_round_new_releases_24,
isChecked = PrefManager.getVal(PrefName.RecentlyListOnly),
switch = { isChecked, _ ->
PrefManager.setVal(PrefName.RecentlyListOnly, isChecked)
},
),
Settings(
type = 2,
name = getString(R.string.adult_only_content),
desc = getString(R.string.adult_only_content_desc),
icon = R.drawable.ic_round_nsfw_24,
isChecked = PrefManager.getVal(PrefName.AdultOnly),
switch = { isChecked, _ ->
PrefManager.setVal(PrefName.AdultOnly, isChecked)
restartApp()
},
isVisible = Anilist.adult,
),
), ),
) )
)
settingsRecyclerView.apply { settingsRecyclerView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
setHasFixedSize(true) setHasFixedSize(true)
} }
var previousStart: View = when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) { var previousStart: View =
0 -> uiSettingsAnime when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) {
1 -> uiSettingsHome 0 -> uiSettingsAnime
2 -> uiSettingsManga 1 -> uiSettingsHome
else -> uiSettingsHome 2 -> uiSettingsManga
} else -> uiSettingsHome
}
previousStart.alpha = 1f previousStart.alpha = 1f
fun uiDefault(mode: Int, current: View) {
fun uiDefault(
mode: Int,
current: View,
) {
previousStart.alpha = 0.33f previousStart.alpha = 0.33f
previousStart = current previousStart = current
current.alpha = 1f current.alpha = 1f
@@ -431,11 +434,13 @@ class SettingsCommonActivity : AppCompatActivity() {
uiSettingsManga.setOnClickListener { uiSettingsManga.setOnClickListener {
uiDefault(2, it) uiDefault(2, it)
} }
} }
} }
private fun passwordAlertDialog(isExporting: Boolean, callback: (CharArray?) -> Unit) { private fun passwordAlertDialog(
isExporting: Boolean,
callback: (CharArray?) -> Unit,
) {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout // Inflate the dialog layout
@@ -445,7 +450,9 @@ class SettingsCommonActivity : AppCompatActivity() {
box.setSingleLine() box.setSingleLine()
val dialog = val dialog =
AlertDialog.Builder(this, R.style.MyPopup).setTitle(getString(R.string.enter_password)) AlertDialog
.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.enter_password))
.setView(dialogView.root) .setView(dialogView.root)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel) { dialog, _ -> .setNegativeButton(R.string.cancel) { dialog, _ ->
@@ -457,7 +464,10 @@ class SettingsCommonActivity : AppCompatActivity() {
fun handleOkAction() { fun handleOkAction() {
val editText = dialogView.userAgentTextBox val editText = dialogView.userAgentTextBox
if (editText.text?.isNotBlank() == true) { if (editText.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password) editText.text
?.toString()
?.trim()
?.toCharArray(password)
dialog.dismiss() dialog.dismiss()
callback(password) callback(password)
} else { } else {
@@ -473,18 +483,20 @@ class SettingsCommonActivity : AppCompatActivity() {
} }
} }
dialogView.subtitle.visibility = View.VISIBLE dialogView.subtitle.visibility = View.VISIBLE
if (!isExporting) dialogView.subtitle.text = if (!isExporting) {
getString(R.string.enter_password_to_decrypt_file) dialogView.subtitle.text =
getString(R.string.enter_password_to_decrypt_file)
}
dialog.window?.apply {
dialog.window?.setDimAmount(0.8f) setDimAmount(0.8f)
attributes.windowAnimations = android.R.style.Animation_Dialog
}
dialog.show() dialog.show()
// Override the positive button here // Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
handleOkAction() handleOkAction()
} }
} }
}
}

View File

@@ -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()
} }
@@ -128,26 +119,26 @@ class SettingsNotificationActivity : AppCompatActivity() {
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes) PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes)
.toMutableSet() .toMutableSet()
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray() val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
val dialog = AlertDialog.Builder(context, R.style.MyPopup) context.customAlertDialog().apply {
.setTitle(R.string.anilist_notification_filters) setTitle(R.string.anilist_notification_filters)
.setMultiChoiceItems( multiChoiceItems(
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
) { _, which, isChecked -> ) { updatedSelected ->
val type = types[which] types.forEachIndexed { index, type ->
if (isChecked) { if (updatedSelected[index]) {
filteredTypes.add(type) filteredTypes.add(type)
} else { } else {
filteredTypes.remove(type) filteredTypes.remove(type)
} }
}
PrefManager.setVal(PrefName.AnilistFilteredTypes, filteredTypes) PrefManager.setVal(PrefName.AnilistFilteredTypes, filteredTypes)
}.create() }
dialog.window?.setDimAmount(0.8f) show()
dialog.show() }
} }
), ),
@@ -160,27 +151,24 @@ class SettingsNotificationActivity : AppCompatActivity() {
desc = getString(R.string.anilist_notifications_checking_time_desc), desc = getString(R.string.anilist_notifications_checking_time_desc),
icon = R.drawable.ic_round_notifications_none_24, icon = R.drawable.ic_round_notifications_none_24,
onClick = { onClick = {
val selected = context.customAlertDialog().apply {
PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval) setTitle(R.string.subscriptions_checking_time)
val dialog = AlertDialog.Builder(context, R.style.MyPopup) singleChoiceItems(
.setTitle(R.string.subscriptions_checking_time)
.setSingleChoiceItems(
aItems.toTypedArray(), aItems.toTypedArray(),
selected PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
) { dialog, i -> ) { i ->
PrefManager.setVal(PrefName.AnilistNotificationInterval, i) PrefManager.setVal(PrefName.AnilistNotificationInterval, i)
it.settingsTitle.text = it.settingsTitle.text =
getString( getString(
R.string.anilist_notifications_checking_time, R.string.anilist_notifications_checking_time,
aItems[i] aItems[i]
) )
dialog.dismiss()
TaskScheduler.create( TaskScheduler.create(
context, PrefManager.getVal(PrefName.UseAlarmManager) context, PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(context) ).scheduleAllTasks(context)
}.create() }
dialog.window?.setDimAmount(0.8f) show()
dialog.show() }
} }
), ),
Settings( Settings(
@@ -192,27 +180,24 @@ class SettingsNotificationActivity : AppCompatActivity() {
desc = getString(R.string.comment_notification_checking_time_desc), desc = getString(R.string.comment_notification_checking_time_desc),
icon = R.drawable.ic_round_notifications_none_24, icon = R.drawable.ic_round_notifications_none_24,
onClick = { onClick = {
val selected = context.customAlertDialog().apply {
PrefManager.getVal<Int>(PrefName.CommentNotificationInterval) setTitle(R.string.subscriptions_checking_time)
val dialog = AlertDialog.Builder(context, R.style.MyPopup) singleChoiceItems(
.setTitle(R.string.subscriptions_checking_time)
.setSingleChoiceItems(
cItems.toTypedArray(), cItems.toTypedArray(),
selected PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
) { dialog, i -> ) { i ->
PrefManager.setVal(PrefName.CommentNotificationInterval, i) PrefManager.setVal(PrefName.CommentNotificationInterval, i)
it.settingsTitle.text = it.settingsTitle.text =
getString( getString(
R.string.comment_notification_checking_time, R.string.comment_notification_checking_time,
cItems[i] cItems[i]
) )
dialog.dismiss()
TaskScheduler.create( TaskScheduler.create(
context, PrefManager.getVal(PrefName.UseAlarmManager) context, PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(context) ).scheduleAllTasks(context)
}.create() }
dialog.window?.setDimAmount(0.8f) show()
dialog.show() }
} }
), ),
Settings( Settings(
@@ -239,10 +224,10 @@ class SettingsNotificationActivity : AppCompatActivity() {
isChecked = PrefManager.getVal(PrefName.UseAlarmManager), isChecked = PrefManager.getVal(PrefName.UseAlarmManager),
switch = { isChecked, view -> switch = { isChecked, view ->
if (isChecked) { if (isChecked) {
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) context.customAlertDialog().apply {
.setTitle(R.string.use_alarm_manager) setTitle(R.string.use_alarm_manager)
.setMessage(R.string.use_alarm_manager_confirm) setMessage(R.string.use_alarm_manager_confirm)
.setPositiveButton(R.string.use) { dialog, _ -> setPosButton(R.string.use) {
PrefManager.setVal(PrefName.UseAlarmManager, true) PrefManager.setVal(PrefName.UseAlarmManager, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) { if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
@@ -252,15 +237,13 @@ class SettingsNotificationActivity : AppCompatActivity() {
view.settingsButton.isChecked = true view.settingsButton.isChecked = true
} }
} }
dialog.dismiss() }
}.setNegativeButton(R.string.cancel) { dialog, _ -> setNegButton(R.string.cancel) {
view.settingsButton.isChecked = false view.settingsButton.isChecked = false
PrefManager.setVal(PrefName.UseAlarmManager, false) PrefManager.setVal(PrefName.UseAlarmManager, false)
}
dialog.dismiss() show()
}.create() }
alertDialog.window?.setDimAmount(0.8f)
alertDialog.show()
} else { } else {
PrefManager.setVal(PrefName.UseAlarmManager, false) PrefManager.setVal(PrefName.UseAlarmManager, false)
TaskScheduler.create(context, true).cancelAllTasks() TaskScheduler.create(context, true).cancelAllTasks()
@@ -277,4 +260,4 @@ class SettingsNotificationActivity : AppCompatActivity() {
} }
} }
} }
} }

View File

@@ -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()
} }

View File

@@ -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.

View File

@@ -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,
)
}
}
}

View File

@@ -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,
vid.audioTracks,
vid.timestamps,
vid.internalData,
vid.initialized,
vid.videoPageUrl,
)
},
)
fun String.toVideoList(): List<Video> =
Json.decodeFromString<List<SerializableVideo>>(this)
.map { sVid ->
Video(
sVid.videoUrl,
sVid.videoTitle,
sVid.resolution,
sVid.bitrate,
sVid.headers
?.flatMap { it.toList() }
?.let { Headers.headersOf(*it.toTypedArray()) },
sVid.preferred,
sVid.subtitleTracks,
sVid.audioTracks,
sVid.timestamps,
sVid.internalData,
sVid.initialized,
sVid.videoPageUrl,
)
} }
}
builder.build()
}
} }
} }

View File

@@ -1,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 {

View File

@@ -1102,10 +1102,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp" android:layout_marginHorizontal="32dp"
android:stepSize="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"

View File

@@ -8,7 +8,7 @@
android:padding="16dp"> android:padding="16dp">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="326dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
@@ -160,8 +160,8 @@
android:orientation="horizontal"> android:orientation="horizontal">
<LinearLayout <LinearLayout
android:layout_width="265dp" android:layout_width="263dp"
android:layout_height="match_parent" android:layout_height="60dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@@ -171,14 +171,23 @@
android:fontFamily="@font/poppins_bold" android:fontFamily="@font/poppins_bold"
android:text="@string/download" /> android:text="@string/download" />
<TextView <EditText
android:id="@+id/downloadNo" android:id="@+id/downloadNo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold" android:fontFamily="@font/poppins_bold"
android:textColor="?attr/colorSecondary" android:textColor="?attr/colorSecondary"
android:textSize="12dp"
tools:ignore="TextContrastCheck" tools:ignore="TextContrastCheck"
tools:text="number" /> tools:text="Number" />
<!-- <TextView-->
<!-- android:id="@+id/downloadNo"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:fontFamily="@font/poppins_bold"-->
<!-- android:textColor="?attr/colorSecondary"-->
<!-- tools:ignore="TextContrastCheck"-->
<!-- tools:text="number" />-->
</LinearLayout> </LinearLayout>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
@@ -191,7 +200,7 @@
<ImageButton <ImageButton
android:id="@+id/mediaDownloadTop" android:id="@+id/mediaDownloadTop"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="60dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
app:srcCompat="@drawable/ic_download_24" app:srcCompat="@drawable/ic_download_24"
app:tint="?attr/colorOnBackground" app:tint="?attr/colorOnBackground"
@@ -313,9 +322,9 @@
android:text="@string/reset" /> android:text="@string/reset" />
<TextView <TextView
android:id="@+id/reset_progress_def"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/reset_progress_def"
android:fontFamily="@font/poppins_bold" android:fontFamily="@font/poppins_bold"
android:text="" android:text=""
android:textColor="?attr/colorSecondary" android:textColor="?attr/colorSecondary"

View File

@@ -1,53 +1,4 @@
# 3.1.0 # 3.2.1
- **New Features:**
- Addons
- Torrent support addon
- Anime downloading addon (mkv files pog)
- Available in app settings
- Anilist reviews in app
- Media subscriptions added to notification tab
- Notification filtering
- Ability to post activitys
- Ability to reply to activities
- Extension tester
- Media subscription Viewer
- Instagram-style stories
- More audio options for some extensions
- Ability to hide items on the home screen
- Ability to set a downloads directory
- 2 functioning widgets
- App lock ( ͡° ͜ʖ ͡°)
- More manga and anime feeds on the home page
- Settings page redesign
- New app crash notifier
- Voice actors
- Additional repo support
- Various UI uplifts
- **Bugfixes:** - **Bugfixes:**
- Scanlator/language not saving after leaving app - Fix a crash after watching a video
- notification red dot not hiding on home pages
- comment/activity scrolling not working on some parts of the screen
- comment notifications falling to the bottom of the list
- Fixed some sources without audio
- Initial app loading time reduced
- activity text more visible
- novel extensions not installing
- Many sources not working
- Subscription notifications not using the correct source
- Notification red dot showing with no new notifications
- Various bug/crash fixes
- General theme tweaks
- Fixed some network-related crashes
- Subscription notifications not working for some people
- Fix for file permissions on older Android versions
- Search list view not working
- Media page opening twice on notification click
- A Special Thanks to all those who contributed :heart:
- **Like what you see?**
- Consider supporting me on [Github](https://github.com/sponsors/rebelonion) or [Buy Me a Coffee](https://www.buymeacoffee.com/rebelonion)!
![alt text](https://media1.tenor.com/m/P7hCyZlzDH4AAAAC/wink-anime.gif)