fix: return jsdeliver

This commit is contained in:
rebelonion 2024-04-04 05:23:11 -05:00
parent 7688ffa39f
commit da56aecd5e
3 changed files with 139 additions and 37 deletions

View file

@ -23,22 +23,16 @@ import android.view.animation.AnimationUtils
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.EditText import android.widget.EditText
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import android.view.HapticFeedbackConstants
import androidx.core.view.ViewCompat.performHapticFeedback
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.BuildConfig import ani.dantotsu.BuildConfig
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
@ -88,9 +82,7 @@ import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.LauncherWrapper import ani.dantotsu.util.LauncherWrapper
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import eltos.simpledialogfragment.SimpleDialog import eltos.simpledialogfragment.SimpleDialog
import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE
@ -100,8 +92,8 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -130,7 +122,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
private var cursedCounter = 0 private var cursedCounter = 0
private val animeExtensionManager: AnimeExtensionManager by injectLazy() private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy() private val mangaExtensionManager: MangaExtensionManager by injectLazy()
@kotlin.OptIn(DelicateCoroutinesApi::class) @kotlin.OptIn(DelicateCoroutinesApi::class)
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -233,7 +225,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsAnilistAvatar.loadImage(Anilist.avatar) settingsAnilistAvatar.loadImage(Anilist.avatar)
settingsAnilistAvatar.setOnClickListener { settingsAnilistAvatar.setOnClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val anilistLink = getString(R.string.anilist_link, PrefManager.getVal<String>(PrefName.AnilistUserName)) val anilistLink = getString(
R.string.anilist_link,
PrefManager.getVal<String>(PrefName.AnilistUserName)
)
openLinkInBrowser(anilistLink) openLinkInBrowser(anilistLink)
true true
} }
@ -649,14 +644,16 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
val entry = if (input.endsWith("/") || input.endsWith("index.min.json")) val entry = if (input.endsWith("/") || input.endsWith("index.min.json"))
input.substring(0, input.lastIndexOf("/")) else input input.substring(0, input.lastIndexOf("/")) else input
if (mediaType == MediaType.ANIME) { if (mediaType == MediaType.ANIME) {
val anime = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry) val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime) PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
} }
} }
if (mediaType == MediaType.MANGA) { if (mediaType == MediaType.MANGA) {
val manga = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry) val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga) PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions() mangaExtensionManager.findAvailableExtensions()
@ -669,7 +666,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
editText.setOnEditorActionListener { textView, action, keyEvent -> editText.setOnEditorActionListener { textView, action, keyEvent ->
if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE || if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE ||
(keyEvent?.action == KeyEvent.ACTION_UP (keyEvent?.action == KeyEvent.ACTION_UP
&& keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) { && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)
) {
processUserInput(textView.text.toString(), mediaType) processUserInput(textView.text.toString(), mediaType)
dialog.dismiss() dialog.dismiss()
return@setOnEditorActionListener true return@setOnEditorActionListener true
@ -681,9 +679,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
setExtensionOutput() setExtensionOutput()
animeAddRepository.setOnClickListener { animeAddRepository.setOnClickListener {
val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null) val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null)
val editText = dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply { val editText =
hint = getString(R.string.anime_add_repository) dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply {
} hint = getString(R.string.anime_add_repository)
}
val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup) val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.anime_add_repository) .setTitle(R.string.anime_add_repository)
.setView(dialogView) .setView(dialogView)
@ -708,9 +707,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
mangaAddRepository.setOnClickListener { mangaAddRepository.setOnClickListener {
val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null) val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null)
val editText = dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply { val editText =
hint = getString(R.string.manga_add_repository) dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox).apply {
} hint = getString(R.string.manga_add_repository)
}
val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup) val alertDialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.manga_add_repository) .setTitle(R.string.manga_add_repository)
.setView(dialogView) .setView(dialogView)
@ -867,7 +867,13 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} }
settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)]) settingsExtensionDns.setText(exDns[PrefManager.getVal(PrefName.DohProvider)])
settingsExtensionDns.setAdapter(ArrayAdapter(this@SettingsActivity, R.layout.item_dropdown, exDns)) settingsExtensionDns.setAdapter(
ArrayAdapter(
this@SettingsActivity,
R.layout.item_dropdown,
exDns
)
)
settingsExtensionDns.setOnItemClickListener { _, _, i, _ -> settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
PrefManager.setVal(PrefName.DohProvider, i) PrefManager.setVal(PrefName.DohProvider, i)
settingsExtensionDns.clearFocus() settingsExtensionDns.clearFocus()
@ -959,7 +965,12 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} }
settingsUi.setOnClickListener { settingsUi.setOnClickListener {
startActivity(Intent(this@SettingsActivity, UserInterfaceSettingsActivity::class.java)) startActivity(
Intent(
this@SettingsActivity,
UserInterfaceSettingsActivity::class.java
)
)
} }
} }
@ -983,7 +994,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
getString(R.string.subscriptions_checking_time_s, timeNames[i]) getString(R.string.subscriptions_checking_time_s, timeNames[i])
PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime) PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime)
dialog.dismiss() dialog.dismiss()
TaskScheduler.create(this@SettingsActivity, TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager) PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity) ).scheduleAllTasks(this@SettingsActivity)
}.show() }.show()
@ -991,7 +1003,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} }
settingsSubscriptionsTime.setOnLongClickListener { settingsSubscriptionsTime.setOnLongClickListener {
TaskScheduler.create(this@SettingsActivity, TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager) PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity) ).scheduleAllTasks(this@SettingsActivity)
true true
@ -1005,7 +1018,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
else getString(R.string.do_not_update) else getString(R.string.do_not_update)
} }
settingsAnilistSubscriptionsTime.text = settingsAnilistSubscriptionsTime.text =
getString(R.string.anilist_notifications_checking_time, aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)]) getString(
R.string.anilist_notifications_checking_time,
aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
)
settingsAnilistSubscriptionsTime.setOnClickListener { settingsAnilistSubscriptionsTime.setOnClickListener {
val selected = PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval) val selected = PrefManager.getVal<Int>(PrefName.AnilistNotificationInterval)
@ -1016,7 +1032,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsAnilistSubscriptionsTime.text = settingsAnilistSubscriptionsTime.text =
getString(R.string.anilist_notifications_checking_time, aItems[i]) getString(R.string.anilist_notifications_checking_time, aItems[i])
dialog.dismiss() dialog.dismiss()
TaskScheduler.create(this@SettingsActivity, TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager) PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity) ).scheduleAllTasks(this@SettingsActivity)
} }
@ -1027,7 +1044,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsAnilistNotifications.setOnClickListener { settingsAnilistNotifications.setOnClickListener {
val types = NotificationType.entries.map { it.name } val types = NotificationType.entries.map { it.name }
val filteredTypes = PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes).toMutableSet() val filteredTypes =
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes).toMutableSet()
val selected = types.map { filteredTypes.contains(it) }.toBooleanArray() val selected = types.map { filteredTypes.contains(it) }.toBooleanArray()
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup) val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
.setTitle(R.string.anilist_notification_filters) .setTitle(R.string.anilist_notification_filters)
@ -1054,7 +1072,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} }
settingsCommentSubscriptionsTime.text = settingsCommentSubscriptionsTime.text =
getString(R.string.comment_notification_checking_time, cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)]) getString(
R.string.comment_notification_checking_time,
cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)]
)
settingsCommentSubscriptionsTime.setOnClickListener { settingsCommentSubscriptionsTime.setOnClickListener {
val selected = PrefManager.getVal<Int>(PrefName.CommentNotificationInterval) val selected = PrefManager.getVal<Int>(PrefName.CommentNotificationInterval)
val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup) val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
@ -1064,7 +1085,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
settingsCommentSubscriptionsTime.text = settingsCommentSubscriptionsTime.text =
getString(R.string.comment_notification_checking_time, cItems[i]) getString(R.string.comment_notification_checking_time, cItems[i])
dialog.dismiss() dialog.dismiss()
TaskScheduler.create(this@SettingsActivity, TaskScheduler.create(
this@SettingsActivity,
PrefManager.getVal(PrefName.UseAlarmManager) PrefManager.getVal(PrefName.UseAlarmManager)
).scheduleAllTasks(this@SettingsActivity) ).scheduleAllTasks(this@SettingsActivity)
} }
@ -1095,7 +1117,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
PrefManager.setVal(PrefName.UseAlarmManager, true) PrefManager.setVal(PrefName.UseAlarmManager, true)
if (SDK_INT >= Build.VERSION_CODES.S) { if (SDK_INT >= Build.VERSION_CODES.S) {
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) { if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
val intent = Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM") val intent =
Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM")
startActivity(intent) startActivity(intent)
settingsNotificationsCheckingSubscriptions.isChecked = true settingsNotificationsCheckingSubscriptions.isChecked = true
} }
@ -1113,7 +1136,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} else { } else {
PrefManager.setVal(PrefName.UseAlarmManager, false) PrefManager.setVal(PrefName.UseAlarmManager, false)
TaskScheduler.create(this@SettingsActivity, true).cancelAllTasks() TaskScheduler.create(this@SettingsActivity, true).cancelAllTasks()
TaskScheduler.create(this@SettingsActivity, false).scheduleAllTasks(this@SettingsActivity) TaskScheduler.create(this@SettingsActivity, false)
.scheduleAllTasks(this@SettingsActivity)
} }
} }
} }
@ -1285,6 +1309,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
callback(null) callback(null)
} }
.create() .create()
fun handleOkAction() { fun handleOkAction() {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox) val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) { if (editText?.text?.isNotBlank() == true) {

View file

@ -39,15 +39,33 @@ internal class AnimeExtensionGithubApi {
val extensions: ArrayList<AnimeExtension.Available> = arrayListOf() val extensions: ArrayList<AnimeExtension.Available> = arrayListOf()
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).forEach { val repos =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
if (repos.isEmpty()) {
repos.add("https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo")
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos.toSet())
}
repos.forEach {
try { try {
val githubResponse = val githubResponse = try {
networkService.client networkService.client
.newCall(GET("${it}/index.min.json")) .newCall(GET("${it}/index.min.json"))
.awaitSuccess() .awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $it")
Logger.log(e)
null
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
.awaitSuccess()
}
val repoExtensions = with(json) { val repoExtensions = with(json) {
githubResponse response
.parseAs<List<AnimeExtensionJsonObject>>() .parseAs<List<AnimeExtensionJsonObject>>()
.toExtensions(it) .toExtensions(it)
} }
@ -61,6 +79,7 @@ internal class AnimeExtensionGithubApi {
extensions.addAll(repoExtensions) extensions.addAll(repoExtensions)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub") Logger.log("Failed to get extensions from GitHub")
Logger.log(e)
} }
} }
@ -146,6 +165,26 @@ internal class AnimeExtensionGithubApi {
fun getApkUrl(extension: AnimeExtension.Available): String { fun getApkUrl(extension: AnimeExtension.Available): String {
return "${extension.repository}/apk/${extension.apkName}" return "${extension.repository}/apk/${extension.apkName}"
} }
private fun fallbackRepoUrl(repoUrl: String): String? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl =
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
val repoUrlParts = strippedRepoUrl.split("/")
if (repoUrlParts.size < 3) {
return null
}
val repoOwner = repoUrlParts[1]
val repoName = repoUrlParts[2]
fallbackRepoUrl += "$repoOwner/$repoName"
val repoBranch = if (repoUrlParts.size > 3) {
repoUrlParts[3]
} else {
"main"
}
fallbackRepoUrl += "@$repoBranch"
return fallbackRepoUrl
}
} }
private fun AnimeExtensionJsonObject.extractLibVersion(): Double { private fun AnimeExtensionJsonObject.extractLibVersion(): Double {

View file

@ -39,16 +39,33 @@ internal class MangaExtensionGithubApi {
val extensions: ArrayList<MangaExtension.Available> = arrayListOf() val extensions: ArrayList<MangaExtension.Available> = arrayListOf()
val repos =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
if (repos.isEmpty()) {
repos.add("https://raw.githubusercontent.com/keiyoushi/extensions/main")
PrefManager.setVal(PrefName.MangaExtensionRepos, repos.toSet())
}
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).forEach { repos.forEach {
try { try {
val githubResponse = val githubResponse = try {
networkService.client networkService.client
.newCall(GET("${it}/index.min.json")) .newCall(GET("${it}/index.min.json"))
.awaitSuccess() .awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $it")
Logger.log(e)
null
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
.awaitSuccess()
}
val repoExtensions = with(json) { val repoExtensions = with(json) {
githubResponse response
.parseAs<List<ExtensionJsonObject>>() .parseAs<List<ExtensionJsonObject>>()
.toExtensions(it) .toExtensions(it)
} }
@ -62,6 +79,7 @@ internal class MangaExtensionGithubApi {
extensions.addAll(repoExtensions) extensions.addAll(repoExtensions)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub") Logger.log("Failed to get extensions from GitHub")
Logger.log(e)
} }
} }
@ -150,6 +168,26 @@ internal class MangaExtensionGithubApi {
private fun ExtensionJsonObject.extractLibVersion(): Double { private fun ExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble() return version.substringBeforeLast('.').toDouble()
} }
private fun fallbackRepoUrl(repoUrl: String): String? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl =
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
val repoUrlParts = strippedRepoUrl.split("/")
if (repoUrlParts.size < 3) {
return null
}
val repoOwner = repoUrlParts[1]
val repoName = repoUrlParts[2]
fallbackRepoUrl += "$repoOwner/$repoName"
val repoBranch = if (repoUrlParts.size > 3) {
repoUrlParts[3]
} else {
"main"
}
fallbackRepoUrl += "@$repoBranch"
return fallbackRepoUrl
}
} }
@Serializable @Serializable