diff --git a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt
index f6f8e8caf69..36b4c35d8ec 100644
--- a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt
+++ b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt
@@ -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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 644d218a8b8..fe6f1a90e1f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -394,11 +394,10 @@
-
-
+
-
+
PrefName.MangaExtensionRepos to "Manga"
+ "aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
+ "novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
+ else -> throw Exception("Invalid scheme")
}
val savedRepos: Set = 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")
diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt
index 42595aaf9d8..3261ca77db2 100644
--- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt
@@ -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,
val iconUrl: String,
) : NovelExtension()
diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt
deleted file mode 100644
index 0a225c9d751..00000000000
--- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt
+++ /dev/null
@@ -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 {
- 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>()
- .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? {
- // 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()
- .map { it.extension }
-
- val extensionsWithUpdate = mutableListOf()
- 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.toExtensions(): List {
- 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.toSources(): List {
- 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?,
-)
-
-@Serializable
-private data class NovelExtensionSourceJsonObject(
- val id: Long,
- val lang: String,
- val name: String,
- val baseUrl: String,
-)
-
diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt
index aab307b41f1..5ef1aa7d35b 100644
--- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt
@@ -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 = 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 {
- return installer.downloadAndInstall(api.getApkUrl(extension), extension.pkgName,
+ return installer.downloadAndInstall(api.getNovelApkUrl(extension), extension.pkgName,
extension.name, MediaType.NOVEL)
}
diff --git a/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt b/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt
index 4581f8cb91f..30dbdaba377 100644
--- a/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt
+++ b/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt
@@ -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() {
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 = 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"))
- return "URL must end with 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>(PrefName.AnimeExtensionRepos)
+ .plus(validLink)
+ PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
+ CoroutineScope(Dispatchers.IO).launch {
+ Injekt.get().findAvailableExtensions()
+ }
+ }
+ MediaType.MANGA -> {
+ val manga =
+ PrefManager.getVal>(PrefName.MangaExtensionRepos)
+ .plus(validLink)
+ PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
+ CoroutineScope(Dispatchers.IO).launch {
+ Injekt.get().findAvailableExtensions()
+ }
+ }
+ MediaType.NOVEL -> {
+ val novel =
+ PrefManager.getVal>(PrefName.NovelExtensionRepos)
+ .plus(validLink)
+ PrefManager.setVal(PrefName.NovelExtensionRepos, novel)
+ CoroutineScope(Dispatchers.IO).launch {
+ Injekt.get().findAvailableExtensions()
+ }
+ }
+ }
+ }
+
+ fun removeRepo(input: String, mediaType: MediaType) {
+ when (mediaType) {
+ MediaType.ANIME -> {
+ val anime =
+ PrefManager.getVal>(PrefName.AnimeExtensionRepos)
+ .minus(input)
+ PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
+ CoroutineScope(Dispatchers.IO).launch {
+ Injekt.get().findAvailableExtensions()
+ }
+ }
+ MediaType.MANGA -> {
+ val manga =
+ PrefManager.getVal>(PrefName.MangaExtensionRepos)
+ .minus(input)
+ PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
+ CoroutineScope(Dispatchers.IO).launch {
+ Injekt.get().findAvailableExtensions()
+ }
+ }
+ MediaType.NOVEL -> {
+ val novel =
+ PrefManager.getVal>(PrefName.NovelExtensionRepos)
+ .minus(input)
+ PrefManager.setVal(PrefName.NovelExtensionRepos, novel)
+ CoroutineScope(Dispatchers.IO).launch {
+ Injekt.get().findAvailableExtensions()
+ }
+ }
+ }
+ }
+
fun newInstance(
mediaType: MediaType,
repositories: List,
onRepositoryAdded: (String, MediaType) -> Unit,
- onRepositoryRemoved: (String) -> Unit
+ onRepositoryRemoved: (String, MediaType) -> Unit
): AddRepositoryBottomSheet {
return AddRepositoryBottomSheet().apply {
this.mediaType = mediaType
diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
index dd11df3a641..de9dbbf0569 100644
--- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
@@ -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>(PrefName.AnimeExtensionRepos).plus(entry)
- PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
- CoroutineScope(Dispatchers.IO).launch {
- animeExtensionManager.findAvailableExtensions()
- }
- }
- if (mediaType == MediaType.MANGA) {
- val manga =
- PrefManager.getVal>(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>(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>(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 generateRepositoryButton(type: MediaType) {
+ binding.openSettingsButton.setOnClickListener {
+ val repos: Set = when (type) {
+ MediaType.ANIME -> {
+ PrefManager.getVal(PrefName.AnimeExtensionRepos)
}
- }
- }
- }
- 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
+ MediaType.MANGA -> {
+ PrefManager.getVal(PrefName.MangaExtensionRepos)
}
- }
- false
- }
- }
-
- private fun generateRepositoryButton(type: MediaType) {
- val hintResource: Int? = when (type) {
- MediaType.ANIME -> {
- R.string.anime_add_repository
- }
-
- MediaType.MANGA -> {
- R.string.manga_add_repository
- }
- 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")
}
}
}
diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt
index 59add57808a..8a84d53d429 100644
--- a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt
@@ -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().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,74 +62,24 @@ class SettingsExtensionsActivity : AppCompatActivity() {
PrefName.MangaExtensionRepos
}
- else -> {
- null
- }
- }
- prefName?.let { repoList ->
- PrefManager.getVal>(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 {
- context.customAlertDialog().apply {
- setTitle(R.string.rem_repository)
- setMessage(item)
- setPosButton(R.string.ok) {
- val repos = PrefManager.getVal>(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)
- true
- }
+ MediaType.NOVEL -> {
+ PrefName.NovelExtensionRepos
}
- repoInventory.isVisible = repoInventory.childCount > 0
}
- }
+ PrefManager.getVal>(prefName).forEach { item ->
+ val view = ItemRepositoryBinding.inflate(
+ LayoutInflater.from(repoInventory.context), repoInventory, true
+ )
+ view.repositoryItem.text =
+ item.removePrefix("https://raw.githubusercontent.com/")
- 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>(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>(PrefName.MangaExtensionRepos).plus(validLink)
- PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
- CoroutineScope(Dispatchers.IO).launch {
- mangaExtensionManager.findAvailableExtensions()
+ view.repositoryItem.setOnLongClickListener {
+ it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ copyToClipboard(item, true)
+ true
}
- setExtensionOutput(view, MediaType.MANGA)
}
+ repoInventory.isVisible = repoInventory.childCount > 0
}
settingsRecyclerView.adapter = SettingsAdapter(
@@ -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>(PrefName.AnimeExtensionRepos)
+ val animeRepos =
+ PrefManager.getVal>(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>(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>(PrefName.MangaExtensionRepos)
+ val mangaRepos =
+ PrefManager.getVal>(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>(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>(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)
@@ -247,7 +219,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
ProxyDialogFragment().show(supportFragmentManager, "dialog")
}
),
- Settings(
+ Settings(
type = 2,
name = getString(R.string.force_legacy_installer),
desc = getString(R.string.force_legacy_installer_desc),
diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt
index 659e399b1af..866871b390e 100644
--- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt
+++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt
@@ -32,6 +32,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
),
AnimeExtensionRepos(Pref(Location.General, Set::class, setOf())),
MangaExtensionRepos(Pref(Location.General, Set::class, setOf())),
+ NovelExtensionRepos(Pref(Location.General, Set::class, setOf())),
AnimeSourcesOrder(Pref(Location.General, List::class, listOf())),
AnimeSearchHistory(Pref(Location.General, Set::class, setOf())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf())),
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 d0a7af33213..c2771afdeca 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
@@ -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 {
+ return withIOContext {
+
+ val extensions: ArrayList = arrayListOf()
+
+ val repos =
+ PrefManager.getVal>(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>()
+ .toNovelExtensions(it)
+ }
+
+ extensions.addAll(repoExtensions)
+ } catch (e: Throwable) {
+ Logger.log("Failed to get extensions from GitHub")
+ Logger.log(e)
+ }
+ }
+
+ extensions
+ }
+ }
+
+ private fun List.toNovelExtensions(repository: String): List {
+ 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.toNovelSources(): List {
+ 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
diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml
index 658110b5be9..6cbf4cb5432 100644
--- a/app/src/main/res/layout/item_repo.xml
+++ b/app/src/main/res/layout/item_repo.xml
@@ -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" />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 504436d0c56..c695eda9bc4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -896,6 +896,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Add Anime Repo
Add Manga Repo
+ Add Novel Repo
Edit repositories
Remove repository?
@@ -963,6 +964,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Show only adult content in the explore page
Add Anime Extensions from various sources
Add Manga Extensions from various sources
+ Add Novel Extensions from various sources
Change your default user agent
Use the legacy installer to install extensions (For older android phones)
Don\'t load icons of extensions on the extension page
@@ -1089,7 +1091,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Subtitle Stroke
Bottom Margin
Add Repository
- A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json
+ A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json\nOr: username/repo/branch
Current Repositories
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.
Privacy Policy