feat: make repo adding easier

This commit is contained in:
rebel onion 2024-12-30 19:25:22 -06:00
parent 6f1bb10dec
commit 43dee6ee49
13 changed files with 383 additions and 346 deletions

3
.gitignore vendored
View file

@ -2,6 +2,9 @@
.gradle/
build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc)
local.properties

View file

@ -116,7 +116,8 @@
<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/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" />
@ -374,25 +375,31 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
<data android:host="*" />
</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
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"

View file

@ -91,7 +91,6 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
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.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
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) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
else -> {}
}
}

View file

@ -116,58 +116,8 @@ class MainActivity : AppCompatActivity() {
}
}
val action = intent.action
val type = intent.type
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")
}
if (Intent.ACTION_VIEW == intent.action) {
handleViewIntent(intent)
}
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
@ -492,6 +442,73 @@ class MainActivity : AppCompatActivity() {
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) {
val password = CharArray(16).apply { fill('0') }

View file

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

View file

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

View file

@ -27,7 +27,6 @@ import ani.dantotsu.restartApp
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.customAlertDialog
import eu.kanade.domain.base.BasePreferences
@ -117,14 +116,13 @@ class SettingsExtensionsActivity : AppCompatActivity() {
}
fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) {
val entry =
if (input.endsWith("/") || input.endsWith("index.min.json")) input.substring(
0,
input.lastIndexOf("/")
) else input
val validLink = if (input.contains("github.com") && input.contains("blob")) {
input.replace("github.com", "raw.githubusercontent.com")
.replace("/blob/", "/")
} else input
if (mediaType == MediaType.ANIME) {
val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
@ -133,7 +131,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
}
if (mediaType == MediaType.MANGA) {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
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(
arrayListOf(
Settings(
@ -169,31 +148,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
desc = getString(R.string.anime_add_repository_desc),
icon = R.drawable.ic_github,
onClick = {
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
val editText = dialogView.userAgentTextBox.apply {
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,
it.attachView
)
val animeRepos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
AddRepositoryBottomSheet.newInstance(
MediaType.ANIME,
animeRepos.toList(),
onRepositoryAdded = { input, mediaType ->
processUserInput(input, mediaType, it.attachView)
},
onRepositoryRemoved = { item ->
val repos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos)
setExtensionOutput(it.attachView, MediaType.ANIME)
}
setNegButton(getString(R.string.cancel))
attach { dialog ->
processEditorAction(
dialog,
editText,
MediaType.ANIME,
it.attachView
)
}
show()
}
).show(supportFragmentManager, "add_repo")
},
attach = {
setExtensionOutput(it.attachView, MediaType.ANIME)
@ -205,31 +172,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
desc = getString(R.string.manga_add_repository_desc),
icon = R.drawable.ic_github,
onClick = {
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
val editText = dialogView.userAgentTextBox.apply {
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,
it.attachView
)
val mangaRepos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
AddRepositoryBottomSheet.newInstance(
MediaType.MANGA,
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)
attach { dialog ->
processEditorAction(
dialog,
editText,
MediaType.MANGA,
it.attachView
)
}
}.show()
).show(supportFragmentManager, "add_repo")
},
attach = {
setExtensionOutput(it.attachView, MediaType.MANGA)

View file

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

View file

@ -89,13 +89,6 @@ internal class ExtensionGithubApi {
.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)
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
@ -156,13 +149,18 @@ internal class ExtensionGithubApi {
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {
"$it${if (it.endsWith('/')) "" else "/"}index.min.json"
}
try {
val githubResponse = try {
networkService.client
.newCall(GET("${it}/index.min.json"))
.newCall(GET(repoUrl))
.awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $it")
Logger.log("Failed to get repo: $repoUrl")
Logger.log(e)
null
}
@ -179,13 +177,6 @@ internal class ExtensionGithubApi {
.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)
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
@ -203,8 +194,11 @@ internal class ExtensionGithubApi {
private fun fallbackRepoUrl(repoUrl: String): String? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl =
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
val strippedRepoUrl = repoUrl
.removePrefix("https://")
.removePrefix("http://")
.removeSuffix("/")
.removeSuffix("/index.min.json")
val repoUrlParts = strippedRepoUrl.split("/")
if (repoUrlParts.size < 3) {
return null

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

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

View file

@ -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_stroke">Subtitle Stroke</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>

View file

@ -12,7 +12,7 @@ buildscript {
}
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-serialization:$kotlin_version"
classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version"