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.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.delay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray

View file

@ -394,11 +394,10 @@
<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:host="add-repo"/>
<data android:scheme="tachiyomi"/>
<data android:host="add-repo"/>
<data android:scheme="aniyomi"/>
<data android:host="add-repo"/>
<data android:scheme="novelyomi"/>
</intent-filter>
</activity>
<activity

View file

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

View file

@ -26,6 +26,7 @@ sealed class NovelExtension {
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
var repository: String,
val sources: List<AvailableNovelSources>,
val iconUrl: String,
) : 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.util.Logger
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.ExtensionInstaller
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.
*/
private val api = NovelExtensionGithubApi()
private val api = ExtensionGithubApi()
/**
* The installer which installs, updates and uninstalls the Novel extensions.
@ -70,7 +71,7 @@ class NovelExtensionManager(private val context: Context) {
*/
suspend fun findAvailableExtensions() {
val extensions: List<NovelExtension.Available> = try {
api.findExtensions()
api.findNovelExtensions()
} catch (e: Exception) {
Logger.log("Error finding extensions: ${e.message}")
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.
*/
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)
}

View file

@ -2,6 +2,7 @@ package ani.dantotsu.settings
import android.content.Context
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
@ -10,29 +11,52 @@ import android.view.inputmethod.EditorInfo
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding
import ani.dantotsu.databinding.ItemRepoBinding
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 com.xwray.groupie.GroupieAdapter
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(
val url: String,
val onRemove: (String) -> Unit
private val mediaType: MediaType,
val onRemove: (String, MediaType) -> Unit
) :BindableItem<ItemRepoBinding>() {
override fun getLayout() = R.layout.item_repo
override fun bind(viewBinding: ItemRepoBinding, position: Int) {
viewBinding.repoNameTextView.text = url
viewBinding.repoNameTextView.text = url.cleanShownUrl()
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 {
return ItemRepoBinding.bind(view)
}
private fun String.cleanShownUrl(): String {
return this
.removePrefix("https://raw.githubusercontent.com/")
.replace("index.min.json", "")
.removeSuffix("/")
}
}
class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
@ -41,7 +65,7 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
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 onRepositoryRemoved: ((String, MediaType) -> Unit)? = null
private var adapter: GroupieAdapter = GroupieAdapter()
override fun onCreateView(
@ -62,24 +86,19 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
LinearLayoutManager.VERTICAL,
false
)
adapter.addAll(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
adapter.addAll(repositories.map { RepoItem(it, mediaType, ::onRepositoryRemoved) })
binding.repositoryInput.hint = when(mediaType) {
MediaType.ANIME -> getString(R.string.anime_add_repository)
MediaType.MANGA -> getString(R.string.manga_add_repository)
else -> ""
MediaType.NOVEL -> getString(R.string.novel_add_repository)
}
binding.addButton.setOnClickListener {
val input = binding.repositoryInput.text.toString()
val error = isValidUrl(input)
if (error == null) {
context?.let { context ->
addRepoWarning(context) {
onRepositoryAdded?.invoke(input, mediaType)
dismiss()
}
}
acceptUrl(input)
} else {
binding.repositoryInput.error = error
}
@ -96,12 +115,7 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
if (url.isNotBlank()) {
val error = isValidUrl(url)
if (error == null) {
context?.let { context ->
addRepoWarning(context) {
onRepositoryAdded?.invoke(url, mediaType)
dismiss()
}
}
acceptUrl(url)
return@setOnEditorActionListener true
} else {
binding.repositoryInput.error = error
@ -112,20 +126,62 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
}
}
private fun onRepositoryRemoved(url: String) {
onRepositoryRemoved?.invoke(url)
repositories.remove(url)
adapter.update(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
private fun acceptUrl(url: String) {
val finalUrl = getRepoUrl(url)
context?.let { context ->
addRepoWarning(context) {
onRepositoryAdded?.invoke(finalUrl, mediaType)
dismiss()
}
}
}
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"))
private fun isValidUrl(input: String): String? {
if (input.startsWith("http://") || input.startsWith("https://")) {
if (!input.removeSuffix("/").endsWith("index.min.json")) {
return "URL must end with index.min.json"
}
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() {
super.onDestroyView()
_binding = null
@ -142,11 +198,81 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
.setNegButton(R.string.cancel) { }
.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(
mediaType: MediaType,
repositories: List<String>,
onRepositoryAdded: (String, MediaType) -> Unit,
onRepositoryRemoved: (String) -> Unit
onRepositoryRemoved: (String, MediaType) -> Unit
): AddRepositoryBottomSheet {
return AddRepositoryBottomSheet().apply {
this.mediaType = mediaType

View file

@ -1,18 +1,12 @@
package ani.dantotsu.settings
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AutoCompleteTextView
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
@ -20,10 +14,7 @@ import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityExtensionsBinding
import ani.dantotsu.databinding.DialogRepositoriesBinding
import ani.dantotsu.databinding.ItemRepositoryBinding
import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
@ -37,20 +28,11 @@ import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.tabs.TabLayout
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
class ExtensionsActivity : AppCompatActivity() {
lateinit var binding: ActivityExtensionsBinding
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -124,6 +106,9 @@ class ExtensionsActivity : AppCompatActivity() {
if (tab.text?.contains("Manga") == true) {
generateRepositoryButton(MediaType.MANGA)
}
if (tab.text?.contains("Novels") == true) {
generateRepositoryButton(MediaType.NOVEL)
}
}
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) {
val hintResource: Int? = when (type) {
binding.openSettingsButton.setOnClickListener {
val repos: Set<String> = when (type) {
MediaType.ANIME -> {
R.string.anime_add_repository
PrefManager.getVal(PrefName.AnimeExtensionRepos)
}
MediaType.MANGA -> {
R.string.manga_add_repository
PrefManager.getVal(PrefName.MangaExtensionRepos)
}
else -> {
null
}
}
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()
MediaType.NOVEL -> {
PrefManager.getVal(PrefName.NovelExtensionRepos)
}
}
AddRepositoryBottomSheet.newInstance(
type,
repos.toList(),
AddRepositoryBottomSheet::addRepo,
AddRepositoryBottomSheet::removeRepo
).show(supportFragmentManager, "add_repo")
}
}
}

View file

@ -1,14 +1,10 @@
package ani.dantotsu.settings
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@ -32,9 +28,6 @@ import ani.dantotsu.util.customAlertDialog
import eu.kanade.domain.base.BasePreferences
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
import uy.kohesive.injekt.injectLazy
@ -42,8 +35,7 @@ import uy.kohesive.injekt.injectLazy
class SettingsExtensionsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsExtensionsBinding
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
@ -61,7 +53,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
}
fun setExtensionOutput(repoInventory: ViewGroup, type: MediaType) {
repoInventory.removeAllViews()
val prefName: PrefName? = when (type) {
val prefName: PrefName = when (type) {
MediaType.ANIME -> {
PrefName.AnimeExtensionRepos
}
@ -70,41 +62,17 @@ class SettingsExtensionsActivity : AppCompatActivity() {
PrefName.MangaExtensionRepos
}
else -> {
null
MediaType.NOVEL -> {
PrefName.NovelExtensionRepos
}
}
prefName?.let { repoList ->
PrefManager.getVal<Set<String>>(repoList).forEach { item ->
PrefManager.getVal<Set<String>>(prefName).forEach { item ->
val view = ItemRepositoryBinding.inflate(
LayoutInflater.from(repoInventory.context), repoInventory, true
)
view.repositoryItem.text =
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 {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
copyToClipboard(item, true)
@ -113,32 +81,6 @@ class SettingsExtensionsActivity : AppCompatActivity() {
}
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(
arrayListOf(
@ -148,17 +90,18 @@ class SettingsExtensionsActivity : AppCompatActivity() {
desc = getString(R.string.anime_add_repository_desc),
icon = R.drawable.ic_github,
onClick = {
val animeRepos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
val animeRepos =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
AddRepositoryBottomSheet.newInstance(
MediaType.ANIME,
animeRepos.toList(),
onRepositoryAdded = { input, mediaType ->
processUserInput(input, mediaType, it.attachView)
AddRepositoryBottomSheet.addRepo(input, mediaType)
setExtensionOutput(it.attachView, mediaType)
},
onRepositoryRemoved = { item ->
val repos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos)
setExtensionOutput(it.attachView, MediaType.ANIME)
onRepositoryRemoved = { item, mediaType ->
AddRepositoryBottomSheet.removeRepo(item, mediaType)
setExtensionOutput(it.attachView, mediaType)
}
).show(supportFragmentManager, "add_repo")
},
@ -172,17 +115,18 @@ class SettingsExtensionsActivity : AppCompatActivity() {
desc = getString(R.string.manga_add_repository_desc),
icon = R.drawable.ic_github,
onClick = {
val mangaRepos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
val mangaRepos =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
AddRepositoryBottomSheet.newInstance(
MediaType.MANGA,
mangaRepos.toList(),
onRepositoryAdded = { input, mediaType ->
processUserInput(input, mediaType, it.attachView)
AddRepositoryBottomSheet.addRepo(input, mediaType)
setExtensionOutput(it.attachView, mediaType)
},
onRepositoryRemoved = { item ->
val repos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).minus(item)
PrefManager.setVal(PrefName.MangaExtensionRepos, repos)
setExtensionOutput(it.attachView, MediaType.MANGA)
onRepositoryRemoved = { item, mediaType ->
AddRepositoryBottomSheet.removeRepo(item, mediaType)
setExtensionOutput(it.attachView, mediaType)
}
).show(supportFragmentManager, "add_repo")
},
@ -190,6 +134,31 @@ class SettingsExtensionsActivity : AppCompatActivity() {
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(
type = 1,
name = getString(R.string.extension_test),
@ -217,7 +186,10 @@ class SettingsExtensionsActivity : AppCompatActivity() {
setTitle(R.string.user_agent)
setCustomView(dialogView.root)
setPosButton(R.string.ok) {
PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString())
PrefManager.setVal(
PrefName.DefaultUserAgent,
editText.text.toString()
)
}
setNeutralButton(R.string.reset) {
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>())),
MangaExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
NovelExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
AnimeSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())),

View file

@ -1,5 +1,7 @@
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.PrefName
import ani.dantotsu.util.Logger
@ -192,6 +194,92 @@ internal class ExtensionGithubApi {
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? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl = repoUrl

View file

@ -25,6 +25,8 @@
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"
@ -32,4 +34,18 @@
app:tint="?attr/colorOnBackground"
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>

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="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="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="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="novel_add_repository_desc">Add Novel Extensions from various sources</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="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_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="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="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>