Skip to content

Commit 6d32c29

Browse files
committed
Add novel plugin update, sort and uninstall support
Add UI and presenter support for updating, sorting, and uninstalling novel plugins. Presenter: add updateNovelPlugins and uninstallNovelPlugin, integrate InstalledExtensionsOrder for sorting, and adapt novel plugin list generation to honor sort preference and show update state. BottomSheet: refresh novel plugins when the novel tab opens, handle long-press uninstall for novel plugins, add uninstall confirmation dialog, handle "update all" and sort actions (persisting preference and refreshing). Adapter/Holder/Item: NovelPluginAdapter gains installedSortOrder and listener callbacks for update-all and sort clicks; NovelPluginGroupHolder wires update/sort buttons and reflects header state; NovelPluginGroupItem adds canUpdate and installedSorting fields. NovelPluginManager: add getPluginLastModified to support recently installed/updated sorting.
1 parent 001ec03 commit 6d32c29

6 files changed

Lines changed: 125 additions & 9 deletions

File tree

app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,27 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
312312
}
313313
}
314314

315+
fun updateNovelPlugins(plugins: List<NovelPluginIndex>) {
316+
if (plugins.isEmpty()) return
317+
presenterScope.launch {
318+
plugins.forEach { plugin ->
319+
novelPluginManager.installPlugin(plugin)
320+
}
321+
refreshNovelPlugins()
322+
}
323+
}
324+
325+
fun uninstallNovelPlugin(pluginId: String) {
326+
presenterScope.launch {
327+
novelPluginManager.uninstallPlugin(pluginId)
328+
refreshNovelPlugins()
329+
}
330+
}
331+
315332
private fun toNovelPluginItems(available: List<NovelPluginIndex>): List<NovelPluginItem> {
316333
val context = view?.context ?: return emptyList()
317334
val activeLangs = preferences.enabledLanguages().get()
335+
val installedSortOrder = InstalledExtensionsOrder.fromPreference(preferences)
318336
val items = mutableListOf<NovelPluginItem>()
319337

320338
// Separate installed (with updates) from available
@@ -323,10 +341,29 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
323341
val installedVersion = novelPluginManager.getInstalledVersion(it.id)
324342
installedVersion != null && installedVersion != it.version
325343
}.sortedBy { it.name }
326-
val installedUpToDate = installedPlugins.filter {
344+
val installedUpToDateUnsorted = installedPlugins.filter {
327345
val installedVersion = novelPluginManager.getInstalledVersion(it.id)
328346
installedVersion == null || installedVersion == it.version
329-
}.sortedBy { it.name }
347+
}
348+
val installedUpToDate = when (installedSortOrder) {
349+
InstalledExtensionsOrder.Name -> {
350+
installedUpToDateUnsorted.sortedBy { it.name }
351+
}
352+
InstalledExtensionsOrder.Language -> {
353+
installedUpToDateUnsorted.sortedWith(
354+
compareBy<NovelPluginIndex> { NovelPluginManager.langFromLnReaderLang(it.lang) }
355+
.thenBy { it.name },
356+
)
357+
}
358+
InstalledExtensionsOrder.RecentlyInstalled,
359+
InstalledExtensionsOrder.RecentlyUpdated,
360+
-> {
361+
installedUpToDateUnsorted.sortedWith(
362+
compareByDescending<NovelPluginIndex> { novelPluginManager.getPluginLastModified(it.id) }
363+
.thenBy { it.name },
364+
)
365+
}
366+
}
330367

331368
// Filter available by enabled languages
332369
val availablePlugins = available.filter { plugin ->
@@ -339,6 +376,7 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
339376
val header = NovelPluginGroupItem(
340377
context.getString(MR.plurals._updates_pending, installedWithUpdates.size, installedWithUpdates.size),
341378
installedWithUpdates.size,
379+
canUpdate = true,
342380
)
343381
installedWithUpdates.forEach { plugin ->
344382
items.add(NovelPluginItem(plugin, header, true, novelPluginManager.getInstalledVersion(plugin.id)))
@@ -347,7 +385,11 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
347385

348386
// Installed section
349387
if (installedUpToDate.isNotEmpty()) {
350-
val header = NovelPluginGroupItem(context.getString(MR.strings.installed), installedUpToDate.size)
388+
val header = NovelPluginGroupItem(
389+
context.getString(MR.strings.installed),
390+
installedUpToDate.size,
391+
installedSorting = preferences.installedExtensionsOrder().get(),
392+
)
351393
installedUpToDate.forEach { plugin ->
352394
items.add(NovelPluginItem(plugin, header, true, novelPluginManager.getInstalledVersion(plugin.id)))
353395
}

app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
181181
this@ExtensionBottomSheet.sheetBehavior?.expand()
182182
getFrameLayoutForTab(tab?.position)?.binding?.recycler?.isNestedScrollingEnabled = true
183183
sheetBehavior?.isDraggable = true
184+
if (tab?.position == 1) {
185+
presenter.refreshNovelPlugins()
186+
}
184187
if (!isExpanding) {
185188
getFrameLayoutForTab(tab?.position)?.binding?.recycler?.smoothScrollToTop()
186189
}
@@ -327,10 +330,18 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
327330
}
328331

329332
override fun onItemLongClick(position: Int) {
330-
if (binding.tabs.selectedTabPosition == 0) {
331-
val extension = (extAdapter?.getItem(position) as? ExtensionItem)?.extension ?: return
332-
if (extension is Extension.Installed || extension is Extension.Untrusted) {
333-
uninstallExtension(extension.name, extension.pkgName)
333+
when (binding.tabs.selectedTabPosition) {
334+
0 -> {
335+
val extension = (extAdapter?.getItem(position) as? ExtensionItem)?.extension ?: return
336+
if (extension is Extension.Installed || extension is Extension.Untrusted) {
337+
uninstallExtension(extension.name, extension.pkgName)
338+
}
339+
}
340+
1 -> {
341+
val item = (novelPluginAdapter?.getItem(position) as? NovelPluginItem) ?: return
342+
if (item.isInstalled) {
343+
uninstallNovelPlugin(item.plugin.name, item.plugin.id)
344+
}
334345
}
335346
}
336347
}
@@ -471,6 +482,16 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
471482
}
472483
}
473484

485+
private fun uninstallNovelPlugin(pluginName: String, pluginId: String) {
486+
controller.activity!!.materialAlertDialog()
487+
.setTitle(pluginName)
488+
.setPositiveButton(MR.strings.remove) { _, _ ->
489+
presenter.uninstallNovelPlugin(pluginId)
490+
}
491+
.setNegativeButton(AR.string.cancel, null)
492+
.show()
493+
}
494+
474495
fun setCanInstallPrivately(installPrivately: Boolean) {
475496
extAdapter?.installPrivately = installPrivately
476497
}
@@ -512,6 +533,30 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
512533
presenter.installNovelPlugin(item.plugin)
513534
}
514535
}
536+
537+
override fun onNovelUpdateAllClicked(position: Int) {
538+
val header = novelPluginAdapter?.getSectionHeader(position) as? NovelPluginGroupItem ?: return
539+
if (header.canUpdate != true) return
540+
541+
val items = novelPluginAdapter?.getSectionItemPositions(header).orEmpty()
542+
val pluginsToUpdate = items.mapNotNull { index ->
543+
val pluginItem = novelPluginAdapter?.getItem(index) as? NovelPluginItem ?: return@mapNotNull null
544+
if (pluginItem.isInstalled && pluginItem.hasUpdate) pluginItem.plugin else null
545+
}
546+
presenter.updateNovelPlugins(pluginsToUpdate)
547+
}
548+
549+
override fun onNovelSortClicked(view: TextView, position: Int) {
550+
view.popupMenu(
551+
InstalledExtensionsOrder.entries.map { it.value to it.nameRes },
552+
presenter.preferences.installedExtensionsOrder().get(),
553+
) {
554+
presenter.preferences.installedExtensionsOrder().set(itemId)
555+
novelPluginAdapter?.installedSortOrder = itemId
556+
view.setText(InstalledExtensionsOrder.fromValue(itemId).nameRes)
557+
presenter.refreshNovelPlugins()
558+
}
559+
}
515560
// NOVEL <--
516561

517562
private inner class TabbedSheetAdapter : RecyclerViewPagerAdapter() {

app/src/main/java/hayai/novel/plugin/NovelPluginManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ class NovelPluginManager(
254254
return _installedPluginsFlow.value.find { it.id == pluginId }?.version
255255
}
256256

257+
fun getPluginLastModified(pluginId: String): Long {
258+
val pluginDir = File(pluginsDir, pluginId)
259+
return if (pluginDir.exists()) pluginDir.lastModified() else 0L
260+
}
261+
257262
private fun getUserAgent(): String {
258263
return try {
259264
android.webkit.WebSettings.getDefaultUserAgent(context)

app/src/main/java/hayai/novel/ui/NovelPluginAdapter.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package hayai.novel.ui
22

33
import eu.davidea.flexibleadapter.FlexibleAdapter
44
import eu.davidea.flexibleadapter.items.IFlexible
5+
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
6+
import uy.kohesive.injekt.injectLazy
7+
import android.widget.TextView
58

69
/**
710
* Adapter for the novel plugins list in the browse bottom sheet.
@@ -10,11 +13,17 @@ import eu.davidea.flexibleadapter.items.IFlexible
1013
class NovelPluginAdapter(val listener: OnButtonClickListener) :
1114
FlexibleAdapter<IFlexible<*>>(null, listener, true) {
1215

16+
private val preferences: PreferencesHelper by injectLazy()
17+
18+
var installedSortOrder = preferences.installedExtensionsOrder().get()
19+
1320
init {
1421
setDisplayHeadersAtStartUp(true)
1522
}
1623

1724
interface OnButtonClickListener {
1825
fun onNovelPluginButtonClick(position: Int)
26+
fun onNovelUpdateAllClicked(position: Int)
27+
fun onNovelSortClicked(view: TextView, position: Int)
1928
}
2029
}

app/src/main/java/hayai/novel/ui/NovelPluginGroupHolder.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import androidx.recyclerview.widget.RecyclerView
66
import eu.davidea.flexibleadapter.FlexibleAdapter
77
import eu.davidea.flexibleadapter.items.IFlexible
88
import eu.kanade.tachiyomi.databinding.ExtensionCardHeaderBinding
9+
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
910
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
11+
import eu.kanade.tachiyomi.util.view.setText
1012

1113
/**
1214
* Holder for novel plugin group headers.
@@ -19,9 +21,20 @@ class NovelPluginGroupHolder(
1921

2022
private val binding = ExtensionCardHeaderBinding.bind(view)
2123

24+
init {
25+
binding.extButton.setOnClickListener {
26+
(adapter as? NovelPluginAdapter)?.listener?.onNovelUpdateAllClicked(bindingAdapterPosition)
27+
}
28+
binding.extSort.setOnClickListener {
29+
(adapter as? NovelPluginAdapter)?.listener?.onNovelSortClicked(binding.extSort, bindingAdapterPosition)
30+
}
31+
}
32+
2233
fun bind(item: NovelPluginGroupItem) {
2334
binding.title.text = "${item.name} (${item.size})"
24-
binding.extButton.isVisible = false
25-
binding.extSort.isVisible = false
35+
binding.extButton.isVisible = item.canUpdate != null
36+
binding.extButton.isEnabled = item.canUpdate == true
37+
binding.extSort.isVisible = item.installedSorting != null
38+
binding.extSort.setText(InstalledExtensionsOrder.fromValue(item.installedSorting ?: 0).nameRes)
2639
}
2740
}

app/src/main/java/hayai/novel/ui/NovelPluginGroupItem.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import eu.kanade.tachiyomi.R
1414
data class NovelPluginGroupItem(
1515
val name: String,
1616
val size: Int,
17+
var canUpdate: Boolean? = null,
18+
var installedSorting: Int? = null,
1719
) : AbstractHeaderItem<NovelPluginGroupHolder>() {
1820

1921
override fun getLayoutRes(): Int = R.layout.extension_card_header

0 commit comments

Comments
 (0)