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!
<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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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)!
![alt text](https://media1.tenor.com/m/P7hCyZlzDH4AAAAC/wink-anime.gif)
- Fix a crash after watching a video