Merge pull request #1050 from BlueGreenMagick/choose-addon-to-update
Choose addon to update
This commit is contained in:
commit
1e7405296f
@ -64,3 +64,4 @@ addons-delete-the-numd-selected-addon =
|
|||||||
[one] Delete the { $count } selected add-on?
|
[one] Delete the { $count } selected add-on?
|
||||||
*[other] Delete the { $count } selected add-ons?
|
*[other] Delete the { $count } selected add-ons?
|
||||||
}
|
}
|
||||||
|
addons-choose-update-window-title = Update Add-ons
|
||||||
|
196
qt/aqt/addons.py
196
qt/aqt/addons.py
@ -10,6 +10,7 @@ import zipfile
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
|
from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@ -104,6 +105,7 @@ class AddonMeta:
|
|||||||
max_point_version: int
|
max_point_version: int
|
||||||
branch_index: int
|
branch_index: int
|
||||||
human_version: Optional[str]
|
human_version: Optional[str]
|
||||||
|
update_enabled: bool
|
||||||
|
|
||||||
def human_name(self) -> str:
|
def human_name(self) -> str:
|
||||||
return self.provided_name or self.dir_name
|
return self.provided_name or self.dir_name
|
||||||
@ -139,6 +141,7 @@ class AddonMeta:
|
|||||||
max_point_version=json_meta.get("max_point_version", 0) or 0,
|
max_point_version=json_meta.get("max_point_version", 0) or 0,
|
||||||
branch_index=json_meta.get("branch_index", 0) or 0,
|
branch_index=json_meta.get("branch_index", 0) or 0,
|
||||||
human_version=json_meta.get("human_version"),
|
human_version=json_meta.get("human_version"),
|
||||||
|
update_enabled=json_meta.get("update_enabled", True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -242,6 +245,7 @@ class AddonManager:
|
|||||||
json_obj["branch_index"] = addon.branch_index
|
json_obj["branch_index"] = addon.branch_index
|
||||||
if addon.human_version is not None:
|
if addon.human_version is not None:
|
||||||
json_obj["human_version"] = addon.human_version
|
json_obj["human_version"] = addon.human_version
|
||||||
|
json_obj["update_enabled"] = addon.update_enabled
|
||||||
|
|
||||||
self.writeAddonMeta(addon.dir_name, json_obj)
|
self.writeAddonMeta(addon.dir_name, json_obj)
|
||||||
|
|
||||||
@ -551,19 +555,19 @@ class AddonManager:
|
|||||||
if updated:
|
if updated:
|
||||||
self.write_addon_meta(addon)
|
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."""
|
"""Return ids of add-ons requiring an update."""
|
||||||
need_update = []
|
need_update = []
|
||||||
for item in items:
|
for item in items:
|
||||||
addon = self.addon_meta(str(item.id))
|
addon = self.addon_meta(str(item.id))
|
||||||
# update if server mtime is newer
|
# update if server mtime is newer
|
||||||
if not addon.is_latest(item.suitable_branch_last_modified):
|
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:
|
elif not addon.compatible() and item.suitable_branch_last_modified > 0:
|
||||||
# Addon is currently disabled, and a suitable branch was found on the
|
# Addon is currently disabled, and a suitable branch was found on the
|
||||||
# server. Ignore our stored mtime (which may have been set incorrectly
|
# server. Ignore our stored mtime (which may have been set incorrectly
|
||||||
# in the past) and require an update.
|
# in the past) and require an update.
|
||||||
need_update.append(item.id)
|
need_update.append(item)
|
||||||
|
|
||||||
return need_update
|
return need_update
|
||||||
|
|
||||||
@ -1132,6 +1136,163 @@ 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.ignore_check_evt = False
|
||||||
|
self.setup()
|
||||||
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
qconnect(self.itemClicked, self.on_click)
|
||||||
|
qconnect(self.itemChanged, self.on_check)
|
||||||
|
qconnect(self.itemDoubleClicked, self.on_double_click)
|
||||||
|
qconnect(self.customContextMenuRequested, self.on_context_menu)
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
header_item = QListWidgetItem("", self)
|
||||||
|
header_item.setFlags(Qt.ItemFlag(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled))
|
||||||
|
header_item.setBackground(Qt.lightGray)
|
||||||
|
self.header_item = header_item
|
||||||
|
for update_info in self.updated_addons:
|
||||||
|
addon_id = update_info.id
|
||||||
|
addon_meta = self.mgr.addon_meta(str(addon_id))
|
||||||
|
update_enabled = addon_meta.update_enabled
|
||||||
|
addon_name = addon_meta.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))
|
||||||
|
if update_enabled:
|
||||||
|
item.setCheckState(Qt.Checked)
|
||||||
|
else:
|
||||||
|
item.setCheckState(Qt.Unchecked)
|
||||||
|
item.setData(self.ADDON_ID_ROLE, addon_id)
|
||||||
|
self.refresh_header_check_state()
|
||||||
|
|
||||||
|
def bool_to_check(self, check_bool: bool) -> Qt.CheckState:
|
||||||
|
if check_bool:
|
||||||
|
return Qt.Checked
|
||||||
|
else:
|
||||||
|
return Qt.Unchecked
|
||||||
|
|
||||||
|
def checked(self, item: QListWidgetItem) -> bool:
|
||||||
|
return item.checkState() == Qt.Checked
|
||||||
|
|
||||||
|
def on_click(self, item: QListWidgetItem) -> None:
|
||||||
|
if item == self.header_item:
|
||||||
|
return
|
||||||
|
checked = self.checked(item)
|
||||||
|
self.check_item(item, self.bool_to_check(not checked))
|
||||||
|
self.refresh_header_check_state()
|
||||||
|
|
||||||
|
def on_check(self, item: QListWidgetItem) -> None:
|
||||||
|
if self.ignore_check_evt:
|
||||||
|
return
|
||||||
|
if item == self.header_item:
|
||||||
|
self.header_checked(item.checkState())
|
||||||
|
|
||||||
|
def on_double_click(self, item: QListWidgetItem) -> None:
|
||||||
|
if item == self.header_item:
|
||||||
|
checked = self.checked(item)
|
||||||
|
self.check_item(self.header_item, self.bool_to_check(not checked))
|
||||||
|
self.header_checked(self.bool_to_check(not checked))
|
||||||
|
|
||||||
|
def on_context_menu(self, point: QPoint) -> None:
|
||||||
|
item = self.itemAt(point)
|
||||||
|
addon_id = item.data(self.ADDON_ID_ROLE)
|
||||||
|
m = QMenu()
|
||||||
|
a = m.addAction(tr(TR.ADDONS_VIEW_ADDON_PAGE))
|
||||||
|
qconnect(a.triggered, lambda _: openLink(f"{aqt.appShared}info/{addon_id}"))
|
||||||
|
m.exec_(QCursor.pos())
|
||||||
|
|
||||||
|
def check_item(self, item: QListWidgetItem, check: Qt.CheckState) -> None:
|
||||||
|
"call item.setCheckState without triggering on_check"
|
||||||
|
self.ignore_check_evt = True
|
||||||
|
item.setCheckState(check)
|
||||||
|
self.ignore_check_evt = False
|
||||||
|
|
||||||
|
def header_checked(self, check: Qt.CheckState) -> None:
|
||||||
|
for i in range(1, self.count()):
|
||||||
|
self.check_item(self.item(i), check)
|
||||||
|
|
||||||
|
def refresh_header_check_state(self) -> None:
|
||||||
|
for i in range(1, self.count()):
|
||||||
|
item = self.item(i)
|
||||||
|
if not self.checked(item):
|
||||||
|
self.check_item(self.header_item, Qt.Unchecked)
|
||||||
|
return
|
||||||
|
self.check_item(self.header_item, Qt.Checked)
|
||||||
|
|
||||||
|
def get_selected_addon_ids(self) -> List[int]:
|
||||||
|
addon_ids = []
|
||||||
|
for i in range(1, self.count()):
|
||||||
|
item = self.item(i)
|
||||||
|
if self.checked(item):
|
||||||
|
addon_id = item.data(self.ADDON_ID_ROLE)
|
||||||
|
addon_ids.append(addon_id)
|
||||||
|
return addon_ids
|
||||||
|
|
||||||
|
def save_check_state(self) -> None:
|
||||||
|
for i in range(1, self.count()):
|
||||||
|
item = self.item(i)
|
||||||
|
addon_id = item.data(self.ADDON_ID_ROLE)
|
||||||
|
addon_meta = self.mgr.addon_meta(str(addon_id))
|
||||||
|
addon_meta.update_enabled = self.checked(item)
|
||||||
|
self.mgr.write_addon_meta(addon_meta)
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
self.addons_list_widget.save_check_state()
|
||||||
|
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]:
|
def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
|
||||||
"""Fetch update info from AnkiWeb in one or more batches."""
|
"""Fetch update info from AnkiWeb in one or more batches."""
|
||||||
all_info: List[Dict] = []
|
all_info: List[Dict] = []
|
||||||
@ -1164,9 +1325,10 @@ def check_and_prompt_for_updates(
|
|||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||||
|
requested_by_user: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
def on_updates_received(client: HttpClient, items: List[Dict]) -> None:
|
def on_updates_received(client: HttpClient, items: List[Dict]) -> None:
|
||||||
handle_update_info(parent, mgr, client, items, on_done)
|
handle_update_info(parent, mgr, client, items, on_done, requested_by_user)
|
||||||
|
|
||||||
check_for_updates(mgr, on_updates_received)
|
check_for_updates(mgr, on_updates_received)
|
||||||
|
|
||||||
@ -1235,35 +1397,39 @@ def handle_update_info(
|
|||||||
client: HttpClient,
|
client: HttpClient,
|
||||||
items: List[Dict],
|
items: List[Dict],
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||||
|
requested_by_user: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
update_info = mgr.extract_update_info(items)
|
update_info = mgr.extract_update_info(items)
|
||||||
mgr.update_supported_versions(update_info)
|
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([])
|
on_done([])
|
||||||
return
|
return
|
||||||
|
|
||||||
prompt_to_update(parent, mgr, client, updated_ids, on_done)
|
prompt_to_update(parent, mgr, client, updated_addons, on_done, requested_by_user)
|
||||||
|
|
||||||
|
|
||||||
def prompt_to_update(
|
def prompt_to_update(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
client: HttpClient,
|
client: HttpClient,
|
||||||
ids: List[int],
|
updated_addons: List[UpdateInfo],
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||||
|
requested_by_user: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
names = map(lambda x: mgr.addonName(str(x)), ids)
|
if not requested_by_user:
|
||||||
if not askUser(
|
prompt_update = False
|
||||||
tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE)
|
for addon in updated_addons:
|
||||||
+ "\n\n"
|
if mgr.addon_meta(str(addon.id)).update_enabled:
|
||||||
+ "\n".join(names)
|
prompt_update = True
|
||||||
):
|
if not prompt_update:
|
||||||
# on_done is not called if the user cancels
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
ids = ChooseAddonsToUpdateDialog(parent, mgr, updated_addons).ask()
|
||||||
|
if not ids:
|
||||||
|
return
|
||||||
download_addons(parent, mgr, ids, on_done, client)
|
download_addons(parent, mgr, ids, on_done, client)
|
||||||
|
|
||||||
|
|
||||||
|
@ -848,7 +848,10 @@ title="%s" %s>%s</button>""" % (
|
|||||||
|
|
||||||
if elap > 86_400:
|
if elap > 86_400:
|
||||||
check_and_prompt_for_updates(
|
check_and_prompt_for_updates(
|
||||||
self, self.addonManager, self.on_updates_installed
|
self,
|
||||||
|
self.addonManager,
|
||||||
|
self.on_updates_installed,
|
||||||
|
requested_by_user=False,
|
||||||
)
|
)
|
||||||
self.pm.set_last_addon_update_check(intTime())
|
self.pm.set_last_addon_update_check(intTime())
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user