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