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] diff --git a/pyproject.toml b/pyproject.toml index 4fdcbd411..c1de697f0 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.9.2", ] [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 bebe348d9..027a78725 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -10,15 +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" - SHOW_FILEPATH = "show_filepath" - AUTOPLAY = "autoplay_videos" THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit" - LANGUAGE = "language" class ShowFilepathOption(int, enum.Enum): @@ -81,5 +75,4 @@ class LibraryPrefs(DefaultEnum): IS_EXCLUDE_LIST = True EXTENSION_LIST = [".json", ".xmp", ".aae"] - PAGE_SIZE = 500 DB_VERSION = 9 diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py new file mode 100644 index 000000000..20c23685d --- /dev/null +++ b/src/tagstudio/core/global_settings.py @@ -0,0 +1,70 @@ +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +import platform +from enum import Enum +from pathlib import Path +from typing import override + +import structlog +import toml +from pydantic import BaseModel, Field + +from tagstudio.core.enums import ShowFilepathOption + +if platform.system() == "Windows": + DEFAULT_GLOBAL_SETTINGS_PATH = ( + Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" + ) +else: + DEFAULT_GLOBAL_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml" + +logger = structlog.get_logger(__name__) + + +class TomlEnumEncoder(toml.TomlEncoder): + @override + def dump_value(self, v): + if isinstance(v, Enum): + return super().dump_value(v.value) + 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. +class GlobalSettings(BaseModel): + 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) + theme: Theme = Field(default=Theme.SYSTEM) + + @staticmethod + 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_GLOBAL_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, encoder=TomlEnumEncoder()) diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index e2da73823..ee7f3ca08 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -76,14 +76,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_size: int + page_index: int = 0 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): @@ -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/main.py b/src/tagstudio/main.py index 18ebc3c11..31d5edf68 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,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) 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 378397892..905c65d82 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -3,105 +3,208 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +from typing import TYPE_CHECKING + from PySide6.QtCore import Qt -from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget +from PySide6.QtWidgets import ( + QCheckBox, + QComboBox, + QFormLayout, + QLabel, + QLineEdit, + QTabWidget, + QVBoxLayout, + QWidget, +) + +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 + +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver -from tagstudio.core.enums import SettingItems, ShowFilepathOption -from tagstudio.qt.translations import Translations -from tagstudio.qt.widgets.panel import PanelWidget +FILEPATH_OPTION_MAP: dict[ShowFilepathOption, str] = { + ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"], + ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations["settings.filepath.option.relative"], + 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): - def __init__(self, driver): + driver: "QtDriver" + + 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(6, 0, 6, 0) + self.root_layout.setContentsMargins(0, 6, 0, 0) + + # Tabs + self.tab_widget = QTabWidget() + + self.__build_global_settings() + self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"]) - self.form_container = QWidget() - self.form_layout = QFormLayout(self.form_container) - self.form_layout.setContentsMargins(0, 0, 0, 0) + # self.__build_library_settings() + # 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) - 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.root_layout.addStretch(1) + self.root_layout.addWidget(self.restart_label) + + self.__update_restart_label() + + def __update_restart_label(self): + show_label = ( + self.language_combobox.currentData() != Translations.current_language + or self.theme_combobox.currentData() != self.driver.applied_theme + ) + 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(self.languages.keys())) - current_lang: str = str( - driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str) + 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 = DEFAULT_TRANSLATION + self.language_combobox.setCurrentIndex(list(LANGUAGES.values()).index(current_lang)) + 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(self.driver.settings.open_last_loaded_on_startup) + form_layout.addRow( + Translations["settings.open_library_on_start"], self.open_last_lib_checkbox ) - 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)) - self.language_combobox.currentIndexChanged.connect( - lambda: self.restart_label.setHidden(False) + + # Autoplay + self.autoplay_checkbox = QCheckBox() + 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(self.driver.settings.show_filenames_in_grid) + form_layout.addRow( + Translations["settings.show_filenames_in_grid"], self.show_filenames_checkbox ) - self.form_layout.addRow(language_label, self.language_combobox) - - filepath_option_map: dict[int, str] = { - ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"], - ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations[ - "settings.filepath.option.relative" - ], - ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"], - } + + # Page Size + self.page_size_line_edit = QLineEdit() + 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(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) + + # Show Filepath self.filepath_combobox = QComboBox() - self.filepath_combobox.addItems(list(filepath_option_map.values())) - filepath_option: int = int( - driver.settings.value( - SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int - ) + for k in FILEPATH_OPTION_MAP: + self.filepath_combobox.addItem(FILEPATH_OPTION_MAP[k], k) + filepath_option: ShowFilepathOption = self.driver.settings.show_filepath + if filepath_option not in FILEPATH_OPTION_MAP: + filepath_option = ShowFilepathOption.DEFAULT + self.filepath_combobox.setCurrentIndex( + list(FILEPATH_OPTION_MAP.keys()).index(filepath_option) ) - filepath_option = 0 if filepath_option not in filepath_option_map else filepath_option - self.filepath_combobox.setCurrentIndex(filepath_option) - self.filepath_combobox.currentIndexChanged.connect(self.apply_filepath_setting) - self.form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox) + form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox) - self.root_layout.addWidget(self.form_container) - self.root_layout.addStretch(1) - self.root_layout.addWidget(self.restart_label) + # Dark Mode + 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() + 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: + return list(LANGUAGES.values())[self.language_combobox.currentIndex()] - def get_language(self) -> str: - values: list[str] = list(self.languages.values()) - return values[self.language_combobox.currentIndex()] - - def apply_filepath_setting(self): - selected_value = self.filepath_combobox.currentIndex() - self.driver.settings.setValue(SettingItems.SHOW_FILEPATH, selected_value) - self.driver.update_recent_lib_menu() - self.driver.preview_panel.update_widgets() - library_directory = self.driver.lib.library_dir - if selected_value == ShowFilepathOption.SHOW_FULL_PATHS: - display_path = library_directory + def get_settings(self) -> dict: + return { + "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(), + "page_size": int(self.page_size_line_edit.text()), + "show_filepath": self.filepath_combobox.currentData(), + "theme": self.theme_combobox.currentData(), + } + + def update_settings(self, driver: "QtDriver"): + settings = self.get_settings() + + 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.page_size = settings["page_size"] + driver.settings.show_filepath = settings["show_filepath"] + driver.settings.theme = settings["theme"] + + driver.settings.save() + + # Apply changes + # Show File Path + driver.update_recent_lib_menu() + driver.preview_panel.update_widgets() + library_directory = driver.lib.library_dir + if settings["show_filepath"] == ShowFilepathOption.SHOW_FULL_PATHS: + display_path = library_directory or "" else: - display_path = library_directory.name - self.driver.main_window.setWindowTitle( - Translations.format( - "app.title", base_title=self.driver.base_title, library_dir=display_path - ) + display_path = library_directory.name if library_directory else "" + driver.main_window.setWindowTitle( + Translations.format("app.title", base_title=driver.base_title, library_dir=display_path) ) + + @classmethod + def build_modal(cls, driver: "QtDriver") -> PanelModal: + settings_panel = cls(driver) + + modal = PanelModal( + widget=settings_panel, + done_callback=lambda: settings_panel.update_settings(driver), + has_save=True, + ) + modal.title_widget.setVisible(False) + modal.setWindowTitle(Translations["settings.title"]) + + return modal diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index f7413764b..721b55ce2 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -38,11 +38,13 @@ # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: from tagstudio.qt.modals.build_tag import BuildTagPanel + from tagstudio.qt.ts_qt import QtDriver 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/translations.py b/src/tagstudio/qt/translations.py index ef775b5d5..2b03c117c 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -9,11 +9,35 @@ 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] _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 +50,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 +61,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,5 +73,9 @@ 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 14b75afdf..a14a0330a 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -17,6 +17,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,7 +56,8 @@ import tagstudio.qt.resources_rc # noqa: F401 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, ShowFilepathOption +from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption +from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme from tagstudio.core.library.alchemy.enums import ( FieldTypeEnum, FilterState, @@ -140,25 +142,30 @@ 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 tag_search_panel: TagSearchPanel | None = None add_tag_modal: PanelModal | None = None + folders_modal: FoldersToTagsModal + about_modal: AboutModal + unlinked_modal: FixUnlinkedEntriesModal + dupe_modal: FixDupeFilesModal + applied_theme: Theme 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 self.lib = Library() self.rm: ResourceManager = ResourceManager() self.args = args - self.filter = FilterState.show_all() self.frame_content: list[int] = [] # List of Entry IDs on the current page self.pages_count = 0 + self.applied_theme = None self.scrollbar_pos = 0 self.thumb_size = 128 @@ -175,35 +182,43 @@ 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, + ) + self.filter = FilterState.show_all(page_size=self.settings.page_size) + + 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) - self.settings = QSettings(str(path), QSettings.Format.IniFormat) - self.config_path = str(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) else: - self.settings = QSettings( + self.cached_values = QSettings( QSettings.Format.IniFormat, QSettings.Scope.UserScope, "TagStudio", "TagStudio", ) logger.info( - "[Config] Config File not specified, using default one", - filename=self.settings.fileName(), + "[Cache] Cache File not specified, using default one", + filename=self.cached_values.fileName(), ) - self.config_path = self.settings.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, @@ -212,8 +227,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)}", ) @@ -252,16 +267,24 @@ 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") + 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") + 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" ) and 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")) @@ -270,7 +293,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() @@ -299,15 +322,15 @@ def start(self) -> None: appid = "cyanvoxel.tagstudio.9" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) # type: ignore[attr-defined,unused-ignore] - app.setApplicationName("tagstudio") - app.setApplicationDisplayName("TagStudio") + self.app.setApplicationName("tagstudio") + self.app.setApplicationDisplayName("TagStudio") if platform.system() != "Darwin": fallback_icon = QIcon() fallback_icon.addFile(str(self.rm.get_path("icon"))) - app.setWindowIcon(QIcon.fromTheme("tagstudio", fallback_icon)) + self.app.setWindowIcon(QIcon.fromTheme("tagstudio", fallback_icon)) if platform.system() != "Windows": - app.setDesktopFileName("tagstudio") + self.app.setDesktopFileName("tagstudio") # Initialize the Tag Manager panel self.tag_manager_panel = PanelModal( @@ -389,12 +412,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() @@ -534,23 +558,19 @@ def start(self) -> None: 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( - bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) - ) + # 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_filenames_in_grid = 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 =========================================================== @@ -617,7 +637,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) @@ -663,14 +683,14 @@ 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 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: @@ -682,7 +702,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): @@ -712,7 +732,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) ) @@ -826,15 +848,15 @@ 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() 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() @@ -925,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) @@ -1300,30 +1322,33 @@ 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, 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) 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) @@ -1538,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: @@ -1721,22 +1747,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 @@ -1744,26 +1770,21 @@ 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): """Updates the recent library menu from the latest values from the settings file.""" actions: list[QAction] = [] lib_items: dict[str, tuple[str, str]] = {} - filepath_option: int = int( - self.settings.value( - SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int - ) - ) - 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)) @@ -1780,10 +1801,10 @@ def update_recent_lib_menu(self): for library_key in libs_sorted: path = Path(library_key[1][0]) action = QAction(self.open_recent_library_menu) - if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS: + if self.settings.show_filepath == ShowFilepathOption.SHOW_FULL_PATHS: action.setText(str(path)) else: - action.setText(str(Path(path).name)) + action.setText(str(path.name)) action.triggered.connect(lambda checked=False, p=path: self.open_library(p)) actions.append(action) @@ -1811,40 +1832,20 @@ 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): - # 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() + SettingsPanel.build_modal(self).show() def open_library(self, path: Path) -> None: """Open a TagStudio library.""" - filepath_option: int = int( - self.settings.value( - SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int - ) - ) library_dir_display = ( - path if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS else path.name + path if self.settings.show_filepath == ShowFilepathOption.SHOW_FULL_PATHS else path.name ) message = Translations.format("splash.opening_library", library_path=library_dir_display) self.main_window.landing_widget.set_status_label(message) @@ -1885,19 +1886,13 @@ 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: self.add_new_files_callback() - library_dir_display = self.lib.library_dir - filepath_option: int = int( - self.settings.value( - SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int - ) - ) - if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS: + if self.settings.show_filepath == ShowFilepathOption.SHOW_FULL_PATHS: library_dir_display = self.lib.library_dir else: library_dir_display = self.lib.library_dir.name 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/preview/file_attributes.py b/src/tagstudio/qt/widgets/preview/file_attributes.py index c6f3c0b2c..d894af18b 100644 --- a/src/tagstudio/qt/widgets/preview/file_attributes.py +++ b/src/tagstudio/qt/widgets/preview/file_attributes.py @@ -17,7 +17,7 @@ from PySide6.QtGui import QGuiApplication from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget -from tagstudio.core.enums import SettingItems, ShowFilepathOption, Theme +from tagstudio.core.enums import ShowFilepathOption, Theme from tagstudio.core.library.alchemy.library import Library from tagstudio.core.media_types import MediaCategories from tagstudio.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel @@ -144,16 +144,13 @@ def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict self.dimensions_label.setText("") self.dimensions_label.setHidden(True) else: - filepath_option = self.driver.settings.value( - SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int - ) self.library_path = self.library.library_dir display_path = filepath - if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS: + if self.driver.settings.show_filepath == ShowFilepathOption.SHOW_FULL_PATHS: display_path = filepath - elif filepath_option == ShowFilepathOption.SHOW_RELATIVE_PATHS: + elif self.driver.settings.show_filepath == ShowFilepathOption.SHOW_RELATIVE_PATHS: display_path = Path(filepath).relative_to(self.library_path) - elif filepath_option == ShowFilepathOption.SHOW_FILENAMES_ONLY: + elif self.driver.settings.show_filepath == ShowFilepathOption.SHOW_FILENAMES_ONLY: display_path = Path(filepath.name) self.layout().setSpacing(6) 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/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: diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index ef32101bb..c86c00e3e 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -221,11 +221,18 @@ "select.all": "Alle auswählen", "select.clear": "Auswahl leeren", "settings.clear_thumb_cache.title": "Vorschaubild-Zwischenspeicher leeren", + "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.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 ed683b611..22c7bbea1 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -228,11 +228,18 @@ "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.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", diff --git a/tests/conftest.py b/tests/conftest.py index 1a1195150..94bd3ba3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -134,13 +134,15 @@ def qt_driver(qtbot, library): with TemporaryDirectory() as tmp_dir: class Args: - config_file = Path(tmp_dir) / "tagstudio.ini" + settings_file = Path(tmp_dir) / "settings.toml" + cache_file = Path(tmp_dir) / "tagstudio.ini" open = Path(tmp_dir) ci = True 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() 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_file_path_options.py b/tests/qt/test_file_path_options.py index 642bcaa84..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 SettingItems, ShowFilepathOption -from tagstudio.core.library.alchemy.library import LibraryStatus +from tagstudio.core.enums import ShowFilepathOption +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) @@ -31,10 +34,10 @@ def test_filepath_setting(qtbot, qt_driver, filepath_option): with patch.object(qt_driver, "update_recent_lib_menu", return_value=None): # Set the file path option settings_panel.filepath_combobox.setCurrentIndex(filepath_option) - settings_panel.apply_filepath_setting() + settings_panel.update_settings(qt_driver) # Assert the setting is applied - assert qt_driver.settings.value(SettingItems.SHOW_FILEPATH) == filepath_option + assert qt_driver.settings.show_filepath == filepath_option # Tests to see if the file paths are being displayed correctly @@ -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 library: pathlib.Path("one/two/bar.md")), - (ShowFilepathOption.SHOW_FILENAMES_ONLY, lambda library: 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, @@ -107,7 +116,7 @@ def test_title_update(qtbot, qt_driver, filepath_option, expected_title): msg_description="", ) # Set the file path option - qt_driver.settings.setValue(SettingItems.SHOW_FILEPATH, filepath_option) + qt_driver.settings.show_filepath = filepath_option menu_bar = QMenuBar() qt_driver.open_recent_library_menu = QMenu(menu_bar) @@ -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)) diff --git a/tests/qt/test_global_settings.py b/tests/qt/test_global_settings.py new file mode 100644 index 000000000..21953e962 --- /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, Theme + + +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 = 2 + """) + + 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 settings.theme == Theme.SYSTEM diff --git a/tests/qt/test_qt_driver.py b/tests/qt/test_qt_driver.py index 86b2c1764..6081450b7 100644 --- a/tests/qt/test_qt_driver.py +++ b/tests/qt/test_qt_driver.py @@ -1,7 +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.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( @@ -61,7 +66,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)) @@ -73,7 +78,7 @@ def test_library_state_update(qt_driver): 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 @@ -88,7 +93,7 @@ def test_library_state_update(qt_driver): 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]) 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) diff --git a/tests/test_library.py b/tests/test_library.py index 6ed6efe00..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,24 +112,25 @@ 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] results = library.search_library( - FilterState.from_tag_name(tag.name), + FilterState.from_tag_name(tag.name, page_size=500), ) assert results.total_count == 1 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().with_page_size(5)) + 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 @@ -181,7 +200,7 @@ def test_search_filter_extensions(library, is_exclude): # When results = library.search_library( - FilterState.show_all(), + FilterState.show_all(page_size=500), ) # Then @@ -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 @@ -202,7 +221,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 @@ -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] @@ -416,98 +443,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)) +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): - results = file_mediatypes_library.search_library(FilterState.from_filetype(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) + ) 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)) +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 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))