Skip to content

Commit 854c805

Browse files
committed
feat: add settings menu
1 parent a272b18 commit 854c805

21 files changed

+364
-86
lines changed

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12.8

requirements.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ PySide6==6.8.0.1
1414
rawpy==0.22.0
1515
SQLAlchemy==2.0.34
1616
structlog==24.4.0
17-
typing_extensions>=3.10.0.0,<=4.11.0
17+
typing_extensions
1818
ujson>=5.8.0,<=5.9.0
19-
vtf2img==0.1.0
19+
vtf2img==0.1.0
20+
toml==0.10.2
21+
appdirs==1.4.4
22+
pydantic==2.10.4

tagstudio/src/core/driver.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from pathlib import Path
22

33
import structlog
4-
from PySide6.QtCore import QSettings
54
from src.core.constants import TS_FOLDER_NAME
6-
from src.core.enums import SettingItems
75
from src.core.library.alchemy.library import LibraryStatus
6+
from src.core.settings import TSSettings
7+
from src.core.tscacheddata import TSCachedData
88

99
logger = structlog.get_logger(__name__)
1010

1111

1212
class DriverMixin:
13-
settings: QSettings
13+
settings: TSSettings
14+
cache: TSCachedData
1415

1516
def evaluate_path(self, open_path: str | None) -> LibraryStatus:
1617
"""Check if the path of library is valid."""
@@ -20,17 +21,15 @@ def evaluate_path(self, open_path: str | None) -> LibraryStatus:
2021
if not library_path.exists():
2122
logger.error("Path does not exist.", open_path=open_path)
2223
return LibraryStatus(success=False, message="Path does not exist.")
23-
elif self.settings.value(
24-
SettingItems.START_LOAD_LAST, defaultValue=True, type=bool
25-
) and self.settings.value(SettingItems.LAST_LIBRARY):
26-
library_path = Path(str(self.settings.value(SettingItems.LAST_LIBRARY)))
24+
elif self.settings.open_last_loaded_on_startup and self.cache.last_library:
25+
library_path = Path(str(self.cache.last_library))
2726
if not (library_path / TS_FOLDER_NAME).exists():
2827
logger.error(
2928
"TagStudio folder does not exist.",
3029
library_path=library_path,
3130
ts_folder=TS_FOLDER_NAME,
3231
)
33-
self.settings.setValue(SettingItems.LAST_LIBRARY, "")
32+
self.cache.last_library = ""
3433
# dont consider this a fatal error, just skip opening the library
3534
library_path = None
3635

tagstudio/src/core/library/alchemy/library.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
TS_FOLDER_NAME,
5454
)
5555
from ...enums import LibraryPrefs
56+
from ...settings import LibSettings
5657
from .db import make_tables
5758
from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum, TagColor
5859
from .fields import (
@@ -159,6 +160,7 @@ class Library:
159160
engine: Engine | None
160161
folder: Folder | None
161162
included_files: set[Path] = set()
163+
settings: LibSettings | None = None
162164

163165
SQL_FILENAME: str = "ts_library.sqlite"
164166
JSON_FILENAME: str = "ts_library.json"
@@ -238,8 +240,8 @@ def migrate_json_to_sqlite(self, json_lib: JsonLibrary):
238240
)
239241

240242
# Preferences
241-
self.set_prefs(LibraryPrefs.EXTENSION_LIST, [x.strip(".") for x in json_lib.ext_list])
242-
self.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, json_lib.is_exclude_list)
243+
self.settings.extension_list = [x.strip(".") for x in json_lib.ext_list]
244+
self.settings.is_exclude_list = json_lib.is_exclude_list
243245

244246
end_time = time.time()
245247
logger.info(f"Library Converted! ({format_timespan(end_time-start_time)})")
@@ -258,6 +260,9 @@ def open_library(self, library_dir: Path, storage_path: str | None = None) -> Li
258260
return self.open_sqlite_library(library_dir, is_new)
259261
else:
260262
self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME
263+
settings_path = library_dir / TS_FOLDER_NAME / "libsettings.toml"
264+
265+
self.settings = LibSettings.open(settings_path)
261266

262267
if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()):
263268
json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME
@@ -587,10 +592,9 @@ def search_library(
587592
f"SQL Expression Builder finished ({format_timespan(end_time - start_time)})"
588593
)
589594

590-
extensions = self.prefs(LibraryPrefs.EXTENSION_LIST)
591-
is_exclude_list = self.prefs(LibraryPrefs.IS_EXCLUDE_LIST)
595+
extensions = self.settings.extension_list
592596

593-
if extensions and is_exclude_list:
597+
if extensions and self.settings.is_exclude_list:
594598
statement = statement.where(Entry.suffix.notin_(extensions))
595599
elif extensions:
596600
statement = statement.where(Entry.suffix.in_(extensions))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .libsettings import LibSettings
2+
from .tssettings import TSSettings
3+
4+
__all__ = ["TSSettings", "LibSettings"]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pathlib import Path
2+
3+
import structlog
4+
import toml
5+
from pydantic import BaseModel, Field
6+
7+
logger = structlog.get_logger(__name__)
8+
9+
10+
class LibSettings(BaseModel):
11+
is_exclude_list: bool = Field(default=True)
12+
extension_list: list[str] = Field(default=[".json", ".xmp", ".aae"])
13+
page_size: int = Field(default=500)
14+
db_version: int = Field(default=2)
15+
filename: str = Field(default="")
16+
17+
@staticmethod
18+
def open(path_value: Path | str) -> "LibSettings":
19+
path: Path = Path(path_value) if not isinstance(path_value, Path) else path_value
20+
21+
if path.exists():
22+
with open(path) as settings_file:
23+
filecontents = settings_file.read()
24+
if len(filecontents.strip()) != 0:
25+
settings_data = toml.loads(filecontents)
26+
settings_data["filename"] = str(path)
27+
return LibSettings(**settings_data)
28+
29+
# either settings file did not exist or was empty - either way, use default settings
30+
settings = LibSettings(**dict(filename=str(path)))
31+
return settings
32+
33+
def save(self):
34+
if not (parent_path := Path(self.filename).parent).exists():
35+
parent_path.mkdir()
36+
37+
with open(self.filename, "w") as settings_file:
38+
toml.dump(dict(self), settings_file)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from pathlib import Path
2+
3+
import toml
4+
from pydantic import BaseModel, Field
5+
6+
7+
# NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings
8+
# properties to be overwritten with environment variables. as tagstudio is not currently using
9+
# environment variables, i did not base it on that, but that may be useful in the future.
10+
class TSSettings(BaseModel):
11+
dark_mode: bool = Field(default=False)
12+
language: str = Field(default="en")
13+
14+
# settings from the old SettingItem enum
15+
open_last_loaded_on_startup: bool = Field(default=False)
16+
show_library_list: bool = Field(default=True)
17+
autoplay: bool = Field(default=False)
18+
show_filenames_in_grid: bool = Field(default=False)
19+
20+
filename: str = Field()
21+
22+
@staticmethod
23+
def read_settings(path: Path | str) -> "TSSettings":
24+
path_value = Path(path)
25+
if path_value.exists():
26+
with open(path) as file:
27+
filecontents = file.read()
28+
if len(filecontents.strip()) != 0:
29+
settings_data = toml.loads(filecontents)
30+
settings = TSSettings(**settings_data)
31+
return settings
32+
33+
return TSSettings(**dict(filename=str(path)))
34+
35+
def save(self, path: Path | str | None = None) -> None:
36+
path_value: Path = Path(path) if isinstance(path, str) else Path(self.filename)
37+
38+
if not path_value.parent.exists():
39+
path_value.parent.mkdir(parents=True, exist_ok=True)
40+
41+
with open(path_value, "w") as f:
42+
toml.dump(dict(self), f)

tagstudio/src/core/tscacheddata.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from datetime import datetime
2+
from pathlib import Path
3+
4+
import structlog
5+
import toml
6+
from appdirs import user_cache_dir
7+
from pydantic import BaseModel, ConfigDict, Field
8+
9+
logger = structlog.get_logger(__name__)
10+
11+
cache_dir = Path(user_cache_dir()) / "TagStudio"
12+
cache_location = cache_dir / "cache.toml"
13+
14+
15+
class TSCachedData(BaseModel):
16+
model_config = ConfigDict(arbitrary_types_allowed=True)
17+
last_library: str | None = Field(default=None)
18+
# a dict of ISO formatted date strings -> paths
19+
library_history: dict[str, str] = Field(default_factory=dict[datetime, str])
20+
21+
path: str = Field()
22+
23+
@staticmethod
24+
def open(path_value: Path | str | None = None) -> "TSCachedData":
25+
path: Path | None = None
26+
default_cache_location = Path(user_cache_dir()) / "ts_cache.toml"
27+
if isinstance(path_value, str):
28+
path = Path(path_value)
29+
elif isinstance(path_value, Path):
30+
path = path_value
31+
else:
32+
logger.info(
33+
"no cache location was specified, using ",
34+
default_cache_location=default_cache_location,
35+
)
36+
path = default_cache_location
37+
38+
if path.exists():
39+
with open(path) as cache_file:
40+
filecontents = cache_file.read()
41+
if len(filecontents.strip()) != 0:
42+
cache_data = toml.loads(filecontents)
43+
cache_data["path"] = str(path)
44+
logger.info("opening cache file at ", cache_location=path)
45+
return TSCachedData(**cache_data)
46+
47+
return TSCachedData(**dict(path=str(default_cache_location)))
48+
49+
def save(self):
50+
with open(self.path, "w") as f:
51+
file_data = dict(self)
52+
file_data.pop("path")
53+
toml.dump(file_data, f)

tagstudio/src/qt/modals/file_extension.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
QVBoxLayout,
1717
QWidget,
1818
)
19-
from src.core.enums import LibraryPrefs
2019
from src.core.library import Library
2120
from src.qt.translations import Translations
2221
from src.qt.widgets.panel import PanelWidget
@@ -43,7 +42,7 @@ def __init__(self, library: "Library"):
4342
self.root_layout.setContentsMargins(6, 6, 6, 6)
4443

4544
# Create Table Widget --------------------------------------------------
46-
self.table = QTableWidget(len(self.lib.prefs(LibraryPrefs.EXTENSION_LIST)), 1)
45+
self.table = QTableWidget(len(self.lib.settings.extension_list), 1)
4746
self.table.horizontalHeader().setVisible(False)
4847
self.table.verticalHeader().setVisible(False)
4948
self.table.horizontalHeader().setStretchLastSection(True)
@@ -74,7 +73,7 @@ def __init__(self, library: "Library"):
7473
lambda text: self.mode_combobox.setItemText(1, text), "ignore_list.mode.exclude"
7574
)
7675

77-
is_exclude_list = int(bool(self.lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST)))
76+
is_exclude_list = int(bool(self.lib.settings.is_exclude_list))
7877

7978
self.mode_combobox.setCurrentIndex(is_exclude_list)
8079
self.mode_combobox.currentIndexChanged.connect(lambda i: self.update_list_mode(i))
@@ -97,10 +96,10 @@ def update_list_mode(self, mode: int):
9796
mode (int): The list mode, given by the index of the mode inside
9897
the mode combobox. 1 for "Exclude", 0 for "Include".
9998
"""
100-
self.lib.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, bool(mode))
99+
self.lib.settings.is_exclude_list = bool(mode)
101100

102101
def refresh_list(self):
103-
for i, ext in enumerate(self.lib.prefs(LibraryPrefs.EXTENSION_LIST)):
102+
for i, ext in enumerate(self.lib.settings.extension_list):
104103
self.table.setItem(i, 0, QTableWidgetItem(ext))
105104

106105
def add_item(self):
@@ -114,4 +113,4 @@ def save(self):
114113
extensions.append(ext.text().strip().lstrip(".").lower())
115114

116115
# save preference
117-
self.lib.set_prefs(LibraryPrefs.EXTENSION_LIST, extensions)
116+
self.lib.settings.extension_list = extensions
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import copy
2+
from pathlib import Path
3+
from typing import Any
4+
5+
from PySide6.QtWidgets import (
6+
QCheckBox,
7+
QComboBox,
8+
QHBoxLayout,
9+
QLabel,
10+
QVBoxLayout,
11+
)
12+
from src.core.settings import TSSettings
13+
from src.qt.widgets.panel import PanelWidget
14+
15+
16+
class SettingsModal(PanelWidget):
17+
def __init__(self, settings: TSSettings):
18+
super().__init__()
19+
self.tempSettings: TSSettings = copy.deepcopy(settings)
20+
21+
self.main = QVBoxLayout(self)
22+
23+
# ---
24+
self.language_Label = QLabel()
25+
self.language_Value = QComboBox()
26+
self.language_Row = QHBoxLayout()
27+
self.language_Row.addWidget(self.language_Label)
28+
self.language_Row.addWidget(self.language_Value)
29+
30+
self.language_Label.setText("Language")
31+
translations_folder = Path("tagstudio/resources/translations")
32+
language_list = [x.stem for x in translations_folder.glob("*.json")]
33+
self.language_Value.addItems(language_list)
34+
self.language_Value.setCurrentIndex(language_list.index(self.tempSettings.language))
35+
self.language_Value.currentTextChanged.connect(
36+
lambda text: setattr(self.tempSettings, "language", text)
37+
)
38+
39+
# ---
40+
self.show_library_list_Label = QLabel()
41+
self.show_library_list_Value = QCheckBox()
42+
self.show_library_list_Row = QHBoxLayout()
43+
self.show_library_list_Row.addWidget(self.show_library_list_Label)
44+
self.show_library_list_Row.addWidget(self.show_library_list_Value)
45+
self.show_library_list_Label.setText("Load library list on startup (requires restart):")
46+
self.show_library_list_Value.setChecked(self.tempSettings.show_library_list)
47+
48+
self.show_library_list_Value.stateChanged.connect(
49+
lambda state: setattr(self.tempSettings, "show_library_list", bool(state))
50+
)
51+
52+
# ---
53+
self.show_filenames_Label = QLabel()
54+
self.show_filenames_Value = QCheckBox()
55+
self.show_filenames_Row = QHBoxLayout()
56+
self.show_filenames_Row.addWidget(self.show_filenames_Label)
57+
self.show_filenames_Row.addWidget(self.show_filenames_Value)
58+
self.show_filenames_Label.setText("Show filenames in grid (requires restart)")
59+
self.show_filenames_Value.setChecked(self.tempSettings.show_filenames_in_grid)
60+
61+
self.show_filenames_Value.stateChanged.connect(
62+
lambda state: setattr(self.tempSettings, "show_filenames_in_grid", bool(state))
63+
)
64+
# ---
65+
self.main.addLayout(self.language_Row)
66+
self.main.addLayout(self.show_library_list_Row)
67+
self.main.addLayout(self.show_filenames_Row)
68+
69+
def set_property(self, prop_name: str, value: Any) -> None:
70+
setattr(self.tempSettings, prop_name, value)
71+
72+
def get_content(self):
73+
return self.tempSettings

0 commit comments

Comments
 (0)