feat: allow partial urls

This commit is contained in:
rebel onion 2025-01-02 03:14:59 -06:00
parent 116de6324e
commit 38d68a7976
13 changed files with 360 additions and 463 deletions

View file

@ -29,7 +29,6 @@ import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.time.delay
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray

View file

@ -394,11 +394,10 @@
<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" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<!-- Support both schemes --> <data android:host="add-repo"/>
<data android:scheme="tachiyomi"/> <data android:scheme="tachiyomi"/>
<data android:host="add-repo"/>
<data android:scheme="aniyomi"/> <data android:scheme="aniyomi"/>
<data android:host="add-repo"/> <data android:scheme="novelyomi"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity

View file

@ -449,19 +449,20 @@ class MainActivity : AppCompatActivity() {
if (uri == null) { if (uri == null) {
throw Exception("Uri is null") throw Exception("Uri is null")
} }
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") { if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi" || uri.scheme == "novelyomi") && uri.host == "add-repo") {
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import") val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
val prefName = if (uri.scheme == "tachiyomi") { val (prefName, name) = when (uri.scheme) {
PrefName.MangaExtensionRepos "tachiyomi" -> PrefName.MangaExtensionRepos to "Manga"
} else { "aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
PrefName.AnimeExtensionRepos "novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
else -> throw Exception("Invalid scheme")
} }
val savedRepos: Set<String> = PrefManager.getVal(prefName) val savedRepos: Set<String> = PrefManager.getVal(prefName)
val newRepos = savedRepos.toMutableSet() val newRepos = savedRepos.toMutableSet()
AddRepositoryBottomSheet.addRepoWarning(this) { AddRepositoryBottomSheet.addRepoWarning(this) {
newRepos.add(url) newRepos.add(url)
PrefManager.setVal(prefName, newRepos) PrefManager.setVal(prefName, newRepos)
toast("${if (uri.scheme == "tachiyomi") "Manga" else "Anime"} Extension Repo added") toast("$name Extension Repo added")
} }
return return
} }
@ -488,9 +489,9 @@ class MainActivity : AppCompatActivity() {
return@passwordAlertDialog return@passwordAlertDialog
} }
if (PreferencePackager.unpack(decryptedJson)) { if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass) val newIntent = Intent(this, this.javaClass)
this.finish() this.finish()
startActivity(intent) startActivity(newIntent)
} }
} else { } else {
toast("Password cannot be empty") toast("Password cannot be empty")
@ -499,9 +500,9 @@ class MainActivity : AppCompatActivity() {
} else if (name.endsWith(".ani")) { } else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8) val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) { if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass) val newIntent = Intent(this, this.javaClass)
this.finish() this.finish()
startActivity(intent) startActivity(newIntent)
} }
} else { } else {
toast("Invalid file type") toast("Invalid file type")

View file

@ -26,6 +26,7 @@ sealed class NovelExtension {
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Long, override val versionCode: Long,
var repository: String,
val sources: List<AvailableNovelSources>, val sources: List<AvailableNovelSources>,
val iconUrl: String, val iconUrl: String,
) : NovelExtension() ) : NovelExtension()

View file

@ -1,186 +0,0 @@
package ani.dantotsu.parsers.novel
import android.content.Context
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.util.Date
import kotlin.time.Duration.Companion.days
class NovelExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val novelExtensionManager: NovelExtensionManager by injectLazy()
private val json: Json by injectLazy()
private val lastExtCheck: Long = PrefManager.getVal(PrefName.NovelLastExtCheck)
private var requiresFallbackSource = false
suspend fun findExtensions(): List<NovelExtension.Available> {
return withIOContext {
val githubResponse = if (requiresFallbackSource) {
null
} else {
try {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
requiresFallbackSource = true
null
}
}
val response = githubResponse ?: run {
Logger.log("using fallback source")
networkService.client
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
}
Logger.log("response: $response")
val extensions = with(json) {
response
.parseAs<List<NovelExtensionJsonObject>>()
.toExtensions()
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
/*if (extensions.size < 10) { //TODO: uncomment when more extensions are added
throw Exception()
}*/
Logger.log("extensions: $extensions")
extensions
}
}
suspend fun checkForUpdates(
context: Context,
fromAvailableExtensionList: Boolean = false
): List<AnimeExtension.Installed>? {
// Limit checks to once a day at most
if (fromAvailableExtensionList && Date().time < lastExtCheck + 1.days.inWholeMilliseconds) {
return null
}
val extensions = if (fromAvailableExtensionList) {
novelExtensionManager.availableExtensionsFlow.value
} else {
findExtensions().also {
PrefManager.setVal(PrefName.NovelLastExtCheck, Date().time)
}
}
val installedExtensions = ExtensionLoader.loadNovelExtensions(context)
.filterIsInstance<AnimeLoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<AnimeExtension.Installed>()
for (installedExt in installedExtensions) {
val pkgName = installedExt.pkgName
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer)
if (hasUpdate) {
extensionsWithUpdate.add(installedExt)
}
}
if (extensionsWithUpdate.isNotEmpty()) {
ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
}
return extensionsWithUpdate
}
private fun List<NovelExtensionJsonObject>.toExtensions(): List<NovelExtension.Available> {
return mapNotNull { extension ->
val sources = extension.sources?.map { source ->
NovelExtensionSourceJsonObject(
source.id,
source.lang,
source.name,
source.baseUrl,
)
}
val iconUrl = "${REPO_URL_PREFIX}icon/${extension.pkg}.png"
NovelExtension.Available(
extension.name,
extension.pkg,
extension.apk,
extension.code,
sources?.toSources() ?: emptyList(),
iconUrl,
)
}
}
private fun List<NovelExtensionSourceJsonObject>.toSources(): List<AvailableNovelSources> {
return map { source ->
AvailableNovelSources(
source.id,
source.lang,
source.name,
source.baseUrl,
)
}
}
fun getApkUrl(extension: NovelExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.pkgName}.apk"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
}
}
private const val REPO_URL_PREFIX =
"https://raw.githubusercontent.com/dannovels/novel-extensions/main/"
private const val FALLBACK_REPO_URL_PREFIX =
"https://gcore.jsdelivr.net/gh/dannovels/novel-extensions@latest/"
@Serializable
private data class NovelExtensionJsonObject(
val name: String,
val pkg: String,
val apk: String,
val lang: String,
val code: Long,
val version: String,
val nsfw: Int,
val hasReadme: Int = 0,
val hasChangelog: Int = 0,
val sources: List<NovelExtensionSourceJsonObject>?,
)
@Serializable
private data class NovelExtensionSourceJsonObject(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
)

View file

@ -6,6 +6,7 @@ import ani.dantotsu.media.MediaType
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
@ -22,7 +23,7 @@ class NovelExtensionManager(private val context: Context) {
/** /**
* API where all the available Novel extensions can be found. * API where all the available Novel extensions can be found.
*/ */
private val api = NovelExtensionGithubApi() private val api = ExtensionGithubApi()
/** /**
* The installer which installs, updates and uninstalls the Novel extensions. * The installer which installs, updates and uninstalls the Novel extensions.
@ -70,7 +71,7 @@ class NovelExtensionManager(private val context: Context) {
*/ */
suspend fun findAvailableExtensions() { suspend fun findAvailableExtensions() {
val extensions: List<NovelExtension.Available> = try { val extensions: List<NovelExtension.Available> = try {
api.findExtensions() api.findNovelExtensions()
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Error finding extensions: ${e.message}") Logger.log("Error finding extensions: ${e.message}")
withUIContext { snackString("Failed to get Novel extensions list") } withUIContext { snackString("Failed to get Novel extensions list") }
@ -119,7 +120,7 @@ class NovelExtensionManager(private val context: Context) {
* @param extension The anime extension to be installed. * @param extension The anime extension to be installed.
*/ */
fun installExtension(extension: NovelExtension.Available): Observable<InstallStep> { fun installExtension(extension: NovelExtension.Available): Observable<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension.pkgName, return installer.downloadAndInstall(api.getNovelApkUrl(extension), extension.pkgName,
extension.name, MediaType.NOVEL) extension.name, MediaType.NOVEL)
} }

View file

@ -2,6 +2,7 @@ package ani.dantotsu.settings
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -10,29 +11,52 @@ import android.view.inputmethod.EditorInfo
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding
import ani.dantotsu.databinding.ItemRepoBinding import ani.dantotsu.databinding.ItemRepoBinding
import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.customAlertDialog import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class RepoItem( class RepoItem(
val url: String, val url: String,
val onRemove: (String) -> Unit private val mediaType: MediaType,
val onRemove: (String, MediaType) -> Unit
) :BindableItem<ItemRepoBinding>() { ) :BindableItem<ItemRepoBinding>() {
override fun getLayout() = R.layout.item_repo override fun getLayout() = R.layout.item_repo
override fun bind(viewBinding: ItemRepoBinding, position: Int) { override fun bind(viewBinding: ItemRepoBinding, position: Int) {
viewBinding.repoNameTextView.text = url viewBinding.repoNameTextView.text = url.cleanShownUrl()
viewBinding.repoDeleteImageView.setOnClickListener { viewBinding.repoDeleteImageView.setOnClickListener {
onRemove(url) onRemove(url, mediaType)
}
viewBinding.repoCopyImageView.setOnClickListener {
viewBinding.repoCopyImageView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
copyToClipboard(url, true)
} }
} }
override fun initializeViewBinding(view: View): ItemRepoBinding { override fun initializeViewBinding(view: View): ItemRepoBinding {
return ItemRepoBinding.bind(view) return ItemRepoBinding.bind(view)
} }
private fun String.cleanShownUrl(): String {
return this
.removePrefix("https://raw.githubusercontent.com/")
.replace("index.min.json", "")
.removeSuffix("/")
}
} }
class AddRepositoryBottomSheet : BottomSheetDialogFragment() { class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
@ -41,7 +65,7 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
private var mediaType: MediaType = MediaType.ANIME private var mediaType: MediaType = MediaType.ANIME
private var onRepositoryAdded: ((String, MediaType) -> Unit)? = null private var onRepositoryAdded: ((String, MediaType) -> Unit)? = null
private var repositories: MutableList<String> = mutableListOf() private var repositories: MutableList<String> = mutableListOf()
private var onRepositoryRemoved: ((String) -> Unit)? = null private var onRepositoryRemoved: ((String, MediaType) -> Unit)? = null
private var adapter: GroupieAdapter = GroupieAdapter() private var adapter: GroupieAdapter = GroupieAdapter()
override fun onCreateView( override fun onCreateView(
@ -62,24 +86,19 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
LinearLayoutManager.VERTICAL, LinearLayoutManager.VERTICAL,
false false
) )
adapter.addAll(repositories.map { RepoItem(it, ::onRepositoryRemoved) }) adapter.addAll(repositories.map { RepoItem(it, mediaType, ::onRepositoryRemoved) })
binding.repositoryInput.hint = when(mediaType) { binding.repositoryInput.hint = when(mediaType) {
MediaType.ANIME -> getString(R.string.anime_add_repository) MediaType.ANIME -> getString(R.string.anime_add_repository)
MediaType.MANGA -> getString(R.string.manga_add_repository) MediaType.MANGA -> getString(R.string.manga_add_repository)
else -> "" MediaType.NOVEL -> getString(R.string.novel_add_repository)
} }
binding.addButton.setOnClickListener { binding.addButton.setOnClickListener {
val input = binding.repositoryInput.text.toString() val input = binding.repositoryInput.text.toString()
val error = isValidUrl(input) val error = isValidUrl(input)
if (error == null) { if (error == null) {
context?.let { context -> acceptUrl(input)
addRepoWarning(context) {
onRepositoryAdded?.invoke(input, mediaType)
dismiss()
}
}
} else { } else {
binding.repositoryInput.error = error binding.repositoryInput.error = error
} }
@ -96,12 +115,7 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
if (url.isNotBlank()) { if (url.isNotBlank()) {
val error = isValidUrl(url) val error = isValidUrl(url)
if (error == null) { if (error == null) {
context?.let { context -> acceptUrl(url)
addRepoWarning(context) {
onRepositoryAdded?.invoke(url, mediaType)
dismiss()
}
}
return@setOnEditorActionListener true return@setOnEditorActionListener true
} else { } else {
binding.repositoryInput.error = error binding.repositoryInput.error = error
@ -112,20 +126,62 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
} }
} }
private fun onRepositoryRemoved(url: String) { private fun acceptUrl(url: String) {
onRepositoryRemoved?.invoke(url) val finalUrl = getRepoUrl(url)
repositories.remove(url) context?.let { context ->
adapter.update(repositories.map { RepoItem(it, ::onRepositoryRemoved) }) addRepoWarning(context) {
onRepositoryAdded?.invoke(finalUrl, mediaType)
dismiss()
}
}
} }
private fun isValidUrl(url: String): String? { private fun isValidUrl(input: String): String? {
if (!url.startsWith("https://") && !url.startsWith("http://")) if (input.startsWith("http://") || input.startsWith("https://")) {
return "URL must start with http:// or https://" if (!input.removeSuffix("/").endsWith("index.min.json")) {
if (!url.removeSuffix("/").endsWith("index.min.json"))
return "URL must end with index.min.json" return "URL must end with index.min.json"
}
return null return null
} }
val parts = input.split("/")
if (parts.size !in 2..3) {
return "Must be a full URL or in format: username/repo[/branch]"
}
val username = parts[0]
val repo = parts[1]
val branch = if (parts.size == 3) parts[2] else "repo"
if (username.isBlank() || repo.isBlank()) {
return "Username and repository name cannot be empty"
}
if (parts.size == 3 && branch.isBlank()) {
return "Branch name cannot be empty"
}
return null
}
private fun getRepoUrl(input: String): String {
if (input.startsWith("http://") || input.startsWith("https://")) {
return input
}
val parts = input.split("/")
val username = parts[0]
val repo = parts[1]
val branch = if (parts.size == 3) parts[2] else "repo"
return "https://raw.githubusercontent.com/$username/$repo/$branch/index.min.json"
}
private fun onRepositoryRemoved(url: String, mediaType: MediaType) {
onRepositoryRemoved?.invoke(url, mediaType)
repositories.remove(url)
adapter.update(repositories.map { RepoItem(it, mediaType, ::onRepositoryRemoved) })
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
@ -142,11 +198,81 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
.setNegButton(R.string.cancel) { } .setNegButton(R.string.cancel) { }
.show() .show()
} }
fun addRepo(input: String, mediaType: MediaType) {
val validLink = if (input.contains("github.com") && input.contains("blob")) {
input.replace("github.com", "raw.githubusercontent.com")
.replace("/blob/", "/")
} else input
when (mediaType) {
MediaType.ANIME -> {
val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
.plus(validLink)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<AnimeExtensionManager>().findAvailableExtensions()
}
}
MediaType.MANGA -> {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
.plus(validLink)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<MangaExtensionManager>().findAvailableExtensions()
}
}
MediaType.NOVEL -> {
val novel =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos)
.plus(validLink)
PrefManager.setVal(PrefName.NovelExtensionRepos, novel)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<NovelExtensionManager>().findAvailableExtensions()
}
}
}
}
fun removeRepo(input: String, mediaType: MediaType) {
when (mediaType) {
MediaType.ANIME -> {
val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
.minus(input)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<AnimeExtensionManager>().findAvailableExtensions()
}
}
MediaType.MANGA -> {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
.minus(input)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<MangaExtensionManager>().findAvailableExtensions()
}
}
MediaType.NOVEL -> {
val novel =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos)
.minus(input)
PrefManager.setVal(PrefName.NovelExtensionRepos, novel)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<NovelExtensionManager>().findAvailableExtensions()
}
}
}
}
fun newInstance( fun newInstance(
mediaType: MediaType, mediaType: MediaType,
repositories: List<String>, repositories: List<String>,
onRepositoryAdded: (String, MediaType) -> Unit, onRepositoryAdded: (String, MediaType) -> Unit,
onRepositoryRemoved: (String) -> Unit onRepositoryRemoved: (String, MediaType) -> Unit
): AddRepositoryBottomSheet { ): AddRepositoryBottomSheet {
return AddRepositoryBottomSheet().apply { return AddRepositoryBottomSheet().apply {
this.mediaType = mediaType this.mediaType = mediaType

View file

@ -1,18 +1,12 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@ -20,10 +14,7 @@ import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityExtensionsBinding import ani.dantotsu.databinding.ActivityExtensionsBinding
import ani.dantotsu.databinding.DialogRepositoriesBinding
import ani.dantotsu.databinding.ItemRepositoryBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
@ -37,20 +28,11 @@ import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.customAlertDialog import ani.dantotsu.util.customAlertDialog
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
class ExtensionsActivity : AppCompatActivity() { class ExtensionsActivity : AppCompatActivity() {
lateinit var binding: ActivityExtensionsBinding lateinit var binding: ActivityExtensionsBinding
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -124,6 +106,9 @@ class ExtensionsActivity : AppCompatActivity() {
if (tab.text?.contains("Manga") == true) { if (tab.text?.contains("Manga") == true) {
generateRepositoryButton(MediaType.MANGA) generateRepositoryButton(MediaType.MANGA)
} }
if (tab.text?.contains("Novels") == true) {
generateRepositoryButton(MediaType.NOVEL)
}
} }
override fun onTabUnselected(tab: TabLayout.Tab) { override fun onTabUnselected(tab: TabLayout.Tab) {
@ -199,136 +184,28 @@ class ExtensionsActivity : AppCompatActivity() {
} }
} }
private fun processUserInput(input: String, mediaType: MediaType) {
val entry = if (input.endsWith("/") || input.endsWith("index.min.json"))
input.substring(0, input.lastIndexOf("/")) else input
if (mediaType == MediaType.ANIME) {
val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
}
}
if (mediaType == MediaType.MANGA) {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions()
}
}
}
private fun getSavedRepositories(repoInventory: ViewGroup, type: MediaType) {
repoInventory.removeAllViews()
val prefName: PrefName? = when (type) {
MediaType.ANIME -> {
PrefName.AnimeExtensionRepos
}
MediaType.MANGA -> {
PrefName.MangaExtensionRepos
}
else -> {
null
}
}
prefName?.let { repoList ->
PrefManager.getVal<Set<String>>(repoList).forEach { item ->
val view = ItemRepositoryBinding.inflate(
LayoutInflater.from(repoInventory.context), repoInventory, true
)
view.repositoryItem.text = item.removePrefix("https://raw.githubusercontent.com")
view.repositoryItem.setOnClickListener {
customAlertDialog().apply {
setTitle(R.string.rem_repository)
setMessage(item)
setPosButton(R.string.ok) {
val repos = PrefManager.getVal<Set<String>>(prefName).minus(item)
PrefManager.setVal(prefName, repos)
repoInventory.removeView(view.root)
CoroutineScope(Dispatchers.IO).launch {
when (type) {
MediaType.ANIME -> {
animeExtensionManager.findAvailableExtensions()
}
MediaType.MANGA -> {
mangaExtensionManager.findAvailableExtensions()
}
else -> {}
}
}
}
setNegButton(R.string.cancel)
show()
}
}
view.repositoryItem.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
copyToClipboard(item, true)
true
}
}
}
}
private fun processEditorAction(editText: EditText, mediaType: MediaType) {
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)
true
}
}
false
}
}
private fun generateRepositoryButton(type: MediaType) { private fun generateRepositoryButton(type: MediaType) {
val hintResource: Int? = when (type) { binding.openSettingsButton.setOnClickListener {
val repos: Set<String> = when (type) {
MediaType.ANIME -> { MediaType.ANIME -> {
R.string.anime_add_repository PrefManager.getVal(PrefName.AnimeExtensionRepos)
} }
MediaType.MANGA -> { MediaType.MANGA -> {
R.string.manga_add_repository PrefManager.getVal(PrefName.MangaExtensionRepos)
} }
else -> { MediaType.NOVEL -> {
null PrefManager.getVal(PrefName.NovelExtensionRepos)
}
}
hintResource?.let { res ->
binding.openSettingsButton.setOnClickListener {
val dialogView = DialogRepositoriesBinding.inflate(
LayoutInflater.from(binding.openSettingsButton.context), null, false
)
dialogView.repositoryTextBox.hint = getString(res)
dialogView.repoInventory.apply {
getSavedRepositories(this, type)
}
processEditorAction(dialogView.repositoryTextBox, type)
customAlertDialog().apply {
setTitle(R.string.edit_repositories)
setCustomView(dialogView.root)
setPosButton(R.string.add_list) {
if (!dialogView.repositoryTextBox.text.isNullOrBlank()) {
processUserInput(dialogView.repositoryTextBox.text.toString(), type)
}
}
setNegButton(R.string.close)
show()
} }
} }
AddRepositoryBottomSheet.newInstance(
type,
repos.toList(),
AddRepositoryBottomSheet::addRepo,
AddRepositoryBottomSheet::removeRepo
).show(supportFragmentManager, "add_repo")
} }
} }
} }

View file

@ -1,14 +1,10 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -32,9 +28,6 @@ import ani.dantotsu.util.customAlertDialog
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -42,8 +35,7 @@ import uy.kohesive.injekt.injectLazy
class SettingsExtensionsActivity : AppCompatActivity() { class SettingsExtensionsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsExtensionsBinding private lateinit var binding: ActivitySettingsExtensionsBinding
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller() private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
@ -61,7 +53,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
} }
fun setExtensionOutput(repoInventory: ViewGroup, type: MediaType) { fun setExtensionOutput(repoInventory: ViewGroup, type: MediaType) {
repoInventory.removeAllViews() repoInventory.removeAllViews()
val prefName: PrefName? = when (type) { val prefName: PrefName = when (type) {
MediaType.ANIME -> { MediaType.ANIME -> {
PrefName.AnimeExtensionRepos PrefName.AnimeExtensionRepos
} }
@ -70,41 +62,17 @@ class SettingsExtensionsActivity : AppCompatActivity() {
PrefName.MangaExtensionRepos PrefName.MangaExtensionRepos
} }
else -> { MediaType.NOVEL -> {
null PrefName.NovelExtensionRepos
} }
} }
prefName?.let { repoList -> PrefManager.getVal<Set<String>>(prefName).forEach { item ->
PrefManager.getVal<Set<String>>(repoList).forEach { item ->
val view = ItemRepositoryBinding.inflate( val view = ItemRepositoryBinding.inflate(
LayoutInflater.from(repoInventory.context), repoInventory, true LayoutInflater.from(repoInventory.context), repoInventory, true
) )
view.repositoryItem.text = view.repositoryItem.text =
item.removePrefix("https://raw.githubusercontent.com/") item.removePrefix("https://raw.githubusercontent.com/")
view.repositoryItem.setOnClickListener {
context.customAlertDialog().apply {
setTitle(R.string.rem_repository)
setMessage(item)
setPosButton(R.string.ok) {
val repos = PrefManager.getVal<Set<String>>(repoList).minus(item)
PrefManager.setVal(repoList, repos)
setExtensionOutput(repoInventory, type)
CoroutineScope(Dispatchers.IO).launch {
when (type) {
MediaType.ANIME -> {
animeExtensionManager.findAvailableExtensions()
}
MediaType.MANGA -> {
mangaExtensionManager.findAvailableExtensions()
}
else -> {}
}
}
}
setNegButton(R.string.cancel)
show()
}
}
view.repositoryItem.setOnLongClickListener { view.repositoryItem.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
copyToClipboard(item, true) copyToClipboard(item, true)
@ -113,32 +81,6 @@ class SettingsExtensionsActivity : AppCompatActivity() {
} }
repoInventory.isVisible = repoInventory.childCount > 0 repoInventory.isVisible = repoInventory.childCount > 0
} }
}
fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) {
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(validLink)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
}
setExtensionOutput(view, MediaType.ANIME)
}
if (mediaType == MediaType.MANGA) {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions()
}
setExtensionOutput(view, MediaType.MANGA)
}
}
settingsRecyclerView.adapter = SettingsAdapter( settingsRecyclerView.adapter = SettingsAdapter(
arrayListOf( arrayListOf(
@ -148,17 +90,18 @@ 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 animeRepos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos) val animeRepos =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
AddRepositoryBottomSheet.newInstance( AddRepositoryBottomSheet.newInstance(
MediaType.ANIME, MediaType.ANIME,
animeRepos.toList(), animeRepos.toList(),
onRepositoryAdded = { input, mediaType -> onRepositoryAdded = { input, mediaType ->
processUserInput(input, mediaType, it.attachView) AddRepositoryBottomSheet.addRepo(input, mediaType)
setExtensionOutput(it.attachView, mediaType)
}, },
onRepositoryRemoved = { item -> onRepositoryRemoved = { item, mediaType ->
val repos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item) AddRepositoryBottomSheet.removeRepo(item, mediaType)
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos) setExtensionOutput(it.attachView, mediaType)
setExtensionOutput(it.attachView, MediaType.ANIME)
} }
).show(supportFragmentManager, "add_repo") ).show(supportFragmentManager, "add_repo")
}, },
@ -172,17 +115,18 @@ 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 mangaRepos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos) val mangaRepos =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
AddRepositoryBottomSheet.newInstance( AddRepositoryBottomSheet.newInstance(
MediaType.MANGA, MediaType.MANGA,
mangaRepos.toList(), mangaRepos.toList(),
onRepositoryAdded = { input, mediaType -> onRepositoryAdded = { input, mediaType ->
processUserInput(input, mediaType, it.attachView) AddRepositoryBottomSheet.addRepo(input, mediaType)
setExtensionOutput(it.attachView, mediaType)
}, },
onRepositoryRemoved = { item -> onRepositoryRemoved = { item, mediaType ->
val repos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).minus(item) AddRepositoryBottomSheet.removeRepo(item, mediaType)
PrefManager.setVal(PrefName.MangaExtensionRepos, repos) setExtensionOutput(it.attachView, mediaType)
setExtensionOutput(it.attachView, MediaType.MANGA)
} }
).show(supportFragmentManager, "add_repo") ).show(supportFragmentManager, "add_repo")
}, },
@ -190,6 +134,31 @@ class SettingsExtensionsActivity : AppCompatActivity() {
setExtensionOutput(it.attachView, MediaType.MANGA) setExtensionOutput(it.attachView, MediaType.MANGA)
} }
), ),
Settings(
type = 1,
name = getString(R.string.novel_add_repository),
desc = getString(R.string.novel_add_repository_desc),
icon = R.drawable.ic_github,
onClick = {
val novelRepos =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos)
AddRepositoryBottomSheet.newInstance(
MediaType.NOVEL,
novelRepos.toList(),
onRepositoryAdded = { input, mediaType ->
AddRepositoryBottomSheet.addRepo(input, mediaType)
setExtensionOutput(it.attachView, mediaType)
},
onRepositoryRemoved = { item, mediaType ->
AddRepositoryBottomSheet.removeRepo(item, mediaType)
setExtensionOutput(it.attachView, mediaType)
}
).show(supportFragmentManager, "add_repo")
},
attach = {
setExtensionOutput(it.attachView, MediaType.NOVEL)
}
),
Settings( Settings(
type = 1, type = 1,
name = getString(R.string.extension_test), name = getString(R.string.extension_test),
@ -217,7 +186,10 @@ class SettingsExtensionsActivity : AppCompatActivity() {
setTitle(R.string.user_agent) setTitle(R.string.user_agent)
setCustomView(dialogView.root) setCustomView(dialogView.root)
setPosButton(R.string.ok) { setPosButton(R.string.ok) {
PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString()) PrefManager.setVal(
PrefName.DefaultUserAgent,
editText.text.toString()
)
} }
setNeutralButton(R.string.reset) { setNeutralButton(R.string.reset) {
PrefManager.removeVal(PrefName.DefaultUserAgent) PrefManager.removeVal(PrefName.DefaultUserAgent)

View file

@ -32,6 +32,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
), ),
AnimeExtensionRepos(Pref(Location.General, Set::class, setOf<String>())), AnimeExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
MangaExtensionRepos(Pref(Location.General, Set::class, setOf<String>())), MangaExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
NovelExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
AnimeSourcesOrder(Pref(Location.General, List::class, listOf<String>())), AnimeSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())), AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())), MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())),

View file

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
import ani.dantotsu.parsers.novel.AvailableNovelSources
import ani.dantotsu.parsers.novel.NovelExtension
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.util.Logger import ani.dantotsu.util.Logger
@ -192,6 +194,92 @@ internal class ExtensionGithubApi {
return "${extension.repository}/apk/${extension.apkName}" return "${extension.repository}/apk/${extension.apkName}"
} }
suspend fun findNovelExtensions(): List<NovelExtension.Available> {
return withIOContext {
val extensions: ArrayList<NovelExtension.Available> = arrayListOf()
val repos =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos).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(repoUrl))
.awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $repoUrl")
Logger.log(e)
null
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET(fallbackRepoUrl(it) + "/index.min.json"))
.awaitSuccess()
}
val repoExtensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toNovelExtensions(it)
}
extensions.addAll(repoExtensions)
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
Logger.log(e)
}
}
extensions
}
}
private fun List<ExtensionJsonObject>.toNovelExtensions(repository: String): List<NovelExtension.Available> {
return mapNotNull { extension ->
val sources = extension.sources?.map { source ->
ExtensionSourceJsonObject(
source.id,
source.lang,
source.name,
source.baseUrl,
)
}
val iconUrl = "${repository.removeSuffix("/index.min.json")}/icon/${extension.pkg}.png"
NovelExtension.Available(
extension.name,
extension.pkg,
extension.apk,
extension.code,
repository,
sources?.toNovelSources() ?: emptyList(),
iconUrl,
)
}
}
private fun List<ExtensionSourceJsonObject>.toNovelSources(): List<AvailableNovelSources> {
return map { source ->
AvailableNovelSources(
source.id,
source.lang,
source.name,
source.baseUrl,
)
}
}
fun getNovelApkUrl(extension: NovelExtension.Available): String {
return "${extension.repository}/apk/${extension.pkgName}.apk"
}
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 = repoUrl val strippedRepoUrl = repoUrl

View file

@ -25,6 +25,8 @@
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="3dp" android:layout_marginStart="3dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
@ -32,4 +34,18 @@
app:tint="?attr/colorOnBackground" app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/repoCopyImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="10dp"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_gravity="center_vertical"
android:layout_marginStart="3dp"
android:background="?android:attr/selectableItemBackground"
app:srcCompat="@drawable/format_link_24"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>

View file

@ -896,6 +896,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="anime_add_repository">Add Anime Repo</string> <string name="anime_add_repository">Add Anime Repo</string>
<string name="manga_add_repository">Add Manga Repo</string> <string name="manga_add_repository">Add Manga Repo</string>
<string name="novel_add_repository">Add Novel Repo</string>
<string name="edit_repositories">Edit repositories</string> <string name="edit_repositories">Edit repositories</string>
<string name="rem_repository">Remove repository?</string> <string name="rem_repository">Remove repository?</string>
@ -963,6 +964,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="adult_only_content_desc">Show only adult content in the explore page</string> <string name="adult_only_content_desc">Show only adult content in the explore page</string>
<string name="anime_add_repository_desc">Add Anime Extensions from various sources</string> <string name="anime_add_repository_desc">Add Anime Extensions from various sources</string>
<string name="manga_add_repository_desc">Add Manga Extensions from various sources</string> <string name="manga_add_repository_desc">Add Manga Extensions from various sources</string>
<string name="novel_add_repository_desc">Add Novel Extensions from various sources</string>
<string name="user_agent_desc">Change your default user agent</string> <string name="user_agent_desc">Change your default user agent</string>
<string name="force_legacy_installer_desc">Use the legacy installer to install extensions (For older android phones)</string> <string name="force_legacy_installer_desc">Use the legacy installer to install extensions (For older android phones)</string>
<string name="skip_loading_extension_icons_desc">Don\'t load icons of extensions on the extension page</string> <string name="skip_loading_extension_icons_desc">Don\'t load icons of extensions on the extension page</string>
@ -1089,7 +1091,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<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">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="add_repository_desc">A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json\nOr: username/repo/branch</string>
<string name="current_repositories">Current Repositories</string> <string name="current_repositories">Current Repositories</string>
<string name="add_repository_warning">Warning: Extensions from the repository can run arbitrary code on your device. Only use repositories you trust. \n\nBy adding a repository, you agree to: \n\n1. Not use the app for viewing or distributing copyrighted content. \n2. Not use the app for any illegal activities. \n3. Not use the app for any activities that violate the terms of service of the content providers. \n\nThe app or it\'s maintainer are not affiliated in any way with extension providers. The developers are not responsible for any damages caused by the app. \n\nBy adding a repository, you agree to these terms.</string> <string name="add_repository_warning">Warning: Extensions from the repository can run arbitrary code on your device. Only use repositories you trust. \n\nBy adding a repository, you agree to: \n\n1. Not use the app for viewing or distributing copyrighted content. \n2. Not use the app for any illegal activities. \n3. Not use the app for any activities that violate the terms of service of the content providers. \n\nThe app or it\'s maintainer are not affiliated in any way with extension providers. The developers are not responsible for any damages caused by the app. \n\nBy adding a repository, you agree to these terms.</string>
<string name="privacy_policy">Privacy Policy</string> <string name="privacy_policy">Privacy Policy</string>