From 854c8050774120aaf668b4bafed3ff9f931fb146 Mon Sep 17 00:00:00 2001 From: python357-1 Date: Mon, 13 Jan 2025 01:16:24 -0600 Subject: [PATCH 1/3] feat: add settings menu --- .python-version | 1 + requirements.txt | 7 +- tagstudio/src/core/driver.py | 15 ++- tagstudio/src/core/library/alchemy/library.py | 14 ++- tagstudio/src/core/settings/__init__.py | 4 + tagstudio/src/core/settings/libsettings.py | 38 +++++++ tagstudio/src/core/settings/tssettings.py | 42 ++++++++ tagstudio/src/core/tscacheddata.py | 53 ++++++++++ tagstudio/src/qt/modals/file_extension.py | 11 +- tagstudio/src/qt/modals/settings_modal.py | 73 +++++++++++++ tagstudio/src/qt/translations.py | 2 +- tagstudio/src/qt/ts_qt.py | 100 ++++++++++++------ tagstudio/src/qt/widgets/migration_modal.py | 7 +- tagstudio/src/qt/widgets/preview_panel.py | 11 ++ tagstudio/src/qt/widgets/video_player.py | 9 +- tagstudio/tests/example_settings.toml | 4 + tagstudio/tests/macros/test_refresh_dir.py | 5 +- tagstudio/tests/test_driver.py | 32 +++--- tagstudio/tests/test_json_migration.py | 5 +- tagstudio/tests/test_library.py | 4 +- tagstudio/tests/test_settings.py | 13 +++ 21 files changed, 364 insertions(+), 86 deletions(-) create mode 100644 .python-version create mode 100644 tagstudio/src/core/settings/__init__.py create mode 100644 tagstudio/src/core/settings/libsettings.py create mode 100644 tagstudio/src/core/settings/tssettings.py create mode 100644 tagstudio/src/core/tscacheddata.py create mode 100644 tagstudio/src/qt/modals/settings_modal.py create mode 100644 tagstudio/tests/example_settings.toml create mode 100644 tagstudio/tests/test_settings.py diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..04e207918 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.8 diff --git a/requirements.txt b/requirements.txt index 9eb290390..5555f09e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,9 @@ PySide6==6.8.0.1 rawpy==0.22.0 SQLAlchemy==2.0.34 structlog==24.4.0 -typing_extensions>=3.10.0.0,<=4.11.0 +typing_extensions ujson>=5.8.0,<=5.9.0 -vtf2img==0.1.0 \ No newline at end of file +vtf2img==0.1.0 +toml==0.10.2 +appdirs==1.4.4 +pydantic==2.10.4 diff --git a/tagstudio/src/core/driver.py b/tagstudio/src/core/driver.py index 1f7dd4f79..1f2f10a45 100644 --- a/tagstudio/src/core/driver.py +++ b/tagstudio/src/core/driver.py @@ -1,16 +1,17 @@ from pathlib import Path import structlog -from PySide6.QtCore import QSettings from src.core.constants import TS_FOLDER_NAME -from src.core.enums import SettingItems from src.core.library.alchemy.library import LibraryStatus +from src.core.settings import TSSettings +from src.core.tscacheddata import TSCachedData logger = structlog.get_logger(__name__) class DriverMixin: - settings: QSettings + settings: TSSettings + cache: TSCachedData def evaluate_path(self, open_path: str | None) -> LibraryStatus: """Check if the path of library is valid.""" @@ -20,17 +21,15 @@ def evaluate_path(self, open_path: str | None) -> LibraryStatus: if not library_path.exists(): logger.error("Path does not exist.", open_path=open_path) return LibraryStatus(success=False, message="Path does not exist.") - elif self.settings.value( - SettingItems.START_LOAD_LAST, defaultValue=True, type=bool - ) and self.settings.value(SettingItems.LAST_LIBRARY): - library_path = Path(str(self.settings.value(SettingItems.LAST_LIBRARY))) + elif self.settings.open_last_loaded_on_startup and self.cache.last_library: + library_path = Path(str(self.cache.last_library)) if not (library_path / TS_FOLDER_NAME).exists(): logger.error( "TagStudio folder does not exist.", library_path=library_path, ts_folder=TS_FOLDER_NAME, ) - self.settings.setValue(SettingItems.LAST_LIBRARY, "") + self.cache.last_library = "" # dont consider this a fatal error, just skip opening the library library_path = None diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 9d3df5491..d0f78bd32 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -53,6 +53,7 @@ TS_FOLDER_NAME, ) from ...enums import LibraryPrefs +from ...settings import LibSettings from .db import make_tables from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum, TagColor from .fields import ( @@ -159,6 +160,7 @@ class Library: engine: Engine | None folder: Folder | None included_files: set[Path] = set() + settings: LibSettings | None = None SQL_FILENAME: str = "ts_library.sqlite" JSON_FILENAME: str = "ts_library.json" @@ -238,8 +240,8 @@ def migrate_json_to_sqlite(self, json_lib: JsonLibrary): ) # Preferences - self.set_prefs(LibraryPrefs.EXTENSION_LIST, [x.strip(".") for x in json_lib.ext_list]) - self.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, json_lib.is_exclude_list) + self.settings.extension_list = [x.strip(".") for x in json_lib.ext_list] + self.settings.is_exclude_list = json_lib.is_exclude_list end_time = time.time() logger.info(f"Library Converted! ({format_timespan(end_time-start_time)})") @@ -258,6 +260,9 @@ def open_library(self, library_dir: Path, storage_path: str | None = None) -> Li return self.open_sqlite_library(library_dir, is_new) else: self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME + settings_path = library_dir / TS_FOLDER_NAME / "libsettings.toml" + + self.settings = LibSettings.open(settings_path) if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()): json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME @@ -587,10 +592,9 @@ def search_library( f"SQL Expression Builder finished ({format_timespan(end_time - start_time)})" ) - extensions = self.prefs(LibraryPrefs.EXTENSION_LIST) - is_exclude_list = self.prefs(LibraryPrefs.IS_EXCLUDE_LIST) + extensions = self.settings.extension_list - if extensions and is_exclude_list: + if extensions and self.settings.is_exclude_list: statement = statement.where(Entry.suffix.notin_(extensions)) elif extensions: statement = statement.where(Entry.suffix.in_(extensions)) diff --git a/tagstudio/src/core/settings/__init__.py b/tagstudio/src/core/settings/__init__.py new file mode 100644 index 000000000..d72c42eaa --- /dev/null +++ b/tagstudio/src/core/settings/__init__.py @@ -0,0 +1,4 @@ +from .libsettings import LibSettings +from .tssettings import TSSettings + +__all__ = ["TSSettings", "LibSettings"] diff --git a/tagstudio/src/core/settings/libsettings.py b/tagstudio/src/core/settings/libsettings.py new file mode 100644 index 000000000..ffad59889 --- /dev/null +++ b/tagstudio/src/core/settings/libsettings.py @@ -0,0 +1,38 @@ +from pathlib import Path + +import structlog +import toml +from pydantic import BaseModel, Field + +logger = structlog.get_logger(__name__) + + +class LibSettings(BaseModel): + is_exclude_list: bool = Field(default=True) + extension_list: list[str] = Field(default=[".json", ".xmp", ".aae"]) + page_size: int = Field(default=500) + db_version: int = Field(default=2) + filename: str = Field(default="") + + @staticmethod + def open(path_value: Path | str) -> "LibSettings": + path: Path = Path(path_value) if not isinstance(path_value, Path) else path_value + + if path.exists(): + with open(path) as settings_file: + filecontents = settings_file.read() + if len(filecontents.strip()) != 0: + settings_data = toml.loads(filecontents) + settings_data["filename"] = str(path) + return LibSettings(**settings_data) + + # either settings file did not exist or was empty - either way, use default settings + settings = LibSettings(**dict(filename=str(path))) + return settings + + def save(self): + if not (parent_path := Path(self.filename).parent).exists(): + parent_path.mkdir() + + with open(self.filename, "w") as settings_file: + toml.dump(dict(self), settings_file) diff --git a/tagstudio/src/core/settings/tssettings.py b/tagstudio/src/core/settings/tssettings.py new file mode 100644 index 000000000..074f0c7a5 --- /dev/null +++ b/tagstudio/src/core/settings/tssettings.py @@ -0,0 +1,42 @@ +from pathlib import Path + +import toml +from pydantic import BaseModel, Field + + +# NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings +# properties to be overwritten with environment variables. as tagstudio is not currently using +# environment variables, i did not base it on that, but that may be useful in the future. +class TSSettings(BaseModel): + dark_mode: bool = Field(default=False) + language: str = Field(default="en") + + # settings from the old SettingItem enum + open_last_loaded_on_startup: bool = Field(default=False) + show_library_list: bool = Field(default=True) + autoplay: bool = Field(default=False) + show_filenames_in_grid: bool = Field(default=False) + + filename: str = Field() + + @staticmethod + def read_settings(path: Path | str) -> "TSSettings": + path_value = Path(path) + if path_value.exists(): + with open(path) as file: + filecontents = file.read() + if len(filecontents.strip()) != 0: + settings_data = toml.loads(filecontents) + settings = TSSettings(**settings_data) + return settings + + return TSSettings(**dict(filename=str(path))) + + def save(self, path: Path | str | None = None) -> None: + path_value: Path = Path(path) if isinstance(path, str) else Path(self.filename) + + if not path_value.parent.exists(): + path_value.parent.mkdir(parents=True, exist_ok=True) + + with open(path_value, "w") as f: + toml.dump(dict(self), f) diff --git a/tagstudio/src/core/tscacheddata.py b/tagstudio/src/core/tscacheddata.py new file mode 100644 index 000000000..23da27378 --- /dev/null +++ b/tagstudio/src/core/tscacheddata.py @@ -0,0 +1,53 @@ +from datetime import datetime +from pathlib import Path + +import structlog +import toml +from appdirs import user_cache_dir +from pydantic import BaseModel, ConfigDict, Field + +logger = structlog.get_logger(__name__) + +cache_dir = Path(user_cache_dir()) / "TagStudio" +cache_location = cache_dir / "cache.toml" + + +class TSCachedData(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + last_library: str | None = Field(default=None) + # a dict of ISO formatted date strings -> paths + library_history: dict[str, str] = Field(default_factory=dict[datetime, str]) + + path: str = Field() + + @staticmethod + def open(path_value: Path | str | None = None) -> "TSCachedData": + path: Path | None = None + default_cache_location = Path(user_cache_dir()) / "ts_cache.toml" + if isinstance(path_value, str): + path = Path(path_value) + elif isinstance(path_value, Path): + path = path_value + else: + logger.info( + "no cache location was specified, using ", + default_cache_location=default_cache_location, + ) + path = default_cache_location + + if path.exists(): + with open(path) as cache_file: + filecontents = cache_file.read() + if len(filecontents.strip()) != 0: + cache_data = toml.loads(filecontents) + cache_data["path"] = str(path) + logger.info("opening cache file at ", cache_location=path) + return TSCachedData(**cache_data) + + return TSCachedData(**dict(path=str(default_cache_location))) + + def save(self): + with open(self.path, "w") as f: + file_data = dict(self) + file_data.pop("path") + toml.dump(file_data, f) diff --git a/tagstudio/src/qt/modals/file_extension.py b/tagstudio/src/qt/modals/file_extension.py index 49b5b1478..aa7a504a9 100644 --- a/tagstudio/src/qt/modals/file_extension.py +++ b/tagstudio/src/qt/modals/file_extension.py @@ -16,7 +16,6 @@ QVBoxLayout, QWidget, ) -from src.core.enums import LibraryPrefs from src.core.library import Library from src.qt.translations import Translations from src.qt.widgets.panel import PanelWidget @@ -43,7 +42,7 @@ def __init__(self, library: "Library"): self.root_layout.setContentsMargins(6, 6, 6, 6) # Create Table Widget -------------------------------------------------- - self.table = QTableWidget(len(self.lib.prefs(LibraryPrefs.EXTENSION_LIST)), 1) + self.table = QTableWidget(len(self.lib.settings.extension_list), 1) self.table.horizontalHeader().setVisible(False) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setStretchLastSection(True) @@ -74,7 +73,7 @@ def __init__(self, library: "Library"): lambda text: self.mode_combobox.setItemText(1, text), "ignore_list.mode.exclude" ) - is_exclude_list = int(bool(self.lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST))) + is_exclude_list = int(bool(self.lib.settings.is_exclude_list)) self.mode_combobox.setCurrentIndex(is_exclude_list) self.mode_combobox.currentIndexChanged.connect(lambda i: self.update_list_mode(i)) @@ -97,10 +96,10 @@ def update_list_mode(self, mode: int): mode (int): The list mode, given by the index of the mode inside the mode combobox. 1 for "Exclude", 0 for "Include". """ - self.lib.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, bool(mode)) + self.lib.settings.is_exclude_list = bool(mode) def refresh_list(self): - for i, ext in enumerate(self.lib.prefs(LibraryPrefs.EXTENSION_LIST)): + for i, ext in enumerate(self.lib.settings.extension_list): self.table.setItem(i, 0, QTableWidgetItem(ext)) def add_item(self): @@ -114,4 +113,4 @@ def save(self): extensions.append(ext.text().strip().lstrip(".").lower()) # save preference - self.lib.set_prefs(LibraryPrefs.EXTENSION_LIST, extensions) + self.lib.settings.extension_list = extensions diff --git a/tagstudio/src/qt/modals/settings_modal.py b/tagstudio/src/qt/modals/settings_modal.py new file mode 100644 index 000000000..23649ef50 --- /dev/null +++ b/tagstudio/src/qt/modals/settings_modal.py @@ -0,0 +1,73 @@ +import copy +from pathlib import Path +from typing import Any + +from PySide6.QtWidgets import ( + QCheckBox, + QComboBox, + QHBoxLayout, + QLabel, + QVBoxLayout, +) +from src.core.settings import TSSettings +from src.qt.widgets.panel import PanelWidget + + +class SettingsModal(PanelWidget): + def __init__(self, settings: TSSettings): + super().__init__() + self.tempSettings: TSSettings = copy.deepcopy(settings) + + self.main = QVBoxLayout(self) + + # --- + self.language_Label = QLabel() + self.language_Value = QComboBox() + self.language_Row = QHBoxLayout() + self.language_Row.addWidget(self.language_Label) + self.language_Row.addWidget(self.language_Value) + + self.language_Label.setText("Language") + translations_folder = Path("tagstudio/resources/translations") + language_list = [x.stem for x in translations_folder.glob("*.json")] + self.language_Value.addItems(language_list) + self.language_Value.setCurrentIndex(language_list.index(self.tempSettings.language)) + self.language_Value.currentTextChanged.connect( + lambda text: setattr(self.tempSettings, "language", text) + ) + + # --- + self.show_library_list_Label = QLabel() + self.show_library_list_Value = QCheckBox() + self.show_library_list_Row = QHBoxLayout() + self.show_library_list_Row.addWidget(self.show_library_list_Label) + self.show_library_list_Row.addWidget(self.show_library_list_Value) + self.show_library_list_Label.setText("Load library list on startup (requires restart):") + self.show_library_list_Value.setChecked(self.tempSettings.show_library_list) + + self.show_library_list_Value.stateChanged.connect( + lambda state: setattr(self.tempSettings, "show_library_list", bool(state)) + ) + + # --- + self.show_filenames_Label = QLabel() + self.show_filenames_Value = QCheckBox() + self.show_filenames_Row = QHBoxLayout() + self.show_filenames_Row.addWidget(self.show_filenames_Label) + self.show_filenames_Row.addWidget(self.show_filenames_Value) + self.show_filenames_Label.setText("Show filenames in grid (requires restart)") + self.show_filenames_Value.setChecked(self.tempSettings.show_filenames_in_grid) + + self.show_filenames_Value.stateChanged.connect( + lambda state: setattr(self.tempSettings, "show_filenames_in_grid", bool(state)) + ) + # --- + self.main.addLayout(self.language_Row) + self.main.addLayout(self.show_library_list_Row) + self.main.addLayout(self.show_filenames_Row) + + def set_property(self, prop_name: str, value: Any) -> None: + setattr(self.tempSettings, prop_name, value) + + def get_content(self): + return self.tempSettings diff --git a/tagstudio/src/qt/translations.py b/tagstudio/src/qt/translations.py index 5d2dcea0a..4e411377c 100644 --- a/tagstudio/src/qt/translations.py +++ b/tagstudio/src/qt/translations.py @@ -32,7 +32,7 @@ def value(self) -> str: def value(self, value: str): if self.__value != value: self.__value = value - self.changed.emit(self.__value) + self.changed.emit(self.value) class Translator: diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index d80e20390..636e7456d 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -9,6 +9,7 @@ import ctypes import dataclasses +import datetime import math import os import re @@ -23,7 +24,14 @@ import structlog from humanfriendly import format_timespan from PySide6 import QtCore -from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal +from PySide6.QtCore import ( + QObject, + Qt, + QThread, + QThreadPool, + QTimer, + Signal, +) from PySide6.QtGui import ( QAction, QColor, @@ -57,7 +65,7 @@ VERSION_BRANCH, ) from src.core.driver import DriverMixin -from src.core.enums import LibraryPrefs, MacroID, SettingItems +from src.core.enums import MacroID from src.core.library.alchemy import Library from src.core.library.alchemy.enums import ( FieldTypeEnum, @@ -68,7 +76,9 @@ from src.core.library.alchemy.fields import _FieldID from src.core.library.alchemy.library import Entry, LibraryStatus from src.core.media_types import MediaCategories +from src.core.settings import TSSettings from src.core.ts_core import TagStudioCore +from src.core.tscacheddata import TSCachedData from src.core.utils.refresh_dir import RefreshDirTracker from src.core.utils.web import strip_web_protocol from src.qt.flowlayout import FlowLayout @@ -81,6 +91,7 @@ from src.qt.modals.fix_dupes import FixDupeFilesModal from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.folders_to_tags import FoldersToTagsModal +from src.qt.modals.settings_modal import SettingsModal from src.qt.modals.tag_database import TagDatabasePanel from src.qt.resource_manager import ResourceManager from src.qt.translations import Translations @@ -165,18 +176,20 @@ def __init__(self, backend, args): if not path.exists(): logger.warning("Config File does not exist creating", path=path) logger.info("Using Config File", path=path) - self.settings = QSettings(str(path), QSettings.Format.IniFormat) + self.settings = TSSettings.read_settings(path) else: - self.settings = QSettings( - QSettings.Format.IniFormat, - QSettings.Scope.UserScope, - "TagStudio", - "TagStudio", - ) + path = Path() + if sys.platform == "win32": + path = Path.home() / "AppData" / "Roaming" / "TagStudio" / "config.toml" + else: # "linux" and "darwin" should use the same config directory + path = Path.home() / ".config" / "TagStudio" / "config.toml" + + self.settings = TSSettings.read_settings(path) logger.info( - "Config File not specified, using default one", - filename=self.settings.fileName(), + "Config File not specified, using default one", filename=self.settings.filename ) + Translations.change_language(self.settings.language) + self.cache = TSCachedData.open() def init_workers(self): """Init workers for rendering thumbnails.""" @@ -275,8 +288,6 @@ def start(self) -> None: Translations.translate_qobject(file_menu, "menu.file") edit_menu = QMenu(menu_bar) Translations.translate_qobject(edit_menu, "generic.edit_alt") - view_menu = QMenu(menu_bar) - Translations.translate_qobject(view_menu, "menu.view") tools_menu = QMenu(menu_bar) Translations.translate_qobject(tools_menu, "menu.tools") macros_menu = QMenu(menu_bar) @@ -358,6 +369,10 @@ def start(self) -> None: file_menu.addSeparator() # Edit Menu ============================================================ + settings_menu_action = QAction("&Settings", menu_bar) + settings_menu_action.triggered.connect(lambda: self.open_settings_menu()) + file_menu.addAction(settings_menu_action) + new_tag_action = QAction(menu_bar) Translations.translate_qobject(new_tag_action, "menu.edit.new_tag") new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) @@ -464,6 +479,18 @@ def create_dupe_files_modal(): ) macros_menu.addAction(self.autofill_action) + show_libs_list_action = QAction(menu_bar) + Translations.translate_qobject(show_libs_list_action, "settings.show_recent_libraries") + show_libs_list_action.setCheckable(True) + show_libs_list_action.setChecked(self.settings.show_library_list) + show_libs_list_action.triggered.connect( + lambda checked: ( + setattr(self.settings, "show_library_list", checked), + self.toggle_libs_list(checked), + ) + ) + # window_menu.addAction(show_libs_list_action) + def create_folders_tags_modal(): if not hasattr(self, "folders_modal"): self.folders_modal = FoldersToTagsModal(self.lib, self) @@ -485,7 +512,6 @@ def create_folders_tags_modal(): menu_bar.addMenu(file_menu) menu_bar.addMenu(edit_menu) - menu_bar.addMenu(view_menu) menu_bar.addMenu(tools_menu) menu_bar.addMenu(macros_menu) menu_bar.addMenu(help_menu) @@ -643,6 +669,7 @@ def handle_sigterm(self): def shutdown(self): """Save Library on Application Exit.""" self.close_library(is_shutdown=True) + self.cache.save() logger.info("[SHUTDOWN] Ending Thumbnail Threads...") for _ in self.thumb_threads: self.thumb_job_queue.put(Consumer.MARKER_QUIT) @@ -663,8 +690,9 @@ def close_library(self, is_shutdown: bool = False): self.main_window.statusbar.showMessage(Translations["status.library_closing"]) start_time = time.time() - self.settings.setValue(SettingItems.LAST_LIBRARY, str(self.lib.library_dir)) - self.settings.sync() + self.cache.last_library = str(self.lib.library_dir) + self.settings.save() + self.lib.settings.save() self.lib.close() @@ -727,6 +755,20 @@ def add_tag_action_callback(self): ) self.modal.show() + def open_settings_menu(self): + self.modal = PanelModal( + SettingsModal(self.settings), + "Settings", + "Settings", + has_save=True, + save_callback = (lambda x: self.update_settings(x)) + ) + + def update_settings(self, settings: TSSettings): + self.settings = settings + self.settings.save(self.settings.filename) + Translations.change_language(self.settings.language) + def select_all_action_callback(self): """Set the selection to all visible items.""" self.selected.clear() @@ -1001,8 +1043,9 @@ def _init_thumb_grid(self): self.lib, self, (self.thumb_size, self.thumb_size), + grid_idx, bool( - self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool) + self.settings.show_filenames_in_grid ), ) @@ -1309,27 +1352,20 @@ def filter_items(self, filter: FilterState | None = None) -> None: self.pages_count, self.filter.page_index, emit=False ) - def remove_recent_library(self, item_key: str): - self.settings.beginGroup(SettingItems.LIBS_LIST) - self.settings.remove(item_key) - self.settings.endGroup() - self.settings.sync() + def remove_recent_library(self, item_key: str) -> None: + self.cache.library_history.pop(item_key) def update_libs_list(self, path: Path | str): - """Add library to list in SettingItems.LIBS_LIST.""" item_limit: int = 5 path = Path(path) - self.settings.beginGroup(SettingItems.LIBS_LIST) - - all_libs = {str(time.time()): str(path)} + all_libs = {datetime.datetime.fromtimestamp(time.time()).isoformat(): str(path)} - for item_key in self.settings.allKeys(): - item_path = str(self.settings.value(item_key, type=str)) - if Path(item_path) != path: - all_libs[item_key] = item_path + for access_time in self.cache.library_history: + lib = self.cache.library_history[access_time] + if Path(lib) != path: + all_libs[str(access_time)] = lib - # sort items, most recent first all_libs_list = sorted(all_libs.items(), key=lambda item: item[0], reverse=True) # remove previously saved items @@ -1436,7 +1472,7 @@ def init_library(self, path: Path, open_status: LibraryStatus): self.init_workers() - self.filter.page_size = self.lib.prefs(LibraryPrefs.PAGE_SIZE) + self.filter.page_size = self.lib.settings.page_size # TODO - make this call optional if self.lib.entries_count < 10000: diff --git a/tagstudio/src/qt/widgets/migration_modal.py b/tagstudio/src/qt/widgets/migration_modal.py index 954ad4722..16fbc1d68 100644 --- a/tagstudio/src/qt/widgets/migration_modal.py +++ b/tagstudio/src/qt/widgets/migration_modal.py @@ -20,7 +20,6 @@ from sqlalchemy import select from sqlalchemy.orm import Session from src.core.constants import LEGACY_TAG_FIELD_IDS, TS_FOLDER_NAME -from src.core.enums import LibraryPrefs from src.core.library.alchemy.enums import TagColor from src.core.library.alchemy.joins import TagParent from src.core.library.alchemy.library import TAG_ARCHIVED, TAG_FAVORITE, TAG_META @@ -442,12 +441,12 @@ def update_sql_value_ui(self, show_msg_box: bool = True): ) self.update_sql_value( self.ext_row, - len(self.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST)), + len(self.sql_lib.settings.extension_list), self.old_ext_count, ) self.update_sql_value( self.ext_type_row, - self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST), + self.sql_lib.settings.is_exclude_list, self.old_ext_type, ) logger.info("Parity check complete!") @@ -649,7 +648,7 @@ def check_subtag_parity(self) -> bool: return self.subtag_parity def check_ext_type(self) -> bool: - return self.json_lib.is_exclude_list == self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST) + return self.json_lib.is_exclude_list == self.sql_lib.settings.is_exclude_list def check_alias_parity(self) -> bool: """Check if all JSON aliases match the new SQL aliases.""" diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 62cf0bd24..8ca676ca3 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -17,6 +17,17 @@ QWidget, ) from src.core.enums import Theme +from src.core.constants import ( + TS_FOLDER_NAME, +) +from src.core.enums import Theme +from src.core.library.alchemy.fields import ( + BaseField, + DatetimeField, + FieldTypeEnum, + TextField, + _FieldID, +) from src.core.library.alchemy.library import Library from src.core.library.alchemy.models import Entry from src.core.palette import ColorType, UiColor, get_ui_color diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index 7e5e6ba15..cb42516c4 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -28,7 +28,6 @@ from PySide6.QtMultimediaWidgets import QGraphicsVideoItem from PySide6.QtSvgWidgets import QSvgWidget from PySide6.QtWidgets import QGraphicsScene, QGraphicsView -from src.core.enums import SettingItems from src.qt.helpers.file_opener import FileOpenerHelper from src.qt.platform_strings import PlatformStrings from src.qt.translations import Translations @@ -120,9 +119,7 @@ def __init__(self, driver: "QtDriver") -> None: Translations.translate_qobject(autoplay_action, "media_player.autoplay") autoplay_action.setCheckable(True) self.addAction(autoplay_action) - autoplay_action.setChecked( - self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool) # type: ignore - ) + autoplay_action.setChecked(self.driver.settings.autoplay) autoplay_action.triggered.connect(lambda: self.toggle_autoplay()) self.autoplay = autoplay_action @@ -142,8 +139,8 @@ def close(self, *args, **kwargs) -> None: def toggle_autoplay(self) -> None: """Toggle the autoplay state of the video.""" - self.driver.settings.setValue(SettingItems.AUTOPLAY, self.autoplay.isChecked()) - self.driver.settings.sync() + self.driver.settings.autoplay = self.autoplay.isChecked() + self.driver.settings.save() def check_media_status(self, media_status: QMediaPlayer.MediaStatus) -> None: if media_status == QMediaPlayer.MediaStatus.EndOfMedia: diff --git a/tagstudio/tests/example_settings.toml b/tagstudio/tests/example_settings.toml new file mode 100644 index 000000000..fc4a36ea9 --- /dev/null +++ b/tagstudio/tests/example_settings.toml @@ -0,0 +1,4 @@ +language = "en" +open_last_loaded_on_startup = false +show_filenames_in_grid = true +autoplay = false \ No newline at end of file diff --git a/tagstudio/tests/macros/test_refresh_dir.py b/tagstudio/tests/macros/test_refresh_dir.py index a4d3e808b..4326d6f82 100644 --- a/tagstudio/tests/macros/test_refresh_dir.py +++ b/tagstudio/tests/macros/test_refresh_dir.py @@ -2,7 +2,6 @@ from tempfile import TemporaryDirectory import pytest -from src.core.enums import LibraryPrefs from src.core.utils.refresh_dir import RefreshDirTracker CWD = pathlib.Path(__file__).parent @@ -12,8 +11,8 @@ @pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True) def test_refresh_new_files(library, exclude_mode): # Given - library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode) - library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"]) + library.settings.is_exclude_list = exclude_mode + library.settings.extension_list = [".md"] registry = RefreshDirTracker(library=library) library.included_files.clear() (library.library_dir / "FOO.MD").touch() diff --git a/tagstudio/tests/test_driver.py b/tagstudio/tests/test_driver.py index 240406f93..bac208d5e 100644 --- a/tagstudio/tests/test_driver.py +++ b/tagstudio/tests/test_driver.py @@ -2,21 +2,23 @@ from pathlib import Path from tempfile import TemporaryDirectory -from PySide6.QtCore import QSettings from src.core.constants import TS_FOLDER_NAME from src.core.driver import DriverMixin -from src.core.enums import SettingItems from src.core.library.alchemy.library import LibraryStatus +from src.core.settings import TSSettings +from src.core.tscacheddata import TSCachedData class TestDriver(DriverMixin): - def __init__(self, settings): + def __init__(self, settings, cache: TSCachedData | None = None): self.settings = settings + if cache: + self.cache = cache def test_evaluate_path_empty(): # Given - settings = QSettings() + settings = TSSettings(**dict(filename="")) driver = TestDriver(settings) # When @@ -28,7 +30,7 @@ def test_evaluate_path_empty(): def test_evaluate_path_missing(): # Given - settings = QSettings() + settings = TSSettings(**dict(filename="")) driver = TestDriver(settings) # When @@ -40,9 +42,10 @@ def test_evaluate_path_missing(): def test_evaluate_path_last_lib_not_exists(): # Given - settings = QSettings() - settings.setValue(SettingItems.LAST_LIBRARY, "/0/4/5/1/") - driver = TestDriver(settings) + settings = TSSettings(**dict(filename="")) + cache = TSCachedData.open() + cache.last_library = "/0/4/5/1/" + driver = TestDriver(settings, cache) # When result = driver.evaluate_path(None) @@ -54,16 +57,17 @@ def test_evaluate_path_last_lib_not_exists(): def test_evaluate_path_last_lib_present(): # Given with TemporaryDirectory() as tmpdir: - settings_file = tmpdir + "/test_settings.ini" - settings = QSettings(settings_file, QSettings.Format.IniFormat) - settings.setValue(SettingItems.LAST_LIBRARY, tmpdir) - settings.sync() + settings_file = tmpdir + "/test_settings.toml" + cache = TSCachedData.open(settings_file) + cache.last_library = tmpdir + cache.save() makedirs(Path(tmpdir) / TS_FOLDER_NAME) - driver = TestDriver(settings) + driver = TestDriver( + TSSettings(**dict(filename="", open_last_loaded_on_startup=True)), cache + ) # When result = driver.evaluate_path(None) - # Then assert result == LibraryStatus(success=True, library_path=Path(tmpdir)) diff --git a/tagstudio/tests/test_json_migration.py b/tagstudio/tests/test_json_migration.py index c8ad58e6c..330b07742 100644 --- a/tagstudio/tests/test_json_migration.py +++ b/tagstudio/tests/test_json_migration.py @@ -5,7 +5,6 @@ import pathlib from time import time -from src.core.enums import LibraryPrefs from src.qt.widgets.migration_modal import JsonMigrationModal CWD = pathlib.Path(__file__) @@ -41,9 +40,9 @@ def test_json_migration(): # Extension Filter List ==================================================== # Count - assert len(modal.json_lib.ext_list) == len(modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST)) + assert len(modal.json_lib.ext_list) == len(modal.sql_lib.settings.extension_list) # List Type assert modal.check_ext_type() # No Leading Dot - for ext in modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST): + for ext in modal.sql_lib.settings.extension_list: assert ext[0] != "." diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index ef88fe8bd..4c6c66751 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -175,8 +175,8 @@ def test_search_filter_extensions(library, is_exclude): entries = list(library.get_entries()) assert len(entries) == 2, entries - library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, is_exclude) - library.set_prefs(LibraryPrefs.EXTENSION_LIST, ["md"]) + library.settings.is_exclude_list = is_exclude + library.settings.extension_list = ["md"] # When results = library.search_library( diff --git a/tagstudio/tests/test_settings.py b/tagstudio/tests/test_settings.py new file mode 100644 index 000000000..3b87f7ca7 --- /dev/null +++ b/tagstudio/tests/test_settings.py @@ -0,0 +1,13 @@ +import pathlib + +from src.core.settings.tssettings import TSSettings + +CWD = pathlib.Path(__file__) + + +def test_read_settings(): + settings = TSSettings.read_settings(CWD.parent / "example_settings.toml") + assert settings.language == "en" + assert not settings.open_last_loaded_on_startup + assert settings.show_filenames_in_grid + assert not settings.autoplay From b74e8726143434c71743b495003217b11fcdd9d7 Mon Sep 17 00:00:00 2001 From: python357-1 Date: Mon, 13 Jan 2025 23:58:39 -0600 Subject: [PATCH 2/3] fix: merge conflicts and review suggestions --- requirements.txt | 8 +- tagstudio/resources/translations/en.json | 1 + tagstudio/src/core/constants.py | 2 + tagstudio/src/core/enums.py | 9 -- tagstudio/src/core/library/alchemy/library.py | 98 +++++++++---------- tagstudio/src/core/settings/libsettings.py | 8 +- tagstudio/src/core/settings/tssettings.py | 8 +- tagstudio/src/core/tscacheddata.py | 5 +- tagstudio/src/qt/modals/settings_modal.py | 53 ++++------ tagstudio/src/qt/ts_qt.py | 82 ++++------------ tagstudio/src/qt/widgets/panel.py | 4 +- tagstudio/src/qt/widgets/preview_panel.py | 11 --- tagstudio/tests/example_settings.toml | 4 - tagstudio/tests/test_driver.py | 10 +- tagstudio/tests/test_library.py | 10 +- tagstudio/tests/test_settings.py | 26 +++-- 16 files changed, 133 insertions(+), 206 deletions(-) delete mode 100644 tagstudio/tests/example_settings.toml diff --git a/requirements.txt b/requirements.txt index 5555f09e8..8a0f69395 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +appdirs==1.4.4 chardet==5.2.0 ffmpeg-python==0.2.0 humanfriendly==10.0 @@ -7,6 +8,7 @@ opencv_python==4.10.0.84 pillow-heif==0.16.0 pillow-jxl-plugin==1.3.0 Pillow==10.3.0 +pydantic==2.10.4 pydub==0.25.1 PySide6_Addons==6.8.0.1 PySide6_Essentials==6.8.0.1 @@ -14,9 +16,7 @@ PySide6==6.8.0.1 rawpy==0.22.0 SQLAlchemy==2.0.34 structlog==24.4.0 +toml==0.10.2 typing_extensions ujson>=5.8.0,<=5.9.0 -vtf2img==0.1.0 -toml==0.10.2 -appdirs==1.4.4 -pydantic==2.10.4 +vtf2img==0.1.0 \ No newline at end of file diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index 988fb0033..8fefc3b86 100644 --- a/tagstudio/resources/translations/en.json +++ b/tagstudio/resources/translations/en.json @@ -177,6 +177,7 @@ "preview.no_selection": "No Items Selected", "select.all": "Select All", "select.clear": "Clear Selection", + "settings.language": "Language", "settings.open_library_on_start": "Open Library on Start", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", diff --git a/tagstudio/src/core/constants.py b/tagstudio/src/core/constants.py index baec4a758..06e3125a6 100644 --- a/tagstudio/src/core/constants.py +++ b/tagstudio/src/core/constants.py @@ -24,3 +24,5 @@ TAG_META = 2 RESERVED_TAG_START = 0 RESERVED_TAG_END = 999 + +DEFAULT_LIB_VERSION = 3 diff --git a/tagstudio/src/core/enums.py b/tagstudio/src/core/enums.py index 74cdcbe4b..5d0a78563 100644 --- a/tagstudio/src/core/enums.py +++ b/tagstudio/src/core/enums.py @@ -62,12 +62,3 @@ def __new__(cls, value): @property def value(self): raise AttributeError("access the value via .default property instead") - - -class LibraryPrefs(DefaultEnum): - """Library preferences with default value accessible via .default property.""" - - IS_EXCLUDE_LIST = True - EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"] - PAGE_SIZE: int = 500 - DB_VERSION: int = 3 diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index d0f78bd32..8be4ec66f 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -12,7 +12,6 @@ from os import makedirs from pathlib import Path from uuid import uuid4 -from warnings import catch_warnings import structlog from humanfriendly import format_timespan @@ -30,6 +29,7 @@ func, or_, select, + text, update, ) from sqlalchemy.exc import IntegrityError @@ -44,6 +44,7 @@ from ...constants import ( BACKUP_FOLDER_NAME, + DEFAULT_LIB_VERSION, LEGACY_TAG_FIELD_IDS, RESERVED_TAG_END, RESERVED_TAG_START, @@ -52,7 +53,6 @@ TAG_META, TS_FOLDER_NAME, ) -from ...enums import LibraryPrefs from ...settings import LibSettings from .db import make_tables from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum, TagColor @@ -257,13 +257,10 @@ def open_library(self, library_dir: Path, storage_path: str | None = None) -> Li if storage_path == ":memory:": self.storage_path = storage_path is_new = True + self.settings = LibSettings(filename="") return self.open_sqlite_library(library_dir, is_new) else: self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME - settings_path = library_dir / TS_FOLDER_NAME / "libsettings.toml" - - self.settings = LibSettings.open(settings_path) - if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()): json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME if json_path.exists(): @@ -307,29 +304,6 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus: session.rollback() # dont check db version when creating new library - if not is_new: - db_version = session.scalar( - select(Preferences).where(Preferences.key == LibraryPrefs.DB_VERSION.name) - ) - - if not db_version: - return LibraryStatus( - success=False, - message=( - "Library version mismatch.\n" - f"Found: v0, expected: v{LibraryPrefs.DB_VERSION.default}" - ), - ) - - for pref in LibraryPrefs: - with catch_warnings(record=True): - try: - session.add(Preferences(key=pref.name, value=pref.default)) - session.commit() - except IntegrityError: - logger.debug("preference already exists", pref=pref) - session.rollback() - for field in _FieldID: try: session.add( @@ -346,21 +320,58 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus: logger.debug("ValueType already exists", field=field) session.rollback() - db_version = session.scalar( - select(Preferences).where(Preferences.key == LibraryPrefs.DB_VERSION.name) - ) + settings_path = library_dir / TS_FOLDER_NAME / "notafile.toml" + + # Will be set already if library was opened in-memory + if self.settings is None: + if settings_path.exists(): + self.settings = LibSettings.open(settings_path) + else: + if ( + session.execute( + text( + """ + SELECT count(*) + FROM sqlite_master + WHERE type='table' AND lower(name)='preferences'; + """ + ) + ).scalar() + == 0 + ): + # db was not created when settings were in db; + # use default settings, store at default location + self.settings = LibSettings(filename=str(settings_path)) + else: + # copy settings from db, store them in default location on next save + prefs = session.scalars(select(Preferences)) + settings = LibSettings(filename=str(settings_path)) + for pref in prefs: + # the type ignores below are due to the fact that a Preference's value + # is defined as a dict, while none of them are actually dicts. + # i dont know why that is how it is, but it is + if pref.key == "IS_EXCLUDE_LIST": + settings.is_exclude_list = pref.value # type: ignore + elif pref.key == "EXTENSION_LIST": + settings.extension_list = pref.value # type: ignore + elif pref.key == "PAGE_SIZE": + settings.page_size = pref.value # type: ignore + elif pref.key == "DB_VERSION": + settings.db_version = pref.value # type: ignore + + self.settings = settings # if the db version is different, we cant proceed - if db_version.value != LibraryPrefs.DB_VERSION.default: + if not is_new and self.settings.db_version != DEFAULT_LIB_VERSION: logger.error( "DB version mismatch", - db_version=db_version.value, - expected=LibraryPrefs.DB_VERSION.default, + db_version=self.settings.db_version, + expected=DEFAULT_LIB_VERSION, ) return LibraryStatus( success=False, message=( "Library version mismatch.\n" - f"Found: v{db_version.value}, expected: v{LibraryPrefs.DB_VERSION.default}" + f"Found: v{self.settings.db_version}, expected: v{DEFAULT_LIB_VERSION}" ), ) @@ -1107,21 +1118,6 @@ def update_parent_tags(self, tag, parent_ids, session): ) session.add(parent_tag) - def prefs(self, key: LibraryPrefs): - # load given item from Preferences table - with Session(self.engine) as session: - return session.scalar(select(Preferences).where(Preferences.key == key.name)).value - - def set_prefs(self, key: LibraryPrefs, value) -> None: - # set given item in Preferences table - with Session(self.engine) as session: - # load existing preference and update value - pref = session.scalar(select(Preferences).where(Preferences.key == key.name)) - pref.value = value - session.add(pref) - session.commit() - # TODO - try/except - def mirror_entry_fields(self, *entries: Entry) -> None: """Mirror fields among multiple Entry items.""" fields = {} diff --git a/tagstudio/src/core/settings/libsettings.py b/tagstudio/src/core/settings/libsettings.py index ffad59889..978109cc2 100644 --- a/tagstudio/src/core/settings/libsettings.py +++ b/tagstudio/src/core/settings/libsettings.py @@ -4,6 +4,8 @@ import toml from pydantic import BaseModel, Field +from ..constants import DEFAULT_LIB_VERSION + logger = structlog.get_logger(__name__) @@ -11,7 +13,7 @@ class LibSettings(BaseModel): is_exclude_list: bool = Field(default=True) extension_list: list[str] = Field(default=[".json", ".xmp", ".aae"]) page_size: int = Field(default=500) - db_version: int = Field(default=2) + db_version: int = Field(default=DEFAULT_LIB_VERSION) filename: str = Field(default="") @staticmethod @@ -27,10 +29,12 @@ def open(path_value: Path | str) -> "LibSettings": return LibSettings(**settings_data) # either settings file did not exist or was empty - either way, use default settings - settings = LibSettings(**dict(filename=str(path))) + settings = LibSettings(filename=str(path)) return settings def save(self): + if self.filename == "": # assume settings were opened for in-memory library + return if not (parent_path := Path(self.filename).parent).exists(): parent_path.mkdir() diff --git a/tagstudio/src/core/settings/tssettings.py b/tagstudio/src/core/settings/tssettings.py index 074f0c7a5..598214da5 100644 --- a/tagstudio/src/core/settings/tssettings.py +++ b/tagstudio/src/core/settings/tssettings.py @@ -27,13 +27,15 @@ def read_settings(path: Path | str) -> "TSSettings": filecontents = file.read() if len(filecontents.strip()) != 0: settings_data = toml.loads(filecontents) - settings = TSSettings(**settings_data) - return settings + return TSSettings(**settings_data) - return TSSettings(**dict(filename=str(path))) + return TSSettings(filename=str(path)) def save(self, path: Path | str | None = None) -> None: path_value: Path = Path(path) if isinstance(path, str) else Path(self.filename) + if path_value == "": + pass + # settings were probably opened for an in-memory library - save to preferences table if not path_value.parent.exists(): path_value.parent.mkdir(parents=True, exist_ok=True) diff --git a/tagstudio/src/core/tscacheddata.py b/tagstudio/src/core/tscacheddata.py index 23da27378..b894da26c 100644 --- a/tagstudio/src/core/tscacheddata.py +++ b/tagstudio/src/core/tscacheddata.py @@ -1,4 +1,3 @@ -from datetime import datetime from pathlib import Path import structlog @@ -16,7 +15,7 @@ class TSCachedData(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) last_library: str | None = Field(default=None) # a dict of ISO formatted date strings -> paths - library_history: dict[str, str] = Field(default_factory=dict[datetime, str]) + library_history: dict[str, str] = Field(default_factory=dict[str, str]) path: str = Field() @@ -44,7 +43,7 @@ def open(path_value: Path | str | None = None) -> "TSCachedData": logger.info("opening cache file at ", cache_location=path) return TSCachedData(**cache_data) - return TSCachedData(**dict(path=str(default_cache_location))) + return TSCachedData(path=str(default_cache_location)) def save(self): with open(self.path, "w") as f: diff --git a/tagstudio/src/qt/modals/settings_modal.py b/tagstudio/src/qt/modals/settings_modal.py index 23649ef50..5a09c0c6a 100644 --- a/tagstudio/src/qt/modals/settings_modal.py +++ b/tagstudio/src/qt/modals/settings_modal.py @@ -10,6 +10,7 @@ QVBoxLayout, ) from src.core.settings import TSSettings +from src.qt.translations import Translations from src.qt.widgets.panel import PanelWidget @@ -21,50 +22,38 @@ def __init__(self, settings: TSSettings): self.main = QVBoxLayout(self) # --- - self.language_Label = QLabel() - self.language_Value = QComboBox() - self.language_Row = QHBoxLayout() - self.language_Row.addWidget(self.language_Label) - self.language_Row.addWidget(self.language_Value) + language_row = QHBoxLayout(self) + language_label = QLabel(self) + Translations.translate_qobject(language_label, "settings.language") + language_value = QComboBox(self) + language_row.addWidget(language_label) + language_row.addWidget(language_value) - self.language_Label.setText("Language") translations_folder = Path("tagstudio/resources/translations") language_list = [x.stem for x in translations_folder.glob("*.json")] - self.language_Value.addItems(language_list) - self.language_Value.setCurrentIndex(language_list.index(self.tempSettings.language)) - self.language_Value.currentTextChanged.connect( + language_value.addItems(language_list) + language_value.setCurrentIndex(language_list.index(self.tempSettings.language)) + language_value.currentTextChanged.connect( lambda text: setattr(self.tempSettings, "language", text) ) # --- - self.show_library_list_Label = QLabel() - self.show_library_list_Value = QCheckBox() - self.show_library_list_Row = QHBoxLayout() - self.show_library_list_Row.addWidget(self.show_library_list_Label) - self.show_library_list_Row.addWidget(self.show_library_list_Value) - self.show_library_list_Label.setText("Load library list on startup (requires restart):") - self.show_library_list_Value.setChecked(self.tempSettings.show_library_list) + show_filenames_row = QHBoxLayout(self) + show_filenames_label = QLabel(self) + Translations.translate_qobject(show_filenames_label, "settings.show_filenames_in_grid") + show_filenames_value = QCheckBox(self) - self.show_library_list_Value.stateChanged.connect( - lambda state: setattr(self.tempSettings, "show_library_list", bool(state)) - ) - - # --- - self.show_filenames_Label = QLabel() - self.show_filenames_Value = QCheckBox() - self.show_filenames_Row = QHBoxLayout() - self.show_filenames_Row.addWidget(self.show_filenames_Label) - self.show_filenames_Row.addWidget(self.show_filenames_Value) - self.show_filenames_Label.setText("Show filenames in grid (requires restart)") - self.show_filenames_Value.setChecked(self.tempSettings.show_filenames_in_grid) + show_filenames_value.setChecked(self.tempSettings.show_filenames_in_grid) + show_filenames_row.addWidget(show_filenames_label) + show_filenames_row.addWidget(show_filenames_value) - self.show_filenames_Value.stateChanged.connect( + show_filenames_value.stateChanged.connect( lambda state: setattr(self.tempSettings, "show_filenames_in_grid", bool(state)) ) # --- - self.main.addLayout(self.language_Row) - self.main.addLayout(self.show_library_list_Row) - self.main.addLayout(self.show_filenames_Row) + + self.main.addLayout(language_row) + self.main.addLayout(show_filenames_row) def set_property(self, prop_name: str, value: Any) -> None: setattr(self.tempSettings, prop_name, value) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 636e7456d..64ff48fa7 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -318,11 +318,9 @@ def start(self) -> None: open_on_start_action = QAction(self) Translations.translate_qobject(open_on_start_action, "settings.open_library_on_start") open_on_start_action.setCheckable(True) - open_on_start_action.setChecked( - bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool)) - ) + open_on_start_action.setChecked(self.settings.open_last_loaded_on_startup) open_on_start_action.triggered.connect( - lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) + lambda checked: setattr(self.settings, "open_last_loaded_on_startup", checked) ) file_menu.addAction(open_on_start_action) @@ -367,12 +365,12 @@ def start(self) -> None: close_library_action.triggered.connect(self.close_library) file_menu.addAction(close_library_action) file_menu.addSeparator() - - # Edit Menu ============================================================ settings_menu_action = QAction("&Settings", menu_bar) settings_menu_action.triggered.connect(lambda: self.open_settings_menu()) file_menu.addAction(settings_menu_action) + # Edit Menu ============================================================ + new_tag_action = QAction(menu_bar) Translations.translate_qobject(new_tag_action, "menu.edit.new_tag") new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) @@ -424,23 +422,7 @@ def start(self) -> None: show_libs_list_action = QAction(menu_bar) Translations.translate_qobject(show_libs_list_action, "settings.show_recent_libraries") show_libs_list_action.setCheckable(True) - show_libs_list_action.setChecked( - bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) - ) - - show_filenames_action = QAction(menu_bar) - Translations.translate_qobject(show_filenames_action, "settings.show_filenames_in_grid") - show_filenames_action.setCheckable(True) - show_filenames_action.setChecked( - bool(self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)) - ) - show_filenames_action.triggered.connect( - lambda checked: ( - self.settings.setValue(SettingItems.SHOW_FILENAMES, checked), - self.show_grid_filenames(checked), - ) - ) - view_menu.addAction(show_filenames_action) + show_libs_list_action.setChecked(self.settings.show_library_list) # Tools Menu =========================================================== def create_fix_unlinked_entries_modal(): @@ -479,18 +461,6 @@ def create_dupe_files_modal(): ) macros_menu.addAction(self.autofill_action) - show_libs_list_action = QAction(menu_bar) - Translations.translate_qobject(show_libs_list_action, "settings.show_recent_libraries") - show_libs_list_action.setCheckable(True) - show_libs_list_action.setChecked(self.settings.show_library_list) - show_libs_list_action.triggered.connect( - lambda checked: ( - setattr(self.settings, "show_library_list", checked), - self.toggle_libs_list(checked), - ) - ) - # window_menu.addAction(show_libs_list_action) - def create_folders_tags_modal(): if not hasattr(self, "folders_modal"): self.folders_modal = FoldersToTagsModal(self.lib, self) @@ -654,10 +624,6 @@ def init_library_window(self): self.splash.finish(self.main_window) self.preview_panel.update_widgets() - def show_grid_filenames(self, value: bool): - for thumb in self.item_thumbs: - thumb.set_filename_visibility(value) - def callback_library_needed_check(self, func): """Check if loaded library has valid path before executing the button function.""" if self.lib.library_dir: @@ -669,7 +635,6 @@ def handle_sigterm(self): def shutdown(self): """Save Library on Application Exit.""" self.close_library(is_shutdown=True) - self.cache.save() logger.info("[SHUTDOWN] Ending Thumbnail Threads...") for _ in self.thumb_threads: self.thumb_job_queue.put(Consumer.MARKER_QUIT) @@ -693,6 +658,7 @@ def close_library(self, is_shutdown: bool = False): self.cache.last_library = str(self.lib.library_dir) self.settings.save() self.lib.settings.save() + self.cache.save() self.lib.close() @@ -761,14 +727,18 @@ def open_settings_menu(self): "Settings", "Settings", has_save=True, - save_callback = (lambda x: self.update_settings(x)) + save_callback=(lambda x: self.update_settings(x)), ) + self.modal.show() def update_settings(self, settings: TSSettings): self.settings = settings self.settings.save(self.settings.filename) Translations.change_language(self.settings.language) + for thumb in self.item_thumbs: + thumb.set_filename_visibility(self.settings.show_filenames_in_grid) + def select_all_action_callback(self): """Set the selection to all visible items.""" self.selected.clear() @@ -1043,10 +1013,7 @@ def _init_thumb_grid(self): self.lib, self, (self.thumb_size, self.thumb_size), - grid_idx, - bool( - self.settings.show_filenames_in_grid - ), + self.settings.show_filenames_in_grid, ) layout.addWidget(item_thumb) @@ -1364,18 +1331,16 @@ def update_libs_list(self, path: Path | str): for access_time in self.cache.library_history: lib = self.cache.library_history[access_time] if Path(lib) != path: - all_libs[str(access_time)] = lib + all_libs[access_time] = lib all_libs_list = sorted(all_libs.items(), key=lambda item: item[0], reverse=True) - # remove previously saved items - self.settings.remove("") - + lib_hist = {} for item_key, item_value in all_libs_list[:item_limit]: - self.settings.setValue(item_key, item_value) + lib_hist[item_key] = item_value + + self.cache.library_history = lib_hist - self.settings.endGroup() - self.settings.sync() self.update_recent_lib_menu() def update_recent_lib_menu(self): @@ -1383,10 +1348,8 @@ def update_recent_lib_menu(self): actions: list[QAction] = [] lib_items: dict[str, tuple[str, str]] = {} - settings = self.settings - settings.beginGroup(SettingItems.LIBS_LIST) - for item_tstamp in settings.allKeys(): - val = str(settings.value(item_tstamp, type=str)) + for item_tstamp in self.cache.library_history: + val = str(self.cache.library_history[item_tstamp]) cut_val = val if len(val) > 45: cut_val = f"{val[0:10]} ... {val[-10:]}" @@ -1394,7 +1357,6 @@ def update_recent_lib_menu(self): # Sort lib_items by the key libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True) - settings.endGroup() # Create actions for each library for library_key in libs_sorted: @@ -1427,11 +1389,7 @@ def update_recent_lib_menu(self): def clear_recent_libs(self): """Clear the list of recent libraries from the settings file.""" - settings = self.settings - settings.beginGroup(SettingItems.LIBS_LIST) - self.settings.remove("") - self.settings.endGroup() - self.settings.sync() + self.cache.library_history = dict() self.update_recent_lib_menu() def open_library(self, path: Path) -> None: diff --git a/tagstudio/src/qt/widgets/panel.py b/tagstudio/src/qt/widgets/panel.py index 573f6f48d..3a5ffc8ac 100755 --- a/tagstudio/src/qt/widgets/panel.py +++ b/tagstudio/src/qt/widgets/panel.py @@ -2,7 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import logging -from typing import Callable +from typing import Any, Callable from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget @@ -111,7 +111,7 @@ class PanelWidget(QWidget): def __init__(self): super().__init__() - def get_content(self) -> str: + def get_content(self) -> Any: pass def reset(self): diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 8ca676ca3..62cf0bd24 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -17,17 +17,6 @@ QWidget, ) from src.core.enums import Theme -from src.core.constants import ( - TS_FOLDER_NAME, -) -from src.core.enums import Theme -from src.core.library.alchemy.fields import ( - BaseField, - DatetimeField, - FieldTypeEnum, - TextField, - _FieldID, -) from src.core.library.alchemy.library import Library from src.core.library.alchemy.models import Entry from src.core.palette import ColorType, UiColor, get_ui_color diff --git a/tagstudio/tests/example_settings.toml b/tagstudio/tests/example_settings.toml deleted file mode 100644 index fc4a36ea9..000000000 --- a/tagstudio/tests/example_settings.toml +++ /dev/null @@ -1,4 +0,0 @@ -language = "en" -open_last_loaded_on_startup = false -show_filenames_in_grid = true -autoplay = false \ No newline at end of file diff --git a/tagstudio/tests/test_driver.py b/tagstudio/tests/test_driver.py index bac208d5e..a861c95cf 100644 --- a/tagstudio/tests/test_driver.py +++ b/tagstudio/tests/test_driver.py @@ -18,7 +18,7 @@ def __init__(self, settings, cache: TSCachedData | None = None): def test_evaluate_path_empty(): # Given - settings = TSSettings(**dict(filename="")) + settings = TSSettings(filename="") driver = TestDriver(settings) # When @@ -30,7 +30,7 @@ def test_evaluate_path_empty(): def test_evaluate_path_missing(): # Given - settings = TSSettings(**dict(filename="")) + settings = TSSettings(filename="") driver = TestDriver(settings) # When @@ -42,7 +42,7 @@ def test_evaluate_path_missing(): def test_evaluate_path_last_lib_not_exists(): # Given - settings = TSSettings(**dict(filename="")) + settings = TSSettings(filename="") cache = TSCachedData.open() cache.last_library = "/0/4/5/1/" driver = TestDriver(settings, cache) @@ -63,9 +63,7 @@ def test_evaluate_path_last_lib_present(): cache.save() makedirs(Path(tmpdir) / TS_FOLDER_NAME) - driver = TestDriver( - TSSettings(**dict(filename="", open_last_loaded_on_startup=True)), cache - ) + driver = TestDriver(TSSettings(filename="", open_last_loaded_on_startup=True), cache) # When result = driver.evaluate_path(None) diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index 4c6c66751..496ca13f8 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -2,7 +2,7 @@ from tempfile import TemporaryDirectory import pytest -from src.core.enums import DefaultEnum, LibraryPrefs +from src.core.enums import DefaultEnum from src.core.library.alchemy import Entry, Library from src.core.library.alchemy.enums import FilterState from src.core.library.alchemy.fields import TextField, _FieldID @@ -211,11 +211,6 @@ def test_search_library_case_insensitive(library): assert results[0].id == entry.id -def test_preferences(library): - for pref in LibraryPrefs: - assert library.prefs(pref) == pref.default - - def test_remove_entry_field(library, entry_full): title_field = entry_full.text_fields[0] @@ -362,9 +357,6 @@ def test_update_field_order(library, entry_full): def test_library_prefs_multiple_identical_vals(): - # check the preferences are inherited from DefaultEnum - assert issubclass(LibraryPrefs, DefaultEnum) - # create custom settings with identical values class TestPrefs(DefaultEnum): FOO = 1 diff --git a/tagstudio/tests/test_settings.py b/tagstudio/tests/test_settings.py index 3b87f7ca7..862bfa1c4 100644 --- a/tagstudio/tests/test_settings.py +++ b/tagstudio/tests/test_settings.py @@ -1,13 +1,23 @@ -import pathlib +from pathlib import Path +from tempfile import TemporaryDirectory from src.core.settings.tssettings import TSSettings -CWD = pathlib.Path(__file__) - def test_read_settings(): - settings = TSSettings.read_settings(CWD.parent / "example_settings.toml") - assert settings.language == "en" - assert not settings.open_last_loaded_on_startup - assert settings.show_filenames_in_grid - assert not settings.autoplay + with TemporaryDirectory() as tmpdir: + settings_path = Path(tmpdir) / "example_settings.toml" + with open(settings_path, "a") as settings_file: + settings_file.write(""" + language = "en" + open_last_loaded_on_startup = false + show_filenames_in_grid = true + autoplay = false + filename = "" + """) + + settings = TSSettings.read_settings(settings_path) + assert settings.language == "en" + assert not settings.open_last_loaded_on_startup + assert settings.show_filenames_in_grid + assert not settings.autoplay From 70a6c154a8a7b09dcd31220476d65fa7b1ab0080 Mon Sep 17 00:00:00 2001 From: python357-1 Date: Tue, 14 Jan 2025 00:03:38 -0600 Subject: [PATCH 3/3] fix: review suggestion --- tagstudio/src/qt/ts_qt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 64ff48fa7..269a22f9b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -178,7 +178,6 @@ def __init__(self, backend, args): logger.info("Using Config File", path=path) self.settings = TSSettings.read_settings(path) else: - path = Path() if sys.platform == "win32": path = Path.home() / "AppData" / "Roaming" / "TagStudio" / "config.toml" else: # "linux" and "darwin" should use the same config directory