mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 23:27:39 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3f6d0ecee | ||
|
|
5124d6a2d8 | ||
|
|
e83a0fe7da | ||
|
|
61a8350043 | ||
|
|
baffbc845c | ||
|
|
afd9f6b884 | ||
|
|
7d0894cd92 | ||
|
|
dec2ed7959 | ||
|
|
e4630df3e0 | ||
|
|
6fd3515d2c | ||
|
|
6fa2f11db2 | ||
|
|
a5babea27c | ||
|
|
8a9b8cca7e | ||
|
|
7479f5f43b | ||
|
|
3ac9307329 |
@@ -14,8 +14,6 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||
|
||||
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
|
||||
## Terms of Use
|
||||
By downloading, installing, or using this application, you agree to:
|
||||
- Use the application in compliance with all applicable laws
|
||||
|
||||
@@ -18,8 +18,8 @@ android {
|
||||
minSdk 21
|
||||
targetSdk 35
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "3.2.0"
|
||||
versionCode 300200000
|
||||
versionName "3.2.1"
|
||||
versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
}
|
||||
@@ -48,6 +48,10 @@ android {
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
|
||||
debuggable System.getenv("CI") == null
|
||||
isDefault true
|
||||
debuggable true
|
||||
jniDebuggable true
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
@@ -81,25 +85,26 @@ android {
|
||||
dependencies {
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0'
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:33.13.0')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.4.0'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.4.3'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.core:core-ktx:1.16.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.6'
|
||||
implementation 'androidx.activity:activity-ktx:1.10.1'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.10.1"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.11.0'
|
||||
implementation 'androidx.webkit:webkit:1.13.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
|
||||
@@ -113,7 +118,7 @@ dependencies {
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.5.0'
|
||||
ext.exo_version = '1.6.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
@@ -124,7 +129,7 @@ dependencies {
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
||||
// Media3 extension
|
||||
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
|
||||
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4"
|
||||
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
@@ -133,7 +138,7 @@ dependencies {
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.3.6'
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
|
||||
|
||||
@@ -162,13 +167,13 @@ dependencies {
|
||||
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
||||
implementation 'com.squareup.logcat:logcat:0.1'
|
||||
implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||
implementation 'com.squareup.okio:okio:3.8.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
||||
implementation 'org.jsoup:jsoup:1.16.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
|
||||
implementation 'com.squareup.okio:okio:3.9.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.14'
|
||||
implementation 'org.jsoup:jsoup:1.18.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.7.3'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
||||
@@ -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"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
|
||||
@@ -113,21 +113,28 @@ class App : MultiDexApplication() {
|
||||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
scope.launch {
|
||||
animeExtensionManager = Injekt.get()
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
scope.launch {
|
||||
mangaExtensionManager = Injekt.get()
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
scope.launch {
|
||||
novelExtensionManager = Injekt.get()
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
launch {
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
}
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ data class MediaStreamingEpisode(
|
||||
|
||||
// The site location of the streaming episode
|
||||
@SerialName("site") var site: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MediaCoverImage(
|
||||
|
||||
@@ -47,9 +47,9 @@ class Login : AppCompatActivity() {
|
||||
view.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken();
|
||||
return wreq;
|
||||
})()
|
||||
const m = []; webpackChunkdiscord_app.push([[""], {}, e => {for (let c in e.c)m.push(e.c[c])}]);
|
||||
return m.find(n => n?.exports?.default?.getToken !== void 0)?.exports?.default?.getToken();
|
||||
})()
|
||||
""".trimIndent()
|
||||
) { result ->
|
||||
login(result.trim('"'))
|
||||
|
||||
@@ -232,12 +232,18 @@ class MangaDownloaderService : Service() {
|
||||
image.page,
|
||||
image.source
|
||||
)
|
||||
if (bitmap == null) {
|
||||
snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
|
||||
}
|
||||
retryCount++
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
|
||||
if (bitmap == null) {
|
||||
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++
|
||||
|
||||
builder.setProgress(task.imageData.size, farthest, false)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.GestureDetector
|
||||
@@ -12,6 +13,8 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -19,8 +22,10 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.color
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -79,6 +84,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||
val id = intent.getIntExtra("mediaId", -1)
|
||||
@@ -109,6 +115,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
// Ui init
|
||||
|
||||
initActivity(this)
|
||||
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
@@ -132,10 +139,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
val navBarBottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
rightMargin = navBarRightMargin
|
||||
bottomMargin = navBarBottomMargin
|
||||
}
|
||||
navBar.setPadding(
|
||||
navBar.paddingLeft,
|
||||
navBar.paddingTop,
|
||||
navBar.paddingRight + navBarRightMargin,
|
||||
navBar.paddingBottom + navBarBottomMargin
|
||||
)
|
||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
@@ -21,28 +23,32 @@ class SubtitleDownloader {
|
||||
suspend fun loadSubtitleType(url: String): SubtitleType =
|
||||
withContext(Dispatchers.IO) {
|
||||
return@withContext try {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
if (!url.startsWith("file")) {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body.string()
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body.string()
|
||||
|
||||
|
||||
val subtitleType = when {
|
||||
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
val subtitleType = getType(responseBody)
|
||||
|
||||
subtitleType
|
||||
} else {
|
||||
SubtitleType.UNKNOWN
|
||||
}
|
||||
|
||||
subtitleType
|
||||
} else {
|
||||
SubtitleType.UNKNOWN
|
||||
val uri = url.toUri()
|
||||
val file = uri.toFile()
|
||||
val fileBody = file.readText()
|
||||
val subtitleType = getType(fileBody)
|
||||
subtitleType
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
@@ -50,6 +56,15 @@ class SubtitleDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getType(content: String): SubtitleType {
|
||||
return when {
|
||||
content.contains("[Script Info]") -> SubtitleType.ASS
|
||||
content.contains("WEBVTT") -> SubtitleType.VTT
|
||||
content.contains("SRT") -> SubtitleType.SRT
|
||||
else -> SubtitleType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
//actually downloads lol
|
||||
@Deprecated("handled externally")
|
||||
suspend fun downloadSubtitle(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
|
||||
data class ImageData(
|
||||
@@ -76,7 +77,7 @@ fun saveImage(
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
bitmap.compress(format, quality, os)
|
||||
}
|
||||
} ?: throw FileNotFoundException("Failed to open output stream for URI: $uri")
|
||||
}
|
||||
} else {
|
||||
val directory =
|
||||
@@ -86,12 +87,20 @@ fun saveImage(
|
||||
}
|
||||
|
||||
val file = File(directory, filename)
|
||||
|
||||
// Check if the file already exists
|
||||
if (file.exists()) {
|
||||
println("File already exists: ${file.absolutePath}")
|
||||
return
|
||||
}
|
||||
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
bitmap.compress(format, quality, outputStream)
|
||||
}
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
println("File not found: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
// Handle exception here
|
||||
println("Exception while saving image: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
@@ -265,19 +267,22 @@ class MangaReadAdapter(
|
||||
}
|
||||
|
||||
// Multi download
|
||||
downloadNo.text = "0"
|
||||
//downloadNo.text = "0"
|
||||
mediaDownloadTop.setOnClickListener {
|
||||
// Alert dialog asking for the number of chapters to download
|
||||
fragment.requireContext().customAlertDialog().apply {
|
||||
setTitle("Multi Chapter Downloader")
|
||||
setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = 20
|
||||
input.value = 1
|
||||
val input = View.inflate(currContext(), R.layout.dialog_layout, null)
|
||||
val editText = input.findViewById<EditText>(R.id.downloadNo)
|
||||
setCustomView(input)
|
||||
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)
|
||||
show()
|
||||
@@ -382,8 +387,9 @@ class MangaReadAdapter(
|
||||
setCustomView(root)
|
||||
setPosButton("OK") {
|
||||
if (run) fragment.onIconPressed(style, reversed)
|
||||
if (downloadNo.text != "0") {
|
||||
fragment.multiDownload(downloadNo.text.toString().toInt())
|
||||
val value = downloadNo.text.toString().toIntOrNull()
|
||||
if (value != null && value > 0) {
|
||||
fragment.multiDownload(value)
|
||||
}
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -232,25 +233,35 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
|
||||
fun multiDownload(n: Int) {
|
||||
// Get last viewed chapter
|
||||
val selected = media.userProgress
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
// Filter by selected language
|
||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
} ?: 0) + 1
|
||||
|
||||
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
|
||||
|
||||
// Calculate the end index
|
||||
val endIndex = minOf(progressChapterIndex + n, chapters.size)
|
||||
|
||||
// Make sure there are enough chapters
|
||||
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
|
||||
|
||||
|
||||
for (chapter in chaptersToDownload) {
|
||||
lifecycleScope.launch {
|
||||
// Get the last viewed chapter
|
||||
val selected = media.userProgress ?: 0
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
// Ensure chapters are available in the extensions
|
||||
if (chapters.isNullOrEmpty() || n < 1) return@launch
|
||||
// Find the index of the last viewed chapter
|
||||
val progressChapterIndex = (chapters.indexOfFirst {
|
||||
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
} + 1).coerceAtLeast(0)
|
||||
// Calculate the end value for the range of chapters to download
|
||||
val endIndex = (progressChapterIndex + n).coerceAtMost(chapters.size)
|
||||
// Get the list of chapters to download
|
||||
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
|
||||
// Trigger the download for each chapter sequentially
|
||||
for (chapter in chaptersToDownload) {
|
||||
try {
|
||||
downloadChapterSequentially(chapter)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Failed to download chapter: ${chapter.title}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
Toast.makeText(requireContext(), "All downloads completed!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
private suspend fun downloadChapterSequentially(chapter: MangaChapter) {
|
||||
withContext(Dispatchers.IO) {
|
||||
onMangaChapterDownloadClick(chapter)
|
||||
delay(2000) // A 2-second download
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +485,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
scanlator = chapter.scanlator ?: "Unknown",
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
retries = 2,
|
||||
retries = 25,
|
||||
simultaneousDownloads = 2
|
||||
)
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ class CrashActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
initActivity(this)
|
||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
|
||||
@@ -26,7 +26,6 @@ object AnimeSources : WatchSources() {
|
||||
)
|
||||
isInitialized = true
|
||||
|
||||
// Update as StateFlow emits new values
|
||||
fromExtensions.collect { extensions ->
|
||||
list = sortPinnedAnimeSources(
|
||||
createParsersFromExtensions(extensions),
|
||||
|
||||
@@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
?: return emptyList())
|
||||
|
||||
return try {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
videos.map { videoToVideoServer(it) }
|
||||
// TODO(1.6): Remove else block when dropping support for ext lib <1.6
|
||||
if ((source as AnimeHttpSource).javaClass.declaredMethods.any { it.name == "getHosterList" }){
|
||||
val hosters = source.getHosterList(sEpisode)
|
||||
val allVideos = hosters.flatMap { hoster ->
|
||||
val videos = source.getVideoList(hoster)
|
||||
videos.map { it.copy(videoTitle = "${hoster.hosterName} - ${it.videoTitle}") }
|
||||
}
|
||||
allVideos.map { videoToVideoServer(it) }
|
||||
} else {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
videos.map { videoToVideoServer(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Exception occurred: ${e.message}")
|
||||
emptyList()
|
||||
@@ -576,7 +586,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
|
||||
number,
|
||||
format!!,
|
||||
FileUrl(videoUrl, headersMap),
|
||||
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -636,7 +646,6 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
|
||||
}
|
||||
|
||||
private fun trackToSubtitle(track: Track): Subtitle {
|
||||
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
|
||||
var type: SubtitleType?
|
||||
runBlocking {
|
||||
type = findSubtitleType(track.url)
|
||||
|
||||
@@ -19,6 +19,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -57,23 +58,16 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
desc = getString(R.string.purge_anime_downloads_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
onClick = {
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.purge_anime_downloads)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.purge_confirm,
|
||||
getString(R.string.anime)
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.purge_anime_downloads)
|
||||
setMessage(R.string.purge_confirm, getString(R.string.anime))
|
||||
setPosButton(R.string.yes, onClick = {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(MediaType.ANIME)
|
||||
dialog.dismiss()
|
||||
}.setNegativeButton(R.string.no) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
})
|
||||
setNegButton(R.string.no)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
),
|
||||
@@ -143,4 +137,4 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class SettingsCommonActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivitySettingsCommonBinding
|
||||
private lateinit var launcher: LauncherWrapper
|
||||
@@ -62,23 +61,27 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val jsonString = contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val jsonString =
|
||||
contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name = DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
// .sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog(false) { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password, encrypted, salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast(getString(R.string.incorrect_password))
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
val decryptedJson =
|
||||
try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast(getString(R.string.incorrect_password))
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson)) restartApp()
|
||||
} else {
|
||||
toast(getString(R.string.password_cannot_be_empty))
|
||||
@@ -100,7 +103,6 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
launcher = LauncherWrapper(this, contract)
|
||||
|
||||
binding.apply {
|
||||
|
||||
settingsCommonLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
@@ -108,27 +110,30 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
commonSettingsBack.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
val exDns = listOf(
|
||||
"None",
|
||||
"Cloudflare",
|
||||
"Google",
|
||||
"AdGuard",
|
||||
"Quad9",
|
||||
"AliDNS",
|
||||
"DNSPod",
|
||||
"360",
|
||||
"Quad101",
|
||||
"Mullvad",
|
||||
"Controld",
|
||||
"Njalla",
|
||||
"Shecan",
|
||||
"Libre"
|
||||
)
|
||||
val exDns =
|
||||
listOf(
|
||||
"None",
|
||||
"Cloudflare",
|
||||
"Google",
|
||||
"AdGuard",
|
||||
"Quad9",
|
||||
"AliDNS",
|
||||
"DNSPod",
|
||||
"360",
|
||||
"Quad101",
|
||||
"Mullvad",
|
||||
"Controld",
|
||||
"Njalla",
|
||||
"Shecan",
|
||||
"Libre",
|
||||
)
|
||||
settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)])
|
||||
settingsExtensionDns.setAdapter(
|
||||
ArrayAdapter(
|
||||
context, R.layout.item_dropdown, exDns
|
||||
)
|
||||
context,
|
||||
R.layout.item_dropdown,
|
||||
exDns,
|
||||
),
|
||||
)
|
||||
settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
||||
PrefManager.setVal(PrefName.DohProvider, i)
|
||||
@@ -136,283 +141,281 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
restartApp()
|
||||
}
|
||||
|
||||
settingsRecyclerView.adapter = SettingsAdapter(
|
||||
arrayListOf(
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.ui_settings),
|
||||
desc = getString(R.string.ui_settings_desc),
|
||||
icon = R.drawable.ic_round_auto_awesome_24,
|
||||
onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
context,
|
||||
UserInterfaceSettingsActivity::class.java
|
||||
settingsRecyclerView.adapter =
|
||||
SettingsAdapter(
|
||||
arrayListOf(
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.ui_settings),
|
||||
desc = getString(R.string.ui_settings_desc),
|
||||
icon = R.drawable.ic_round_auto_awesome_24,
|
||||
onClick = {
|
||||
startActivity(
|
||||
Intent(
|
||||
context,
|
||||
UserInterfaceSettingsActivity::class.java,
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
isActivity = true
|
||||
),
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.open_animanga_directly),
|
||||
desc = getString(R.string.open_animanga_directly_info),
|
||||
icon = R.drawable.ic_round_search_24,
|
||||
isChecked = PrefManager.getVal(PrefName.AniMangaSearchDirect),
|
||||
switch = { isChecked, _ ->
|
||||
PrefManager.setVal(PrefName.AniMangaSearchDirect, isChecked)
|
||||
}
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.download_manager_select),
|
||||
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)
|
||||
},
|
||||
isActivity = true,
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.download_manager_select),
|
||||
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)
|
||||
}
|
||||
val password = view.passwordInput.text.toString()
|
||||
val confirmPassword = view.confirmPasswordInput.text.toString()
|
||||
if (password == confirmPassword && password.isNotEmpty()) {
|
||||
PrefManager.setVal(PrefName.AppPassword, password)
|
||||
if (view.biometricCheckbox.isChecked) {
|
||||
val canBiometricPrompt =
|
||||
BiometricManager.from(applicationContext)
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|
||||
if (canBiometricPrompt) {
|
||||
val biometricPrompt =
|
||||
BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
|
||||
val token = UUID.randomUUID().toString()
|
||||
PrefManager.setVal(
|
||||
PrefName.BiometricToken,
|
||||
token
|
||||
)
|
||||
toast(R.string.success)
|
||||
}
|
||||
val promptInfo =
|
||||
BiometricPromptUtils.createPromptInfo(this@SettingsCommonActivity)
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
|
||||
} else {
|
||||
PrefManager.setVal(PrefName.BiometricToken, "")
|
||||
toast(R.string.success)
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
toast(R.string.password_mismatch)
|
||||
}
|
||||
}
|
||||
setNegButton(R.string.cancel)
|
||||
setNeutralButton(R.string.remove) {
|
||||
PrefManager.setVal(PrefName.AppPassword, "")
|
||||
PrefManager.setVal(PrefName.BiometricToken, "")
|
||||
PrefManager.setVal(PrefName.OverridePassword, false)
|
||||
toast(R.string.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()
|
||||
}
|
||||
}
|
||||
val password = view.passwordInput.text.toString()
|
||||
val confirmPassword = view.confirmPasswordInput.text.toString()
|
||||
if (password == confirmPassword && password.isNotEmpty()) {
|
||||
PrefManager.setVal(PrefName.AppPassword, password)
|
||||
if (view.biometricCheckbox.isChecked) {
|
||||
val canBiometricPrompt =
|
||||
BiometricManager
|
||||
.from(applicationContext)
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ==
|
||||
BiometricManager.BIOMETRIC_SUCCESS
|
||||
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.backup_restore),
|
||||
desc = getString(R.string.backup_restore_desc),
|
||||
icon = R.drawable.backup_restore,
|
||||
onClick = {
|
||||
StoragePermissions.downloadsPermission(context)
|
||||
val selectedArray = mutableListOf(false)
|
||||
val filteredLocations = Location.entries.filter { it.exportable }
|
||||
selectedArray.addAll(List(filteredLocations.size - 1) { false })
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.backup_restore).setMultiChoiceItems(
|
||||
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
|
||||
)
|
||||
if (canBiometricPrompt) {
|
||||
val biometricPrompt =
|
||||
BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
|
||||
val token = UUID.randomUUID().toString()
|
||||
PrefManager.setVal(
|
||||
PrefName.BiometricToken,
|
||||
token,
|
||||
)
|
||||
toast(R.string.success)
|
||||
}
|
||||
val promptInfo =
|
||||
BiometricPromptUtils.createPromptInfo(this@SettingsCommonActivity)
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
} 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, _ ->
|
||||
dialog.dismiss()
|
||||
}.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
},
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.change_download_location),
|
||||
desc = getString(R.string.change_download_location_desc),
|
||||
icon = R.drawable.ic_round_source_24,
|
||||
onClick = {
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.change_download_location)
|
||||
setMessage(R.string.download_location_msg)
|
||||
setPosButton(R.string.ok) {
|
||||
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
launcher.registerForCallback { success ->
|
||||
if (success) {
|
||||
toast(getString(R.string.please_wait))
|
||||
val newUri =
|
||||
PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Injekt.get<DownloadsManager>().moveDownloadsDir(
|
||||
context, Uri.parse(oldUri), Uri.parse(newUri)
|
||||
) { finished, message ->
|
||||
if (finished) {
|
||||
toast(getString(R.string.success))
|
||||
} else {
|
||||
toast(message)
|
||||
}
|
||||
setNegButton(R.string.cancel)
|
||||
setNeutralButton(R.string.remove) {
|
||||
PrefManager.setVal(PrefName.AppPassword, "")
|
||||
PrefManager.setVal(PrefName.BiometricToken, "")
|
||||
PrefManager.setVal(PrefName.OverridePassword, false)
|
||||
toast(R.string.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()
|
||||
}
|
||||
},
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.backup_restore),
|
||||
desc = getString(R.string.backup_restore_desc),
|
||||
icon = R.drawable.backup_restore,
|
||||
onClick = {
|
||||
StoragePermissions.downloadsPermission(context)
|
||||
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 {
|
||||
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 = 2,
|
||||
name = getString(R.string.always_continue_content),
|
||||
desc = getString(R.string.always_continue_content_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
isChecked = PrefManager.getVal(PrefName.ContinueMedia),
|
||||
switch = { isChecked, _ ->
|
||||
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
|
||||
}
|
||||
),
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.hide_private),
|
||||
desc = getString(R.string.hide_private_desc),
|
||||
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
|
||||
|
||||
},
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.change_download_location),
|
||||
desc = getString(R.string.change_download_location_desc),
|
||||
icon = R.drawable.ic_round_source_24,
|
||||
onClick = {
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.change_download_location)
|
||||
setMessage(R.string.download_location_msg)
|
||||
setPosButton(R.string.ok) {
|
||||
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
launcher.registerForCallback { success ->
|
||||
if (success) {
|
||||
toast(getString(R.string.please_wait))
|
||||
val newUri =
|
||||
PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Injekt.get<DownloadsManager>().moveDownloadsDir(
|
||||
context,
|
||||
Uri.parse(oldUri),
|
||||
Uri.parse(newUri),
|
||||
) { finished, message ->
|
||||
if (finished) {
|
||||
toast(getString(R.string.success))
|
||||
} else {
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast(getString(R.string.error))
|
||||
}
|
||||
}
|
||||
launcher.launch()
|
||||
}
|
||||
setNegButton(R.string.cancel)
|
||||
show()
|
||||
}
|
||||
},
|
||||
),
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.always_continue_content),
|
||||
desc = getString(R.string.always_continue_content_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
isChecked = PrefManager.getVal(PrefName.ContinueMedia),
|
||||
switch = { isChecked, _ ->
|
||||
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
|
||||
},
|
||||
),
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.hide_private),
|
||||
desc = getString(R.string.hide_private_desc),
|
||||
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 {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
var previousStart: View = when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) {
|
||||
0 -> uiSettingsAnime
|
||||
1 -> uiSettingsHome
|
||||
2 -> uiSettingsManga
|
||||
else -> uiSettingsHome
|
||||
}
|
||||
var previousStart: View =
|
||||
when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) {
|
||||
0 -> uiSettingsAnime
|
||||
1 -> uiSettingsHome
|
||||
2 -> uiSettingsManga
|
||||
else -> uiSettingsHome
|
||||
}
|
||||
previousStart.alpha = 1f
|
||||
fun uiDefault(mode: Int, current: View) {
|
||||
|
||||
fun uiDefault(
|
||||
mode: Int,
|
||||
current: View,
|
||||
) {
|
||||
previousStart.alpha = 0.33f
|
||||
previousStart = current
|
||||
current.alpha = 1f
|
||||
@@ -431,11 +434,13 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
uiSettingsManga.setOnClickListener {
|
||||
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') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
@@ -445,7 +450,9 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
box.setSingleLine()
|
||||
|
||||
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)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
@@ -457,7 +464,10 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
fun handleOkAction() {
|
||||
val editText = dialogView.userAgentTextBox
|
||||
if (editText.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
editText.text
|
||||
?.toString()
|
||||
?.trim()
|
||||
?.toCharArray(password)
|
||||
dialog.dismiss()
|
||||
callback(password)
|
||||
} else {
|
||||
@@ -473,18 +483,20 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
dialogView.subtitle.visibility = View.VISIBLE
|
||||
if (!isExporting) dialogView.subtitle.text =
|
||||
getString(R.string.enter_password_to_decrypt_file)
|
||||
if (!isExporting) {
|
||||
dialogView.subtitle.text =
|
||||
getString(R.string.enter_password_to_decrypt_file)
|
||||
}
|
||||
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.window?.apply {
|
||||
setDimAmount(0.8f)
|
||||
attributes.windowAnimations = android.R.style.Animation_Dialog
|
||||
}
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
handleOkAction()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,18 +82,9 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
setTitle(R.string.subscriptions_checking_time)
|
||||
singleChoiceItems(timeNames, curTime) { i ->
|
||||
curTime = i
|
||||
it.settingsTitle.text = getString(
|
||||
R.string.subscriptions_checking_time_s,
|
||||
timeNames[i]
|
||||
)
|
||||
PrefManager.setVal(
|
||||
PrefName.SubscriptionNotificationInterval,
|
||||
curTime
|
||||
)
|
||||
TaskScheduler.create(
|
||||
context,
|
||||
PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(context)
|
||||
it.settingsTitle.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
|
||||
PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime)
|
||||
TaskScheduler.create(context, PrefManager.getVal(PrefName.UseAlarmManager)).scheduleAllTasks(context)
|
||||
}
|
||||
show()
|
||||
}
|
||||
@@ -128,26 +119,26 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes)
|
||||
.toMutableSet()
|
||||
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.anilist_notification_filters)
|
||||
.setMultiChoiceItems(
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.anilist_notification_filters)
|
||||
multiChoiceItems(
|
||||
types.map { name ->
|
||||
name.replace("_", " ").lowercase().replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
|
||||
}
|
||||
}.toTypedArray(),
|
||||
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
|
||||
} }.toTypedArray(),
|
||||
selected
|
||||
) { _, which, isChecked ->
|
||||
val type = types[which]
|
||||
if (isChecked) {
|
||||
) { updatedSelected ->
|
||||
types.forEachIndexed { index, type ->
|
||||
if (updatedSelected[index]) {
|
||||
filteredTypes.add(type)
|
||||
} else {
|
||||
filteredTypes.remove(type)
|
||||
}
|
||||
}
|
||||
PrefManager.setVal(PrefName.AnilistFilteredTypes, filteredTypes)
|
||||
}.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
),
|
||||
@@ -160,27 +151,24 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
desc = getString(R.string.anilist_notifications_checking_time_desc),
|
||||
icon = R.drawable.ic_round_notifications_none_24,
|
||||
onClick = {
|
||||
val selected =
|
||||
PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.subscriptions_checking_time)
|
||||
.setSingleChoiceItems(
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.subscriptions_checking_time)
|
||||
singleChoiceItems(
|
||||
aItems.toTypedArray(),
|
||||
selected
|
||||
) { dialog, i ->
|
||||
PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
|
||||
) { i ->
|
||||
PrefManager.setVal(PrefName.AnilistNotificationInterval, i)
|
||||
it.settingsTitle.text =
|
||||
getString(
|
||||
R.string.anilist_notifications_checking_time,
|
||||
aItems[i]
|
||||
)
|
||||
dialog.dismiss()
|
||||
TaskScheduler.create(
|
||||
context, PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(context)
|
||||
}.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
),
|
||||
Settings(
|
||||
@@ -192,27 +180,24 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
desc = getString(R.string.comment_notification_checking_time_desc),
|
||||
icon = R.drawable.ic_round_notifications_none_24,
|
||||
onClick = {
|
||||
val selected =
|
||||
PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.subscriptions_checking_time)
|
||||
.setSingleChoiceItems(
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.subscriptions_checking_time)
|
||||
singleChoiceItems(
|
||||
cItems.toTypedArray(),
|
||||
selected
|
||||
) { dialog, i ->
|
||||
PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
|
||||
) { i ->
|
||||
PrefManager.setVal(PrefName.CommentNotificationInterval, i)
|
||||
it.settingsTitle.text =
|
||||
getString(
|
||||
R.string.comment_notification_checking_time,
|
||||
cItems[i]
|
||||
)
|
||||
dialog.dismiss()
|
||||
TaskScheduler.create(
|
||||
context, PrefManager.getVal(PrefName.UseAlarmManager)
|
||||
).scheduleAllTasks(context)
|
||||
}.create()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
),
|
||||
Settings(
|
||||
@@ -239,10 +224,10 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
isChecked = PrefManager.getVal(PrefName.UseAlarmManager),
|
||||
switch = { isChecked, view ->
|
||||
if (isChecked) {
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.use_alarm_manager)
|
||||
.setMessage(R.string.use_alarm_manager_confirm)
|
||||
.setPositiveButton(R.string.use) { dialog, _ ->
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.use_alarm_manager)
|
||||
setMessage(R.string.use_alarm_manager_confirm)
|
||||
setPosButton(R.string.use) {
|
||||
PrefManager.setVal(PrefName.UseAlarmManager, true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
|
||||
@@ -252,15 +237,13 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
view.settingsButton.isChecked = true
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}.setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
}
|
||||
setNegButton(R.string.cancel) {
|
||||
view.settingsButton.isChecked = false
|
||||
PrefManager.setVal(PrefName.UseAlarmManager, false)
|
||||
|
||||
dialog.dismiss()
|
||||
}.create()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
alertDialog.show()
|
||||
}
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
PrefManager.setVal(PrefName.UseAlarmManager, false)
|
||||
TaskScheduler.create(context, true).cancelAllTasks()
|
||||
@@ -277,4 +260,4 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package ani.dantotsu.util
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import android.view.View
|
||||
import ani.dantotsu.R
|
||||
|
||||
@@ -205,8 +207,14 @@ class AlertDialogBuilder(private val context: Context) {
|
||||
onShow?.invoke()
|
||||
}
|
||||
dialog.window?.apply {
|
||||
setDimAmount(0.8f)
|
||||
setDimAmount(0.5f)
|
||||
attributes.windowAnimations = android.R.style.Animation_Dialog
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val params = attributes
|
||||
params.flags = params.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
|
||||
params.setBlurBehindRadius(20)
|
||||
attributes = params
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animesource
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Hoster
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
@@ -48,6 +49,25 @@ interface AnimeSource {
|
||||
return fetchEpisodeList(anime).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of hoster for an episode. The first hoster in the list should
|
||||
* be the preferred hoster.
|
||||
*
|
||||
* @since extensions-lib 16
|
||||
* @param episode the episode.
|
||||
* @return the hosters for the episode.
|
||||
*/
|
||||
suspend fun getHosterList(episode: SEpisode): List<Hoster> = throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Get the list of videos for a hoster.
|
||||
*
|
||||
* @since extensions-lib 16
|
||||
* @param hoster the hoster.
|
||||
* @return the videos for the hoster.
|
||||
*/
|
||||
suspend fun getVideoList(hoster: Hoster): List<Video> = throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Get the list of videos a episode has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SerializableVideo.Companion.serialize
|
||||
import eu.kanade.tachiyomi.animesource.model.SerializableVideo.Companion.toVideoList
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
open class Hoster(
|
||||
val hosterUrl: String = "",
|
||||
val hosterName: String = "",
|
||||
val videoList: List<Video>? = null,
|
||||
val internalData: String = "",
|
||||
) {
|
||||
@Transient
|
||||
@Volatile
|
||||
var status: State = State.IDLE
|
||||
|
||||
enum class State {
|
||||
IDLE,
|
||||
LOADING,
|
||||
READY,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
fun copy(
|
||||
hosterUrl: String = this.hosterUrl,
|
||||
hosterName: String = this.hosterName,
|
||||
videoList: List<Video>? = this.videoList,
|
||||
internalData: String = this.internalData,
|
||||
): Hoster {
|
||||
return Hoster(hosterUrl, hosterName, videoList, internalData)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NO_HOSTER_LIST = "no_hoster_list"
|
||||
|
||||
fun List<Video>.toHosterList(): List<Hoster> {
|
||||
return listOf(
|
||||
Hoster(
|
||||
hosterUrl = "",
|
||||
hosterName = NO_HOSTER_LIST,
|
||||
videoList = this,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SerializableHoster(
|
||||
val hosterUrl: String = "",
|
||||
val hosterName: String = "",
|
||||
val videoList: String? = null,
|
||||
val internalData: String = "",
|
||||
) {
|
||||
companion object {
|
||||
fun List<Hoster>.serialize(): String =
|
||||
Json.encodeToString(
|
||||
this.map { host ->
|
||||
SerializableHoster(
|
||||
host.hosterUrl,
|
||||
host.hosterName,
|
||||
host.videoList?.serialize(),
|
||||
host.internalData,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
fun String.toHosterList(): List<Hoster> =
|
||||
Json.decodeFromString<List<SerializableHoster>>(this)
|
||||
.map { sHost ->
|
||||
Hoster(
|
||||
sHost.hosterUrl,
|
||||
sHost.hosterName,
|
||||
sHost.videoList?.toVideoList(),
|
||||
sHost.internalData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,101 @@
|
||||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import rx.subjects.Subject
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class Track(val url: String, val lang: String) : Serializable
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
enum class ChapterType {
|
||||
Opening,
|
||||
Ending,
|
||||
Recap,
|
||||
MixedOp,
|
||||
Other,
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class TimeStamp(
|
||||
val start: Double,
|
||||
val end: Double,
|
||||
val name: String,
|
||||
val type: ChapterType = ChapterType.Other,
|
||||
)
|
||||
|
||||
open class Video(
|
||||
val url: String = "",
|
||||
val quality: String = "",
|
||||
var videoUrl: String? = null,
|
||||
headers: Headers? = null,
|
||||
// "url", "language-label-2", "url2", "language-label-2"
|
||||
var videoUrl: String = "",
|
||||
val videoTitle: String = "",
|
||||
val resolution: Int? = null,
|
||||
val bitrate: Int? = null,
|
||||
val headers: Headers? = null,
|
||||
val preferred: Boolean = false,
|
||||
val subtitleTracks: List<Track> = emptyList(),
|
||||
val audioTracks: List<Track> = emptyList(),
|
||||
) : Serializable, ProgressListener {
|
||||
val timestamps: List<TimeStamp> = emptyList(),
|
||||
val internalData: String = "",
|
||||
val initialized: Boolean = false,
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
val videoPageUrl: String = "",
|
||||
) {
|
||||
|
||||
@Transient
|
||||
var headers: Headers? = headers
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
@Deprecated("Use videoTitle instead", ReplaceWith("videoTitle"))
|
||||
val quality: String
|
||||
get() = videoTitle
|
||||
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
@Deprecated("Use videoPageUrl instead", ReplaceWith("videoPageUrl"))
|
||||
val url: String
|
||||
get() = videoPageUrl
|
||||
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
constructor(
|
||||
url: String,
|
||||
quality: String,
|
||||
videoUrl: String?,
|
||||
headers: Headers? = null,
|
||||
subtitleTracks: List<Track> = emptyList(),
|
||||
audioTracks: List<Track> = emptyList(),
|
||||
) : this(
|
||||
videoPageUrl = url,
|
||||
videoTitle = quality,
|
||||
videoUrl = videoUrl ?: "null",
|
||||
headers = headers,
|
||||
subtitleTracks = subtitleTracks,
|
||||
audioTracks = audioTracks,
|
||||
)
|
||||
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
constructor(
|
||||
videoUrl: String = "",
|
||||
videoTitle: String = "",
|
||||
resolution: Int? = null,
|
||||
bitrate: Int? = null,
|
||||
headers: Headers? = null,
|
||||
preferred: Boolean = false,
|
||||
subtitleTracks: List<Track> = emptyList(),
|
||||
audioTracks: List<Track> = emptyList(),
|
||||
timestamps: List<TimeStamp> = emptyList(),
|
||||
internalData: String = "",
|
||||
) : this(
|
||||
videoUrl = videoUrl,
|
||||
videoTitle = videoTitle,
|
||||
resolution = resolution,
|
||||
bitrate = bitrate,
|
||||
headers = headers,
|
||||
preferred = preferred,
|
||||
subtitleTracks = subtitleTracks,
|
||||
audioTracks = audioTracks,
|
||||
timestamps = timestamps,
|
||||
internalData = internalData,
|
||||
videoPageUrl = "",
|
||||
)
|
||||
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
constructor(
|
||||
url: String,
|
||||
@@ -38,83 +108,132 @@ open class Video(
|
||||
@Transient
|
||||
@Volatile
|
||||
var status: State = State.QUEUE
|
||||
|
||||
@Transient
|
||||
private val _progressFlow = MutableStateFlow(0)
|
||||
|
||||
@Transient
|
||||
val progressFlow = _progressFlow.asStateFlow()
|
||||
var progress: Int
|
||||
get() = _progressFlow.value
|
||||
set(value) {
|
||||
_progressFlow.value = value
|
||||
}
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var totalBytesDownloaded: Long = 0L
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var totalContentLength: Long = 0L
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var bytesDownloaded: Long = 0L
|
||||
set(value) {
|
||||
totalBytesDownloaded += if (value < field) {
|
||||
value
|
||||
} else {
|
||||
value - field
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
@Transient
|
||||
var progressSubject: Subject<State, State>? = null
|
||||
fun copy(
|
||||
videoUrl: String = this.videoUrl,
|
||||
videoTitle: String = this.videoTitle,
|
||||
resolution: Int? = this.resolution,
|
||||
bitrate: Int? = this.bitrate,
|
||||
headers: Headers? = this.headers,
|
||||
preferred: Boolean = this.preferred,
|
||||
subtitleTracks: List<Track> = this.subtitleTracks,
|
||||
audioTracks: List<Track> = this.audioTracks,
|
||||
timestamps: List<TimeStamp> = this.timestamps,
|
||||
internalData: String = this.internalData,
|
||||
): Video {
|
||||
return Video(
|
||||
videoUrl = videoUrl,
|
||||
videoTitle = videoTitle,
|
||||
resolution = resolution,
|
||||
bitrate = bitrate,
|
||||
headers = headers,
|
||||
preferred = preferred,
|
||||
subtitleTracks = subtitleTracks,
|
||||
audioTracks = audioTracks,
|
||||
timestamps = timestamps,
|
||||
internalData = internalData,
|
||||
)
|
||||
}
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
bytesDownloaded = bytesRead
|
||||
if (contentLength > totalContentLength) {
|
||||
totalContentLength = contentLength
|
||||
}
|
||||
val newProgress = if (totalContentLength > 0) {
|
||||
(100 * totalBytesDownloaded / totalContentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
if (progress != newProgress) progress = newProgress
|
||||
fun copy(
|
||||
videoUrl: String = this.videoUrl,
|
||||
videoTitle: String = this.videoTitle,
|
||||
resolution: Int? = this.resolution,
|
||||
bitrate: Int? = this.bitrate,
|
||||
headers: Headers? = this.headers,
|
||||
preferred: Boolean = this.preferred,
|
||||
subtitleTracks: List<Track> = this.subtitleTracks,
|
||||
audioTracks: List<Track> = this.audioTracks,
|
||||
timestamps: List<TimeStamp> = this.timestamps,
|
||||
internalData: String = this.internalData,
|
||||
initialized: Boolean = this.initialized,
|
||||
videoPageUrl: String = this.videoPageUrl,
|
||||
): Video {
|
||||
return Video(
|
||||
videoUrl = videoUrl,
|
||||
videoTitle = videoTitle,
|
||||
resolution = resolution,
|
||||
bitrate = bitrate,
|
||||
headers = headers,
|
||||
preferred = preferred,
|
||||
subtitleTracks = subtitleTracks,
|
||||
audioTracks = audioTracks,
|
||||
timestamps = timestamps,
|
||||
internalData = internalData,
|
||||
initialized = initialized,
|
||||
videoPageUrl = videoPageUrl,
|
||||
)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
QUEUE,
|
||||
LOAD_VIDEO,
|
||||
DOWNLOAD_IMAGE,
|
||||
READY,
|
||||
ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeObject(out: ObjectOutputStream) {
|
||||
out.defaultWriteObject()
|
||||
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
|
||||
out.writeObject(headersMap)
|
||||
}
|
||||
@kotlinx.serialization.Serializable
|
||||
data class SerializableVideo(
|
||||
val videoUrl: String = "",
|
||||
val videoTitle: String = "",
|
||||
val resolution: Int? = null,
|
||||
val bitrate: Int? = null,
|
||||
val headers: List<Pair<String, String>>? = null,
|
||||
val preferred: Boolean = false,
|
||||
val subtitleTracks: List<Track> = emptyList(),
|
||||
val audioTracks: List<Track> = emptyList(),
|
||||
val timestamps: List<TimeStamp> = emptyList(),
|
||||
val internalData: String = "",
|
||||
val initialized: Boolean = false,
|
||||
// TODO(1.6): Remove after ext lib bump
|
||||
val videoPageUrl: String = "",
|
||||
) {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
private fun readObject(input: ObjectInputStream) {
|
||||
input.defaultReadObject()
|
||||
val headersMap = input.readObject() as? Map<String, List<String>>
|
||||
headers = headersMap?.let { map ->
|
||||
val builder = Headers.Builder()
|
||||
for ((key, values) in map) {
|
||||
for (value in values) {
|
||||
builder.add(key, value)
|
||||
companion object {
|
||||
fun List<Video>.serialize(): String =
|
||||
Json.encodeToString(
|
||||
this.map { vid ->
|
||||
SerializableVideo(
|
||||
vid.videoUrl,
|
||||
vid.videoTitle,
|
||||
vid.resolution,
|
||||
vid.bitrate,
|
||||
vid.headers?.toList(),
|
||||
vid.preferred,
|
||||
vid.subtitleTracks,
|
||||
vid.audioTracks,
|
||||
vid.timestamps,
|
||||
vid.internalData,
|
||||
vid.initialized,
|
||||
vid.videoPageUrl,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
fun String.toVideoList(): List<Video> =
|
||||
Json.decodeFromString<List<SerializableVideo>>(this)
|
||||
.map { sVid ->
|
||||
Video(
|
||||
sVid.videoUrl,
|
||||
sVid.videoTitle,
|
||||
sVid.resolution,
|
||||
sVid.bitrate,
|
||||
sVid.headers
|
||||
?.flatMap { it.toList() }
|
||||
?.let { Headers.headersOf(*it.toTypedArray()) },
|
||||
sVid.preferred,
|
||||
sVid.subtitleTracks,
|
||||
sVid.audioTracks,
|
||||
sVid.timestamps,
|
||||
sVid.internalData,
|
||||
sVid.initialized,
|
||||
sVid.videoPageUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import ani.dantotsu.asyncMap
|
||||
import ani.dantotsu.parsers.novel.AvailableNovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtension
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -67,7 +68,7 @@ internal class ExtensionGithubApi {
|
||||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
repos.asyncMap {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
@@ -155,7 +156,7 @@ internal class ExtensionGithubApi {
|
||||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
repos.asyncMap {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
@@ -207,7 +208,7 @@ internal class ExtensionGithubApi {
|
||||
val repos =
|
||||
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
repos.asyncMap {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
|
||||
@@ -1102,10 +1102,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:stepSize="5.0"
|
||||
android:value="15.0"
|
||||
android:valueFrom="5.0"
|
||||
android:valueTo="45.0"
|
||||
android:stepSize="1.0"
|
||||
android:value="10.0"
|
||||
android:valueFrom="1.0"
|
||||
android:valueTo="50.0"
|
||||
app:labelBehavior="floating"
|
||||
app:labelStyle="@style/fontTooltip"
|
||||
app:thumbColor="?attr/colorSecondary"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="326dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
@@ -160,8 +160,8 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="265dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="263dp"
|
||||
android:layout_height="60dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
@@ -171,14 +171,23 @@
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="@string/download" />
|
||||
|
||||
<TextView
|
||||
<EditText
|
||||
android:id="@+id/downloadNo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:textSize="12dp"
|
||||
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>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
@@ -191,7 +200,7 @@
|
||||
<ImageButton
|
||||
android:id="@+id/mediaDownloadTop"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_height="60dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:srcCompat="@drawable/ic_download_24"
|
||||
app:tint="?attr/colorOnBackground"
|
||||
@@ -313,9 +322,9 @@
|
||||
android:text="@string/reset" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reset_progress_def"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/reset_progress_def"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text=""
|
||||
android:textColor="?attr/colorSecondary"
|
||||
|
||||
53
stable.md
53
stable.md
@@ -1,53 +1,4 @@
|
||||
# 3.1.0
|
||||
|
||||
- **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
|
||||
# 3.2.1
|
||||
|
||||
- **Bugfixes:**
|
||||
- Scanlator/language not saving after leaving app
|
||||
- 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)!
|
||||
|
||||

|
||||
- Fix a crash after watching a video
|
||||
|
||||
Reference in New Issue
Block a user