Compare commits

...

11 commits

Author SHA1 Message Date
rebel onion
a93b4f5b11 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2025-05-14 21:40:08 -05:00
rebel onion
69c44b7d20 chore: formatting changes 2025-05-14 21:40:06 -05:00
Rishvaish
a684aac0b1
To install multiple mangas (#582)
users can enter the value required to install as there is an EditText field instead of the Text View
2025-04-02 10:40:39 +05:30
Daniele Santoru
6c49839f87
Fixed missing manga pages when downloading (#586) 2025-04-02 10:39:33 +05:30
rebel onion
7053a7b4b2
Update README.md 2025-01-16 20:27:24 -06:00
rebel onion
1c156053d0
Merge pull request #565 from rebelonion/dev
Dev
2025-01-16 00:15:34 -06:00
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
19 changed files with 1555 additions and 1326 deletions

View file

@ -14,7 +14,7 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge! > **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a> <a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>
## Terms of Use ## Terms of Use
By downloading, installing, or using this application, you agree to: By downloading, installing, or using this application, you agree to:

View file

@ -17,9 +17,8 @@ android {
applicationId "ani.dantotsu" applicationId "ani.dantotsu"
minSdk 21 minSdk 21
targetSdk 35 targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger()) versionName "3.2.2"
versionName "3.2.0" versionCode 300200200
versionCode 300200000
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

View file

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

View file

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

View file

@ -50,8 +50,9 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json) val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
suspend fun String.discordUrl() = assetApi.getDiscordUri(this) suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
return json.encodeToString(Presence.Response( return json.encodeToString(
3, Presence.Response(
3,
Presence( Presence(
activities = listOf( activities = listOf(
Activity( Activity(

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -474,7 +474,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
scanlator = chapter.scanlator ?: "Unknown", scanlator = chapter.scanlator ?: "Unknown",
imageData = images, imageData = images,
sourceMedia = media, sourceMedia = media,
retries = 2, retries = 25,
simultaneousDownloads = 2 simultaneousDownloads = 2
) )

View file

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

View file

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

View file

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

View file

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

View file

@ -96,7 +96,8 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
themeSwitcher.apply { themeSwitcher.apply {
setText(themeText) setText(themeText)
setAdapter( setAdapter(
ArrayAdapter(context, ArrayAdapter(
context,
R.layout.item_dropdown, R.layout.item_dropdown,
ThemeManager.Companion.Theme.entries.map { ThemeManager.Companion.Theme.entries.map {
it.theme.substring( it.theme.substring(

View file

@ -52,14 +52,15 @@ class SubscriptionsBottomDialog : BottomSheetDialogFragment() {
} }
groupedSubscriptions.forEach { (parserName, mediaList) -> groupedSubscriptions.forEach { (parserName, mediaList) ->
adapter.add(SubscriptionSource( adapter.add(
parserName, SubscriptionSource(
mediaList.toMutableList(), parserName,
adapter, mediaList.toMutableList(),
getParserIcon(parserName) adapter,
) { group -> getParserIcon(parserName)
adapter.remove(group) ) { group ->
}) adapter.remove(group)
})
} }
} }

View file

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

View file

@ -12,7 +12,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.7.3' classpath 'com.android.tools.build:gradle:8.9.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version" classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version"

View file

@ -1,6 +1,6 @@
#Wed Aug 30 19:57:04 IST 2023 #Wed Aug 30 19:57:04 IST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

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