feat: new settings menu + settings backend (#859)

* feat: add tab widget

* refactor: move languages dict to translations.py

* refactor: move build of Settings Modal to SettingsPanel class

* feat: hide title label

* feat: global settings class

* fix: initialise settings

* fix: properly store grid files changes

* fix: placeholder text for library settings

* feat: add ui elements for remaining global settings

* feat: add page size setting

* fix: version mismatch between pydantic and typing_extensions

* fix: update test_driver.py

* fix(test_file_path_options): replace patch with change of settings

* feat: setting for dark mode

* fix: only show restart_label when necessary

* fix: change modal from "done" type to "Save/Cancel" type

* feat: add test for GlobalSettings

* docs: mark roadmap item as completed

* fix(test_filepath_setting): Mock the app field of QtDriver

* Update src/tagstudio/main.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* fix: address review suggestions

* fix: page size setting

* feat: change dark mode option to theme dropdown

* fix: test was expecting wrong behaviour

* fix: test was testing for correct behaviour, fix behaviour instead

* fix: test fr fr

* fix: tests fr fr fr

* fix: tests fr fr fr fr

* fix: update test

* fix: tests fr fr fr fr fr

* fix: select all was selecting hidden entries

* fix: create more thumbitems as necessary
This commit is contained in:
Jann Stute
2025-03-25 23:02:53 +01:00
committed by GitHub
parent e112788466
commit adb996e1d2
27 changed files with 680 additions and 399 deletions

View File

@@ -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]

View File

@@ -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]

View File

@@ -5,13 +5,15 @@ from PySide6.QtCore import QSettings
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 @@ class DriverMixin:
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

View File

@@ -10,15 +10,9 @@ from uuid import uuid4
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

View File

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

View File

@@ -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 @@ class FilterState:
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)

View File

@@ -52,7 +52,7 @@ class DupeRegistry:
continue
results = self.library.search_library(
FilterState.from_path(path_relative),
FilterState.from_path(path_relative, page_size=500),
)
if not results:

View File

@@ -33,11 +33,18 @@ def main():
help="Path to a TagStudio Library folder to open on start.",
)
parser.add_argument(
"-c",
"--config-file",
dest="config_file",
"-s",
"--settings-file",
dest="settings_file",
type=str,
help="Path to a TagStudio .ini or .plist config file to use.",
help="Path to a TagStudio .toml global settings file to use.",
)
parser.add_argument(
"-c",
"--cache-file",
dest="cache_file",
type=str,
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)

View File

@@ -32,7 +32,7 @@ class CacheManager(metaclass=Singleton):
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:

View File

@@ -3,105 +3,208 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget
from typing import TYPE_CHECKING
from tagstudio.core.enums import SettingItems, ShowFilepathOption
from tagstudio.qt.translations import Translations
from tagstudio.qt.widgets.panel import PanelWidget
from PySide6.QtCore import Qt
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
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)
self.form_container = QWidget()
self.form_layout = QFormLayout(self.form_container)
self.form_layout.setContentsMargins(0, 0, 0, 0)
# Tabs
self.tab_widget = QTabWidget()
self.__build_global_settings()
self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"])
# self.__build_library_settings()
# self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"])
self.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.language_combobox = QComboBox()
self.language_combobox.addItems(list(self.languages.keys()))
current_lang: str = str(
driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str)
)
current_lang = "en" if current_lang not in self.languages.values() else current_lang
self.language_combobox.setCurrentIndex(list(self.languages.values()).index(current_lang))
self.language_combobox.currentIndexChanged.connect(
lambda: self.restart_label.setHidden(False)
)
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"],
}
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
)
)
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)
self.root_layout.addWidget(self.form_container)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.restart_label)
def get_language(self) -> str:
values: list[str] = list(self.languages.values())
return values[self.language_combobox.currentIndex()]
self.__update_restart_label()
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
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
)
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()
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
)
# 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
)
# 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()
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)
)
form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox)
# 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_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 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

View File

@@ -38,11 +38,13 @@ logger = structlog.get_logger(__name__)
# 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 @@ class TagSearchPanel(PanelWidget):
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)

View File

@@ -10,11 +10,35 @@ logger = structlog.get_logger(__name__)
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)
@@ -27,7 +51,7 @@ class Translator:
return ujson.loads(f.read())
def change_language(self, lang: str):
self._lang = lang
self.__lang = lang
self._strings = self.__get_translation_dict(lang)
if system() == "Darwin":
for k, v in self._strings.items():
@@ -43,7 +67,7 @@ class Translator:
"[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)
@@ -55,5 +79,9 @@ class Translator:
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()

View File

@@ -17,6 +17,7 @@ import platform
import re
import sys
import time
from argparse import Namespace
from pathlib import Path
from queue import Queue
from shutil import which
@@ -56,7 +57,8 @@ from PySide6.QtWidgets import (
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,
@@ -142,25 +144,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
@@ -177,35 +184,43 @@ class QtDriver(DriverMixin, QObject):
self.SIGTERM.connect(self.handle_sigterm)
self.config_path = ""
if self.args.config_file:
path = Path(self.args.config_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)
self.global_settings_path = DEFAULT_GLOBAL_SETTINGS_PATH
if self.args.settings_file:
self.global_settings_path = Path(self.args.settings_file)
else:
self.settings = QSettings(
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("[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.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,
@@ -214,8 +229,8 @@ class QtDriver(DriverMixin, QObject):
)
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)}",
)
@@ -254,16 +269,24 @@ class QtDriver(DriverMixin, QObject):
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"))
@@ -272,7 +295,7 @@ class QtDriver(DriverMixin, QObject):
QPalette.ColorGroup.Inactive, QPalette.ColorRole.ButtonText, QColor("#666666")
)
app.setPalette(pal)
self.app.setPalette(pal)
# Handle OS signals
self.setup_signals()
@@ -301,15 +324,15 @@ class QtDriver(DriverMixin, QObject):
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(
@@ -391,12 +414,13 @@ class QtDriver(DriverMixin, QObject):
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()
@@ -536,23 +560,19 @@ class QtDriver(DriverMixin, QObject):
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 ===========================================================
@@ -619,7 +639,7 @@ class QtDriver(DriverMixin, QObject):
# 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)
@@ -665,14 +685,14 @@ class QtDriver(DriverMixin, QObject):
]
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 @@ class QtDriver(DriverMixin, QObject):
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
FfmpegChecker().show()
app.exec()
self.app.exec()
self.shutdown()
def show_error_message(self, error_name: str, error_desc: str | None = None):
@@ -712,7 +732,9 @@ class QtDriver(DriverMixin, QObject):
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 @@ class QtDriver(DriverMixin, QObject):
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 @@ class QtDriver(DriverMixin, QObject):
"""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 @@ class QtDriver(DriverMixin, QObject):
self.frame_content[grid_idx] = None
self.item_thumbs[grid_idx].hide()
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),
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)
# TODO - init after library is loaded, it can have different page_size
for _ in range(self.filter.page_size):
item_thumb = ItemThumb(
None,
self.lib,
self,
(self.thumb_size, self.thumb_size),
bool(
self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)
),
)
layout.addWidget(item_thumb)
self.item_thumbs.append(item_thumb)
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 @@ class QtDriver(DriverMixin, QObject):
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 @@ class QtDriver(DriverMixin, QObject):
)
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 @@ class QtDriver(DriverMixin, QObject):
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 @@ class QtDriver(DriverMixin, QObject):
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 @@ class QtDriver(DriverMixin, QObject):
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 @@ class QtDriver(DriverMixin, QObject):
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

View File

@@ -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 @@ class ItemThumb(FlowWidget):
):
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

View File

@@ -17,7 +17,7 @@ from PySide6.QtCore import Qt
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 @@ class FileAttributes(QWidget):
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)

View File

@@ -67,7 +67,9 @@ class TagBoxWidget(FieldWidget):
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)
),
)
)

View File

@@ -21,7 +21,6 @@ from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
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 @@ class VideoPlayer(QGraphicsView):
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 @@ class VideoPlayer(QGraphicsView):
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:

View File

@@ -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",

View File

@@ -231,11 +231,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",

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import os
import pathlib
from pathlib import Path
from unittest.mock import patch
import pytest
@@ -7,10 +7,13 @@ from PySide6.QtGui import (
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 @@ from tagstudio.qt.widgets.preview_panel import PreviewPanel
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))
qt_driver.settings.show_filepath = filepath_option
# 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"<a style='color: #777777'><b>{os.path.sep}</a>" # 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}</b>"
else:
if file_str != "":
file_str += "<br>"
file_str += f"<b>{"\u200b".join(part_)}</b>"
# 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)
# Assert the file path is displayed correctly
assert panel.file_attrs.file_label.text() == file_str
# 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"<a style='color: #777777'><b>{os.path.sep}</a>" # 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}</b>"
else:
if file_str != "":
file_str += "<br>"
file_str += f"<b>{"\u200b".join(part_)}</b>"
# 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))

View File

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

View File

@@ -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 @@ from tagstudio.qt.widgets.item_thumb import ItemThumb
# 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])

View File

@@ -7,18 +7,19 @@ from PySide6.QtCore import QSettings
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)

View File

@@ -10,7 +10,7 @@ from tagstudio.core.library.alchemy.library import Library
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 @@ def test_library_prefs_multiple_identical_vals():
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

View File

@@ -6,7 +6,7 @@ from tagstudio.core.query_lang.util import ParsingError
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))