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 createLegacyBackup = "create_legacy_backup"
|
||||||
|
|
||||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
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 sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
||||||
|
|
||||||
fun dontDeleteFromCategories() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet())
|
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) {
|
return withContext(Dispatchers.IO) {
|
||||||
val response = service.getRepo()
|
val response = service.getRepo()
|
||||||
parseResponse(response)
|
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> {
|
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
||||||
@ -58,7 +63,7 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
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
|
return json
|
||||||
.filter { element ->
|
.filter { element ->
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||||
@ -73,14 +78,16 @@ internal class ExtensionGithubApi {
|
|||||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
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 {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
@ -7,6 +7,7 @@ import kotlinx.serialization.json.JsonArray
|
|||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Url
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,6 +28,8 @@ interface ExtensionGithubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json")
|
// SY -->
|
||||||
suspend fun getRepo(): JsonArray
|
@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 lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String
|
val iconUrl: String,
|
||||||
|
// SY -->
|
||||||
|
val repoUrl: String
|
||||||
|
// SY <--
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
data class Untrusted(
|
data class Untrusted(
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
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.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
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)
|
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||||
// SY -->
|
// SY -->
|
||||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
|
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 <--
|
// SY <--
|
||||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
|
||||||
else -> ""
|
|
||||||
}.toUpperCase()
|
}.toUpperCase()
|
||||||
|
|
||||||
GlideApp.with(itemView.context).clear(binding.image)
|
GlideApp.with(itemView.context).clear(binding.image)
|
||||||
@ -57,6 +58,23 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
bindButton(item)
|
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")
|
@Suppress("ResourceType")
|
||||||
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
|
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
|
||||||
isEnabled = true
|
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.data.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
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.ui.category.sources.SourceCategoryController
|
||||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||||
import eu.kanade.tachiyomi.util.preference.infoPreference
|
import eu.kanade.tachiyomi.util.preference.infoPreference
|
||||||
@ -84,6 +85,19 @@ class SettingsBrowseController : SettingsController() {
|
|||||||
true
|
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 {
|
preferenceCategory {
|
||||||
|
@ -314,6 +314,20 @@
|
|||||||
<string name="ext_redundant">Redundant</string>
|
<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>
|
<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 -->
|
<!-- Migration -->
|
||||||
<string name="select_sources">Select sources</string>
|
<string name="select_sources">Select sources</string>
|
||||||
<string name="select_none">Select none</string>
|
<string name="select_none">Select none</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user