diff --git a/novelwriter/config.py b/novelwriter/config.py index bfcc73a7..2a83da59 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -32,6 +32,7 @@ import sys from datetime import datetime from pathlib import Path from time import time +from typing import TYPE_CHECKING from PyQt5.QtCore import ( PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo, @@ -47,6 +48,9 @@ from novelwriter.common import ( from novelwriter.constants import nwFiles, nwUnicode from novelwriter.error import formatException, logException +if TYPE_CHECKING: # pragma: no cover + from novelwriter.core.projectdata import NWProjectData + logger = logging.getLogger(__name__) @@ -845,29 +849,30 @@ class RecentProjects: def __init__(self, config: Config) -> None: self._conf = config - self._data = {} + self._data: dict[str, dict[str, str | int]] = {} + self._map: dict[str, str] = {} return def loadCache(self) -> bool: """Load the cache file for recent projects.""" self._data = {} - + self._map = {} cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE) if cacheFile.is_file(): try: with open(cacheFile, mode="r", encoding="utf-8") as inFile: data = json.load(inFile) for path, entry in data.items(): - self._data[path] = { - "title": entry.get("title", ""), - "words": entry.get("words", 0), - "time": entry.get("time", 0), - } + puuid = str(entry.get("uuid", "")) + title = str(entry.get("title", "")) + words = checkInt(entry.get("words", 0), 0) + saved = checkInt(entry.get("time", 0), 0) + if path and title: + self._setEntry(puuid, path, title, words, saved) except Exception: logger.error("Could not load recent project cache") logException() return False - return True def saveCache(self) -> bool: @@ -882,7 +887,6 @@ class RecentProjects: logger.error("Could not save recent project cache") logException() return False - return True def listEntries(self) -> list[tuple[str, str, int, int]]: @@ -892,14 +896,15 @@ class RecentProjects: for k, e in self._data.items() ] - def update(self, path: str | Path, title: str, words: int, saved: float | int) -> None: + def update(self, path: str | Path, data: NWProjectData, saved: float | int) -> None: """Add or update recent cache information on a given project.""" - self._data[str(path)] = { - "title": title, - "words": int(words), - "time": int(saved), - } - self.saveCache() + try: + if (remove := self._map.get(data.uuid)) and (remove != str(path)): + self.remove(remove) + self._setEntry(data.uuid, str(path), data.name, sum(data.currCounts), int(saved)) + self.saveCache() + except Exception: + pass return def remove(self, path: str | Path) -> None: @@ -909,6 +914,13 @@ class RecentProjects: self.saveCache() return + def _setEntry(self, puuid: str, path: str, title: str, words: int, saved: int) -> None: + """Set an entry in the recent projects record.""" + self._data[path] = {"uuid": puuid, "title": title, "words": words, "time": saved} + if puuid: + self._map[puuid] = path + return + class RecentPaths: diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index e568e1cc..f16cd49d 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -353,9 +353,7 @@ class NWProject: # Update recent projects if storePath := self._storage.storagePath: - CONFIG.recentProjects.update( - storePath, self._data.name, sum(self._data.initCounts), time() - ) + CONFIG.recentProjects.update(storePath, self._data, time()) # Check the project tree consistency # This also handles any orphaned files found @@ -421,9 +419,7 @@ class NWProject: # Update recent projects if storagePath := self._storage.storagePath: - CONFIG.recentProjects.update( - storagePath, self._data.name, sum(self._data.currCounts), saveTime - ) + CONFIG.recentProjects.update(storagePath, self._data, saveTime) SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name)) self.setProjectChanged(False) diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 864f9910..46d40634 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -31,6 +31,7 @@ import pytest from novelwriter import CONFIG from novelwriter.config import Config, RecentPaths, RecentProjects from novelwriter.constants import nwFiles +from novelwriter.core.project import NWProject from tests.mocked import MockApp, causeOSError from tests.tools import cmpFiles, writeFile @@ -382,7 +383,7 @@ def testBaseConfig_Internal(monkeypatch, fncPath): @pytest.mark.base -def testBaseConfig_RecentCache(monkeypatch, tstPaths): +def testBaseConfig_RecentCache(monkeypatch, tstPaths, nwGUI): """Test recent cache file.""" cacheFile = tstPaths.cnfDir / nwFiles.RECENT_FILE recent = RecentProjects(CONFIG) @@ -396,8 +397,18 @@ def testBaseConfig_RecentCache(monkeypatch, tstPaths): pathOne = tstPaths.cnfDir / "projPathOne" / nwFiles.PROJ_FILE pathTwo = tstPaths.cnfDir / "projPathTwo" / nwFiles.PROJ_FILE - recent.update(pathOne, "Proj One", 100, 1600002000) - recent.update(pathTwo, "Proj Two", 200, 1600005600) + prjOne = NWProject() + prjTwo = NWProject() + + prjOne.data.setUuid(None) + prjTwo.data.setUuid(None) + prjOne.data.setName("Proj One") + prjTwo.data.setName("Proj Two") + prjOne.data.setCurrCounts(100, 0) + prjTwo.data.setCurrCounts(200, 0) + + recent.update(pathOne, prjOne.data, 1600002000) + recent.update(pathTwo, prjTwo.data, 1600005600) assert recent.listEntries() == [ (str(pathOne), "Proj One", 100, 1600002000), (str(pathTwo), "Proj Two", 200, 1600005600), @@ -429,6 +440,14 @@ def testBaseConfig_RecentCache(monkeypatch, tstPaths): (str(pathTwo), "Proj Two", 200, 1600005600), ] + # Pass Invalid + recent.update(None, None, 0) # type: ignore + recent.update(None, None, 0) # type: ignore + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + # Remove Non-Existent Entry recent.remove("stuff") assert recent.listEntries() == [ diff --git a/tests/test_tools/test_tools_welcome.py b/tests/test_tools/test_tools_welcome.py index e827d0cb..6e199dc6 100644 --- a/tests/test_tools/test_tools_welcome.py +++ b/tests/test_tools/test_tools_welcome.py @@ -31,6 +31,7 @@ from pytestqt.qtbot import QtBot from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles +from novelwriter.core.projectdata import NWProjectData from novelwriter.enum import nwItemClass from novelwriter.tools.welcome import GuiWelcome from novelwriter.types import QtMouseLeft @@ -72,8 +73,18 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath): monkeypatch.setattr(QMenu, "exec", lambda *a: None) monkeypatch.setattr(QMenu, "setParent", lambda *a: None) - CONFIG.recentProjects.update("/stuff/project_one", "Project One", 12345, 1690000000) - CONFIG.recentProjects.update("/stuff/project_two", "Project Two", 54321, 1700000000) + data1 = NWProjectData(SHARED.project) + data2 = NWProjectData(SHARED.project) + + data1.setUuid(None) + data2.setUuid(None) + data1.setName("Project One") + data2.setName("Project Two") + data1.setCurrCounts(12345, 0) + data2.setCurrCounts(54321, 0) + + CONFIG.recentProjects.update("/stuff/project_one", data1, 1690000000) + CONFIG.recentProjects.update("/stuff/project_two", data2, 1700000000) dateOne = CONFIG.localDate(datetime.fromtimestamp(1700000000)) dateTwo = CONFIG.localDate(datetime.fromtimestamp(1690000000))