From 5b3ad20f872d1f70da3a5a2e2bea7f95cfa9ea73 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 21:26:25 +0100 Subject: [PATCH 01/32] feat: add tab widget --- src/tagstudio/qt/modals/settings_panel.py | 88 +++++++++++++------- src/tagstudio/resources/translations/de.json | 2 + src/tagstudio/resources/translations/en.json | 2 + 3 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index c400c4a08..c70668480 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -4,7 +4,14 @@ from PySide6.QtCore import Qt -from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget +from PySide6.QtWidgets import ( + QComboBox, + QFormLayout, + QLabel, + QTabWidget, + QVBoxLayout, + QWidget, +) from tagstudio.core.enums import SettingItems from tagstudio.qt.translations import Translations @@ -12,45 +19,63 @@ class SettingsPanel(PanelWidget): + languages = { + # "Cantonese (Traditional)": "yue_Hant", # Empty + "Chinese (Traditional)": "zh_Hant", + # "Czech": "cs", # Minimal + # "Danish": "da", # Minimal + "Dutch": "nl", + "English": "en", + "Filipino": "fil", + "French": "fr", + "German": "de", + "Hungarian": "hu", + # "Italian": "it", # Minimal + "Norwegian Bokmål": "nb_NO", + "Polish": "pl", + "Portuguese (Brazil)": "pt_BR", + # "Portuguese (Portugal)": "pt", # Empty + "Russian": "ru", + "Spanish": "es", + "Swedish": "sv", + "Tamil": "ta", + "Toki Pona": "tok", + "Turkish": "tr", + } + def __init__(self, driver): super().__init__() self.driver = driver self.setMinimumSize(320, 200) + self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(6, 0, 6, 0) + self.root_layout.setContentsMargins(0, 0, 0, 0) + + # Tabs + self.tab_widget = QTabWidget() - self.form_container = QWidget() - self.form_layout = QFormLayout(self.form_container) - self.form_layout.setContentsMargins(0, 0, 0, 0) + self.__build_global_settings(driver) + self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"]) + self.__build_library_settings(driver) + self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"]) + + self.root_layout.addWidget(self.tab_widget) + + # Restart Label self.restart_label = QLabel(Translations["settings.restart_required"]) self.restart_label.setHidden(True) self.restart_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.root_layout.addStretch(1) + self.root_layout.addWidget(self.restart_label) + + def __build_global_settings(self, driver): + self.global_settings_container = QWidget() + form_layout = QFormLayout(self.global_settings_container) + form_layout.setContentsMargins(6, 6, 6, 6) + language_label = QLabel(Translations["settings.language"]) - self.languages = { - # "Cantonese (Traditional)": "yue_Hant", # Empty - "Chinese (Traditional)": "zh_Hant", - # "Czech": "cs", # Minimal - # "Danish": "da", # Minimal - "Dutch": "nl", - "English": "en", - "Filipino": "fil", - "French": "fr", - "German": "de", - "Hungarian": "hu", - # "Italian": "it", # Minimal - "Norwegian Bokmål": "nb_NO", - "Polish": "pl", - "Portuguese (Brazil)": "pt_BR", - # "Portuguese (Portugal)": "pt", # Empty - "Russian": "ru", - "Spanish": "es", - "Swedish": "sv", - "Tamil": "ta", - "Toki Pona": "tok", - "Turkish": "tr", - } self.language_combobox = QComboBox() self.language_combobox.addItems(list(self.languages.keys())) current_lang: str = str( @@ -61,11 +86,10 @@ def __init__(self, driver): self.language_combobox.currentIndexChanged.connect( lambda: self.restart_label.setHidden(False) ) - self.form_layout.addRow(language_label, self.language_combobox) + form_layout.addRow(language_label, self.language_combobox) - self.root_layout.addWidget(self.form_container) - self.root_layout.addStretch(1) - self.root_layout.addWidget(self.restart_label) + def __build_library_settings(self, driver): + self.library_settings_container = QWidget() def get_language(self) -> str: values: list[str] = list(self.languages.values()) diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 44b96a5c0..79368455e 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -226,6 +226,8 @@ "settings.show_filenames_in_grid": "Dateinamen in Raster darstellen", "settings.show_recent_libraries": "Zuletzt verwendete Bibliotheken anzeigen", "settings.title": "Einstellungen", + "settings.global": "Globale Einstellungen", + "settings.library": "Bibliothekseinstellungen", "sorting.direction.ascending": "Aufsteigend", "sorting.direction.descending": "Absteigend", "splash.opening_library": "Öffne Bibliothek \"{library_path}\"...", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index c85f18779..8885499ca 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -229,6 +229,8 @@ "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", "settings.title": "Settings", + "settings.global": "Global Settings", + "settings.library": "Library Settings", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", "splash.opening_library": "Opening Library \"{library_path}\"...", From 0c942e2c291e07c26fdb893d602da256a64d2dc8 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 21:31:15 +0100 Subject: [PATCH 02/32] refactor: move languages dict to translations.py --- src/tagstudio/qt/modals/settings_panel.py | 36 +++++------------------ src/tagstudio/qt/translations.py | 24 +++++++++++++++ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index c70668480..afe4fd65f 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -14,35 +14,11 @@ ) from tagstudio.core.enums import SettingItems -from tagstudio.qt.translations import Translations +from tagstudio.qt.translations import LANGUAGES, Translations from tagstudio.qt.widgets.panel import PanelWidget class SettingsPanel(PanelWidget): - languages = { - # "Cantonese (Traditional)": "yue_Hant", # Empty - "Chinese (Traditional)": "zh_Hant", - # "Czech": "cs", # Minimal - # "Danish": "da", # Minimal - "Dutch": "nl", - "English": "en", - "Filipino": "fil", - "French": "fr", - "German": "de", - "Hungarian": "hu", - # "Italian": "it", # Minimal - "Norwegian Bokmål": "nb_NO", - "Polish": "pl", - "Portuguese (Brazil)": "pt_BR", - # "Portuguese (Portugal)": "pt", # Empty - "Russian": "ru", - "Spanish": "es", - "Swedish": "sv", - "Tamil": "ta", - "Toki Pona": "tok", - "Turkish": "tr", - } - def __init__(self, driver): super().__init__() self.driver = driver @@ -77,12 +53,12 @@ def __build_global_settings(self, driver): language_label = QLabel(Translations["settings.language"]) self.language_combobox = QComboBox() - self.language_combobox.addItems(list(self.languages.keys())) + self.language_combobox.addItems(list(LANGUAGES.keys())) current_lang: str = str( driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str) ) - current_lang = "en" if current_lang not in self.languages.values() else current_lang - self.language_combobox.setCurrentIndex(list(self.languages.values()).index(current_lang)) + current_lang = "en" if current_lang not in LANGUAGES.values() else current_lang + self.language_combobox.setCurrentIndex(list(LANGUAGES.values()).index(current_lang)) self.language_combobox.currentIndexChanged.connect( lambda: self.restart_label.setHidden(False) ) @@ -90,7 +66,9 @@ def __build_global_settings(self, driver): def __build_library_settings(self, driver): self.library_settings_container = QWidget() + form_layout = QFormLayout(self.global_settings_container) + form_layout.setContentsMargins(6, 6, 6, 6) def get_language(self) -> str: - values: list[str] = list(self.languages.values()) + values: list[str] = list(LANGUAGES.values()) return values[self.language_combobox.currentIndex()] diff --git a/src/tagstudio/qt/translations.py b/src/tagstudio/qt/translations.py index ef775b5d5..5977de38d 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -51,3 +51,27 @@ def __getitem__(self, key: str) -> str: Translations = Translator() + +LANGUAGES = { + # "Cantonese (Traditional)": "yue_Hant", # Empty + "Chinese (Traditional)": "zh_Hant", + # "Czech": "cs", # Minimal + # "Danish": "da", # Minimal + "Dutch": "nl", + "English": "en", + "Filipino": "fil", + "French": "fr", + "German": "de", + "Hungarian": "hu", + # "Italian": "it", # Minimal + "Norwegian Bokmål": "nb_NO", + "Polish": "pl", + "Portuguese (Brazil)": "pt_BR", + # "Portuguese (Portugal)": "pt", # Empty + "Russian": "ru", + "Spanish": "es", + "Swedish": "sv", + "Tamil": "ta", + "Toki Pona": "tok", + "Turkish": "tr", +} From 2403529c4bf1b894d94046c3336929f9375bc2f0 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 21:41:51 +0100 Subject: [PATCH 03/32] refactor: move build of Settings Modal to SettingsPanel class --- src/tagstudio/qt/modals/settings_panel.py | 22 +++++++++++++++++++++- src/tagstudio/qt/ts_qt.py | 18 ++---------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index afe4fd65f..d0cf116d3 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -13,9 +13,10 @@ QWidget, ) +from tagstudio.core.driver import DriverMixin from tagstudio.core.enums import SettingItems from tagstudio.qt.translations import LANGUAGES, Translations -from tagstudio.qt.widgets.panel import PanelWidget +from tagstudio.qt.widgets.panel import PanelModal, PanelWidget class SettingsPanel(PanelWidget): @@ -72,3 +73,22 @@ def __build_library_settings(self, driver): def get_language(self) -> str: values: list[str] = list(LANGUAGES.values()) return values[self.language_combobox.currentIndex()] + + @classmethod + def build_modal(cls, driver: DriverMixin) -> PanelModal: + settings_panel = cls(driver) + + def update_language(): + Translations.change_language(settings_panel.get_language()) + driver.settings.setValue(SettingItems.LANGUAGE, settings_panel.get_language()) + driver.settings.sync() + + modal = PanelModal( + widget=settings_panel, + done_callback=update_language, + has_save=False, + ) + modal.setTitle(Translations["settings.title"]) + modal.setWindowTitle(Translations["settings.title"]) + + return modal diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 33ba48e16..e8794e329 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1803,22 +1803,8 @@ def clear_recent_libs(self): self.update_recent_lib_menu() def open_settings_modal(self): - # TODO: Implement a proper settings panel, and don't re-create it each time it's opened. - settings_panel = SettingsPanel(self) - modal = PanelModal( - widget=settings_panel, - done_callback=lambda: self.update_language_settings(settings_panel.get_language()), - has_save=False, - ) - modal.setTitle(Translations["settings.title"]) - modal.setWindowTitle(Translations["settings.title"]) - modal.show() - - def update_language_settings(self, language: str): - Translations.change_language(language) - - self.settings.setValue(SettingItems.LANGUAGE, language) - self.settings.sync() + # TODO: don't re-create each time this is opened. + SettingsPanel.build_modal(self).show() def open_library(self, path: Path) -> None: """Open a TagStudio library.""" From 7968cffd38148e26157cb602715d470edf978c2d Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 21:51:36 +0100 Subject: [PATCH 04/32] feat: hide title label --- src/tagstudio/qt/modals/settings_panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index d0cf116d3..bbee2366b 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -26,7 +26,7 @@ def __init__(self, driver): self.setMinimumSize(320, 200) self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(0, 0, 0, 0) + self.root_layout.setContentsMargins(0, 6, 0, 0) # Tabs self.tab_widget = QTabWidget() @@ -88,7 +88,7 @@ def update_language(): done_callback=update_language, has_save=False, ) - modal.setTitle(Translations["settings.title"]) + modal.title_widget.setVisible(False) modal.setWindowTitle(Translations["settings.title"]) return modal From c65eec68bfaa5a2633e48737affc2eb9e4eacdf1 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 23:05:57 +0100 Subject: [PATCH 05/32] feat: global settings class --- pyproject.toml | 2 + src/tagstudio/core/driver.py | 14 ++-- src/tagstudio/core/enums.py | 5 -- src/tagstudio/core/global_settings.py | 46 ++++++++++ src/tagstudio/core/library/alchemy/enums.py | 6 +- src/tagstudio/qt/modals/settings_panel.py | 17 ++-- src/tagstudio/qt/ts_qt.py | 93 ++++++++++----------- src/tagstudio/qt/widgets/item_thumb.py | 4 +- src/tagstudio/qt/widgets/video_player.py | 9 +- 9 files changed, 115 insertions(+), 81 deletions(-) create mode 100644 src/tagstudio/core/global_settings.py diff --git a/pyproject.toml b/pyproject.toml index dda76018e..5f285820c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,8 @@ dependencies = [ "typing_extensions>=3.10.0.0,<4.11.0", "ujson>=5.8.0,<5.9.0", "vtf2img==0.1.0", + "toml==0.10.2", + "pydantic==2.10.4", ] [project.optional-dependencies] diff --git a/src/tagstudio/core/driver.py b/src/tagstudio/core/driver.py index f4f073d8f..0a59a98e5 100644 --- a/src/tagstudio/core/driver.py +++ b/src/tagstudio/core/driver.py @@ -5,13 +5,15 @@ from tagstudio.core.constants import TS_FOLDER_NAME from tagstudio.core.enums import SettingItems +from tagstudio.core.global_settings import GlobalSettings from tagstudio.core.library.alchemy.library import LibraryStatus logger = structlog.get_logger(__name__) class DriverMixin: - settings: QSettings + cached_values: QSettings + settings: GlobalSettings def evaluate_path(self, open_path: str | None) -> LibraryStatus: """Check if the path of library is valid.""" @@ -21,17 +23,17 @@ 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.cached_values.value( + SettingItems.LAST_LIBRARY + ): + library_path = Path(str(self.cached_values.value(SettingItems.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.cached_values.setValue(SettingItems.LAST_LIBRARY, "") # dont consider this a fatal error, just skip opening the library library_path = None diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py index 2a11a12fd..acb85dc15 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -10,14 +10,9 @@ class SettingItems(str, enum.Enum): """List of setting item names.""" - START_LOAD_LAST = "start_load_last" LAST_LIBRARY = "last_library" LIBS_LIST = "libs_list" - WINDOW_SHOW_LIBS = "window_show_libs" - SHOW_FILENAMES = "show_filenames" - AUTOPLAY = "autoplay_videos" THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit" - LANGUAGE = "language" class Theme(str, enum.Enum): diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py new file mode 100644 index 000000000..64aac2567 --- /dev/null +++ b/src/tagstudio/core/global_settings.py @@ -0,0 +1,46 @@ +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +import sys +from pathlib import Path + +import toml +from pydantic import BaseModel, Field + +if sys.platform == "win32": + DEFAULT_SETTINGS_PATH = Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" +else: + DEFAULT_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml" + + +# 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 GlobalSettings(BaseModel): + # TODO: dark mode, page size + language: str = Field(default="en") + + # settings from the old SettingItems 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) + + @staticmethod + def read_settings(path: Path = DEFAULT_SETTINGS_PATH) -> "GlobalSettings": + if path.exists(): + with open(path) as file: + filecontents = file.read() + if len(filecontents.strip()) != 0: + settings_data = toml.loads(filecontents) + settings = GlobalSettings(**settings_data) + return settings + + return GlobalSettings() + + def save(self, path: Path = DEFAULT_SETTINGS_PATH) -> None: + if not path.parent.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + + with open(path, "w") as f: + toml.dump(dict(self), f) diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index dc3b8b56b..1dae3b65e 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -74,14 +74,14 @@ class FilterState: """Represent a state of the Library grid view.""" # these should remain - page_index: int | None = 0 - page_size: int | None = 500 + page_index: int = 0 + page_size: int = 500 sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED ascending: bool = True # these should be erased on update # Abstract Syntax Tree Of the current Search Query - ast: AST = None + ast: AST | None = None @property def limit(self): diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index bbee2366b..02d249b32 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -14,7 +14,6 @@ ) from tagstudio.core.driver import DriverMixin -from tagstudio.core.enums import SettingItems from tagstudio.qt.translations import LANGUAGES, Translations from tagstudio.qt.widgets.panel import PanelModal, PanelWidget @@ -55,10 +54,9 @@ def __build_global_settings(self, driver): language_label = QLabel(Translations["settings.language"]) self.language_combobox = QComboBox() self.language_combobox.addItems(list(LANGUAGES.keys())) - current_lang: str = str( - driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str) - ) - current_lang = "en" if current_lang not in LANGUAGES.values() else current_lang + current_lang: str = driver.settings.language + if current_lang not in LANGUAGES.values(): + current_lang = "en" self.language_combobox.setCurrentIndex(list(LANGUAGES.values()).index(current_lang)) self.language_combobox.currentIndexChanged.connect( lambda: self.restart_label.setHidden(False) @@ -78,14 +76,15 @@ def get_language(self) -> str: def build_modal(cls, driver: DriverMixin) -> PanelModal: settings_panel = cls(driver) - def update_language(): + def update_settings(): Translations.change_language(settings_panel.get_language()) - driver.settings.setValue(SettingItems.LANGUAGE, settings_panel.get_language()) - driver.settings.sync() + driver.settings.language = settings_panel.get_language() + + driver.settings.save() modal = PanelModal( widget=settings_panel, - done_callback=update_language, + done_callback=update_settings, has_save=False, ) modal.title_widget.setVisible(False) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index e8794e329..0761ba8e5 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -180,10 +180,10 @@ def __init__(self, args): if not path.exists(): logger.warning("[Config] Config File does not exist creating", path=path) logger.info("[Config] Using Config File", path=path) - self.settings = QSettings(str(path), QSettings.Format.IniFormat) + self.cached_values = QSettings(str(path), QSettings.Format.IniFormat) self.config_path = str(path) else: - self.settings = QSettings( + self.cached_values = QSettings( QSettings.Format.IniFormat, QSettings.Scope.UserScope, "TagStudio", @@ -191,18 +191,16 @@ def __init__(self, args): ) logger.info( "[Config] Config File not specified, using default one", - filename=self.settings.fileName(), + filename=self.cached_values.fileName(), ) - self.config_path = self.settings.fileName() + self.config_path = self.cached_values.fileName() - Translations.change_language( - str(self.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str)) - ) + Translations.change_language(self.settings.language) # NOTE: This should be a per-library setting rather than an application setting. thumb_cache_size_limit: int = int( str( - self.settings.value( + self.cached_values.value( SettingItems.THUMB_CACHE_SIZE_LIMIT, defaultValue=CacheManager.size_limit, type=int, @@ -211,8 +209,8 @@ def __init__(self, args): ) CacheManager.size_limit = thumb_cache_size_limit - self.settings.setValue(SettingItems.THUMB_CACHE_SIZE_LIMIT, CacheManager.size_limit) - self.settings.sync() + self.cached_values.setValue(SettingItems.THUMB_CACHE_SIZE_LIMIT, CacheManager.size_limit) + self.cached_values.sync() logger.info( f"[Config] Thumbnail cache size limit: {format_size(CacheManager.size_limit)}", ) @@ -381,12 +379,13 @@ def start(self) -> None: open_on_start_action = QAction(Translations["settings.open_library_on_start"], self) 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.triggered.connect( - lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) - ) + open_on_start_action.setChecked(self.settings.open_last_loaded_on_startup) + + def set_open_last_loaded_on_startup(checked: bool): + self.settings.open_last_loaded_on_startup = checked + self.settings.save() + + open_on_start_action.triggered.connect(set_open_last_loaded_on_startup) file_menu.addAction(open_on_start_action) file_menu.addSeparator() @@ -528,21 +527,17 @@ def start(self) -> None: # View Menu ============================================================ show_libs_list_action = QAction(Translations["settings.show_recent_libraries"], menu_bar) show_libs_list_action.setCheckable(True) - show_libs_list_action.setChecked( - bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) - ) + show_libs_list_action.setChecked(self.settings.show_library_list) + + def on_show_filenames_action(checked: bool): + self.settings.show_library_list = checked + self.settings.save() + self.show_grid_filenames(checked) show_filenames_action = QAction(Translations["settings.show_filenames_in_grid"], menu_bar) 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), - ) - ) + show_filenames_action.setChecked(self.settings.show_filenames_in_grid) + show_filenames_action.triggered.connect(on_show_filenames_action) view_menu.addAction(show_filenames_action) # Tools Menu =========================================================== @@ -662,7 +657,7 @@ def create_about_modal(): path_result = self.evaluate_path(str(self.args.open).lstrip().rstrip()) if path_result.success and path_result.library_path: self.open_library(path_result.library_path) - elif self.settings.value(SettingItems.START_LOAD_LAST): + elif self.settings.open_last_loaded_on_startup: # evaluate_path() with argument 'None' returns a LibraryStatus for the last library path_result = self.evaluate_path(None) if path_result.success and path_result.library_path: @@ -818,8 +813,8 @@ 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.cached_values.setValue(SettingItems.LAST_LIBRARY, str(self.lib.library_dir)) + self.cached_values.sync() # Reset library state self.preview_panel.update_widgets() @@ -1305,9 +1300,7 @@ def _init_thumb_grid(self): self.lib, self, (self.thumb_size, self.thumb_size), - bool( - self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool) - ), + self.settings.show_filenames_in_grid, ) layout.addWidget(item_thumb) @@ -1713,22 +1706,22 @@ def filter_items(self, filter: FilterState | None = None) -> None: ) 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() + self.cached_values.beginGroup(SettingItems.LIBS_LIST) + self.cached_values.remove(item_key) + self.cached_values.endGroup() + self.cached_values.sync() 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) + self.cached_values.beginGroup(SettingItems.LIBS_LIST) all_libs = {str(time.time()): str(path)} - for item_key in self.settings.allKeys(): - item_path = str(self.settings.value(item_key, type=str)) + for item_key in self.cached_values.allKeys(): + item_path = str(self.cached_values.value(item_key, type=str)) if Path(item_path) != path: all_libs[item_key] = item_path @@ -1736,13 +1729,13 @@ def update_libs_list(self, path: Path | str): all_libs_list = sorted(all_libs.items(), key=lambda item: item[0], reverse=True) # remove previously saved items - self.settings.remove("") + self.cached_values.remove("") for item_key, item_value in all_libs_list[:item_limit]: - self.settings.setValue(item_key, item_value) + self.cached_values.setValue(item_key, item_value) - self.settings.endGroup() - self.settings.sync() + self.cached_values.endGroup() + self.cached_values.sync() self.update_recent_lib_menu() def update_recent_lib_menu(self): @@ -1750,7 +1743,7 @@ def update_recent_lib_menu(self): actions: list[QAction] = [] lib_items: dict[str, tuple[str, str]] = {} - settings = self.settings + settings = self.cached_values settings.beginGroup(SettingItems.LIBS_LIST) for item_tstamp in settings.allKeys(): val = str(settings.value(item_tstamp, type=str)) @@ -1795,11 +1788,11 @@ 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 = self.cached_values settings.beginGroup(SettingItems.LIBS_LIST) - self.settings.remove("") - self.settings.endGroup() - self.settings.sync() + self.cached_values.remove("") + self.cached_values.endGroup() + self.cached_values.sync() self.update_recent_lib_menu() def open_settings_modal(self): diff --git a/src/tagstudio/qt/widgets/item_thumb.py b/src/tagstudio/qt/widgets/item_thumb.py index 8b4f15d27..42a82926e 100644 --- a/src/tagstudio/qt/widgets/item_thumb.py +++ b/src/tagstudio/qt/widgets/item_thumb.py @@ -116,7 +116,7 @@ class ItemThumb(FlowWidget): def __init__( self, - mode: ItemType, + mode: ItemType | None, library: Library, driver: "QtDriver", thumb_size: tuple[int, int], @@ -124,7 +124,7 @@ def __init__( ): super().__init__() self.lib = library - self.mode: ItemType = mode + self.mode: ItemType | None = mode self.driver = driver self.item_id: int | None = None self.thumb_size: tuple[int, int] = thumb_size diff --git a/src/tagstudio/qt/widgets/video_player.py b/src/tagstudio/qt/widgets/video_player.py index 49e2e89cb..9ea82243b 100644 --- a/src/tagstudio/qt/widgets/video_player.py +++ b/src/tagstudio/qt/widgets/video_player.py @@ -21,7 +21,6 @@ from PySide6.QtSvgWidgets import QSvgWidget from PySide6.QtWidgets import QGraphicsScene, QGraphicsView -from tagstudio.core.enums import SettingItems from tagstudio.qt.helpers.file_opener import FileOpenerHelper from tagstudio.qt.platform_strings import open_file_str from tagstudio.qt.translations import Translations @@ -112,9 +111,7 @@ def __init__(self, driver: "QtDriver") -> None: autoplay_action = QAction(Translations["media_player.autoplay"], self) autoplay_action.setCheckable(True) self.addAction(autoplay_action) - autoplay_action.setChecked( - self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool) - ) + autoplay_action.setChecked(self.driver.settings.autoplay) autoplay_action.triggered.connect(lambda: self.toggle_autoplay()) self.autoplay = autoplay_action @@ -133,8 +130,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: From fad74a0afc40e657c12b70af763491cd0d3ec775 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 23:31:38 +0100 Subject: [PATCH 06/32] fix: initialise settings --- src/tagstudio/core/global_settings.py | 14 ++++++++--- src/tagstudio/main.py | 25 ++++++++++++------- src/tagstudio/qt/ts_qt.py | 35 +++++++++++++++++++-------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 64aac2567..36758e21b 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -4,13 +4,18 @@ import sys from pathlib import Path +import structlog import toml from pydantic import BaseModel, Field if sys.platform == "win32": - DEFAULT_SETTINGS_PATH = Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" + DEFAULT_GLOBAL_SETTINGS_PATH = ( + Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" + ) else: - DEFAULT_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml" + DEFAULT_GLOBAL_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml" + +logger = structlog.get_logger(__name__) # NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings @@ -27,18 +32,19 @@ class GlobalSettings(BaseModel): show_filenames_in_grid: bool = Field(default=False) @staticmethod - def read_settings(path: Path = DEFAULT_SETTINGS_PATH) -> "GlobalSettings": + def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings": if path.exists(): with open(path) as file: filecontents = file.read() if len(filecontents.strip()) != 0: + logger.info("[Settings] Reading Global Settings File", path=path) settings_data = toml.loads(filecontents) settings = GlobalSettings(**settings_data) return settings return GlobalSettings() - def save(self, path: Path = DEFAULT_SETTINGS_PATH) -> None: + def save(self, path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> None: if not path.parent.exists(): path.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/tagstudio/main.py b/src/tagstudio/main.py index 18ebc3c11..bc015507f 100755 --- a/src/tagstudio/main.py +++ b/src/tagstudio/main.py @@ -32,12 +32,19 @@ def main(): type=str, help="Path to a TagStudio Library folder to open on start.", ) + parser.add_argument( + "-s", + "--settings-file", + dest="settings_file", + type=str, + help="Path to a TagStudio .toml global settings file to use.", + ) parser.add_argument( "-c", - "--config-file", - dest="config_file", + "--cache-file", + dest="cache_file", type=str, - help="Path to a TagStudio .ini or .plist config file to use.", + help="Path to a TagStudio .ini or .plist cache file to use.", ) # parser.add_argument('--browse', dest='browse', action='store_true', @@ -50,12 +57,12 @@ def main(): action="store_true", help="Reveals additional internal data useful for debugging.", ) - parser.add_argument( - "--ui", - dest="ui", - type=str, - help="User interface option for TagStudio. Options: qt, cli (Default: qt)", - ) + # parser.add_argument( + # "--ui", + # dest="ui", + # type=str, + # help="User interface option for TagStudio. Options: qt, cli (Default: qt)", + # ) args = parser.parse_args() driver = QtDriver(args) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 0761ba8e5..b2928a172 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -16,6 +16,7 @@ import re import sys import time +from argparse import Namespace from pathlib import Path from queue import Queue from warnings import catch_warnings @@ -55,6 +56,7 @@ from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH from tagstudio.core.driver import DriverMixin from tagstudio.core.enums import LibraryPrefs, MacroID, SettingItems +from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings from tagstudio.core.library.alchemy.enums import ( FieldTypeEnum, FilterState, @@ -145,10 +147,14 @@ class QtDriver(DriverMixin, QObject): file_extension_panel: PanelModal | None = None tag_search_panel: TagSearchPanel | None = None add_tag_modal: PanelModal | None = None + folders_modal: FoldersToTagsModal + about_modal: AboutModal + unlinked_modal: FixUnlinkedEntriesModal + dupe_modal: FixDupeFilesModal lib: Library - def __init__(self, args): + def __init__(self, args: Namespace): super().__init__() # prevent recursive badges update when multiple items selected self.badge_update_lock = False @@ -174,14 +180,24 @@ def __init__(self, args): self.SIGTERM.connect(self.handle_sigterm) - self.config_path = "" - if self.args.config_file: - path = Path(self.args.config_file) + self.global_settings_path = DEFAULT_GLOBAL_SETTINGS_PATH + if self.args.settings_file: + self.global_settings_path = Path(self.args.settings_file) + else: + logger.info("[Settings] Global Settings File Path not specified, using default") + self.settings = GlobalSettings.read_settings(self.global_settings_path) + if not self.global_settings_path.exists(): + logger.warning( + "[Settings] Global Settings File does not exist creating", + path=self.global_settings_path, + ) + + if self.args.cache_file: + path = Path(self.args.cache_file) if not path.exists(): - logger.warning("[Config] Config File does not exist creating", path=path) - logger.info("[Config] Using Config File", path=path) + logger.warning("[Cache] Cache File does not exist creating", path=path) + logger.info("[Cache] Using Cache File", path=path) self.cached_values = QSettings(str(path), QSettings.Format.IniFormat) - self.config_path = str(path) else: self.cached_values = QSettings( QSettings.Format.IniFormat, @@ -190,10 +206,9 @@ def __init__(self, args): "TagStudio", ) logger.info( - "[Config] Config File not specified, using default one", + "[Cache] Cache File not specified, using default one", filename=self.cached_values.fileName(), ) - self.config_path = self.cached_values.fileName() Translations.change_language(self.settings.language) @@ -604,7 +619,7 @@ def create_folders_tags_modal(): # Help Menu ============================================================ def create_about_modal(): if not hasattr(self, "about_modal"): - self.about_modal = AboutModal(self.config_path) + self.about_modal = AboutModal(self.global_settings_path) self.about_modal.show() self.about_action = QAction(Translations["menu.help.about"], menu_bar) From a56d70cca5be07ab738f90ec0c23298c32ab30bc Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 23:31:51 +0100 Subject: [PATCH 07/32] fix: properly store grid files changes --- src/tagstudio/core/global_settings.py | 1 - src/tagstudio/qt/ts_qt.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 36758e21b..42d19f1f7 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -27,7 +27,6 @@ class GlobalSettings(BaseModel): # settings from the old SettingItems 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) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index b2928a172..136bb4015 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -540,12 +540,12 @@ def set_open_last_loaded_on_startup(checked: bool): edit_menu.addAction(self.color_manager_action) # View Menu ============================================================ - show_libs_list_action = QAction(Translations["settings.show_recent_libraries"], menu_bar) - show_libs_list_action.setCheckable(True) - show_libs_list_action.setChecked(self.settings.show_library_list) + # show_libs_list_action = QAction(Translations["settings.show_recent_libraries"], menu_bar) + # show_libs_list_action.setCheckable(True) + # show_libs_list_action.setChecked(self.settings.show_library_list) def on_show_filenames_action(checked: bool): - self.settings.show_library_list = checked + self.settings.show_filenames_in_grid = checked self.settings.save() self.show_grid_filenames(checked) From 84384f4c5acaa67171a275084c3fb113a6c400cd Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 23:38:42 +0100 Subject: [PATCH 08/32] fix: placeholder text for library settings --- src/tagstudio/qt/modals/settings_panel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 02d249b32..1397cce40 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -65,9 +65,12 @@ def __build_global_settings(self, driver): def __build_library_settings(self, driver): self.library_settings_container = QWidget() - form_layout = QFormLayout(self.global_settings_container) + form_layout = QFormLayout(self.library_settings_container) form_layout.setContentsMargins(6, 6, 6, 6) + todo_label = QLabel("TODO") + form_layout.addRow(todo_label) + def get_language(self) -> str: values: list[str] = list(LANGUAGES.values()) return values[self.language_combobox.currentIndex()] From 2593b592b668acbce283f739fa2f2ecba506e939 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Wed, 12 Mar 2025 23:52:01 +0100 Subject: [PATCH 09/32] feat: add ui elements for remaining global settings --- src/tagstudio/qt/cache_manager.py | 2 +- src/tagstudio/qt/modals/settings_panel.py | 40 ++++++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/tagstudio/qt/cache_manager.py b/src/tagstudio/qt/cache_manager.py index cfd594542..3ac5f29ac 100644 --- a/src/tagstudio/qt/cache_manager.py +++ b/src/tagstudio/qt/cache_manager.py @@ -32,7 +32,7 @@ def __init__(self): self.last_lib_path: Path | None = None @staticmethod - def clear_cache(library_dir: Path) -> bool: + def clear_cache(library_dir: Path | None) -> bool: """Clear all files and folders within the cached folder. Returns: diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 1397cce40..1ee223569 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -5,6 +5,7 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( + QCheckBox, QComboBox, QFormLayout, QLabel, @@ -51,6 +52,7 @@ def __build_global_settings(self, driver): form_layout = QFormLayout(self.global_settings_container) form_layout.setContentsMargins(6, 6, 6, 6) + # Language language_label = QLabel(Translations["settings.language"]) self.language_combobox = QComboBox() self.language_combobox.addItems(list(LANGUAGES.keys())) @@ -63,6 +65,24 @@ def __build_global_settings(self, driver): ) form_layout.addRow(language_label, self.language_combobox) + # Open Last Library on Start + open_last_lib_label = QLabel(Translations["settings.open_library_on_start"]) + self.open_last_lib_checkbox = QCheckBox() + self.open_last_lib_checkbox.setChecked(driver.settings.open_last_loaded_on_startup) + form_layout.addRow(open_last_lib_label, self.open_last_lib_checkbox) + + # Autoplay + autoplay_label = QLabel(Translations["media_player.autoplay"]) + self.autoplay_checkbox = QCheckBox() + self.autoplay_checkbox.setChecked(driver.settings.autoplay) + form_layout.addRow(autoplay_label, self.autoplay_checkbox) + + # Show Filenames in Grid + show_filenames_label = QLabel(Translations["settings.show_filenames_in_grid"]) + self.show_filenames_checkbox = QCheckBox() + self.show_filenames_checkbox.setChecked(driver.settings.show_filenames_in_grid) + form_layout.addRow(show_filenames_label, self.show_filenames_checkbox) + def __build_library_settings(self, driver): self.library_settings_container = QWidget() form_layout = QFormLayout(self.library_settings_container) @@ -71,17 +91,27 @@ def __build_library_settings(self, driver): todo_label = QLabel("TODO") form_layout.addRow(todo_label) - def get_language(self) -> str: - values: list[str] = list(LANGUAGES.values()) - return values[self.language_combobox.currentIndex()] + def get_settings(self) -> dict: + return { + "language": list(LANGUAGES.values())[self.language_combobox.currentIndex()], + "open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(), + "autoplay": self.autoplay_checkbox.isChecked(), + "show_filenames_in_grid": self.show_filenames_checkbox.isChecked(), + } @classmethod def build_modal(cls, driver: DriverMixin) -> PanelModal: settings_panel = cls(driver) def update_settings(): - Translations.change_language(settings_panel.get_language()) - driver.settings.language = settings_panel.get_language() + settings = settings_panel.get_settings() + + Translations.change_language(settings["language"]) + driver.settings.language = settings["language"] + + driver.settings.open_last_loaded_on_startup = settings["open_last_loaded_on_startup"] + driver.settings.autoplay = settings["autoplay"] + driver.settings.show_filenames_in_grid = settings["show_filenames_in_grid"] driver.settings.save() From 4e7406fee604dd3c22c1dfd381bd9a416d2390fb Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 00:05:22 +0100 Subject: [PATCH 10/32] feat: add page size setting --- src/tagstudio/core/enums.py | 1 - src/tagstudio/core/global_settings.py | 5 ++--- src/tagstudio/qt/modals/settings_panel.py | 16 ++++++++++++++++ src/tagstudio/qt/ts_qt.py | 2 +- src/tagstudio/resources/translations/de.json | 1 + src/tagstudio/resources/translations/en.json | 1 + 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py index acb85dc15..b7bb48115 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -66,5 +66,4 @@ class LibraryPrefs(DefaultEnum): IS_EXCLUDE_LIST = True EXTENSION_LIST = [".json", ".xmp", ".aae"] - PAGE_SIZE = 500 DB_VERSION = 8 diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 42d19f1f7..4d7443d85 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -22,13 +22,12 @@ # 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 GlobalSettings(BaseModel): - # TODO: dark mode, page size + # TODO: dark mode language: str = Field(default="en") - - # settings from the old SettingItems enum open_last_loaded_on_startup: bool = Field(default=False) autoplay: bool = Field(default=False) show_filenames_in_grid: bool = Field(default=False) + page_size: int = Field(default=500) @staticmethod def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings": diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 1ee223569..cd51a1312 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -9,6 +9,7 @@ QComboBox, QFormLayout, QLabel, + QLineEdit, QTabWidget, QVBoxLayout, QWidget, @@ -83,6 +84,19 @@ def __build_global_settings(self, driver): self.show_filenames_checkbox.setChecked(driver.settings.show_filenames_in_grid) form_layout.addRow(show_filenames_label, self.show_filenames_checkbox) + # Page Size + page_size_label = QLabel(Translations["settings.page_size"]) + self.page_size_line_edit = QLineEdit() + self.page_size_line_edit.setText(str(driver.settings.page_size)) + + def on_page_size_changed(): + text = self.page_size_line_edit.text() + if not text.isdigit() or int(text) < 1: + self.page_size_line_edit.setText(str(driver.settings.page_size)) + + self.page_size_line_edit.editingFinished.connect(on_page_size_changed) + form_layout.addRow(page_size_label, self.page_size_line_edit) + def __build_library_settings(self, driver): self.library_settings_container = QWidget() form_layout = QFormLayout(self.library_settings_container) @@ -97,6 +111,7 @@ def get_settings(self) -> dict: "open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(), "autoplay": self.autoplay_checkbox.isChecked(), "show_filenames_in_grid": self.show_filenames_checkbox.isChecked(), + "page_size": int(self.page_size_line_edit.text()), } @classmethod @@ -112,6 +127,7 @@ def update_settings(): driver.settings.open_last_loaded_on_startup = settings["open_last_loaded_on_startup"] driver.settings.autoplay = settings["autoplay"] driver.settings.show_filenames_in_grid = settings["show_filenames_in_grid"] + driver.settings.page_size = settings["page_size"] driver.settings.save() diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 136bb4015..4e5ea1203 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1855,7 +1855,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.settings.page_size # TODO - make this call optional if self.lib.entries_count < 10000: diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 79368455e..aa5fa8500 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -228,6 +228,7 @@ "settings.title": "Einstellungen", "settings.global": "Globale Einstellungen", "settings.library": "Bibliothekseinstellungen", + "settings.page_size": "Elemente pro Seite", "sorting.direction.ascending": "Aufsteigend", "sorting.direction.descending": "Absteigend", "splash.opening_library": "Öffne Bibliothek \"{library_path}\"...", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 8885499ca..7e7c6c78f 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -231,6 +231,7 @@ "settings.title": "Settings", "settings.global": "Global Settings", "settings.library": "Library Settings", + "settings.page_size": "Page Size", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", "splash.opening_library": "Opening Library \"{library_path}\"...", From 7f3d8caa496e2fa1ba3f3e088f9b799aff281b96 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 01:36:39 +0100 Subject: [PATCH 11/32] fix: version mismatch between pydantic and typing_extensions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5f285820c..47d2c725e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "ujson>=5.8.0,<5.9.0", "vtf2img==0.1.0", "toml==0.10.2", - "pydantic==2.10.4", + "pydantic==2.9.2", ] [project.optional-dependencies] From e44316c3e7aa1b6da4190f1c2943b5e84c2f4df0 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 15:40:19 +0100 Subject: [PATCH 12/32] fix: update test_driver.py --- tests/test_driver.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_driver.py b/tests/test_driver.py index fc30a115e..c69b9757f 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -7,18 +7,19 @@ from tagstudio.core.constants import TS_FOLDER_NAME from tagstudio.core.driver import DriverMixin from tagstudio.core.enums import SettingItems +from tagstudio.core.global_settings import GlobalSettings from tagstudio.core.library.alchemy.library import LibraryStatus class TestDriver(DriverMixin): - def __init__(self, settings): + def __init__(self, settings: GlobalSettings, cache: QSettings): self.settings = settings + self.cached_values = cache def test_evaluate_path_empty(): # Given - settings = QSettings() - driver = TestDriver(settings) + driver = TestDriver(GlobalSettings(), QSettings()) # When result = driver.evaluate_path(None) @@ -29,8 +30,7 @@ def test_evaluate_path_empty(): def test_evaluate_path_missing(): # Given - settings = QSettings() - driver = TestDriver(settings) + driver = TestDriver(GlobalSettings(), QSettings()) # When result = driver.evaluate_path("/0/4/5/1/") @@ -41,9 +41,9 @@ 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) + cache = QSettings() + cache.setValue(SettingItems.LAST_LIBRARY, "/0/4/5/1/") + driver = TestDriver(GlobalSettings(), cache) # When result = driver.evaluate_path(None) @@ -55,13 +55,16 @@ 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() + cache_file = tmpdir + "/test_settings.ini" + cache = QSettings(cache_file, QSettings.Format.IniFormat) + cache.setValue(SettingItems.LAST_LIBRARY, tmpdir) + cache.sync() + + settings = GlobalSettings() + settings.open_last_loaded_on_startup = True makedirs(Path(tmpdir) / TS_FOLDER_NAME) - driver = TestDriver(settings) + driver = TestDriver(settings, cache) # When result = driver.evaluate_path(None) From 4ef2b16b7396e9e8dd6f6143843898ab46861445 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 16:10:33 +0100 Subject: [PATCH 13/32] fix(test_file_path_options): replace patch with change of settings --- tests/qt/test_file_path_options.py | 73 +++++++++++++++++------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/tests/qt/test_file_path_options.py b/tests/qt/test_file_path_options.py index c41330e98..97052e095 100644 --- a/tests/qt/test_file_path_options.py +++ b/tests/qt/test_file_path_options.py @@ -1,5 +1,5 @@ import os -import pathlib +from pathlib import Path from unittest.mock import patch import pytest @@ -7,10 +7,13 @@ QAction, ) from PySide6.QtWidgets import QMenu, QMenuBar +from pytestqt.qtbot import QtBot from tagstudio.core.enums import ShowFilepathOption -from tagstudio.core.library.alchemy.library import LibraryStatus +from tagstudio.core.library.alchemy.library import Library, LibraryStatus +from tagstudio.core.library.alchemy.models import Entry from tagstudio.qt.modals.settings_panel import SettingsPanel +from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.widgets.preview_panel import PreviewPanel @@ -23,7 +26,7 @@ ShowFilepathOption.SHOW_FILENAMES_ONLY.value, ], ) -def test_filepath_setting(qtbot, qt_driver, filepath_option): +def test_filepath_setting(qtbot: QtBot, qt_driver: QtDriver, filepath_option: ShowFilepathOption): settings_panel = SettingsPanel(qt_driver) qtbot.addWidget(settings_panel) @@ -43,41 +46,47 @@ def test_filepath_setting(qtbot, qt_driver, filepath_option): [ ( ShowFilepathOption.SHOW_FULL_PATHS, - lambda library: pathlib.Path(library.library_dir / "one/two/bar.md"), + lambda library: Path(library.library_dir / "one/two/bar.md"), ), - (ShowFilepathOption.SHOW_RELATIVE_PATHS, lambda _: pathlib.Path("one/two/bar.md")), - (ShowFilepathOption.SHOW_FILENAMES_ONLY, lambda _: pathlib.Path("bar.md")), + (ShowFilepathOption.SHOW_RELATIVE_PATHS, lambda _: Path("one/two/bar.md")), + (ShowFilepathOption.SHOW_FILENAMES_ONLY, lambda _: Path("bar.md")), ], ) -def test_file_path_display(qt_driver, library, filepath_option, expected_path): +def test_file_path_display( + qt_driver: QtDriver, library: Library, filepath_option: ShowFilepathOption, expected_path +): panel = PreviewPanel(library, qt_driver) # Select 2 qt_driver.toggle_item_selection(2, append=False, bridge=False) panel.update_widgets() - with patch.object(qt_driver.settings, "value", return_value=filepath_option): - # Apply the mock value - filename = library.get_entry(2).path - panel.file_attrs.update_stats(filepath=pathlib.Path(library.library_dir / filename)) - - # Generate the expected file string. - # This is copied directly from the file_attributes.py file - # can be imported as a function in the future - display_path = expected_path(library) - file_str: str = "" - separator: str = f"{os.path.sep}" # Gray - for i, part in enumerate(display_path.parts): - part_ = part.strip(os.path.sep) - if i != len(display_path.parts) - 1: - file_str += f"{"\u200b".join(part_)}{separator}" - else: - if file_str != "": - file_str += "
" - file_str += f"{"\u200b".join(part_)}" - - # Assert the file path is displayed correctly - assert panel.file_attrs.file_label.text() == file_str + qt_driver.settings.show_filepath = filepath_option + + # Apply the mock value + entry = library.get_entry(2) + assert isinstance(entry, Entry) + filename = entry.path + assert library.library_dir is not None + panel.file_attrs.update_stats(filepath=library.library_dir / filename) + + # Generate the expected file string. + # This is copied directly from the file_attributes.py file + # can be imported as a function in the future + display_path = expected_path(library) + file_str: str = "" + separator: str = f"{os.path.sep}" # Gray + for i, part in enumerate(display_path.parts): + part_ = part.strip(os.path.sep) + if i != len(display_path.parts) - 1: + file_str += f"{"\u200b".join(part_)}{separator}" + else: + if file_str != "": + file_str += "
" + file_str += f"{"\u200b".join(part_)}" + + # Assert the file path is displayed correctly + assert panel.file_attrs.file_label.text() == file_str @pytest.mark.parametrize( @@ -97,9 +106,9 @@ def test_file_path_display(qt_driver, library, filepath_option, expected_path): ), ], ) -def test_title_update(qtbot, qt_driver, filepath_option, expected_title): +def test_title_update(qt_driver: QtDriver, filepath_option: ShowFilepathOption, expected_title): base_title = qt_driver.base_title - test_path = pathlib.Path("/dev/null") + test_path = Path("/dev/null") open_status = LibraryStatus( success=True, library_path=test_path, @@ -124,7 +133,7 @@ def test_title_update(qtbot, qt_driver, filepath_option, expected_title): qt_driver.folders_to_tags_action = QAction(menu_bar) # Trigger the update - qt_driver.init_library(pathlib.Path(test_path), open_status) + qt_driver.init_library(test_path, open_status) # Assert the title is updated correctly qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(test_path, base_title)) From fca2ce4740b86445d22cdd2b5bd6c6174d8d3876 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 16:42:39 +0100 Subject: [PATCH 14/32] feat: setting for dark mode --- src/tagstudio/core/global_settings.py | 2 +- src/tagstudio/qt/modals/settings_panel.py | 15 +++++++++++++++ src/tagstudio/qt/ts_qt.py | 5 +++-- src/tagstudio/resources/translations/de.json | 1 + src/tagstudio/resources/translations/en.json | 1 + 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index a0d15398b..60e14d726 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -32,13 +32,13 @@ def dump_value(self, v): # 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 GlobalSettings(BaseModel): - # TODO: dark mode language: str = Field(default="en") open_last_loaded_on_startup: bool = Field(default=False) autoplay: bool = Field(default=False) show_filenames_in_grid: bool = Field(default=False) page_size: int = Field(default=500) show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT) + dark_mode: bool = Field(default=True) @staticmethod def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings": diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 0d581c7fd..0a9c34897 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -121,6 +121,16 @@ def on_page_size_changed(): ) form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox) + # Dark Mode + self.dark_mode_checkbox = QCheckBox() + self.dark_mode_checkbox.setChecked(driver.settings.dark_mode) + self.dark_mode_checkbox.checkStateChanged.connect( + lambda: self.restart_label.setHidden( + self.dark_mode_checkbox.isChecked() == driver.settings.dark_mode + ) + ) + form_layout.addRow(Translations["settings.dark_mode"], self.dark_mode_checkbox) + def __build_library_settings(self, driver: "QtDriver"): self.library_settings_container = QWidget() form_layout = QFormLayout(self.library_settings_container) @@ -137,6 +147,7 @@ def get_settings(self) -> dict: "show_filenames_in_grid": self.show_filenames_checkbox.isChecked(), "page_size": int(self.page_size_line_edit.text()), "show_filepath": self.filepath_combobox.currentData(), + "dark_mode": self.dark_mode_checkbox.isChecked(), } def update_settings(self, driver: "QtDriver"): @@ -148,11 +159,15 @@ def update_settings(self, driver: "QtDriver"): driver.settings.show_filenames_in_grid = settings["show_filenames_in_grid"] driver.settings.page_size = settings["page_size"] driver.settings.show_filepath = settings["show_filepath"] + driver.settings.dark_mode = settings["dark_mode"] driver.settings.save() + # Apply changes + # Language Translations.change_language(settings["language"]) + # Show File Path driver.update_recent_lib_menu() driver.preview_panel.update_widgets() library_directory = driver.lib.library_dir diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 4fb05e059..603f0257e 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -264,11 +264,12 @@ def setup_signals(self): def start(self) -> None: """Launch the main Qt window.""" _ = QUiLoader() - if os.name == "nt": - sys.argv += ["-platform", "windows:darkmode=2"] app = QApplication(sys.argv) app.setStyle("Fusion") + app.styleHints().setColorScheme( + Qt.ColorScheme.Dark if self.settings.dark_mode else Qt.ColorScheme.Light + ) if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark: pal: QPalette = app.palette() diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 4b0532254..e03694b11 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -230,6 +230,7 @@ "settings.global": "Globale Einstellungen", "settings.library": "Bibliothekseinstellungen", "settings.page_size": "Elemente pro Seite", + "settings.dark_mode": "Dunkles Design", "sorting.direction.ascending": "Aufsteigend", "sorting.direction.descending": "Absteigend", "splash.opening_library": "Öffne Bibliothek \"{library_path}\"...", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 74057b29c..7db7773f7 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -237,6 +237,7 @@ "settings.global": "Global Settings", "settings.library": "Library Settings", "settings.page_size": "Page Size", + "settings.dark_mode": "Dark Mode", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", "splash.opening_library": "Opening Library \"{library_path}\"...", From 1aba3eb4c2c4073e33e4b823a37027fad623d596 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 16:49:57 +0100 Subject: [PATCH 15/32] fix: only show restart_label when necessary --- src/tagstudio/qt/modals/settings_panel.py | 63 +++++++++++++---------- src/tagstudio/qt/translations.py | 10 ++-- src/tagstudio/qt/ts_qt.py | 14 ++--- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 0a9c34897..21b252f1e 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -18,7 +18,7 @@ ) from tagstudio.core.enums import ShowFilepathOption -from tagstudio.qt.translations import LANGUAGES, Translations +from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations from tagstudio.qt.widgets.panel import PanelModal, PanelWidget if TYPE_CHECKING: @@ -37,7 +37,7 @@ class SettingsPanel(PanelWidget): def __init__(self, driver: "QtDriver"): super().__init__() self.driver = driver - self.setMinimumSize(320, 200) + self.setMinimumSize(400, 300) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(0, 6, 0, 0) @@ -45,10 +45,10 @@ def __init__(self, driver: "QtDriver"): # Tabs self.tab_widget = QTabWidget() - self.__build_global_settings(driver) + self.__build_global_settings() self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"]) - self.__build_library_settings(driver) + self.__build_library_settings() self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"]) self.root_layout.addWidget(self.tab_widget) @@ -61,50 +61,61 @@ def __init__(self, driver: "QtDriver"): self.root_layout.addStretch(1) self.root_layout.addWidget(self.restart_label) - def __build_global_settings(self, driver: "QtDriver"): + self.__update_restart_label() + + def __update_restart_label(self): + show_label = ( + self.language_combobox.currentData() != Translations.current_language + or ( + Qt.ColorScheme.Dark if self.dark_mode_checkbox.isChecked() else Qt.ColorScheme.Light + ) + != self.driver.app.styleHints().colorScheme() + ) + self.restart_label.setHidden(not show_label) + + def __build_global_settings(self): self.global_settings_container = QWidget() form_layout = QFormLayout(self.global_settings_container) form_layout.setContentsMargins(6, 6, 6, 6) # Language self.language_combobox = QComboBox() - self.language_combobox.addItems(list(LANGUAGES.keys())) - current_lang: str = driver.settings.language + for k in LANGUAGES: + self.language_combobox.addItem(k, LANGUAGES[k]) + current_lang: str = self.driver.settings.language if current_lang not in LANGUAGES.values(): - current_lang = "en" + current_lang = DEFAULT_TRANSLATION self.language_combobox.setCurrentIndex(list(LANGUAGES.values()).index(current_lang)) - self.language_combobox.currentIndexChanged.connect( - lambda: self.restart_label.setHidden(False) - ) + self.language_combobox.currentIndexChanged.connect(self.__update_restart_label) form_layout.addRow(Translations["settings.language"], self.language_combobox) # Open Last Library on Start self.open_last_lib_checkbox = QCheckBox() - self.open_last_lib_checkbox.setChecked(driver.settings.open_last_loaded_on_startup) + self.open_last_lib_checkbox.setChecked(self.driver.settings.open_last_loaded_on_startup) form_layout.addRow( Translations["settings.open_library_on_start"], self.open_last_lib_checkbox ) # Autoplay self.autoplay_checkbox = QCheckBox() - self.autoplay_checkbox.setChecked(driver.settings.autoplay) + self.autoplay_checkbox.setChecked(self.driver.settings.autoplay) form_layout.addRow(Translations["media_player.autoplay"], self.autoplay_checkbox) # Show Filenames in Grid self.show_filenames_checkbox = QCheckBox() - self.show_filenames_checkbox.setChecked(driver.settings.show_filenames_in_grid) + self.show_filenames_checkbox.setChecked(self.driver.settings.show_filenames_in_grid) form_layout.addRow( Translations["settings.show_filenames_in_grid"], self.show_filenames_checkbox ) # Page Size self.page_size_line_edit = QLineEdit() - self.page_size_line_edit.setText(str(driver.settings.page_size)) + self.page_size_line_edit.setText(str(self.driver.settings.page_size)) def on_page_size_changed(): text = self.page_size_line_edit.text() if not text.isdigit() or int(text) < 1: - self.page_size_line_edit.setText(str(driver.settings.page_size)) + self.page_size_line_edit.setText(str(self.driver.settings.page_size)) self.page_size_line_edit.editingFinished.connect(on_page_size_changed) form_layout.addRow(Translations["settings.page_size"], self.page_size_line_edit) @@ -113,7 +124,7 @@ def on_page_size_changed(): self.filepath_combobox = QComboBox() for k in FILEPATH_OPTION_MAP: self.filepath_combobox.addItem(FILEPATH_OPTION_MAP[k], k) - filepath_option: ShowFilepathOption = driver.settings.show_filepath + filepath_option: ShowFilepathOption = self.driver.settings.show_filepath if filepath_option not in FILEPATH_OPTION_MAP: filepath_option = ShowFilepathOption.DEFAULT self.filepath_combobox.setCurrentIndex( @@ -123,15 +134,11 @@ def on_page_size_changed(): # Dark Mode self.dark_mode_checkbox = QCheckBox() - self.dark_mode_checkbox.setChecked(driver.settings.dark_mode) - self.dark_mode_checkbox.checkStateChanged.connect( - lambda: self.restart_label.setHidden( - self.dark_mode_checkbox.isChecked() == driver.settings.dark_mode - ) - ) + self.dark_mode_checkbox.setChecked(self.driver.settings.dark_mode) + self.dark_mode_checkbox.checkStateChanged.connect(self.__update_restart_label) form_layout.addRow(Translations["settings.dark_mode"], self.dark_mode_checkbox) - def __build_library_settings(self, driver: "QtDriver"): + def __build_library_settings(self): self.library_settings_container = QWidget() form_layout = QFormLayout(self.library_settings_container) form_layout.setContentsMargins(6, 6, 6, 6) @@ -139,9 +146,12 @@ def __build_library_settings(self, driver: "QtDriver"): todo_label = QLabel("TODO") form_layout.addRow(todo_label) + def __get_language(self) -> str: + return list(LANGUAGES.values())[self.language_combobox.currentIndex()] + def get_settings(self) -> dict: return { - "language": list(LANGUAGES.values())[self.language_combobox.currentIndex()], + "language": self.__get_language(), "open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(), "autoplay": self.autoplay_checkbox.isChecked(), "show_filenames_in_grid": self.show_filenames_checkbox.isChecked(), @@ -164,9 +174,6 @@ def update_settings(self, driver: "QtDriver"): driver.settings.save() # Apply changes - # Language - Translations.change_language(settings["language"]) - # Show File Path driver.update_recent_lib_menu() driver.preview_panel.update_widgets() diff --git a/src/tagstudio/qt/translations.py b/src/tagstudio/qt/translations.py index 5977de38d..e332a9a2e 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -13,7 +13,7 @@ class Translator: _default_strings: dict[str, str] _strings: dict[str, str] = {} - _lang: str = DEFAULT_TRANSLATION + __lang: str = DEFAULT_TRANSLATION def __init__(self): self._default_strings = self.__get_translation_dict(DEFAULT_TRANSLATION) @@ -26,7 +26,7 @@ def __get_translation_dict(self, lang: str) -> dict[str, str]: return ujson.loads(f.read()) def change_language(self, lang: str): - self._lang = lang + self.__lang = lang self._strings = self.__get_translation_dict(lang) def __format(self, text: str, **kwargs) -> str: @@ -37,7 +37,7 @@ def __format(self, text: str, **kwargs) -> str: "[Translations] Error while formatting translation.", text=text, kwargs=kwargs, - language=self._lang, + language=self.__lang, ) params: defaultdict[str, Any] = defaultdict(lambda: "{unknown_key}") params.update(kwargs) @@ -49,6 +49,10 @@ def format(self, key: str, **kwargs) -> str: def __getitem__(self, key: str) -> str: return self._strings.get(key) or self._default_strings.get(key) or f"[{key}]" + @property + def current_language(self) -> str: + return self.__lang + Translations = Translator() diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 603f0257e..a2ae121d0 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -265,14 +265,14 @@ def start(self) -> None: """Launch the main Qt window.""" _ = QUiLoader() - app = QApplication(sys.argv) - app.setStyle("Fusion") - app.styleHints().setColorScheme( + self.app = QApplication(sys.argv) + self.app.setStyle("Fusion") + self.app.styleHints().setColorScheme( Qt.ColorScheme.Dark if self.settings.dark_mode else Qt.ColorScheme.Light ) if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark: - pal: QPalette = app.palette() + pal: QPalette = self.app.palette() pal.setColor(QPalette.ColorGroup.Normal, QPalette.ColorRole.Window, QColor("#1e1e1e")) pal.setColor(QPalette.ColorGroup.Normal, QPalette.ColorRole.Button, QColor("#1e1e1e")) pal.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Window, QColor("#232323")) @@ -281,7 +281,7 @@ def start(self) -> None: QPalette.ColorGroup.Inactive, QPalette.ColorRole.ButtonText, QColor("#666666") ) - app.setPalette(pal) + self.app.setPalette(pal) # Handle OS signals self.setup_signals() @@ -313,7 +313,7 @@ def start(self) -> None: if sys.platform != "darwin": icon = QIcon() icon.addFile(str(self.rm.get_path("icon"))) - app.setWindowIcon(icon) + self.app.setWindowIcon(icon) # Initialize the Tag Manager panel self.tag_manager_panel = PanelModal( @@ -685,7 +685,7 @@ def create_about_modal(): if not self.ffmpeg_checker.installed(): self.ffmpeg_checker.show_warning() - app.exec() + self.app.exec() self.shutdown() def show_error_message(self, error_name: str, error_desc: str | None = None): From 6f0e3ed3d95f6809e5938b31b1bb8282f6f38913 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 17:06:12 +0100 Subject: [PATCH 16/32] fix: change modal from "done" type to "Save/Cancel" type --- src/tagstudio/qt/modals/settings_panel.py | 2 +- src/tagstudio/qt/ts_qt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 21b252f1e..cf902c4c4 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -193,7 +193,7 @@ def build_modal(cls, driver: "QtDriver") -> PanelModal: modal = PanelModal( widget=settings_panel, done_callback=lambda: settings_panel.update_settings(driver), - has_save=False, + has_save=True, ) modal.title_widget.setVisible(False) modal.setWindowTitle(Translations["settings.title"]) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index a2ae121d0..e2d48852e 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -141,7 +141,7 @@ class QtDriver(DriverMixin, QObject): SIGTERM = Signal() - preview_panel: PreviewPanel | None = None + preview_panel: PreviewPanel tag_manager_panel: PanelModal | None = None color_manager_panel: TagColorManager | None = None file_extension_panel: PanelModal | None = None From 3023051d6475cbb5737a2b50aa37af434a9808f8 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 17:18:38 +0100 Subject: [PATCH 17/32] feat: add test for GlobalSettings --- tests/qt/test_global_settings.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/qt/test_global_settings.py diff --git a/tests/qt/test_global_settings.py b/tests/qt/test_global_settings.py new file mode 100644 index 000000000..ad94e6be4 --- /dev/null +++ b/tests/qt/test_global_settings.py @@ -0,0 +1,28 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + +from tagstudio.core.global_settings import GlobalSettings + + +def test_read_settings(): + with TemporaryDirectory() as tmp_dir: + settings_path = Path(tmp_dir) / "settings.toml" + with open(settings_path, "a") as settings_file: + settings_file.write(""" + language = "de" + open_last_loaded_on_startup = true + autoplay = true + show_filenames_in_grid = true + page_size = 1337 + show_filepath = 0 + dark_mode = false + """) + + settings = GlobalSettings.read_settings(settings_path) + assert settings.language == "de" + assert settings.open_last_loaded_on_startup + assert settings.autoplay + assert settings.show_filenames_in_grid + assert settings.page_size == 1337 + assert settings.show_filepath == 0 + assert not settings.dark_mode From f4621df08d0ca73c3e27330de7122be106144d41 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 17:22:11 +0100 Subject: [PATCH 18/32] docs: mark roadmap item as completed --- docs/updates/roadmap.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index 5d7a0ca67..cb512f9ac 100644 --- a/docs/updates/roadmap.md +++ b/docs/updates/roadmap.md @@ -106,9 +106,9 @@ These version milestones are rough estimations for when the previous core featur - [ ] 3D Model Previews [MEDIUM] - [ ] STL Previews [HIGH] - [ ] Word count/line count on text thumbnails [LOW] -- [ ] Settings Menu [HIGH] -- [ ] Application Settings [HIGH] - - [ ] Stored in system user folder/designated folder [HIGH] +- [x] Settings Menu [HIGH] +- [x] Application Settings [HIGH] + - [x] Stored in system user folder/designated folder [HIGH] - [ ] Library Settings [HIGH] - [ ] Stored in `.TagStudio` folder [HIGH] - [ ] Tagging Panel [HIGH] From 76972dab9612e7cd294efbe76b921a684595a570 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 13 Mar 2025 17:25:19 +0100 Subject: [PATCH 19/32] fix(test_filepath_setting): Mock the app field of QtDriver --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 281f59785..94bd3ba3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -142,6 +142,7 @@ class Args: with patch("tagstudio.qt.ts_qt.Consumer"), patch("tagstudio.qt.ts_qt.CustomRunnable"): driver = QtDriver(Args()) + driver.app = Mock() driver.main_window = Mock() driver.preview_panel = Mock() driver.flow_container = Mock() From 14d1e04c3c3508a060c466ed56e65cf55a1a85b4 Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:24:09 +0100 Subject: [PATCH 20/32] Update src/tagstudio/main.py Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> --- src/tagstudio/main.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/tagstudio/main.py b/src/tagstudio/main.py index bc015507f..31d5edf68 100755 --- a/src/tagstudio/main.py +++ b/src/tagstudio/main.py @@ -57,12 +57,6 @@ def main(): action="store_true", help="Reveals additional internal data useful for debugging.", ) - # parser.add_argument( - # "--ui", - # dest="ui", - # type=str, - # help="User interface option for TagStudio. Options: qt, cli (Default: qt)", - # ) args = parser.parse_args() driver = QtDriver(args) From 28a663f71c5cc2fd17a82e86e521cf7dd19d2dcd Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 17:42:47 +0100 Subject: [PATCH 21/32] fix: address review suggestions --- src/tagstudio/core/global_settings.py | 6 ++- src/tagstudio/qt/modals/settings_panel.py | 4 +- src/tagstudio/qt/translations.py | 48 ++++++++++---------- src/tagstudio/qt/ts_qt.py | 1 - src/tagstudio/resources/translations/de.json | 8 ++-- src/tagstudio/resources/translations/en.json | 8 ++-- 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 60e14d726..92de11698 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -1,9 +1,10 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -import sys +import platform from enum import Enum from pathlib import Path +from typing import override import structlog import toml @@ -11,7 +12,7 @@ from tagstudio.core.enums import ShowFilepathOption -if sys.platform == "win32": +if platform.system() == "Windows": DEFAULT_GLOBAL_SETTINGS_PATH = ( Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" ) @@ -22,6 +23,7 @@ class TomlEnumEncoder(toml.TomlEncoder): + @override def dump_value(self, v): if isinstance(v, Enum): return super().dump_value(v.value) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index cf902c4c4..70ff411f2 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -48,8 +48,8 @@ def __init__(self, driver: "QtDriver"): self.__build_global_settings() self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"]) - self.__build_library_settings() - self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"]) + # self.__build_library_settings() + # self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"]) self.root_layout.addWidget(self.tab_widget) diff --git a/src/tagstudio/qt/translations.py b/src/tagstudio/qt/translations.py index e332a9a2e..2b03c117c 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -9,6 +9,30 @@ DEFAULT_TRANSLATION = "en" +LANGUAGES = { + # "Cantonese (Traditional)": "yue_Hant", # Empty + "Chinese (Traditional)": "zh_Hant", + # "Czech": "cs", # Minimal + # "Danish": "da", # Minimal + "Dutch": "nl", + "English": "en", + "Filipino": "fil", + "French": "fr", + "German": "de", + "Hungarian": "hu", + # "Italian": "it", # Minimal + "Norwegian Bokmål": "nb_NO", + "Polish": "pl", + "Portuguese (Brazil)": "pt_BR", + # "Portuguese (Portugal)": "pt", # Empty + "Russian": "ru", + "Spanish": "es", + "Swedish": "sv", + "Tamil": "ta", + "Toki Pona": "tok", + "Turkish": "tr", +} + class Translator: _default_strings: dict[str, str] @@ -55,27 +79,3 @@ def current_language(self) -> str: Translations = Translator() - -LANGUAGES = { - # "Cantonese (Traditional)": "yue_Hant", # Empty - "Chinese (Traditional)": "zh_Hant", - # "Czech": "cs", # Minimal - # "Danish": "da", # Minimal - "Dutch": "nl", - "English": "en", - "Filipino": "fil", - "French": "fr", - "German": "de", - "Hungarian": "hu", - # "Italian": "it", # Minimal - "Norwegian Bokmål": "nb_NO", - "Polish": "pl", - "Portuguese (Brazil)": "pt_BR", - # "Portuguese (Portugal)": "pt", # Empty - "Russian": "ru", - "Spanish": "es", - "Swedish": "sv", - "Tamil": "ta", - "Toki Pona": "tok", - "Turkish": "tr", -} diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 07eec043a..c29e75ec9 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1823,7 +1823,6 @@ def clear_recent_libs(self): self.update_recent_lib_menu() def open_settings_modal(self): - # TODO: don't re-create each time this is opened. SettingsPanel.build_modal(self).show() def open_library(self, path: Path) -> None: diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index e03694b11..6b2f233d8 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -221,16 +221,16 @@ "select.all": "Alle auswählen", "select.clear": "Auswahl leeren", "settings.clear_thumb_cache.title": "Vorschaubild-Zwischenspeicher leeren", + "settings.dark_mode": "Dunkles Design", + "settings.global": "Globale Einstellungen", "settings.language": "Sprache", + "settings.library": "Bibliothekseinstellungen", "settings.open_library_on_start": "Bibliothek zum Start öffnen", + "settings.page_size": "Elemente pro Seite", "settings.restart_required": "Bitte TagStudio neustarten, um Änderungen anzuwenden.", "settings.show_filenames_in_grid": "Dateinamen in Raster darstellen", "settings.show_recent_libraries": "Zuletzt verwendete Bibliotheken anzeigen", "settings.title": "Einstellungen", - "settings.global": "Globale Einstellungen", - "settings.library": "Bibliothekseinstellungen", - "settings.page_size": "Elemente pro Seite", - "settings.dark_mode": "Dunkles Design", "sorting.direction.ascending": "Aufsteigend", "sorting.direction.descending": "Absteigend", "splash.opening_library": "Öffne Bibliothek \"{library_path}\"...", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 7db7773f7..d9b15d272 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -224,20 +224,20 @@ "select.all": "Select All", "select.clear": "Clear Selection", "settings.clear_thumb_cache.title": "Clear Thumbnail Cache", + "settings.dark_mode": "Dark Mode", "settings.filepath.label": "Filepath Visibility", "settings.filepath.option.full": "Show Full Paths", "settings.filepath.option.name": "Show Filenames Only", "settings.filepath.option.relative": "Show Relative Paths", + "settings.global": "Global Settings", "settings.language": "Language", + "settings.library": "Library Settings", "settings.open_library_on_start": "Open Library on Start", + "settings.page_size": "Page Size", "settings.restart_required": "Please restart TagStudio for changes to take effect.", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", "settings.title": "Settings", - "settings.global": "Global Settings", - "settings.library": "Library Settings", - "settings.page_size": "Page Size", - "settings.dark_mode": "Dark Mode", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", "splash.opening_library": "Opening Library \"{library_path}\"...", From aba2f1c317899daaeb166943eb5b382070bab652 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 17:52:06 +0100 Subject: [PATCH 22/32] fix: page size setting --- src/tagstudio/qt/ts_qt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index c29e75ec9..ff12a38f8 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1697,6 +1697,7 @@ def filter_items(self, filter: FilterState | None = None) -> None: if filter: self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter)) else: + self.filter.page_size = self.settings.page_size self.filter.sorting_mode = self.sorting_mode self.filter.ascending = self.sorting_direction From eb32baa70bb1a67ff15356bedeef0309f7a56786 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 18:21:10 +0100 Subject: [PATCH 23/32] feat: change dark mode option to theme dropdown --- src/tagstudio/core/global_settings.py | 9 +++++- src/tagstudio/qt/modals/settings_panel.py | 29 +++++++++++++------- src/tagstudio/qt/ts_qt.py | 16 ++++++++--- src/tagstudio/resources/translations/de.json | 5 +++- src/tagstudio/resources/translations/en.json | 5 +++- 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 92de11698..20c23685d 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -30,6 +30,13 @@ def dump_value(self, v): return super().dump_value(v) +class Theme(Enum): + DARK = 0 + LIGHT = 1 + SYSTEM = 2 + DEFAULT = SYSTEM + + # 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. @@ -40,7 +47,7 @@ class GlobalSettings(BaseModel): show_filenames_in_grid: bool = Field(default=False) page_size: int = Field(default=500) show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT) - dark_mode: bool = Field(default=True) + theme: Theme = Field(default=Theme.SYSTEM) @staticmethod def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings": diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 70ff411f2..905c65d82 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -18,6 +18,7 @@ ) from tagstudio.core.enums import ShowFilepathOption +from tagstudio.core.global_settings import Theme from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations from tagstudio.qt.widgets.panel import PanelModal, PanelWidget @@ -30,6 +31,12 @@ ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"], } +THEME_MAP: dict[Theme, str] = { + Theme.DARK: Translations["settings.theme.dark"], + Theme.LIGHT: Translations["settings.theme.light"], + Theme.SYSTEM: Translations["settings.theme.system"], +} + class SettingsPanel(PanelWidget): driver: "QtDriver" @@ -66,10 +73,7 @@ def __init__(self, driver: "QtDriver"): def __update_restart_label(self): show_label = ( self.language_combobox.currentData() != Translations.current_language - or ( - Qt.ColorScheme.Dark if self.dark_mode_checkbox.isChecked() else Qt.ColorScheme.Light - ) - != self.driver.app.styleHints().colorScheme() + or self.theme_combobox.currentData() != self.driver.applied_theme ) self.restart_label.setHidden(not show_label) @@ -133,10 +137,15 @@ def on_page_size_changed(): form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox) # Dark Mode - self.dark_mode_checkbox = QCheckBox() - self.dark_mode_checkbox.setChecked(self.driver.settings.dark_mode) - self.dark_mode_checkbox.checkStateChanged.connect(self.__update_restart_label) - form_layout.addRow(Translations["settings.dark_mode"], self.dark_mode_checkbox) + self.theme_combobox = QComboBox() + for k in THEME_MAP: + self.theme_combobox.addItem(THEME_MAP[k], k) + theme: Theme = self.driver.settings.theme + if theme not in THEME_MAP: + theme = Theme.DEFAULT + self.theme_combobox.setCurrentIndex(list(THEME_MAP.keys()).index(theme)) + self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label) + form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox) def __build_library_settings(self): self.library_settings_container = QWidget() @@ -157,7 +166,7 @@ def get_settings(self) -> dict: "show_filenames_in_grid": self.show_filenames_checkbox.isChecked(), "page_size": int(self.page_size_line_edit.text()), "show_filepath": self.filepath_combobox.currentData(), - "dark_mode": self.dark_mode_checkbox.isChecked(), + "theme": self.theme_combobox.currentData(), } def update_settings(self, driver: "QtDriver"): @@ -169,7 +178,7 @@ def update_settings(self, driver: "QtDriver"): driver.settings.show_filenames_in_grid = settings["show_filenames_in_grid"] driver.settings.page_size = settings["page_size"] driver.settings.show_filepath = settings["show_filepath"] - driver.settings.dark_mode = settings["dark_mode"] + driver.settings.theme = settings["theme"] driver.settings.save() diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index ff12a38f8..504413948 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -57,7 +57,7 @@ from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH from tagstudio.core.driver import DriverMixin from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption -from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings +from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme from tagstudio.core.library.alchemy.enums import ( FieldTypeEnum, FilterState, @@ -152,6 +152,7 @@ class QtDriver(DriverMixin, QObject): about_modal: AboutModal unlinked_modal: FixUnlinkedEntriesModal dupe_modal: FixDupeFilesModal + applied_theme: Theme lib: Library @@ -266,11 +267,18 @@ def start(self) -> None: """Launch the main Qt window.""" _ = QUiLoader() + if self.settings.theme == Theme.SYSTEM and platform.system() == "Windows": + sys.argv += ["-platform", "windows:darkmode=2"] self.app = QApplication(sys.argv) self.app.setStyle("Fusion") - self.app.styleHints().setColorScheme( - Qt.ColorScheme.Dark if self.settings.dark_mode else Qt.ColorScheme.Light - ) + if self.settings.theme == Theme.SYSTEM: + # TODO: detect theme instead of always setting dark + self.app.styleHints().setColorScheme(Qt.ColorScheme.Dark) + else: + self.app.styleHints().setColorScheme( + Qt.ColorScheme.Dark if self.settings.theme == Theme.DARK else Qt.ColorScheme.Light + ) + self.applied_theme = self.settings.theme if ( platform.system() == "Darwin" or platform.system() == "Windows" diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 6b2f233d8..c86c00e3e 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -221,7 +221,6 @@ "select.all": "Alle auswählen", "select.clear": "Auswahl leeren", "settings.clear_thumb_cache.title": "Vorschaubild-Zwischenspeicher leeren", - "settings.dark_mode": "Dunkles Design", "settings.global": "Globale Einstellungen", "settings.language": "Sprache", "settings.library": "Bibliothekseinstellungen", @@ -230,6 +229,10 @@ "settings.restart_required": "Bitte TagStudio neustarten, um Änderungen anzuwenden.", "settings.show_filenames_in_grid": "Dateinamen in Raster darstellen", "settings.show_recent_libraries": "Zuletzt verwendete Bibliotheken anzeigen", + "settings.theme.dark": "Dunkel", + "settings.theme.label": "Design:", + "settings.theme.light": "Hell", + "settings.theme.system": "System", "settings.title": "Einstellungen", "sorting.direction.ascending": "Aufsteigend", "sorting.direction.descending": "Absteigend", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index d9b15d272..22c7bbea1 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -224,7 +224,6 @@ "select.all": "Select All", "select.clear": "Clear Selection", "settings.clear_thumb_cache.title": "Clear Thumbnail Cache", - "settings.dark_mode": "Dark Mode", "settings.filepath.label": "Filepath Visibility", "settings.filepath.option.full": "Show Full Paths", "settings.filepath.option.name": "Show Filenames Only", @@ -237,6 +236,10 @@ "settings.restart_required": "Please restart TagStudio for changes to take effect.", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", + "settings.theme.dark": "Dark", + "settings.theme.label": "Theme:", + "settings.theme.light": "Light", + "settings.theme.system": "System", "settings.title": "Settings", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", From 32a2c2c81eaa5cef42efa6ba2d59f43f1221ee01 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 18:30:59 +0100 Subject: [PATCH 24/32] fix: test was expecting wrong behaviour --- tests/qt/test_qt_driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/qt/test_qt_driver.py b/tests/qt/test_qt_driver.py index 86b2c1764..66bf49727 100644 --- a/tests/qt/test_qt_driver.py +++ b/tests/qt/test_qt_driver.py @@ -1,5 +1,6 @@ from tagstudio.core.library.alchemy.enums import FilterState from tagstudio.core.library.json.library import ItemType +from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.widgets.item_thumb import ItemThumb # def test_update_thumbs(qt_driver): @@ -61,7 +62,7 @@ # assert qt_driver.selected == [0, 1, 2] -def test_library_state_update(qt_driver): +def test_library_state_update(qt_driver: "QtDriver"): # Given for entry in qt_driver.lib.get_entries(with_joins=True): thumb = ItemThumb(ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100)) @@ -82,7 +83,6 @@ def test_library_state_update(qt_driver): # When state is not changed, previous one is still applied qt_driver.filter_items() - assert qt_driver.filter.page_size == 10 assert len(qt_driver.frame_content) == 1 entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0]) assert list(entry.tags)[0].name == "foo" From 66ac03462a53c96742ceb295f49e7d0f19c479aa Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 18:47:51 +0100 Subject: [PATCH 25/32] fix: test was testing for correct behaviour, fix behaviour instead --- src/tagstudio/core/library/alchemy/enums.py | 33 ++++++++++----------- src/tagstudio/core/utils/dupe_files.py | 2 +- src/tagstudio/qt/modals/tag_search.py | 6 +++- src/tagstudio/qt/ts_qt.py | 11 +++---- src/tagstudio/qt/widgets/tag_box.py | 4 ++- tests/qt/test_qt_driver.py | 5 ++-- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index f248eec23..ee7f3ca08 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -76,8 +76,8 @@ class FilterState: """Represent a state of the Library grid view.""" # these should remain + page_size: int page_index: int = 0 - page_size: int = 500 sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED ascending: bool = True @@ -94,35 +94,32 @@ def offset(self): return self.page_size * self.page_index @classmethod - def show_all(cls) -> "FilterState": - return FilterState() + def show_all(cls, page_size: int) -> "FilterState": + return FilterState(page_size=page_size) @classmethod - def from_search_query(cls, search_query: str) -> "FilterState": - return cls(ast=Parser(search_query).parse()) + def from_search_query(cls, search_query: str, page_size: int) -> "FilterState": + return cls(ast=Parser(search_query).parse(), page_size=page_size) @classmethod - def from_tag_id(cls, tag_id: int | str) -> "FilterState": - return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), [])) + def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState": + return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size) @classmethod - def from_path(cls, path: Path | str) -> "FilterState": - return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), [])) + def from_path(cls, path: Path | str, page_size: int) -> "FilterState": + return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size) @classmethod - def from_mediatype(cls, mediatype: str) -> "FilterState": - return cls(ast=Constraint(ConstraintType.MediaType, mediatype, [])) + def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState": + return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size) @classmethod - def from_filetype(cls, filetype: str) -> "FilterState": - return cls(ast=Constraint(ConstraintType.FileType, filetype, [])) + def from_filetype(cls, filetype: str, page_size: int) -> "FilterState": + return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size) @classmethod - def from_tag_name(cls, tag_name: str) -> "FilterState": - return cls(ast=Constraint(ConstraintType.Tag, tag_name, [])) - - def with_page_size(self, page_size: int) -> "FilterState": - return replace(self, page_size=page_size) + def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState": + return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size) def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState": return replace(self, sorting_mode=mode) diff --git a/src/tagstudio/core/utils/dupe_files.py b/src/tagstudio/core/utils/dupe_files.py index 673cefe8e..4ffc08d2b 100644 --- a/src/tagstudio/core/utils/dupe_files.py +++ b/src/tagstudio/core/utils/dupe_files.py @@ -52,7 +52,7 @@ def refresh_dupe_files(self, results_filepath: str | Path): continue results = self.library.search_library( - FilterState.from_path(path_relative), + FilterState.from_path(path_relative, page_size=500), ) if not results: diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index f7413764b..0fc516079 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -30,6 +30,7 @@ from tagstudio.core.palette import ColorType, get_tag_color from tagstudio.qt.modals import build_tag from tagstudio.qt.translations import Translations +from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.widgets.panel import PanelModal, PanelWidget from tagstudio.qt.widgets.tag import TagWidget @@ -43,6 +44,7 @@ class TagSearchPanel(PanelWidget): tag_chosen = Signal(int) lib: Library + driver: QtDriver is_initialized: bool = False first_tag_id: int | None = None is_tag_chooser: bool @@ -290,7 +292,9 @@ def set_tag_widget(self, tag: Tag | None, index: int): tag_widget.search_for_tag_action.triggered.connect( lambda checked=False, tag_id=tag.id: ( self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"), - self.driver.filter_items(FilterState.from_tag_id(tag_id)), + self.driver.filter_items( + FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size) + ), ) ) tag_widget.search_for_tag_action.setEnabled(True) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 504413948..7d8ed3d5d 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -163,7 +163,7 @@ def __init__(self, args: Namespace): self.lib = Library() self.rm: ResourceManager = ResourceManager() self.args = args - self.filter = FilterState.show_all() + self.filter = FilterState.show_all(page_size=self.settings.page_size) self.frame_content: list[int] = [] # List of Entry IDs on the current page self.pages_count = 0 @@ -682,7 +682,7 @@ def create_about_modal(): ] self.item_thumbs: list[ItemThumb] = [] self.thumb_renderers: list[ThumbRenderer] = [] - self.filter = FilterState.show_all() + self.filter = FilterState.show_all(page_size=self.settings.page_size) self.init_library_window() self.migration_modal: JsonMigrationModal = None @@ -731,7 +731,9 @@ def init_library_window(self): def _filter_items(): try: self.filter_items( - FilterState.from_search_query(self.main_window.searchField.text()) + FilterState.from_search_query( + self.main_window.searchField.text(), page_size=self.settings.page_size + ) .with_sorting_mode(self.sorting_mode) .with_sorting_direction(self.sorting_direction) ) @@ -853,7 +855,7 @@ def close_library(self, is_shutdown: bool = False): self.main_window.searchField.setText("") scrollbar: QScrollArea = self.main_window.scrollArea scrollbar.verticalScrollBar().setValue(0) - self.filter = FilterState.show_all() + self.filter = FilterState.show_all(page_size=self.settings.page_size) self.lib.close() @@ -1705,7 +1707,6 @@ def filter_items(self, filter: FilterState | None = None) -> None: if filter: self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter)) else: - self.filter.page_size = self.settings.page_size self.filter.sorting_mode = self.sorting_mode self.filter.ascending = self.sorting_direction diff --git a/src/tagstudio/qt/widgets/tag_box.py b/src/tagstudio/qt/widgets/tag_box.py index 8fdf1e9ca..68ac0fc2a 100644 --- a/src/tagstudio/qt/widgets/tag_box.py +++ b/src/tagstudio/qt/widgets/tag_box.py @@ -67,7 +67,9 @@ def set_tags(self, tags: typing.Iterable[Tag]): tag_widget.search_for_tag_action.triggered.connect( lambda checked=False, tag_id=tag.id: ( self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"), - self.driver.filter_items(FilterState.from_tag_id(tag_id)), + self.driver.filter_items( + FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size) + ), ) ) diff --git a/tests/qt/test_qt_driver.py b/tests/qt/test_qt_driver.py index 66bf49727..49b8946c3 100644 --- a/tests/qt/test_qt_driver.py +++ b/tests/qt/test_qt_driver.py @@ -74,7 +74,7 @@ def test_library_state_update(qt_driver: "QtDriver"): assert len(qt_driver.frame_content) == 2 # filter by tag - state = FilterState.from_tag_name("foo").with_page_size(10) + state = FilterState.from_tag_name("foo", page_size=10) qt_driver.filter_items(state) assert qt_driver.filter.page_size == 10 assert len(qt_driver.frame_content) == 1 @@ -83,12 +83,13 @@ def test_library_state_update(qt_driver: "QtDriver"): # When state is not changed, previous one is still applied qt_driver.filter_items() + assert qt_driver.filter.page_size == 10 assert len(qt_driver.frame_content) == 1 entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0]) assert list(entry.tags)[0].name == "foo" # When state property is changed, previous one is overwritten - state = FilterState.from_path("*bar.md") + state = FilterState.from_path("*bar.md", page_size=qt_driver.settings.page_size) qt_driver.filter_items(state) assert len(qt_driver.frame_content) == 1 entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0]) From 740f1ecbc7e0d47139a151797de4420bca3a4ccf Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 18:55:22 +0100 Subject: [PATCH 26/32] fix: test fr fr --- src/tagstudio/qt/modals/tag_search.py | 4 ++-- src/tagstudio/qt/ts_qt.py | 2 +- tests/qt/test_qt_driver.py | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index 0fc516079..1568e5470 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -30,7 +30,6 @@ from tagstudio.core.palette import ColorType, get_tag_color from tagstudio.qt.modals import build_tag from tagstudio.qt.translations import Translations -from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.widgets.panel import PanelModal, PanelWidget from tagstudio.qt.widgets.tag import TagWidget @@ -38,13 +37,14 @@ # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.modals.build_tag import BuildTagPanel class TagSearchPanel(PanelWidget): tag_chosen = Signal(int) lib: Library - driver: QtDriver + driver: "QtDriver" is_initialized: bool = False first_tag_id: int | None = None is_tag_chooser: bool diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 7d8ed3d5d..d7b864f70 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -163,7 +163,6 @@ def __init__(self, args: Namespace): self.lib = Library() self.rm: ResourceManager = ResourceManager() self.args = args - self.filter = FilterState.show_all(page_size=self.settings.page_size) self.frame_content: list[int] = [] # List of Entry IDs on the current page self.pages_count = 0 @@ -193,6 +192,7 @@ def __init__(self, args: Namespace): "[Settings] Global Settings File does not exist creating", path=self.global_settings_path, ) + self.filter = FilterState.show_all(page_size=self.settings.page_size) if self.args.cache_file: path = Path(self.args.cache_file) diff --git a/tests/qt/test_qt_driver.py b/tests/qt/test_qt_driver.py index 49b8946c3..6081450b7 100644 --- a/tests/qt/test_qt_driver.py +++ b/tests/qt/test_qt_driver.py @@ -1,8 +1,12 @@ +from typing import TYPE_CHECKING + from tagstudio.core.library.alchemy.enums import FilterState from tagstudio.core.library.json.library import ItemType -from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.widgets.item_thumb import ItemThumb +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + # def test_update_thumbs(qt_driver): # qt_driver.frame_content = [ # Entry( From 67339d6869b623b0d0c2631cc635fe87bc484497 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 19:04:01 +0100 Subject: [PATCH 27/32] fix: tests fr fr fr --- src/tagstudio/qt/modals/tag_search.py | 3 +- tests/test_library.py | 58 ++++++++++++++------------- tests/test_search.py | 4 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index 1568e5470..dbaa7ffa8 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -37,8 +37,8 @@ # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from tagstudio.qt.ts_qt import QtDriver from tagstudio.qt.modals.build_tag import BuildTagPanel + from tagstudio.qt.ts_qt import QtDriver class TagSearchPanel(PanelWidget): @@ -63,7 +63,6 @@ def __init__( ): super().__init__() self.lib = library - self.driver = None self.exclude = exclude or [] self.is_tag_chooser = is_tag_chooser diff --git a/tests/test_library.py b/tests/test_library.py index 6ed6efe00..6180bdc5a 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -107,7 +107,7 @@ def test_library_search(library, generate_tag, entry_full): tag = list(entry_full.tags)[0] results = library.search_library( - FilterState.from_tag_name(tag.name), + FilterState.from_tag_name(tag.name, page_size=500), ) assert results.total_count == 1 @@ -181,7 +181,7 @@ def test_search_filter_extensions(library, is_exclude): # When results = library.search_library( - FilterState.show_all(), + FilterState.show_all(page_size=500), ) # Then @@ -202,7 +202,7 @@ def test_search_library_case_insensitive(library): # When results = library.search_library( - FilterState.from_tag_name(tag.name.upper()), + FilterState.from_tag_name(tag.name.upper(), page_size=500), ) # Then @@ -416,98 +416,100 @@ class TestPrefs(DefaultEnum): def test_path_search_ilike(library: Library): - results = library.search_library(FilterState.from_path("bar.md")) + results = library.search_library(FilterState.from_path("bar.md", page_size=500)) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_like(library: Library): - results = library.search_library(FilterState.from_path("BAR.MD")) + results = library.search_library(FilterState.from_path("BAR.MD", page_size=500)) assert results.total_count == 0 assert len(results.items) == 0 def test_path_search_default_with_sep(library: Library): - results = library.search_library(FilterState.from_path("one/two")) + results = library.search_library(FilterState.from_path("one/two", page_size=500)) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_glob_after(library: Library): - results = library.search_library(FilterState.from_path("foo*")) + results = library.search_library(FilterState.from_path("foo*", page_size=500)) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_glob_in_front(library: Library): - results = library.search_library(FilterState.from_path("*bar.md")) + results = library.search_library(FilterState.from_path("*bar.md", page_size=500)) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_glob_both_sides(library: Library): - results = library.search_library(FilterState.from_path("*one/two*")) + results = library.search_library(FilterState.from_path("*one/two*", page_size=500)) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_ilike_glob_equality(library: Library): - results_ilike = library.search_library(FilterState.from_path("one/two")) - results_glob = library.search_library(FilterState.from_path("*one/two*")) + results_ilike = library.search_library(FilterState.from_path("one/two", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*one/two*", page_size=500)) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar.md")) - results_glob = library.search_library(FilterState.from_path("*bar.md*")) + results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500)) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar")) - results_glob = library.search_library(FilterState.from_path("*bar*")) + results_ilike = library.search_library(FilterState.from_path("bar", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*bar*", page_size=500)) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar.md")) - results_glob = library.search_library(FilterState.from_path("*bar.md*")) + results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500)) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None def test_path_search_like_glob_equality(library: Library): - results_ilike = library.search_library(FilterState.from_path("ONE/two")) - results_glob = library.search_library(FilterState.from_path("*ONE/two*")) + results_ilike = library.search_library(FilterState.from_path("ONE/two", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*ONE/two*", page_size=500)) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("BAR.MD")) - results_glob = library.search_library(FilterState.from_path("*BAR.MD*")) + results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500)) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("BAR.MD")) - results_glob = library.search_library(FilterState.from_path("*bar.md*")) + results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500)) assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar.md")) - results_glob = library.search_library(FilterState.from_path("*BAR.MD*")) + results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500)) + results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500)) assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items] results_ilike, results_glob = None, None @pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)]) def test_filetype_search(library, filetype, num_of_filetype): - results = library.search_library(FilterState.from_filetype(filetype)) + results = library.search_library(FilterState.from_filetype(filetype, page_size=500)) assert len(results.items) == num_of_filetype @pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)]) def test_filetype_return_one_filetype(file_mediatypes_library, filetype, num_of_filetype): - results = file_mediatypes_library.search_library(FilterState.from_filetype(filetype)) + results = file_mediatypes_library.search_library( + FilterState.from_filetype(filetype, page_size=500) + ) assert len(results.items) == num_of_filetype @pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)]) def test_mediatype_search(library, mediatype, num_of_mediatype): - results = library.search_library(FilterState.from_mediatype(mediatype)) + results = library.search_library(FilterState.from_mediatype(mediatype, page_size=500)) assert len(results.items) == num_of_mediatype diff --git a/tests/test_search.py b/tests/test_search.py index 98e3b8b4c..f8828819f 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -6,7 +6,7 @@ def verify_count(lib: Library, query: str, count: int): - results = lib.search_library(FilterState.from_search_query(query)) + results = lib.search_library(FilterState.from_search_query(query, page_size=500)) assert results.total_count == count assert len(results.items) == count @@ -136,4 +136,4 @@ def test_parent_tags(search_library: Library, query: str, count: int): ) def test_syntax(search_library: Library, invalid_query: str): with pytest.raises(ParsingError) as e_info: # noqa: F841 - search_library.search_library(FilterState.from_search_query(invalid_query)) + search_library.search_library(FilterState.from_search_query(invalid_query, page_size=500)) From e119b3bd9d738e8eaad43c49d019e926662eafe0 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 19:11:14 +0100 Subject: [PATCH 28/32] fix: tests fr fr fr fr --- src/tagstudio/qt/modals/tag_search.py | 1 + src/tagstudio/qt/ts_qt.py | 1 + tests/macros/test_missing_files.py | 2 +- tests/qt/test_global_settings.py | 2 +- tests/test_library.py | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index dbaa7ffa8..721b55ce2 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -63,6 +63,7 @@ def __init__( ): super().__init__() self.lib = library + self.driver = None self.exclude = exclude or [] self.is_tag_chooser = is_tag_chooser diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index d7b864f70..ac45720f6 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -165,6 +165,7 @@ def __init__(self, args: Namespace): self.args = args self.frame_content: list[int] = [] # List of Entry IDs on the current page self.pages_count = 0 + self.applied_theme = None # type: ignore self.scrollbar_pos = 0 self.thumb_size = 128 diff --git a/tests/macros/test_missing_files.py b/tests/macros/test_missing_files.py index 05f0c9eea..bfbe0667e 100644 --- a/tests/macros/test_missing_files.py +++ b/tests/macros/test_missing_files.py @@ -28,5 +28,5 @@ def test_refresh_missing_files(library: Library): assert list(registry.fix_unlinked_entries()) == [0, 1] # `bar.md` should be relinked to new correct path - results = library.search_library(FilterState.from_path("bar.md")) + results = library.search_library(FilterState.from_path("bar.md", page_size=500)) assert results[0].path == Path("bar.md") diff --git a/tests/qt/test_global_settings.py b/tests/qt/test_global_settings.py index ad94e6be4..6c56c2af4 100644 --- a/tests/qt/test_global_settings.py +++ b/tests/qt/test_global_settings.py @@ -25,4 +25,4 @@ def test_read_settings(): assert settings.show_filenames_in_grid assert settings.page_size == 1337 assert settings.show_filepath == 0 - assert not settings.dark_mode + assert not settings.theme diff --git a/tests/test_library.py b/tests/test_library.py index 6180bdc5a..5a7d600ec 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -135,7 +135,7 @@ def test_entries_count(library): new_ids = library.add_entries(entries) assert len(new_ids) == 10 - results = library.search_library(FilterState.show_all().with_page_size(5)) + results = library.search_library(FilterState.show_all(page_size=500)) assert results.total_count == 12 assert len(results) == 5 From 01a2bc394d15fa6e87f08bc7e563d3c63ffa4bd7 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 19:14:41 +0100 Subject: [PATCH 29/32] fix: update test --- tests/qt/test_global_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/qt/test_global_settings.py b/tests/qt/test_global_settings.py index 6c56c2af4..21953e962 100644 --- a/tests/qt/test_global_settings.py +++ b/tests/qt/test_global_settings.py @@ -1,7 +1,7 @@ from pathlib import Path from tempfile import TemporaryDirectory -from tagstudio.core.global_settings import GlobalSettings +from tagstudio.core.global_settings import GlobalSettings, Theme def test_read_settings(): @@ -15,7 +15,7 @@ def test_read_settings(): show_filenames_in_grid = true page_size = 1337 show_filepath = 0 - dark_mode = false + dark_mode = 2 """) settings = GlobalSettings.read_settings(settings_path) @@ -25,4 +25,4 @@ def test_read_settings(): assert settings.show_filenames_in_grid assert settings.page_size == 1337 assert settings.show_filepath == 0 - assert not settings.theme + assert settings.theme == Theme.SYSTEM From 891db6d568bbb387ab6ec33a5dfb36abae5874d6 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 20 Mar 2025 19:31:31 +0100 Subject: [PATCH 30/32] fix: tests fr fr fr fr fr --- src/tagstudio/qt/ts_qt.py | 2 +- tests/test_library.py | 99 +++++++++++++++++++++++++-------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index ac45720f6..b24a18f7d 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -165,7 +165,7 @@ def __init__(self, args: Namespace): self.args = args self.frame_content: list[int] = [] # List of Entry IDs on the current page self.pages_count = 0 - self.applied_theme = None # type: ignore + self.applied_theme = None self.scrollbar_pos = 0 self.thumb_size = 128 diff --git a/tests/test_library.py b/tests/test_library.py index 5a7d600ec..498db3d02 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -10,7 +10,7 @@ from tagstudio.core.library.alchemy.models import Entry, Tag -def test_library_add_alias(library, generate_tag): +def test_library_add_alias(library: Library, generate_tag): tag = library.add_tag(generate_tag("xxx", id=123)) assert tag @@ -19,50 +19,64 @@ def test_library_add_alias(library, generate_tag): alias_names: set[str] = set() alias_names.add("test_alias") library.update_tag(tag, parent_ids, alias_names, alias_ids) - alias_ids = library.get_tag(tag.id).alias_ids + tag = library.get_tag(tag.id) + assert tag is not None + alias_ids = set(tag.alias_ids) assert len(alias_ids) == 1 -def test_library_get_alias(library, generate_tag): +def test_library_get_alias(library: Library, generate_tag): tag = library.add_tag(generate_tag("xxx", id=123)) assert tag parent_ids: set[int] = set() - alias_ids: set[int] = set() + alias_ids: list[int] = [] alias_names: set[str] = set() alias_names.add("test_alias") library.update_tag(tag, parent_ids, alias_names, alias_ids) - alias_ids = library.get_tag(tag.id).alias_ids + tag = library.get_tag(tag.id) + assert tag is not None + alias_ids = tag.alias_ids - assert library.get_alias(tag.id, alias_ids[0]).name == "test_alias" + alias = library.get_alias(tag.id, alias_ids[0]) + assert alias is not None + assert alias.name == "test_alias" -def test_library_update_alias(library, generate_tag): - tag: Tag = library.add_tag(generate_tag("xxx", id=123)) - assert tag +def test_library_update_alias(library: Library, generate_tag): + tag: Tag | None = library.add_tag(generate_tag("xxx", id=123)) + assert tag is not None parent_ids: set[int] = set() - alias_ids: set[int] = set() + alias_ids: list[int] = [] alias_names: set[str] = set() alias_names.add("test_alias") library.update_tag(tag, parent_ids, alias_names, alias_ids) - alias_ids = library.get_tag(tag.id).alias_ids + tag = library.get_tag(tag.id) + assert tag is not None + alias_ids = tag.alias_ids - assert library.get_alias(tag.id, alias_ids[0]).name == "test_alias" + alias = library.get_alias(tag.id, alias_ids[0]) + assert alias is not None + assert alias.name == "test_alias" alias_names.remove("test_alias") alias_names.add("alias_update") library.update_tag(tag, parent_ids, alias_names, alias_ids) tag = library.get_tag(tag.id) + assert tag is not None assert len(tag.alias_ids) == 1 - assert library.get_alias(tag.id, tag.alias_ids[0]).name == "alias_update" + alias = library.get_alias(tag.id, tag.alias_ids[0]) + assert alias is not None + assert alias.name == "alias_update" @pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True) -def test_library_add_file(library): +def test_library_add_file(library: Library): """Check Entry.path handling for insert vs lookup""" + assert library.folder is not None entry = Entry( path=Path("bar.txt"), @@ -75,7 +89,7 @@ def test_library_add_file(library): assert library.has_path_entry(entry.path) -def test_create_tag(library, generate_tag): +def test_create_tag(library: Library, generate_tag): # tag already exists assert not library.add_tag(generate_tag("foo", id=1000)) @@ -85,10 +99,11 @@ def test_create_tag(library, generate_tag): assert tag.id == 123 tag_inc = library.add_tag(generate_tag("yyy")) + assert tag_inc is not None assert tag_inc.id > 1000 -def test_tag_self_parent(library, generate_tag): +def test_tag_self_parent(library: Library, generate_tag): # tag already exists assert not library.add_tag(generate_tag("foo", id=1000)) @@ -97,12 +112,13 @@ def test_tag_self_parent(library, generate_tag): assert tag assert tag.id == 123 - library.update_tag(tag, {tag.id}, {}, {}) + library.update_tag(tag, {tag.id}, [], []) tag = library.get_tag(tag.id) + assert tag is not None assert len(tag.parent_ids) == 0 -def test_library_search(library, generate_tag, entry_full): +def test_library_search(library: Library, generate_tag, entry_full): assert library.entries_count == 2 tag = list(entry_full.tags)[0] @@ -114,7 +130,7 @@ def test_library_search(library, generate_tag, entry_full): assert len(results) == 1 -def test_tag_search(library): +def test_tag_search(library: Library): tag = library.tags[0] assert library.search_tags(tag.name.lower()) @@ -130,24 +146,26 @@ def test_get_entry(library: Library, entry_min): assert len(result.tags) == 1 -def test_entries_count(library): +def test_entries_count(library: Library): + assert library.folder is not None entries = [Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[]) for x in range(10)] new_ids = library.add_entries(entries) assert len(new_ids) == 10 - results = library.search_library(FilterState.show_all(page_size=500)) + results = library.search_library(FilterState.show_all(page_size=5)) assert results.total_count == 12 assert len(results) == 5 -def test_parents_add(library, generate_tag): +def test_parents_add(library: Library, generate_tag): # Given - tag: Tag = library.tags[0] + tag: Tag | None = library.tags[0] assert tag.id is not None parent_tag = generate_tag("parent_tag_01") parent_tag = library.add_tag(parent_tag) + assert parent_tag is not None assert parent_tag.id is not None # When @@ -156,10 +174,11 @@ def test_parents_add(library, generate_tag): # Then assert tag.id is not None tag = library.get_tag(tag.id) + assert tag is not None assert tag.parent_ids -def test_remove_tag(library, generate_tag): +def test_remove_tag(library: Library, generate_tag): tag = library.add_tag(generate_tag("food", id=123)) assert tag @@ -171,7 +190,7 @@ def test_remove_tag(library, generate_tag): @pytest.mark.parametrize("is_exclude", [True, False]) -def test_search_filter_extensions(library, is_exclude): +def test_search_filter_extensions(library: Library, is_exclude: bool): # Given entries = list(library.get_entries()) assert len(entries) == 2, entries @@ -192,7 +211,7 @@ def test_search_filter_extensions(library, is_exclude): assert (entry.path.suffix == ".txt") == is_exclude -def test_search_library_case_insensitive(library): +def test_search_library_case_insensitive(library: Library): # Given entries = list(library.get_entries(with_joins=True)) assert len(entries) == 2, entries @@ -212,12 +231,12 @@ def test_search_library_case_insensitive(library): assert results[0].id == entry.id -def test_preferences(library): +def test_preferences(library: Library): for pref in LibraryPrefs: assert library.prefs(pref) == pref.default -def test_remove_entry_field(library, entry_full): +def test_remove_entry_field(library: Library, entry_full): title_field = entry_full.text_fields[0] library.remove_entry_field(title_field, [entry_full.id]) @@ -226,7 +245,7 @@ def test_remove_entry_field(library, entry_full): assert not entry.text_fields -def test_remove_field_entry_with_multiple_field(library, entry_full): +def test_remove_field_entry_with_multiple_field(library: Library, entry_full): # Given title_field = entry_full.text_fields[0] @@ -242,7 +261,7 @@ def test_remove_field_entry_with_multiple_field(library, entry_full): assert len(entry.text_fields) == 1 -def test_update_entry_field(library, entry_full): +def test_update_entry_field(library: Library, entry_full): title_field = entry_full.text_fields[0] library.update_entry_field( @@ -255,7 +274,7 @@ def test_update_entry_field(library, entry_full): assert entry.text_fields[0].value == "new value" -def test_update_entry_with_multiple_identical_fields(library, entry_full): +def test_update_entry_with_multiple_identical_fields(library: Library, entry_full): # Given title_field = entry_full.text_fields[0] @@ -278,6 +297,7 @@ def test_update_entry_with_multiple_identical_fields(library, entry_full): def test_mirror_entry_fields(library: Library, entry_full): # new entry + assert library.folder is not None target_entry = Entry( folder=library.folder, path=Path("xxx"), @@ -295,12 +315,14 @@ def test_mirror_entry_fields(library: Library, entry_full): # get new entry from library new_entry = library.get_entry_full(entry_id) + assert new_entry is not None # mirror fields onto new entry library.mirror_entry_fields(new_entry, entry_full) # get new entry from library again entry = library.get_entry_full(entry_id) + assert entry is not None # make sure fields are there after getting it from the library again assert len(entry.fields) == 2 @@ -311,6 +333,7 @@ def test_mirror_entry_fields(library: Library, entry_full): def test_merge_entries(library: Library): + assert library.folder is not None a = Entry( folder=library.folder, path=Path("a"), @@ -327,10 +350,14 @@ def test_merge_entries(library: Library): try: ids = library.add_entries([a, b]) entry_a = library.get_entry_full(ids[0]) + assert entry_a is not None entry_b = library.get_entry_full(ids[1]) + assert entry_b is not None tag_0 = library.add_tag(Tag(id=1000, name="tag_0")) tag_1 = library.add_tag(Tag(id=1001, name="tag_1")) + assert tag_1 is not None tag_2 = library.add_tag(Tag(id=1002, name="tag_2")) + assert tag_2 is not None library.add_tags_to_entries(ids[0], [tag_0.id, tag_2.id]) library.add_tags_to_entries(ids[1], [tag_1.id]) library.merge_entries(entry_a, entry_b) @@ -345,7 +372,7 @@ def test_merge_entries(library: Library): AssertionError() -def test_remove_tags_from_entries(library, entry_full): +def test_remove_tags_from_entries(library: Library, entry_full): removed_tag_id = -1 for tag in entry_full.tags: removed_tag_id = tag.id @@ -370,7 +397,7 @@ def test_search_entry_id(library: Library, query_name: int, has_result): assert (result is not None) == has_result -def test_update_field_order(library, entry_full): +def test_update_field_order(library: Library, entry_full): # Given title_field = entry_full.text_fields[0] @@ -496,13 +523,13 @@ def test_path_search_like_glob_equality(library: Library): @pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)]) -def test_filetype_search(library, filetype, num_of_filetype): +def test_filetype_search(library: Library, filetype, num_of_filetype): results = library.search_library(FilterState.from_filetype(filetype, page_size=500)) assert len(results.items) == num_of_filetype @pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)]) -def test_filetype_return_one_filetype(file_mediatypes_library, filetype, num_of_filetype): +def test_filetype_return_one_filetype(file_mediatypes_library: Library, filetype, num_of_filetype): results = file_mediatypes_library.search_library( FilterState.from_filetype(filetype, page_size=500) ) @@ -510,6 +537,6 @@ def test_filetype_return_one_filetype(file_mediatypes_library, filetype, num_of_ @pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)]) -def test_mediatype_search(library, mediatype, num_of_mediatype): +def test_mediatype_search(library: Library, mediatype, num_of_mediatype): results = library.search_library(FilterState.from_mediatype(mediatype, page_size=500)) assert len(results.items) == num_of_mediatype From de8543e15313ed1882a521726a687b7edf4d19f1 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Tue, 25 Mar 2025 22:48:57 +0100 Subject: [PATCH 31/32] fix: select all was selecting hidden entries --- src/tagstudio/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index b24a18f7d..23e5d5a94 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -947,7 +947,7 @@ def select_all_action_callback(self): """Set the selection to all visible items.""" self.selected.clear() for item in self.item_thumbs: - if item.mode and item.item_id not in self.selected: + if item.mode and item.item_id not in self.selected and not item.isHidden(): self.selected.append(item.item_id) item.thumb_button.set_selected(True) From 0478eb4c8a1cd6d2998a1c62921669e97ad12217 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Tue, 25 Mar 2025 22:57:19 +0100 Subject: [PATCH 32/32] fix: create more thumbitems as necessary --- src/tagstudio/qt/ts_qt.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 23e5d5a94..a14a0330a 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1322,14 +1322,10 @@ def remove_grid_item(self, grid_idx: int): self.frame_content[grid_idx] = None self.item_thumbs[grid_idx].hide() - def _init_thumb_grid(self): - layout = FlowLayout() - layout.enable_grid_optimizations(value=True) - layout.setSpacing(min(self.thumb_size // 10, 12)) - layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - - # TODO - init after library is loaded, it can have different page_size - for _ in range(self.filter.page_size): + def _update_thumb_count(self): + missing_count = max(0, self.filter.page_size - len(self.item_thumbs)) + layout = self.flow_container.layout() + for _ in range(missing_count): item_thumb = ItemThumb( None, self.lib, @@ -1341,9 +1337,18 @@ def _init_thumb_grid(self): layout.addWidget(item_thumb) self.item_thumbs.append(item_thumb) + def _init_thumb_grid(self): + layout = FlowLayout() + layout.enable_grid_optimizations(value=True) + layout.setSpacing(min(self.thumb_size // 10, 12)) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.flow_container: QWidget = QWidget() self.flow_container.setObjectName("flowContainer") self.flow_container.setLayout(layout) + + self._update_thumb_count() + sa: QScrollArea = self.main_window.scrollArea sa.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) sa.setWidgetResizable(True) @@ -1558,6 +1563,7 @@ def update_completions_list(self, text: str) -> None: def update_thumbs(self): """Update search thumbnails.""" + self._update_thumb_count() # start_time = time.time() # logger.info(f'Current Page: {self.cur_page_idx}, Stack Length:{len(self.nav_stack)}') with self.thumb_job_queue.mutex: