From faa13c18994a5dacaac3d3eae2d302e3d0697147 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 21 Aug 2025 11:44:02 +0530 Subject: [PATCH] Plugin updater: Allow filtering plugins by category --- src/calibre/gui2/dialogs/plugin_updater.py | 120 +++++++++++++++++++-- 1 file changed, 109 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/dialogs/plugin_updater.py b/src/calibre/gui2/dialogs/plugin_updater.py index 5d7387a951..a4bbf99927 100644 --- a/src/calibre/gui2/dialogs/plugin_updater.py +++ b/src/calibre/gui2/dialogs/plugin_updater.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' import datetime import re import traceback +from enum import Enum from qt.core import ( QAbstractItemView, @@ -61,6 +62,47 @@ FILTER_UPDATE_AVAILABLE = 2 FILTER_NOT_INSTALLED = 3 +class Category(Enum): + Store = 'Store' + FileType = 'File Type' + LibraryClosed = 'Library Closed' + Editor = 'Editor' + ConversionInput = 'Conversion Input' + ConversionOutput = 'Conversion Output' + Device = 'Device' + MetadataWriter = 'Metadata Writer' + MetadataReader = 'Metadata Reader' + MetadataSource = 'Metadata Source' + UserInterface = 'GUI' + + @property + def human_name(self) -> str: + match self: + case Category.Store: + return _('Sources of books') + case Category.FileType: + return _('Customize handling of ebooks') + case Category.LibraryClosed: + return _('Actions when closing libraries') + case Category.Editor: + return _('Extend the calibre Editor') + case Category.ConversionInput: + return _('Conversion from extra formats') + case Category.ConversionOutput: + return _('Conversion to extra formats') + case Category.Device: + return _('Devices to manage') + case Category.MetadataWriter: + return _('Set metadata in files') + case Category.MetadataReader: + return _('Get metadata from files') + case Category.MetadataSource: + return _('Download metadata for books') + case Category.UserInterface: + return _('Extend calibre generally') + return self.value + + def get_plugin_updates_available(raise_error=False): ''' API exposed to read whether there are updates available for any @@ -199,6 +241,30 @@ class PluginFilterComboBox(QComboBox): self.addItems(items) +class CategoryFilterComboBox(QComboBox): + + def __init__(self, parent): + QComboBox.__init__(self, parent) + self.addItem(_('All'), None) + for c in Category: + self.addItem(c.human_name, c) + + @property + def filter_value(self) -> str: + v = self.currentData() + if v: + return v.value + return '' + + def set_category(self, c: Category | None) -> None: + if c is None: + self.setCurrentIndex(0) + else: + idx = self.findData(c) + if idx > -1: + self.setCurrentIndex(idx) + + class DisplayPlugin: def __init__(self, plugin): @@ -213,6 +279,7 @@ class DisplayPlugin: self.release_date = datetime.datetime(*tuple(map(int, re.split(r'\D', plugin['last_modified'])))[:6]).date() self.calibre_required_version = tuple(plugin['minimum_calibre_version']) self.author = plugin['author'] + self.category = plugin.get('category', '') self.platforms = plugin['supported_platforms'] self.uninstall_plugins = plugin['uninstall'] or [] self.has_changelog = plugin['history'] @@ -231,6 +298,11 @@ class DisplayPlugin: # filter_text is already lowercase @set_filter_text return filter_text in icu_lower(self.name) # case-insensitive filtering + def category_matches_filter(self, filter_text): + if not filter_text: + return True + return filter_text == self.category + def is_upgrade_available(self): if isinstance(self.installed_version, str): return True @@ -258,18 +330,22 @@ class DisplayPluginSortFilterModel(QSortFilterProxyModel): self.setSortCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self.filter_criteria = FILTER_ALL self.filter_text = '' + self.filter_by_category = '' def filterAcceptsRow(self, sourceRow, sourceParent): index = self.sourceModel().index(sourceRow, 0, sourceParent) display_plugin = self.sourceModel().display_plugins[index.row()] + matches_filters = display_plugin.name_matches_filter(self.filter_text) and display_plugin.category_matches_filter(self.filter_by_category) if self.filter_criteria == FILTER_ALL: - return not (display_plugin.is_deprecated and not display_plugin.is_installed()) and display_plugin.name_matches_filter(self.filter_text) + return ( + not (display_plugin.is_deprecated and not display_plugin.is_installed()) and + matches_filters) if self.filter_criteria == FILTER_INSTALLED: - return display_plugin.is_installed() and display_plugin.name_matches_filter(self.filter_text) + return display_plugin.is_installed() and matches_filters if self.filter_criteria == FILTER_UPDATE_AVAILABLE: - return display_plugin.is_upgrade_available() and display_plugin.name_matches_filter(self.filter_text) + return display_plugin.is_upgrade_available() and matches_filters if self.filter_criteria == FILTER_NOT_INSTALLED: - return not display_plugin.is_installed() and not display_plugin.is_deprecated and display_plugin.name_matches_filter(self.filter_text) + return not display_plugin.is_installed() and not display_plugin.is_deprecated and matches_filters return False def set_filter_criteria(self, filter_value): @@ -280,6 +356,10 @@ class DisplayPluginSortFilterModel(QSortFilterProxyModel): self.filter_text = icu_lower(str(filter_text_value)) self.invalidateFilter() + def set_filter_category(self, filter_text_value): + self.filter_by_category = filter_text_value + self.invalidateFilter() + class DisplayPluginModel(QAbstractTableModel): @@ -459,9 +539,13 @@ class PluginUpdaterDialog(SizePersistedDialog): initial_extra_size = QSize(350, 100) forum_label_text = _('Plugin homepage') - def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE): - SizePersistedDialog.__init__(self, gui, 'Plugin Updater plugin:plugin updater dialog') - self.gui = gui + @property + def gui(self): + from calibre.gui2.ui import get_gui + return get_gui() + + def __init__(self, parent, initial_filter=FILTER_UPDATE_AVAILABLE, initial_category: Category | None = None): + SizePersistedDialog.__init__(self, parent, 'Plugin Updater plugin:plugin updater dialog') self.forum_link = None self.zip_url = None self.model = None @@ -474,7 +558,7 @@ class PluginUpdaterDialog(SizePersistedDialog): except Exception: display_plugins = [] import traceback - error_dialog(self.gui, _('Update Check Failed'), + error_dialog(self.parent(), _('Update Check Failed'), _('Unable to reach the plugin index page.'), det_msg=traceback.format_exc(), show=True) @@ -487,6 +571,8 @@ class PluginUpdaterDialog(SizePersistedDialog): self.plugin_view.selectionModel().currentRowChanged.connect(self._plugin_current_changed) self.plugin_view.doubleClicked.connect(self.install_button.click) self.filter_combo.setCurrentIndex(initial_filter) + if initial_category: + self.category_combo.set_category(initial_category) self._select_and_focus_view() else: self.filter_combo.setEnabled(False) @@ -505,12 +591,18 @@ class PluginUpdaterDialog(SizePersistedDialog): header_layout = QHBoxLayout() layout.addLayout(header_layout) self.filter_combo = PluginFilterComboBox(self) - self.filter_combo.setMinimumContentsLength(20) + self.filter_combo.setMinimumContentsLength(12) self.filter_combo.currentIndexChanged.connect(self._filter_combo_changed) - la = QLabel(_('Filter list of &plugins')+':', self) + la = QLabel(_('&Install type')+':', self) la.setBuddy(self.filter_combo) header_layout.addWidget(la) header_layout.addWidget(self.filter_combo) + self.category_combo = CategoryFilterComboBox(self) + self.category_combo.currentIndexChanged.connect(self._category_combo_changed) + la = QLabel(_('&Category')+':', self) + la.setBuddy(self.filter_combo) + header_layout.addWidget(la) + header_layout.addWidget(self.category_combo) header_layout.addStretch(10) # filter plugins by name @@ -616,7 +708,8 @@ class PluginUpdaterDialog(SizePersistedDialog): def _finished(self, *args): if self.model: update_plugins = list(filter(filter_upgradeable_plugins, self.model.display_plugins)) - self.gui.recalc_update_label(len(update_plugins)) + if self.gui is not None: + self.gui.recalc_update_label(len(update_plugins)) def _plugin_current_changed(self, current, previous): if current.isValid(): @@ -669,6 +762,11 @@ class PluginUpdaterDialog(SizePersistedDialog): self.plugin_view.sortByColumn(0, Qt.SortOrder.AscendingOrder) self._select_and_focus_view() + def _category_combo_changed(self, idx): + self.filter_by_name_lineedit.setText('') # clear the name filter text when a different group was selected + self.proxy_model.set_filter_category(self.category_combo.filter_value) + self._select_and_focus_view() + def _filter_name_lineedit_changed(self, text): self.proxy_model.set_filter_text(text) # set the filter text for filterAcceptsRow