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))