From 69006b5872e781a381ac1cddf6aeb1c8b8f102d5 Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Wed, 3 Mar 2021 10:34:43 +0900 Subject: [PATCH] add dialog to choose addons to update --- ftl/qt/addons.ftl | 1 + qt/aqt/addons.py | 147 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 15 deletions(-) diff --git a/ftl/qt/addons.ftl b/ftl/qt/addons.ftl index 43894125d..07ed57887 100644 --- a/ftl/qt/addons.ftl +++ b/ftl/qt/addons.ftl @@ -64,3 +64,4 @@ addons-delete-the-numd-selected-addon = [one] Delete the { $count } selected add-on? *[other] Delete the { $count } selected add-ons? } +addons-choose-update-window-title = Update Add-ons diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 0166276c9..9957780de 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -10,6 +10,7 @@ import zipfile from collections import defaultdict from concurrent.futures import Future from dataclasses import dataclass +from datetime import datetime from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import parse_qs, urlparse from zipfile import ZipFile @@ -551,19 +552,19 @@ class AddonManager: if updated: self.write_addon_meta(addon) - def updates_required(self, items: List[UpdateInfo]) -> List[int]: + def updates_required(self, items: List[UpdateInfo]) -> List[UpdateInfo]: """Return ids of add-ons requiring an update.""" need_update = [] for item in items: addon = self.addon_meta(str(item.id)) # update if server mtime is newer if not addon.is_latest(item.suitable_branch_last_modified): - need_update.append(item.id) + need_update.append(item) elif not addon.compatible() and item.suitable_branch_last_modified > 0: # Addon is currently disabled, and a suitable branch was found on the # server. Ignore our stored mtime (which may have been set incorrectly # in the past) and require an update. - need_update.append(item.id) + need_update.append(item) return need_update @@ -1132,6 +1133,128 @@ def download_addons( ###################################################################### +class ChooseAddonsToUpdateList(QListWidget): + ADDON_ID_ROLE = 101 + + def __init__( + self, + parent: QWidget, + mgr: AddonManager, + updated_addons: List[UpdateInfo], + ) -> None: + QListWidget.__init__(self, parent) + self.mgr = mgr + self.updated_addons = sorted( + updated_addons, key=lambda addon: addon.suitable_branch_last_modified + ) + self.setup() + qconnect(self.itemClicked, self.toggle_check) + qconnect(self.itemDoubleClicked, self.double_click) + + def setup(self) -> None: + check_state = Qt.Unchecked + header_item = QListWidgetItem("", self) + header_item.setFlags(Qt.ItemFlag(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)) + header_item.setBackground(Qt.lightGray) + header_item.setCheckState(check_state) + self.header_item = header_item + for update_info in self.updated_addons: + addon_id = update_info.id + addon_name = self.mgr.addon_meta(str(addon_id)).human_name() + update_timestamp = update_info.suitable_branch_last_modified + update_time = datetime.fromtimestamp(update_timestamp) + + addon_label = f"{update_time:%Y-%m-%d} {addon_name}" + item = QListWidgetItem(addon_label, self) + # Not user checkable because it overlaps with itemClicked signal + item.setFlags(Qt.ItemFlag(Qt.ItemIsEnabled)) + item.setCheckState(check_state) + item.setData(self.ADDON_ID_ROLE, addon_id) + + def toggle_check(self, item: QListWidgetItem) -> None: + if item == self.header_item: + if item.checkState() == Qt.Checked: + check = Qt.Checked + else: + check = Qt.Unchecked + self.check_all_items(check) + return + # Normal Item + if item.checkState() == Qt.Checked: + item.setCheckState(Qt.Unchecked) + self.header_item.setCheckState(Qt.Unchecked) + else: + item.setCheckState(Qt.Checked) + if self.every_item_is_checked(): + self.header_item.setCheckState(Qt.Checked) + + def double_click(self, item: QListWidgetItem) -> None: + if item == self.header_item: + if item.checkState() == Qt.Checked: + check = Qt.Unchecked + else: + check = Qt.Checked + self.header_item.setCheckState(check) + self.check_all_items(check) + + def check_all_items(self, check: Qt.CheckState = Qt.Checked) -> None: + for i in range(1, self.count()): + self.item(i).setCheckState(check) + + def every_item_is_checked(self) -> bool: + for i in range(1, self.count()): + item = self.item(i) + if item.checkState() == Qt.Unchecked: + return False + return True + + def get_selected_addon_ids(self) -> List[int]: + addon_ids = [] + for i in range(1, self.count()): + item = self.item(i) + if item.checkState() == Qt.Checked: + addon_ids.append(item.data(self.ADDON_ID_ROLE)) + return addon_ids + + +class ChooseAddonsToUpdateDialog(QDialog): + def __init__( + self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo] + ) -> None: + QDialog.__init__(self, parent) + self.setWindowTitle(tr(TR.ADDONS_CHOOSE_UPDATE_WINDOW_TITLE)) + self.setWindowModality(Qt.WindowModal) + self.mgr = mgr + self.updated_addons = updated_addons + self.setup() + restoreGeom(self, "addonsChooseUpdate") + + def setup(self) -> None: + layout = QVBoxLayout() + label = QLabel(tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE)) + layout.addWidget(label) + addons_list_widget = ChooseAddonsToUpdateList( + self, self.mgr, self.updated_addons + ) + layout.addWidget(addons_list_widget) + self.addons_list_widget = addons_list_widget + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # type: ignore + qconnect(button_box.button(QDialogButtonBox.Ok).clicked, self.accept) + qconnect(button_box.button(QDialogButtonBox.Cancel).clicked, self.reject) + layout.addWidget(button_box) + self.setLayout(layout) + + def ask(self) -> List[int]: + "Returns a list of selected addons' ids" + ret = self.exec_() + saveGeom(self, "addonsChooseUpdate") + if ret == QDialog.Accepted: + return self.addons_list_widget.get_selected_addon_ids() + else: + return [] + + def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]: """Fetch update info from AnkiWeb in one or more batches.""" all_info: List[Dict] = [] @@ -1239,31 +1362,25 @@ def handle_update_info( update_info = mgr.extract_update_info(items) mgr.update_supported_versions(update_info) - updated_ids = mgr.updates_required(update_info) + updated_addons = mgr.updates_required(update_info) - if not updated_ids: + if not updated_addons: on_done([]) return - prompt_to_update(parent, mgr, client, updated_ids, on_done) + prompt_to_update(parent, mgr, client, updated_addons, on_done) def prompt_to_update( parent: QWidget, mgr: AddonManager, client: HttpClient, - ids: List[int], + updated_addons: List[UpdateInfo], on_done: Callable[[List[DownloadLogEntry]], None], ) -> None: - names = map(lambda x: mgr.addonName(str(x)), ids) - if not askUser( - tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE) - + "\n\n" - + "\n".join(names) - ): - # on_done is not called if the user cancels + ids = ChooseAddonsToUpdateDialog(parent, mgr, updated_addons).ask() + if not ids: return - download_addons(parent, mgr, ids, on_done, client)