Compare commits

...
Sign in to create a new pull request.

6 commits
dev ... main

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
14 changed files with 178 additions and 120 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!
<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
By downloading, installing, or using this application, you agree to:

View file

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

View file

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

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

@ -427,7 +427,8 @@ class ExoplayerView :
false -> 0f
}
val textElevation = PrefManager.getVal<Float>(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels
val textElevation =
PrefManager.getVal<Float>(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels
textView.translationY = -textElevation
}
@ -606,9 +607,9 @@ class ExoplayerView :
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
pipEnabled =
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) &&
PrefManager.getVal(
PrefName.Pip,
)
PrefManager.getVal(
PrefName.Pip,
)
if (pipEnabled) {
exoPip.visibility = View.VISIBLE
exoPip.setOnClickListener {
@ -1044,7 +1045,8 @@ class ExoplayerView :
}
}
override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(false, event) else handleController()
override fun onSingleClick(event: MotionEvent) =
if (isSeeking) doubleTap(false, event) else handleController()
},
)
val rewindArea = playerView.findViewById<View>(R.id.exo_rewind_area)
@ -1079,7 +1081,8 @@ class ExoplayerView :
}
}
override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(true, event) else handleController()
override fun onSingleClick(event: MotionEvent) =
if (isSeeking) doubleTap(true, event) else handleController()
},
)
val forwardArea = playerView.findViewById<View>(R.id.exo_forward_area)
@ -1449,7 +1452,8 @@ class ExoplayerView :
else -> mutableListOf()
}
val startTimestamp = Calendar.getInstance()
val durationInSeconds = if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440
val durationInSeconds =
if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440
val endTimestamp =
Calendar.getInstance().apply {
@ -1502,12 +1506,12 @@ class ExoplayerView :
@Suppress("UNCHECKED_CAST")
val list =
(
PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java,
) as List<Int>
).toMutableList()
PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java,
) as List<Int>
).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueAnimeList", list)
@ -1567,7 +1571,11 @@ class ExoplayerView :
subtitle = intent.getSerialized("subtitle")
?: when (
val subLang: String? =
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)
PrefManager.getNullableCustomVal(
"subLang_${media.id}",
null,
String::class.java
)
) {
null -> {
when (episode.selectedSubtitle) {
@ -1575,8 +1583,12 @@ class ExoplayerView :
-1 ->
ext.subtitles.find {
it.language.contains(lang, ignoreCase = true) ||
it.language.contains(getLanguageCode(lang), ignoreCase = true)
it.language.contains(
getLanguageCode(lang),
ignoreCase = true
)
}
else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!)
}
}
@ -1651,7 +1663,8 @@ class ExoplayerView :
}.build()
val dataSourceFactory =
DataSource.Factory {
val dataSource: HttpDataSource = OkHttpDataSource.Factory(httpClient).createDataSource()
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(httpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
@ -1717,16 +1730,18 @@ class ExoplayerView :
val docFile =
directory.listFiles().firstOrNull {
it.name?.endsWith(".mp4") == true ||
it.name?.endsWith(".mkv") == true ||
it.name?.endsWith(
".${Injekt
.get<DownloadAddonManager>()
.extension
?.extension
?.getFileExtension()
?.first ?: "ts"}",
) ==
true
it.name?.endsWith(".mkv") == true ||
it.name?.endsWith(
".${
Injekt
.get<DownloadAddonManager>()
.extension
?.extension
?.getFileExtension()
?.first ?: "ts"
}",
) ==
true
}
if (docFile != null) {
val uri = docFile.uri
@ -1840,30 +1855,30 @@ class ExoplayerView :
"%02d:%02d:%02d",
TimeUnit.MILLISECONDS.toHours(playbackPosition),
TimeUnit.MILLISECONDS.toMinutes(playbackPosition) -
TimeUnit.HOURS.toMinutes(
TimeUnit.MILLISECONDS.toHours(
playbackPosition,
TimeUnit.HOURS.toMinutes(
TimeUnit.MILLISECONDS.toHours(
playbackPosition,
),
),
),
TimeUnit.MILLISECONDS.toSeconds(playbackPosition) -
TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(
playbackPosition,
TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(
playbackPosition,
),
),
),
)
customAlertDialog().apply {
setTitle(getString(R.string.continue_from, time))
setCancelable(false)
setPosButton(getString(R.string.yes)) {
buildExoplayer()
}
setNegButton(getString(R.string.no)) {
playbackPosition = 0L
buildExoplayer()
}
show()
customAlertDialog().apply {
setTitle(getString(R.string.continue_from, time))
setCancelable(false)
setPosButton(getString(R.string.yes)) {
buildExoplayer()
}
setNegButton(getString(R.string.no)) {
playbackPosition = 0L
buildExoplayer()
}
show()
}
} else {
buildExoplayer()
}
@ -1928,7 +1943,7 @@ class ExoplayerView :
if (PrefManager.getVal<Boolean>(PrefName.TextviewSubtitles)) {
exoSubtitleView.visibility = View.GONE
customSubtitleView.visibility = View.VISIBLE
val newCues = cueGroup.cues.map { it.text.toString() ?: "" }
val newCues = cueGroup.cues.map { it.text.toString() }
if (newCues.isEmpty()) {
customSubtitleView.text = ""
@ -1940,7 +1955,9 @@ class ExoplayerView :
val currentPosition = exoPlayer.currentPosition
if ((lastSubtitle?.length ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)) {
if ((lastSubtitle?.length
?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)
) {
activeSubtitles.clear()
}
@ -2187,7 +2204,7 @@ class ExoplayerView :
currentTimeStamp =
model.timeStamps.value?.find { timestamp ->
timestamp.interval.startTime < playerCurrentTime &&
playerCurrentTime < (timestamp.interval.endTime - 1)
playerCurrentTime < (timestamp.interval.endTime - 1)
}
val new = currentTimeStamp
@ -2213,7 +2230,8 @@ class ExoplayerView :
override fun onTick(millisUntilFinished: Long) {
if (new == null) {
skipTimeButton.visibility = View.GONE
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
exoSkip.isVisible =
PrefManager.getVal<Int>(PrefName.SkipTime) > 0
disappeared = false
functionstarted = false
cancelTimer()
@ -2222,7 +2240,8 @@ class ExoplayerView :
override fun onFinish() {
skipTimeButton.visibility = View.GONE
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
exoSkip.isVisible =
PrefManager.getVal<Int>(PrefName.SkipTime) > 0
disappeared = true
functionstarted = false
cancelTimer()
@ -2310,7 +2329,7 @@ class ExoplayerView :
tracks.groups.forEach {
println(
"Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" +
"Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}",
"Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}",
)
when (it.type) {
TRACK_TYPE_AUDIO -> {
@ -2365,7 +2384,7 @@ class ExoplayerView :
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
-> {
-> {
toast("Source Exception : ${error.message}")
isPlayerPlaying = true
sourceClick()
@ -2403,9 +2422,9 @@ class ExoplayerView :
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
val episodeEnd =
exoPlayer.currentPosition / episodeLength >
PrefManager.getVal<Float>(
PrefName.WatchPercentage,
)
PrefManager.getVal<Float>(
PrefName.WatchPercentage,
)
val episode0 = currentEpisodeIndex == 0 && PrefManager.getVal(PrefName.ChapterZeroPlayer)
if (!incognito && (episodeEnd || episode0) && Anilist.userid != null
) {

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

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

View file

@ -193,7 +193,8 @@ class SettingsCommonActivity : AppCompatActivity() {
PrefManager.setVal(PrefName.OverridePassword, true)
}
val password = view.passwordInput.text.toString()
val confirmPassword = view.confirmPasswordInput.text.toString()
val confirmPassword =
view.confirmPasswordInput.text.toString()
if (password == confirmPassword && password.isNotEmpty()) {
PrefManager.setVal(PrefName.AppPassword, password)
if (view.biometricCheckbox.isChecked) {
@ -201,11 +202,13 @@ class SettingsCommonActivity : AppCompatActivity() {
BiometricManager
.from(applicationContext)
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ==
BiometricManager.BIOMETRIC_SUCCESS
BiometricManager.BIOMETRIC_SUCCESS
if (canBiometricPrompt) {
val biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(this@SettingsCommonActivity) { _ ->
BiometricPromptUtils.createBiometricPrompt(
this@SettingsCommonActivity
) { _ ->
val token = UUID.randomUUID().toString()
PrefManager.setVal(
PrefName.BiometricToken,
@ -235,12 +238,14 @@ class SettingsCommonActivity : AppCompatActivity() {
setOnShowListener {
view.passwordInput.requestFocus()
val canAuthenticate =
BiometricManager.from(applicationContext).canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK,
) == BiometricManager.BIOMETRIC_SUCCESS
BiometricManager.from(applicationContext)
.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK,
) == BiometricManager.BIOMETRIC_SUCCESS
view.biometricCheckbox.isVisible = canAuthenticate
view.biometricCheckbox.isChecked =
PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()
PrefManager.getVal(PrefName.BiometricToken, "")
.isNotEmpty()
view.forgotPasswordCheckbox.isChecked =
PrefManager.getVal(PrefName.OverridePassword)
}
@ -314,7 +319,8 @@ class SettingsCommonActivity : AppCompatActivity() {
setTitle(R.string.change_download_location)
setMessage(R.string.download_location_msg)
setPosButton(R.string.ok) {
val oldUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
val oldUri =
PrefManager.getVal<String>(PrefName.DownloadsDir)
launcher.registerForCallback { success ->
if (success) {
toast(getString(R.string.please_wait))

View file

@ -82,9 +82,18 @@ 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()
}
@ -120,21 +129,22 @@ class SettingsNotificationActivity : AppCompatActivity() {
.toMutableSet()
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
context.customAlertDialog().apply {
setTitle(R.string.anilist_notification_filters)
multiChoiceItems(
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
) { updatedSelected ->
types.forEachIndexed { index, type ->
if (updatedSelected[index]) {
filteredTypes.add(type)
} else {
filteredTypes.remove(type)
types.forEachIndexed { index, type ->
if (updatedSelected[index]) {
filteredTypes.add(type)
} else {
filteredTypes.remove(type)
}
}
}
PrefManager.setVal(PrefName.AnilistFilteredTypes, filteredTypes)
}
show()
@ -152,8 +162,8 @@ class SettingsNotificationActivity : AppCompatActivity() {
icon = R.drawable.ic_round_notifications_none_24,
onClick = {
context.customAlertDialog().apply {
setTitle(R.string.subscriptions_checking_time)
singleChoiceItems(
setTitle(R.string.subscriptions_checking_time)
singleChoiceItems(
aItems.toTypedArray(),
PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
) { i ->
@ -181,11 +191,11 @@ class SettingsNotificationActivity : AppCompatActivity() {
icon = R.drawable.ic_round_notifications_none_24,
onClick = {
context.customAlertDialog().apply {
setTitle(R.string.subscriptions_checking_time)
singleChoiceItems(
setTitle(R.string.subscriptions_checking_time)
singleChoiceItems(
cItems.toTypedArray(),
PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
) { i ->
) { i ->
PrefManager.setVal(PrefName.CommentNotificationInterval, i)
it.settingsTitle.text =
getString(
@ -225,9 +235,9 @@ class SettingsNotificationActivity : AppCompatActivity() {
switch = { isChecked, view ->
if (isChecked) {
context.customAlertDialog().apply {
setTitle(R.string.use_alarm_manager)
setMessage(R.string.use_alarm_manager_confirm)
setPosButton(R.string.use) {
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()) {

View file

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

View file

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

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

@ -12,7 +12,7 @@ buildscript {
}
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-serialization:$kotlin_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
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists