From 96213900ac5fb8af3dbe7f06531aaf380445c6b4 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 18 Dec 2020 16:24:17 -0500 Subject: [PATCH] Add external repo support --- .../data/preference/PreferenceKeys.kt | 2 + .../data/preference/PreferencesHelper.kt | 2 + .../extension/api/ExtensionGithubApi.kt | 15 +- .../extension/api/ExtensionGithubService.kt | 7 +- .../tachiyomi/extension/model/Extension.kt | 5 +- .../ui/browse/extension/ExtensionHolder.kt | 22 +- .../ui/category/repos/RepoAdapter.kt | 30 ++ .../ui/category/repos/RepoController.kt | 320 ++++++++++++++++++ .../ui/category/repos/RepoCreateDialog.kt | 51 +++ .../tachiyomi/ui/category/repos/RepoHolder.kt | 28 ++ .../tachiyomi/ui/category/repos/RepoItem.kt | 62 ++++ .../ui/category/repos/RepoPresenter.kt | 89 +++++ .../ui/setting/SettingsBrowseController.kt | 14 + app/src/main/res/values/strings_sy.xml | 14 + 14 files changed, 652 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 2fa178e0d..043ecd826 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -332,4 +332,6 @@ object PreferenceKeys { const val createLegacyBackup = "create_legacy_backup" const val dontDeleteFromCategories = "dont_delete_from_categories" + + const val extensionRepos = "extension_repos" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 3e2749db2..485b095f6 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -445,4 +445,6 @@ class PreferencesHelper(val context: Context) { fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf()) fun dontDeleteFromCategories() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet()) + + fun extensionRepos() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet()) } 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 9a1a393ef..143de3997 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 @@ -25,7 +25,12 @@ internal class ExtensionGithubApi { return withContext(Dispatchers.IO) { val response = service.getRepo() parseResponse(response) + } /* SY --> */ + preferences.extensionRepos().get().flatMap { + val url = "$BASE_URL$it/repo/" + val response = service.getRepo("${url}index.min.json") + parseResponse(response, url) } + // SY <-- } suspend fun checkForUpdates(context: Context): List { @@ -58,7 +63,7 @@ internal class ExtensionGithubApi { return extensionsWithUpdate } - private fun parseResponse(json: JsonArray): List { + private fun parseResponse(json: JsonArray /* SY --> */, repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List { return json .filter { element -> val versionName = element.jsonObject["version"]!!.jsonPrimitive.content @@ -73,14 +78,16 @@ internal class ExtensionGithubApi { val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int val lang = element.jsonObject["lang"]!!.jsonPrimitive.content val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1 - val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}" + // SY --> + val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}" + // SY <-- - Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) + Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon /* SY --> */, repoUrl /* SY <-- */) } } fun getApkUrl(extension: Extension.Available): String { - return "$REPO_URL_PREFIX/apk/${extension.apkName}" + return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" /* SY <-- */ } // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt index ecc5940ae..52a73e642 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.json.JsonArray import okhttp3.MediaType.Companion.toMediaType import retrofit2.Retrofit import retrofit2.http.GET +import retrofit2.http.Url import uy.kohesive.injekt.injectLazy /** @@ -27,6 +28,8 @@ interface ExtensionGithubService { } } - @GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json") - suspend fun getRepo(): JsonArray + // SY --> + @GET + suspend fun getRepo(@Url url: String = "${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json"): JsonArray + // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 7cf4ad1ea..18b12f99b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -35,7 +35,10 @@ sealed class Extension { override val lang: String, override val isNsfw: Boolean, val apkName: String, - val iconUrl: String + val iconUrl: String, + // SY --> + val repoUrl: String + // SY <-- ) : Extension() data class Untrusted( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt index 65b4eccb5..57e83c7f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding +import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.source.ConfigurableSource @@ -41,9 +42,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial) // SY --> extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant) + extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short).plusRepo(extension) + else -> "".plusRepo(extension) // SY <-- - extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short) - else -> "" }.toUpperCase() GlideApp.with(itemView.context).clear(binding.image) @@ -57,6 +58,23 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : bindButton(item) } + // SY --> + private fun String.plusRepo(extension: Extension): String { + return if (extension is Extension.Available) { + when (extension.repoUrl) { + ExtensionGithubApi.REPO_URL_PREFIX -> this + else -> { + this + if (this.isEmpty()) { + "" + } else { + " • " + } + itemView.context.getString(R.string.repo_source) + } + } + } else this + } + // SY <-- + @Suppress("ResourceType") fun bindButton(item: ExtensionItem) = with(binding.extButton) { isEnabled = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt new file mode 100644 index 000000000..b90302176 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import eu.davidea.flexibleadapter.FlexibleAdapter + +/** + * Custom adapter for repos. + * + * @param controller The containing controller. + */ +class RepoAdapter(controller: RepoController) : + FlexibleAdapter(null, controller, true) { + + /** + * Clears the active selections from the list and the model. + */ + override fun clearSelection() { + super.clearSelection() + (0 until itemCount).forEach { getItem(it)?.isSelected = false } + } + + /** + * Toggles the selection of the given position. + * + * @param position The position to toggle. + */ + override fun toggleSelection(position: Int) { + super.toggleSelection(position) + getItem(position)?.isSelected = isSelected(position) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt new file mode 100644 index 000000000..d77a62183 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt @@ -0,0 +1,320 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.snackbar.Snackbar +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.helpers.UndoHelper +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding +import eu.kanade.tachiyomi.ui.base.controller.FabController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks + +/** + * Controller to manage the categories for the users' library. + */ +class RepoController : + NucleusController(), + FabController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + RepoCreateDialog.Listener, + UndoHelper.OnActionListener { + + /** + * Object used to show ActionMode toolbar. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing repo items. + */ + private var adapter: RepoAdapter? = null + + private var actionFab: ExtendedFloatingActionButton? = null + private var actionFabScrollListener: RecyclerView.OnScrollListener? = null + + /** + * Undo helper used for restoring a deleted repo. + */ + private var undoHelper: UndoHelper? = null + + /** + * Creates the presenter for this controller. Not to be manually called. + */ + override fun createPresenter() = RepoPresenter() + + /** + * Returns the toolbar title to show when this controller is attached. + */ + override fun getTitle(): String? { + return resources?.getString(R.string.action_edit_repos) + } + + /** + * Returns the view of this controller. + * + * @param inflater The layout inflater to create the view from XML. + * @param container The parent view for this one. + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + binding = CategoriesControllerBinding.inflate(inflater) + return binding.root + } + + /** + * Called after view inflation. Used to initialize the view. + * + * @param view The view of this controller. + */ + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = RepoAdapter(this@RepoController) + binding.recycler.layoutManager = LinearLayoutManager(view.context) + binding.recycler.setHasFixedSize(true) + binding.recycler.adapter = adapter + adapter?.isPermanentDelete = false + + actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) + } + + override fun configureFab(fab: ExtendedFloatingActionButton) { + actionFab = fab + fab.setText(R.string.action_add) + fab.setIconResource(R.drawable.ic_add_24dp) + fab.clicks() + .onEach { + RepoCreateDialog(this@RepoController).showDialog(router, null) + } + .launchIn(scope) + } + + override fun cleanupFab(fab: ExtendedFloatingActionButton) { + actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } + actionFab = null + } + + /** + * Called when the view is being destroyed. Used to release references and remove callbacks. + * + * @param view The view of this controller. + */ + override fun onDestroyView(view: View) { + // Manually call callback to delete repos if required + undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) + undoHelper = null + actionMode = null + adapter = null + super.onDestroyView(view) + } + + /** + * Called from the presenter when the repos are updated. + * + * @param repos The new list of repos to display. + */ + fun setRepos(repos: List) { + actionMode?.finish() + adapter?.updateDataSet(repos) + if (repos.isNotEmpty()) { + binding.emptyView.hide() + val selected = repos.filter { it.isSelected } + if (selected.isNotEmpty()) { + selected.forEach { onItemLongClick(repos.indexOf(it)) } + } + } else { + binding.emptyView.show(R.string.information_empty_repos) + } + } + + /** + * Called when action mode is first created. The menu supplied will be used to generate action + * buttons for the action mode. + * + * @param mode ActionMode being created. + * @param menu Menu used to populate action buttons. + * @return true if the action mode should be created, false if entering this mode should be + * aborted. + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate menu. + mode.menuInflater.inflate(R.menu.category_selection, menu) + // Enable adapter multi selection. + adapter?.mode = SelectableAdapter.Mode.MULTI + return true + } + + /** + * Called to refresh an action mode's action menu whenever it is invalidated. + * + * @param mode ActionMode being prepared. + * @param menu Menu used to populate action buttons. + * @return true if the menu or action mode was updated, false otherwise. + */ + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val adapter = adapter ?: return false + val count = adapter.selectedItemCount + mode.title = count.toString() + + // Show edit button only when one item is selected + val editItem = mode.menu.findItem(R.id.action_edit) + editItem.isVisible = false + return true + } + + /** + * Called to report a user click on an action button. + * + * @param mode The current ActionMode. + * @param item The item that was clicked. + * @return true if this callback handled the event, false if the standard MenuItem invocation + * should continue. + */ + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val adapter = adapter ?: return false + + when (item.itemId) { + R.id.action_delete -> { + undoHelper = UndoHelper(adapter, this) + undoHelper?.start( + adapter.selectedPositions, + (activity as? MainActivity)?.binding?.rootCoordinator!!, + R.string.snack_repo_deleted, + R.string.action_undo, + 3000 + ) + + mode.finish() + } + else -> return false + } + return true + } + + /** + * Called when an action mode is about to be exited and destroyed. + * + * @param mode The current ActionMode being destroyed. + */ + override fun onDestroyActionMode(mode: ActionMode) { + // Reset adapter to single selection + adapter?.mode = SelectableAdapter.Mode.IDLE + adapter?.clearSelection() + actionMode = null + } + + /** + * Called when an item in the list is clicked. + * + * @param position The position of the clicked item. + * @return true if this click should enable selection mode. + */ + override fun onItemClick(view: View, position: Int): Boolean { + // Check if action mode is initialized and selected item exist. + return if (actionMode != null && position != RecyclerView.NO_POSITION) { + toggleSelection(position) + true + } else { + false + } + } + + /** + * Called when an item in the list is long clicked. + * + * @param position The position of the clicked item. + */ + override fun onItemLongClick(position: Int) { + val activity = activity as? AppCompatActivity ?: return + + // Check if action mode is initialized. + if (actionMode == null) { + // Initialize action mode + actionMode = activity.startSupportActionMode(this) + } + + // Set item as selected + toggleSelection(position) + } + + /** + * Toggle the selection state of an item. + * If the item was the last one in the selection and is unselected, the ActionMode is finished. + * + * @param position The position of the item to toggle. + */ + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + + // Mark the position selected + adapter.toggleSelection(position) + + if (adapter.selectedItemCount == 0) { + actionMode?.finish() + } else { + actionMode?.invalidate() + } + } + + /** + * Called when the undo action is clicked in the snackbar. + * + * @param action The action performed. + */ + override fun onActionCanceled(action: Int, positions: MutableList?) { + adapter?.restoreDeletedItems() + undoHelper = null + } + + /** + * Called when the time to restore the items expires. + * + * @param action The action performed. + * @param event The event that triggered the action + */ + override fun onActionConfirmed(action: Int, event: Int) { + val adapter = adapter ?: return + presenter.deleteRepos(adapter.deletedItems.map { it.repo }) + undoHelper = null + } + + /** + * Creates a new repo with the given name. + * + * @param name The name of the new repo. + */ + override fun createRepo(name: String) { + presenter.createRepo(name) + } + + /** + * Called from the presenter when a repo already exists. + */ + fun onRepoExistsError() { + activity?.toast(R.string.error_repo_exists) + } + + /** + * Called from the presenter when a invalid repo is made + */ + fun onRepoInvalidNameError() { + activity?.toast(R.string.invalid_repo_name) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt new file mode 100644 index 000000000..1f2c4a5f0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt @@ -0,0 +1,51 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.input.input +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +/** + * Dialog to create a new repo for the library. + */ +class RepoCreateDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : RepoCreateDialog.Listener { + + /** + * Name of the new repo. Value updated with each input from the user. + */ + private var currentName = "" + + constructor(target: T) : this() { + targetController = target + } + + /** + * Called when creating the dialog for this controller. + * + * @param savedViewState The saved state of this dialog. + * @return a new dialog instance. + */ + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog(activity!!) + .title(R.string.action_add_repo) + .message(R.string.action_add_repo_message) + .negativeButton(android.R.string.cancel) + .input( + hint = resources?.getString(R.string.name), + prefill = currentName + ) { _, input -> + currentName = input.toString() + } + .positiveButton(android.R.string.ok) { + (targetController as? Listener)?.createRepo(currentName) + } + } + + interface Listener { + fun createRepo(name: String) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt new file mode 100644 index 000000000..20302f755 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import android.view.View +import androidx.core.view.isVisible +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.databinding.CategoriesItemBinding + +/** + * Holder used to display repo items. + * + * @param view The view used by repo items. + * @param adapter The adapter containing this holder. + */ +class RepoHolder(view: View, val adapter: RepoAdapter) : FlexibleViewHolder(view, adapter) { + + private val binding = CategoriesItemBinding.bind(view) + + /** + * Binds this holder with the given category. + * + * @param category The category to bind. + */ + fun bind(category: String) { + // Set capitalized title. + binding.title.text = category + binding.reorder.isVisible = false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt new file mode 100644 index 000000000..3fb6f7b27 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt @@ -0,0 +1,62 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R + +/** + * Repo item for a recycler view. + */ +class RepoItem(val repo: String) : AbstractFlexibleItem() { + + /** + * Whether this item is currently selected. + */ + var isSelected = false + + /** + * Returns the layout resource for this item. + */ + override fun getLayoutRes(): Int { + return R.layout.categories_item + } + + /** + * Returns a new view holder for this item. + * + * @param view The view of this item. + * @param adapter The adapter of this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): RepoHolder { + return RepoHolder(view, adapter as RepoAdapter) + } + + /** + * Binds the given view holder with this item. + * + * @param adapter The adapter of this item. + * @param holder The holder to bind. + * @param position The position of this item in the adapter. + * @param payloads List of partial changes. + */ + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: RepoHolder, + position: Int, + payloads: List? + ) { + holder.bind(repo) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return false + } + + override fun hashCode(): Int { + return repo.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt new file mode 100644 index 000000000..135c7c299 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt @@ -0,0 +1,89 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import android.os.Bundle +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of [RepoController]. Used to manage the repos for the extensions. + */ +class RepoPresenter( + private val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + val scope = CoroutineScope(Job() + Dispatchers.Main) + + /** + * List containing repos. + */ + private var repos: List = emptyList() + + /** + * Called when the presenter is created. + * + * @param savedState The saved state of this presenter. + */ + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + preferences.extensionRepos().asFlow().onEach { repos -> + this.repos = repos.toList().sortedBy { it.toLowerCase() } + + Observable.just(this.repos) + .map { it.map(::RepoItem) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(RepoController::setRepos) + }.launchIn(scope) + } + + /** + * Creates and adds a new repo to the database. + * + * @param name The name of the repo to create. + */ + fun createRepo(name: String) { + // Do not allow duplicate repos. + if (repoExists(name)) { + Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoExistsError() }) + return + } + + // Do not allow invalid formats + if (!name.matches(repoRegex)) { + Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoInvalidNameError() }) + return + } + + preferences.extensionRepos().set((repos + name).toSet()) + } + + /** + * Deletes the given repos from the database. + * + * @param repos The list of repos to delete. + */ + fun deleteRepos(repos: List) { + preferences.extensionRepos().set( + this.repos.filterNot { it in repos }.toSet() + ) + } + + /** + * Returns true if a repo with the given name already exists. + */ + private fun repoExists(name: String): Boolean { + return repos.any { it.equals(name, true) } + } + + companion object { + val repoRegex = """^[a-zA-Z-_.]*?\/[a-zA-Z-_.]*?$""".toRegex() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index b0e73dfd8..c0725f030 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.category.repos.RepoController import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryController import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.infoPreference @@ -84,6 +85,19 @@ class SettingsBrowseController : SettingsController() { true } } + // SY --> + preference { + key = "pref_edit_extension_repos" + titleRes = R.string.action_edit_repos + + val catCount = preferences.extensionRepos().get().count() + summary = context.resources.getQuantityString(R.plurals.num_repos, catCount, catCount) + + onClick { + router.pushController(RepoController().withFadeTransaction()) + } + } + // SY <-- } preferenceCategory { diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index df613c4e8..742c130d1 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -314,6 +314,20 @@ Redundant This extension is redundant and will not be used inside this version of Tachiyomi. + + You have no additional repos. Tap the plus button to create one for adding external extensions. + Add repo + Add additional repos to Tachiyomi, the format of a repo is \'username/repo\' , with username being the repo owner, and repo being the repo name + Edit repos + + %d additional repo + %d additional repos + + This repo already exists! + Repo deleted + Invalid category name + Repo source + Select sources Select none