-
-
Notifications
You must be signed in to change notification settings - Fork 634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ability to view new add-ons #16728
Changes from all commits
5b16029
a2db450
e73b778
9354028
e6ab46a
bc66d8e
5409e3e
3fa7ecd
e3707ba
6cacda9
2c1ccb8
d158951
6e79517
39d4546
b4a42b3
cd2737b
640e9ae
5ba758b
5f1b64f
3eb80bf
e085bdf
ff97ce7
d427f2d
32b8a9f
4431cf4
35a6228
6d8f8c4
a0a323a
75ead41
9e53cd0
013e732
bc52842
f51e88a
14fa539
8faf944
7fd286e
fcdf66a
a55a8fc
05791d7
cd2a606
fa4d01b
be0bccc
3e46d83
d6c93f8
d45bf26
cde140d
d9e4528
2a1f067
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import os | ||
import pathlib | ||
import threading | ||
from datetime import datetime, timedelta | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Optional, | ||
|
@@ -80,6 +81,7 @@ def terminate(): | |
class _DataManager: | ||
_cacheLatestFilename: str = "_cachedLatestAddons.json" | ||
_cacheCompatibleFilename: str = "_cachedCompatibleAddons.json" | ||
_cacheCompatibleOldFilename: str = "_cachedCompatibleAddons-old.json" | ||
_downloadsPendingInstall: Set[Tuple["AddonListItemVM[_AddonStoreModel]", os.PathLike]] = set() | ||
_downloadsPendingCompletion: Set["AddonListItemVM[_AddonStoreModel]"] = set() | ||
|
||
|
@@ -88,8 +90,10 @@ def __init__(self): | |
self._preferredChannel = Channel.ALL | ||
self._cacheLatestFile = os.path.join(WritePaths.addonStoreDir, _DataManager._cacheLatestFilename) | ||
self._cacheCompatibleFile = os.path.join( | ||
WritePaths.addonStoreDir, | ||
_DataManager._cacheCompatibleFilename, | ||
WritePaths.addonStoreDir, _DataManager._cacheCompatibleFilename | ||
) | ||
self._cacheCompatibleOldFile = os.path.join( | ||
WritePaths.addonStoreDir, _DataManager._cacheCompatibleOldFilename | ||
) | ||
self._installedAddonDataCacheDir = WritePaths.addonsDir | ||
|
||
|
@@ -100,6 +104,7 @@ def __init__(self): | |
|
||
self._latestAddonCache = self._getCachedAddonData(self._cacheLatestFile) | ||
self._compatibleAddonCache = self._getCachedAddonData(self._cacheCompatibleFile) | ||
self._oldAddonCache = self._getCachedAddonData(self._cacheCompatibleOldFile) | ||
self._installedAddonsCache = _InstalledAddonsCache() | ||
# Fetch available add-ons cache early | ||
self._initialiseAvailableAddonsThread = threading.Thread( | ||
|
@@ -114,6 +119,8 @@ def terminate(self): | |
self._initialiseAvailableAddonsThread.join(timeout=1) | ||
if self._initialiseAvailableAddonsThread.is_alive(): | ||
log.debugWarning("initialiseAvailableAddons thread did not terminate immediately") | ||
if self._shouldCacheCompatibleAddonsBackup(): | ||
self._cacheCompatibleAddonsBackup() | ||
|
||
def _getLatestAddonsDataForVersion(self, apiVersion: str) -> Optional[bytes]: | ||
url = _getAddonStoreURL(self._preferredChannel, self._lang, apiVersion) | ||
|
@@ -162,6 +169,52 @@ def _cacheCompatibleAddons(self, addonData: str, cacheHash: Optional[str]): | |
with open(self._cacheCompatibleFile, "w", encoding="utf-8") as cacheFile: | ||
json.dump(cacheData, cacheFile, ensure_ascii=False) | ||
|
||
def _shouldCacheCompatibleAddonsBackup(self) -> bool: | ||
if not os.path.exists(self._cacheCompatibleOldFile): | ||
return True | ||
resetNewAddons = config.conf["addonStore"]["resetNewAddons"] | ||
if resetNewAddons == "startup": | ||
return True | ||
lastBackupTime = os.path.getmtime(self._cacheCompatibleOldFile) | ||
lastBackupDate = datetime.fromtimestamp(lastBackupTime) | ||
nowDate = datetime.now() | ||
diffDate = nowDate - lastBackupDate | ||
if resetNewAddons == "weekly" and diffDate.days >= 7: | ||
return True | ||
if resetNewAddons == "monthly" and diffDate.days >= 30: | ||
return True | ||
return False | ||
|
||
def _getResetNewAddonsDate(self) -> str: | ||
resetNewAddons = config.conf["addonStore"]["resetNewAddons"] | ||
if resetNewAddons == "startup": | ||
# Translators: Message presented in the new add-ons combo box, informing that new add-ons will be reset at startup | ||
return _("Will be reset at startup") | ||
if not os.path.exists(self._cacheCompatibleOldFile): | ||
# Translators: Message presented in the new add-ons combo box, informing that new add-ons will be retrieved when NVDA is restarted | ||
return _("Empty list: NVDA needs to be restarted to retrieve new add-ons") | ||
lastBackupTime = os.path.getmtime(self._cacheCompatibleOldFile) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't read the code, but you should be catching a file not found error here I believe.
|
||
lastBackupDate = datetime.fromtimestamp(lastBackupTime) | ||
formattedLastBackupDate = lastBackupDate.strftime("%x") | ||
if resetNewAddons == "monthly": | ||
timedeltaDays = 31 | ||
else: # weekly | ||
timedeltaDays = 7 | ||
nextResetDate = lastBackupDate + timedelta(days=timedeltaDays) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way this is presenting in the UI, makes it look like it's doing the math inversely. That is, on my system, if I don't choose all, the filtering date appears to be trying to show add-ons that are new between July 4th and August 3rd. I think it should be instead between June 3rd to July 4th. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @XLTechie , thanks for your review. No, add-ons will be considered new from 4 July, and will be reset on August. Add-on metadata doesn't have a timestamp, so NVDA will start saving all compatible add-ons cnsidering them as old add-ons, and add-ons published from 4 Jul will be considered as new add-ons. Hope this helps. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seanbudd how hard would it be to start embedding a generation timestamp in the JSON? I should think, not very. |
||
formattedNextResetDate = nextResetDate.strftime("%x") | ||
return f"{formattedLastBackupDate}-{formattedNextResetDate}" | ||
|
||
def _cacheCompatibleAddonsBackup(self): | ||
if not NVDAState.shouldWriteToDisk(): | ||
return | ||
try: | ||
with open(self._cacheCompatibleFile, "r", encoding="utf-8") as cacheFile: | ||
cacheData = json.load(cacheFile) | ||
except Exception: | ||
log.exception("Invalid add-on store cache") | ||
with open(self._cacheCompatibleOldFile, "w", encoding="utf-8") as cacheFile: | ||
json.dump(cacheData, cacheFile, ensure_ascii=False) | ||
nvdaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def _cacheLatestAddons(self, addonData: str, cacheHash: Optional[str]): | ||
if not NVDAState.shouldWriteToDisk(): | ||
return | ||
|
@@ -287,6 +340,26 @@ def getLatestAddons( | |
return _createAddonGUICollection() | ||
return deepcopy(self._latestAddonCache.cachedAddonData) | ||
|
||
def _checkForNewAddons(self) -> bool: | ||
oldAddons = self._getOldAddons() | ||
compatibleAddons = self.getLatestCompatibleAddons() | ||
installedAddons = self._installedAddonsCache._get_installedAddons() | ||
for channel in compatibleAddons: | ||
for addonId in compatibleAddons[channel]: | ||
compatibleAddon = compatibleAddons[channel][addonId] | ||
if ( | ||
addonId not in oldAddons[channel] | ||
or compatibleAddon.addonVersionNumber != oldAddons[channel][addonId].addonVersionNumber | ||
) and addonId not in installedAddons: | ||
return True | ||
return False | ||
nvdaes marked this conversation as resolved.
Show resolved
Hide resolved
nvdaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def _getOldAddons(self) -> "AddonGUICollectionT": | ||
if self._oldAddonCache is None: | ||
return _createAddonGUICollection() | ||
oldAddons = deepcopy(self._oldAddonCache.cachedAddonData) | ||
return oldAddons | ||
|
||
def _deleteCacheInstalledAddon(self, addonId: str): | ||
addonCachePath = os.path.join(self._installedAddonDataCacheDir, f"{addonId}.json") | ||
if pathlib.Path(addonCachePath).exists(): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nvdaes you may have missed this one, or at least it's not showing as resolved at my end.