From 43dee6ee497f984a4ab5336de4c80173de3aa48b Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:25:22 -0600 Subject: [PATCH] feat: make repo adding easier --- .gitignore | 3 + app/src/main/AndroidManifest.xml | 17 ++- app/src/main/java/ani/dantotsu/Functions.kt | 41 +----- .../main/java/ani/dantotsu/MainActivity.kt | 121 ++++++++------- .../connections/bakaupdates/MangaUpdates.kt | 133 ----------------- .../settings/AddRepositoryBottomSheet.kt | 138 ++++++++++++++++++ .../settings/SettingsExtensionsActivity.kt | 105 ++++--------- .../java/ani/dantotsu/util/CountUpTimer.kt | 22 --- .../extension/api/ExtensionGithubApi.kt | 30 ++-- .../layout/bottom_sheet_add_repository.xml | 79 ++++++++++ app/src/main/res/layout/item_repo.xml | 35 +++++ app/src/main/res/values/strings.xml | 3 + build.gradle | 2 +- 13 files changed, 383 insertions(+), 346 deletions(-) delete mode 100644 app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt create mode 100644 app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt delete mode 100644 app/src/main/java/ani/dantotsu/util/CountUpTimer.kt create mode 100644 app/src/main/res/layout/bottom_sheet_add_repository.xml create mode 100644 app/src/main/res/layout/item_repo.xml diff --git a/.gitignore b/.gitignore index 9900c88788..0fc4c86f69 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .gradle/ build/ +#kotlin +.kotlin/ + # Local configuration file (sdk path, etc) local.properties diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d8d4d6a35..644d218a8b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -116,7 +116,8 @@ - + + @@ -374,25 +375,31 @@ android:exported="true"> - - - - + + + + + + + + + + countDown(media, view) - media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view) - else -> {} // No timer yet + else -> {} } } diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index 2e87a7a5df..1020aeb324 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -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(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 = 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') } diff --git a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt deleted file mode 100644 index 53a88815ac..0000000000 --- a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt +++ /dev/null @@ -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() - } 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? = 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 - ) - } - } - } - } -} diff --git a/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt b/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt new file mode 100644 index 0000000000..dc1cf5d1a6 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt @@ -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() { + 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 = 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, + onRepositoryAdded: (String, MediaType) -> Unit, + onRepositoryRemoved: (String) -> Unit + ): AddRepositoryBottomSheet { + return AddRepositoryBottomSheet().apply { + this.mediaType = mediaType + this.repositories.addAll(repositories) + this.onRepositoryAdded = onRepositoryAdded + this.onRepositoryRemoved = onRepositoryRemoved + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt index 4388231a21..59add57808 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt @@ -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>(PrefName.AnimeExtensionRepos).plus(entry) + PrefManager.getVal>(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>(PrefName.MangaExtensionRepos).plus(entry) + PrefManager.getVal>(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>(PrefName.AnimeExtensionRepos) + AddRepositoryBottomSheet.newInstance( + MediaType.ANIME, + animeRepos.toList(), + onRepositoryAdded = { input, mediaType -> + processUserInput(input, mediaType, it.attachView) + }, + onRepositoryRemoved = { item -> + val repos = PrefManager.getVal>(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 - ) - } - setNegButton(R.string.cancel) - attach { dialog -> - processEditorAction( - dialog, - editText, - MediaType.MANGA, - it.attachView - ) + val mangaRepos = PrefManager.getVal>(PrefName.MangaExtensionRepos) + AddRepositoryBottomSheet.newInstance( + MediaType.MANGA, + mangaRepos.toList(), + onRepositoryAdded = { input, mediaType -> + processUserInput(input, mediaType, it.attachView) + }, + onRepositoryRemoved = { item -> + val repos = PrefManager.getVal>(PrefName.MangaExtensionRepos).minus(item) + PrefManager.setVal(PrefName.MangaExtensionRepos, repos) + setExtensionOutput(it.attachView, MediaType.MANGA) } - }.show() - + ).show(supportFragmentManager, "add_repo") }, attach = { setExtensionOutput(it.attachView, MediaType.MANGA) diff --git a/app/src/main/java/ani/dantotsu/util/CountUpTimer.kt b/app/src/main/java/ani/dantotsu/util/CountUpTimer.kt deleted file mode 100644 index 725781ff95..0000000000 --- a/app/src/main/java/ani/dantotsu/util/CountUpTimer.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index d42ece546d..8e230f4de2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -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>(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 diff --git a/app/src/main/res/layout/bottom_sheet_add_repository.xml b/app/src/main/res/layout/bottom_sheet_add_repository.xml new file mode 100644 index 0000000000..2ec072bcdb --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_add_repository.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + +