Add external repo support
This commit is contained in:
parent
85e30ef6ca
commit
96213900ac
@ -332,4 +332,6 @@ object PreferenceKeys {
|
||||
const val createLegacyBackup = "create_legacy_backup"
|
||||
|
||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
||||
|
||||
const val extensionRepos = "extension_repos"
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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<Extension.Installed> {
|
||||
@ -58,7 +63,7 @@ internal class ExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
private fun parseResponse(json: JsonArray /* SY --> */, repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List<Extension.Available> {
|
||||
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 -->
|
||||
|
@ -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 <--
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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<RepoItem>(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)
|
||||
}
|
||||
}
|
@ -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<CategoriesControllerBinding, RepoPresenter>(),
|
||||
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<RepoItem>) {
|
||||
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<Int>?) {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<T>(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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<RepoHolder>() {
|
||||
|
||||
/**
|
||||
* 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<IFlexible<RecyclerView.ViewHolder>>): 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<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: RepoHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(repo)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return repo.hashCode()
|
||||
}
|
||||
}
|
@ -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<RepoController>() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* List containing repos.
|
||||
*/
|
||||
private var repos: List<String> = 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<String>) {
|
||||
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()
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -314,6 +314,20 @@
|
||||
<string name="ext_redundant">Redundant</string>
|
||||
<string name="redundant_extension_message">This extension is redundant and will not be used inside this version of Tachiyomi.</string>
|
||||
|
||||
<!-- Extension Repos -->
|
||||
<string name="information_empty_repos">You have no additional repos. Tap the plus button to create one for adding external extensions.</string>
|
||||
<string name="action_add_repo">Add repo</string>
|
||||
<string name="action_add_repo_message">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</string>
|
||||
<string name="action_edit_repos">Edit repos</string>
|
||||
<plurals name="num_repos">
|
||||
<item quantity="one">%d additional repo</item>
|
||||
<item quantity="other">%d additional repos</item>
|
||||
</plurals>
|
||||
<string name="error_repo_exists">This repo already exists!</string>
|
||||
<string name="snack_repo_deleted">Repo deleted</string>
|
||||
<string name="invalid_repo_name">Invalid category name</string>
|
||||
<string name="repo_source">Repo source</string>
|
||||
|
||||
<!-- Migration -->
|
||||
<string name="select_sources">Select sources</string>
|
||||
<string name="select_none">Select none</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user