feat: make repo adding easier
This commit is contained in:
parent
6f1bb10dec
commit
43dee6ee49
13 changed files with 383 additions and 346 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,6 +2,9 @@
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
#kotlin
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,8 @@
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="application/epub+zip" />
|
<data android:host="*"/>
|
||||||
|
<data android:mimeType="application/epub+zip"/>
|
||||||
<data android:mimeType="application/x-mobipocket-ebook" />
|
<data android:mimeType="application/x-mobipocket-ebook" />
|
||||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||||
<data android:mimeType="application/fb2+zip" />
|
<data android:mimeType="application/fb2+zip" />
|
||||||
|
@ -374,25 +375,31 @@
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.Main" />
|
<action android:name="android.intent.action.Main" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
<data android:pathPattern=".*\\.ani" />
|
<data android:pathPattern=".*\\.ani" />
|
||||||
<data android:pathPattern=".*\\.sani" />
|
<data android:pathPattern=".*\\.sani" />
|
||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<!-- Support both schemes -->
|
||||||
|
<data android:scheme="tachiyomi"/>
|
||||||
|
<data android:host="add-repo"/>
|
||||||
|
<data android:scheme="aniyomi"/>
|
||||||
|
<data android:host="add-repo"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||||
|
|
|
@ -91,7 +91,6 @@ import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||||
import ani.dantotsu.connections.anilist.Genre
|
import ani.dantotsu.connections.anilist.Genre
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
@ -106,7 +105,6 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||||
import ani.dantotsu.util.CountUpTimer
|
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestBuilder
|
import com.bumptech.glide.RequestBuilder
|
||||||
|
@ -1013,47 +1011,10 @@ fun countDown(media: Media, view: ViewGroup) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sinceWhen(media: Media, view: ViewGroup) {
|
|
||||||
if (media.status != "RELEASING" && media.status != "HIATUS") return
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
|
|
||||||
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
|
|
||||||
val timeSince = (System.currentTimeMillis() -
|
|
||||||
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
val v =
|
|
||||||
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
|
||||||
view.addView(v.root, 0)
|
|
||||||
v.mediaCountdownText.text =
|
|
||||||
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
|
|
||||||
|
|
||||||
object : CountUpTimer(86400000) {
|
|
||||||
override fun onTick(second: Int) {
|
|
||||||
val a = second + timeSince
|
|
||||||
v.mediaCountdown.text = currActivity()?.getString(
|
|
||||||
R.string.time_format,
|
|
||||||
a / 86400,
|
|
||||||
a % 86400 / 3600,
|
|
||||||
a % 86400 % 3600 / 60,
|
|
||||||
a % 86400 % 3600 % 60
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
|
||||||
// The legend will never die.
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayTimer(media: Media, view: ViewGroup) {
|
fun displayTimer(media: Media, view: ViewGroup) {
|
||||||
when {
|
when {
|
||||||
media.anime != null -> countDown(media, view)
|
media.anime != null -> countDown(media, view)
|
||||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
else -> {}
|
||||||
else -> {} // No timer yet
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,58 +116,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val action = intent.action
|
if (Intent.ACTION_VIEW == intent.action) {
|
||||||
val type = intent.type
|
handleViewIntent(intent)
|
||||||
if (Intent.ACTION_VIEW == action && type != null) {
|
|
||||||
val uri: Uri? = intent.data
|
|
||||||
try {
|
|
||||||
if (uri == null) {
|
|
||||||
throw Exception("Uri is null")
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if (name.endsWith(".sani")) {
|
|
||||||
passwordAlertDialog { 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("Incorrect password")
|
|
||||||
return@passwordAlertDialog
|
|
||||||
}
|
|
||||||
if (PreferencePackager.unpack(decryptedJson)) {
|
|
||||||
val intent = Intent(this, this.javaClass)
|
|
||||||
this.finish()
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast("Password cannot be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (name.endsWith(".ani")) {
|
|
||||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
|
||||||
if (PreferencePackager.unpack(decryptedJson)) {
|
|
||||||
val intent = Intent(this, this.javaClass)
|
|
||||||
this.finish()
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast("Invalid file type")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
toast("Error importing settings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||||
|
@ -492,6 +442,73 @@ class MainActivity : AppCompatActivity() {
|
||||||
params.updateMargins(bottom = margin.toPx)
|
params.updateMargins(bottom = margin.toPx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleViewIntent(intent: Intent) {
|
||||||
|
val uri: Uri? = intent.data
|
||||||
|
try {
|
||||||
|
if (uri == null) {
|
||||||
|
throw Exception("Uri is null")
|
||||||
|
}
|
||||||
|
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") {
|
||||||
|
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
|
||||||
|
val prefName = if (uri.scheme == "tachiyomi") {
|
||||||
|
PrefName.MangaExtensionRepos
|
||||||
|
} else {
|
||||||
|
PrefName.AnimeExtensionRepos
|
||||||
|
}
|
||||||
|
val savedRepos: Set<String> = PrefManager.getVal(prefName)
|
||||||
|
val newRepos = savedRepos.toMutableSet()
|
||||||
|
newRepos.add(url)
|
||||||
|
PrefManager.setVal(prefName, newRepos)
|
||||||
|
toast("${if (uri.scheme == "tachiyomi") "Manga" else "Anime"} Extension Repo added")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (intent.type == null) return
|
||||||
|
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
|
||||||
|
if (name.endsWith(".sani")) {
|
||||||
|
passwordAlertDialog { 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("Incorrect password")
|
||||||
|
return@passwordAlertDialog
|
||||||
|
}
|
||||||
|
if (PreferencePackager.unpack(decryptedJson)) {
|
||||||
|
val intent = Intent(this, this.javaClass)
|
||||||
|
this.finish()
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast("Password cannot be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (name.endsWith(".ani")) {
|
||||||
|
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||||
|
if (PreferencePackager.unpack(decryptedJson)) {
|
||||||
|
val intent = Intent(this, this.javaClass)
|
||||||
|
this.finish()
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast("Invalid file type")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
toast("Error importing settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
||||||
val password = CharArray(16).apply { fill('0') }
|
val password = CharArray(16).apply { fill('0') }
|
||||||
|
|
||||||
|
|
|
@ -1,133 +0,0 @@
|
||||||
package ani.dantotsu.connections.bakaupdates
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import ani.dantotsu.R
|
|
||||||
import ani.dantotsu.client
|
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
|
||||||
import ani.dantotsu.tryWithSuspend
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import okio.ByteString.Companion.encode
|
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
|
|
||||||
|
|
||||||
class MangaUpdates {
|
|
||||||
|
|
||||||
private val Int?.dateFormat get() = String.format("%02d", this)
|
|
||||||
|
|
||||||
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
|
|
||||||
|
|
||||||
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
|
|
||||||
return tryWithSuspend {
|
|
||||||
val query = JSONObject().apply {
|
|
||||||
try {
|
|
||||||
put("search", title.encode(Charset.forName("UTF-8")))
|
|
||||||
startDate?.let {
|
|
||||||
put(
|
|
||||||
"start_date",
|
|
||||||
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
put("include_metadata", true)
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val res = try {
|
|
||||||
client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.log(e.toString())
|
|
||||||
return@tryWithSuspend null
|
|
||||||
}
|
|
||||||
coroutineScope {
|
|
||||||
res.results?.map {
|
|
||||||
async(Dispatchers.IO) {
|
|
||||||
Logger.log(it.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.awaitAll()
|
|
||||||
res.results?.first {
|
|
||||||
it.metadata.series.lastUpdated?.timestamp != null
|
|
||||||
&& (it.metadata.series.latestChapter != null
|
|
||||||
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
|
|
||||||
return results.metadata.series.latestChapter?.let {
|
|
||||||
context.getString(R.string.chapter_number, it)
|
|
||||||
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
|
|
||||||
chapter.takeIf {
|
|
||||||
it.toIntOrNull() == null
|
|
||||||
} ?: context.getString(R.string.chapter_number, chapter.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MangaUpdatesResponse(
|
|
||||||
@SerialName("total_hits")
|
|
||||||
val totalHits: Int?,
|
|
||||||
@SerialName("page")
|
|
||||||
val page: Int?,
|
|
||||||
@SerialName("per_page")
|
|
||||||
val perPage: Int?,
|
|
||||||
val results: List<Results>? = null
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Results(
|
|
||||||
val record: Record,
|
|
||||||
val metadata: MetaData
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Record(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("title")
|
|
||||||
val title: String,
|
|
||||||
@SerialName("volume")
|
|
||||||
val volume: String?,
|
|
||||||
@SerialName("chapter")
|
|
||||||
val chapter: String?,
|
|
||||||
@SerialName("release_date")
|
|
||||||
val releaseDate: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MetaData(
|
|
||||||
val series: Series
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Series(
|
|
||||||
@SerialName("series_id")
|
|
||||||
val seriesId: Long?,
|
|
||||||
@SerialName("title")
|
|
||||||
val title: String?,
|
|
||||||
@SerialName("latest_chapter")
|
|
||||||
val latestChapter: Int?,
|
|
||||||
@SerialName("last_updated")
|
|
||||||
val lastUpdated: LastUpdated?
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class LastUpdated(
|
|
||||||
@SerialName("timestamp")
|
|
||||||
val timestamp: Long,
|
|
||||||
@SerialName("as_rfc3339")
|
|
||||||
val asRfc3339: String,
|
|
||||||
@SerialName("as_string")
|
|
||||||
val asString: String
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
package ani.dantotsu.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding
|
||||||
|
import ani.dantotsu.databinding.ItemRepoBinding
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import com.xwray.groupie.GroupieAdapter
|
||||||
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
|
|
||||||
|
class RepoItem(
|
||||||
|
val url: String,
|
||||||
|
val onRemove: (String) -> Unit
|
||||||
|
) :BindableItem<ItemRepoBinding>() {
|
||||||
|
override fun getLayout() = R.layout.item_repo
|
||||||
|
|
||||||
|
override fun bind(viewBinding: ItemRepoBinding, position: Int) {
|
||||||
|
viewBinding.repoNameTextView.text = url
|
||||||
|
viewBinding.repoDeleteImageView.setOnClickListener {
|
||||||
|
onRemove(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initializeViewBinding(view: View): ItemRepoBinding {
|
||||||
|
return ItemRepoBinding.bind(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
private var _binding: BottomSheetAddRepositoryBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private var mediaType: MediaType = MediaType.ANIME
|
||||||
|
private var onRepositoryAdded: ((String, MediaType) -> Unit)? = null
|
||||||
|
private var repositories: MutableList<String> = mutableListOf()
|
||||||
|
private var onRepositoryRemoved: ((String) -> Unit)? = null
|
||||||
|
private var adapter: GroupieAdapter = GroupieAdapter()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = BottomSheetAddRepositoryBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.repositoriesRecyclerView.adapter = adapter
|
||||||
|
binding.repositoriesRecyclerView.layoutManager = LinearLayoutManager(
|
||||||
|
context,
|
||||||
|
LinearLayoutManager.VERTICAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
adapter.addAll(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
|
||||||
|
|
||||||
|
binding.repositoryInput.hint = when(mediaType) {
|
||||||
|
MediaType.ANIME -> getString(R.string.anime_add_repository)
|
||||||
|
MediaType.MANGA -> getString(R.string.manga_add_repository)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.addButton.setOnClickListener {
|
||||||
|
val input = binding.repositoryInput.text.toString()
|
||||||
|
val error = isValidUrl(input)
|
||||||
|
if (error == null) {
|
||||||
|
onRepositoryAdded?.invoke(input, mediaType)
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
binding.repositoryInput.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cancelButton.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.repositoryInput.setOnEditorActionListener { textView, action, keyEvent ->
|
||||||
|
if (action == EditorInfo.IME_ACTION_DONE ||
|
||||||
|
(keyEvent?.action == KeyEvent.ACTION_UP && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||||
|
if (!textView.text.isNullOrBlank()) {
|
||||||
|
val error = isValidUrl(textView.text.toString())
|
||||||
|
if (error == null) {
|
||||||
|
onRepositoryAdded?.invoke(textView.text.toString(), mediaType)
|
||||||
|
dismiss()
|
||||||
|
return@setOnEditorActionListener true
|
||||||
|
} else {
|
||||||
|
binding.repositoryInput.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRepositoryRemoved(url: String) {
|
||||||
|
onRepositoryRemoved?.invoke(url)
|
||||||
|
repositories.remove(url)
|
||||||
|
adapter.update(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidUrl(url: String): String? {
|
||||||
|
if (!url.startsWith("https://") && !url.startsWith("http://"))
|
||||||
|
return "URL must start with http:// or https://"
|
||||||
|
if (!url.removeSuffix("/").endsWith("index.min.json"))
|
||||||
|
return "URL must end with index.min.json"
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(
|
||||||
|
mediaType: MediaType,
|
||||||
|
repositories: List<String>,
|
||||||
|
onRepositoryAdded: (String, MediaType) -> Unit,
|
||||||
|
onRepositoryRemoved: (String) -> Unit
|
||||||
|
): AddRepositoryBottomSheet {
|
||||||
|
return AddRepositoryBottomSheet().apply {
|
||||||
|
this.mediaType = mediaType
|
||||||
|
this.repositories.addAll(repositories)
|
||||||
|
this.onRepositoryAdded = onRepositoryAdded
|
||||||
|
this.onRepositoryRemoved = onRepositoryRemoved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ import ani.dantotsu.restartApp
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
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.others.CustomBottomDialog
|
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.util.customAlertDialog
|
import ani.dantotsu.util.customAlertDialog
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
|
@ -117,14 +116,13 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) {
|
fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) {
|
||||||
val entry =
|
val validLink = if (input.contains("github.com") && input.contains("blob")) {
|
||||||
if (input.endsWith("/") || input.endsWith("index.min.json")) input.substring(
|
input.replace("github.com", "raw.githubusercontent.com")
|
||||||
0,
|
.replace("/blob/", "/")
|
||||||
input.lastIndexOf("/")
|
} else input
|
||||||
) else input
|
|
||||||
if (mediaType == MediaType.ANIME) {
|
if (mediaType == MediaType.ANIME) {
|
||||||
val anime =
|
val anime =
|
||||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
|
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(validLink)
|
||||||
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
|
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
animeExtensionManager.findAvailableExtensions()
|
animeExtensionManager.findAvailableExtensions()
|
||||||
|
@ -133,7 +131,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
if (mediaType == MediaType.MANGA) {
|
if (mediaType == MediaType.MANGA) {
|
||||||
val manga =
|
val manga =
|
||||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
|
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(validLink)
|
||||||
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
|
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
mangaExtensionManager.findAvailableExtensions()
|
mangaExtensionManager.findAvailableExtensions()
|
||||||
|
@ -142,25 +140,6 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processEditorAction(
|
|
||||||
dialog: AlertDialog,
|
|
||||||
editText: EditText,
|
|
||||||
mediaType: MediaType,
|
|
||||||
view: ViewGroup
|
|
||||||
) {
|
|
||||||
editText.setOnEditorActionListener { textView, action, keyEvent ->
|
|
||||||
if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE || (keyEvent?.action == KeyEvent.ACTION_UP && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
|
||||||
return@setOnEditorActionListener if (textView.text.isNullOrBlank()) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
processUserInput(textView.text.toString(), mediaType, view)
|
|
||||||
dialog.dismiss()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsRecyclerView.adapter = SettingsAdapter(
|
settingsRecyclerView.adapter = SettingsAdapter(
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Settings(
|
Settings(
|
||||||
|
@ -169,31 +148,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||||
desc = getString(R.string.anime_add_repository_desc),
|
desc = getString(R.string.anime_add_repository_desc),
|
||||||
icon = R.drawable.ic_github,
|
icon = R.drawable.ic_github,
|
||||||
onClick = {
|
onClick = {
|
||||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
val animeRepos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
|
||||||
val editText = dialogView.userAgentTextBox.apply {
|
AddRepositoryBottomSheet.newInstance(
|
||||||
hint = getString(R.string.anime_add_repository)
|
|
||||||
}
|
|
||||||
context.customAlertDialog().apply {
|
|
||||||
setTitle(R.string.anime_add_repository)
|
|
||||||
setCustomView(dialogView.root)
|
|
||||||
setPosButton(getString(R.string.ok)) {
|
|
||||||
if (!editText.text.isNullOrBlank()) processUserInput(
|
|
||||||
editText.text.toString(),
|
|
||||||
MediaType.ANIME,
|
MediaType.ANIME,
|
||||||
it.attachView
|
animeRepos.toList(),
|
||||||
)
|
onRepositoryAdded = { input, mediaType ->
|
||||||
}
|
processUserInput(input, mediaType, it.attachView)
|
||||||
setNegButton(getString(R.string.cancel))
|
},
|
||||||
attach { dialog ->
|
onRepositoryRemoved = { item ->
|
||||||
processEditorAction(
|
val repos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
|
||||||
dialog,
|
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos)
|
||||||
editText,
|
setExtensionOutput(it.attachView, MediaType.ANIME)
|
||||||
MediaType.ANIME,
|
|
||||||
it.attachView
|
|
||||||
)
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
|
).show(supportFragmentManager, "add_repo")
|
||||||
},
|
},
|
||||||
attach = {
|
attach = {
|
||||||
setExtensionOutput(it.attachView, MediaType.ANIME)
|
setExtensionOutput(it.attachView, MediaType.ANIME)
|
||||||
|
@ -205,31 +172,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||||
desc = getString(R.string.manga_add_repository_desc),
|
desc = getString(R.string.manga_add_repository_desc),
|
||||||
icon = R.drawable.ic_github,
|
icon = R.drawable.ic_github,
|
||||||
onClick = {
|
onClick = {
|
||||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
val mangaRepos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
|
||||||
val editText = dialogView.userAgentTextBox.apply {
|
AddRepositoryBottomSheet.newInstance(
|
||||||
hint = getString(R.string.manga_add_repository)
|
|
||||||
}
|
|
||||||
context.customAlertDialog().apply {
|
|
||||||
setTitle(R.string.manga_add_repository)
|
|
||||||
setCustomView(dialogView.root)
|
|
||||||
setPosButton(R.string.ok) {
|
|
||||||
if (!editText.text.isNullOrBlank()) processUserInput(
|
|
||||||
editText.text.toString(),
|
|
||||||
MediaType.MANGA,
|
MediaType.MANGA,
|
||||||
it.attachView
|
mangaRepos.toList(),
|
||||||
)
|
onRepositoryAdded = { input, mediaType ->
|
||||||
|
processUserInput(input, mediaType, it.attachView)
|
||||||
|
},
|
||||||
|
onRepositoryRemoved = { item ->
|
||||||
|
val repos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).minus(item)
|
||||||
|
PrefManager.setVal(PrefName.MangaExtensionRepos, repos)
|
||||||
|
setExtensionOutput(it.attachView, MediaType.MANGA)
|
||||||
}
|
}
|
||||||
setNegButton(R.string.cancel)
|
).show(supportFragmentManager, "add_repo")
|
||||||
attach { dialog ->
|
|
||||||
processEditorAction(
|
|
||||||
dialog,
|
|
||||||
editText,
|
|
||||||
MediaType.MANGA,
|
|
||||||
it.attachView
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
},
|
},
|
||||||
attach = {
|
attach = {
|
||||||
setExtensionOutput(it.attachView, MediaType.MANGA)
|
setExtensionOutput(it.attachView, MediaType.MANGA)
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package ani.dantotsu.util
|
|
||||||
|
|
||||||
import android.os.CountDownTimer
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/40422151/461982
|
|
||||||
abstract class CountUpTimer protected constructor(
|
|
||||||
private val duration: Long
|
|
||||||
) : CountDownTimer(duration, INTERVAL_MS) {
|
|
||||||
abstract fun onTick(second: Int)
|
|
||||||
override fun onTick(msUntilFinished: Long) {
|
|
||||||
val second = ((duration - msUntilFinished) / 1000).toInt()
|
|
||||||
onTick(second)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
|
||||||
onTick(duration / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val INTERVAL_MS: Long = 1000
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -89,13 +89,6 @@ internal class ExtensionGithubApi {
|
||||||
.toAnimeExtensions(it)
|
.toAnimeExtensions(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check - a small number of extensions probably means something broke
|
|
||||||
// with the repo generator
|
|
||||||
//if (repoExtensions.size < 10) {
|
|
||||||
// throw Exception()
|
|
||||||
//}
|
|
||||||
// No official repo now so this won't be needed anymore. User-made repo can have less than 10 extensions
|
|
||||||
|
|
||||||
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")
|
||||||
|
@ -156,13 +149,18 @@ internal class ExtensionGithubApi {
|
||||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
||||||
|
|
||||||
repos.forEach {
|
repos.forEach {
|
||||||
|
val repoUrl = if (it.contains("index.min.json")) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
"$it${if (it.endsWith('/')) "" else "/"}index.min.json"
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val githubResponse = try {
|
val githubResponse = try {
|
||||||
networkService.client
|
networkService.client
|
||||||
.newCall(GET("${it}/index.min.json"))
|
.newCall(GET(repoUrl))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.log("Failed to get repo: $it")
|
Logger.log("Failed to get repo: $repoUrl")
|
||||||
Logger.log(e)
|
Logger.log(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -179,13 +177,6 @@ internal class ExtensionGithubApi {
|
||||||
.toMangaExtensions(it)
|
.toMangaExtensions(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check - a small number of extensions probably means something broke
|
|
||||||
// with the repo generator
|
|
||||||
//if (repoExtensions.size < 10) {
|
|
||||||
// throw Exception()
|
|
||||||
//}
|
|
||||||
// No official repo now so this won't be needed anymore. User made repo can have less than 10 extensions.
|
|
||||||
|
|
||||||
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")
|
||||||
|
@ -203,8 +194,11 @@ internal class ExtensionGithubApi {
|
||||||
|
|
||||||
private fun fallbackRepoUrl(repoUrl: String): String? {
|
private fun fallbackRepoUrl(repoUrl: String): String? {
|
||||||
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
|
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
|
||||||
val strippedRepoUrl =
|
val strippedRepoUrl = repoUrl
|
||||||
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
.removePrefix("https://")
|
||||||
|
.removePrefix("http://")
|
||||||
|
.removeSuffix("/")
|
||||||
|
.removeSuffix("/index.min.json")
|
||||||
val repoUrlParts = strippedRepoUrl.split("/")
|
val repoUrlParts = strippedRepoUrl.split("/")
|
||||||
if (repoUrlParts.size < 3) {
|
if (repoUrlParts.size < 3) {
|
||||||
return null
|
return null
|
||||||
|
|
79
app/src/main/res/layout/bottom_sheet_add_repository.xml
Normal file
79
app/src/main/res/layout/bottom_sheet_add_repository.xml
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/addRepositoryText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/add_repository"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/addRepositoryDescription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/add_repository_desc"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/repositoryInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/cancelButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/addButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ok"
|
||||||
|
style="@style/Widget.MaterialComponents.Button" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/currentRepositoriesText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/current_repositories"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/repositoriesRecyclerView"
|
||||||
|
tools:listitem="@layout/item_repo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
35
app/src/main/res/layout/item_repo.xml
Normal file
35
app/src/main/res/layout/item_repo.xml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/extensionCardView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/repoNameTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:fontFamily="@font/poppins_semi_bold"
|
||||||
|
android:text="@string/placeholder"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/repoDeleteImageView"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="3dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
app:srcCompat="@drawable/ic_delete"
|
||||||
|
app:tint="?attr/colorOnBackground"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -1088,5 +1088,8 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
|
||||||
<string name="textview_sub">Textview Subtitles (Experimental)</string>
|
<string name="textview_sub">Textview Subtitles (Experimental)</string>
|
||||||
<string name="textview_sub_stroke">Subtitle Stroke</string>
|
<string name="textview_sub_stroke">Subtitle Stroke</string>
|
||||||
<string name="textview_sub_bottom_margin">Bottom Margin</string>
|
<string name="textview_sub_bottom_margin">Bottom Margin</string>
|
||||||
|
<string name="add_repository">Add Repository</string>
|
||||||
|
<string name="add_repository_desc">A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json</string>
|
||||||
|
<string name="current_repositories">Current Repositories</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.7.0'
|
classpath 'com.android.tools.build:gradle:8.7.3'
|
||||||
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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue