Skip to content

Commit adb996e

Browse files
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
1 parent e112788 commit adb996e

27 files changed

+671
-390
lines changed

docs/updates/roadmap.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ These version milestones are rough estimations for when the previous core featur
106106
- [ ] 3D Model Previews [MEDIUM]
107107
- [ ] STL Previews [HIGH]
108108
- [ ] Word count/line count on text thumbnails [LOW]
109-
- [ ] Settings Menu [HIGH]
110-
- [ ] Application Settings [HIGH]
111-
- [ ] Stored in system user folder/designated folder [HIGH]
109+
- [x] Settings Menu [HIGH]
110+
- [x] Application Settings [HIGH]
111+
- [x] Stored in system user folder/designated folder [HIGH]
112112
- [ ] Library Settings [HIGH]
113113
- [ ] Stored in `.TagStudio` folder [HIGH]
114114
- [ ] Tagging Panel [HIGH]

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ dependencies = [
2727
"typing_extensions>=3.10.0.0,<4.11.0",
2828
"ujson>=5.8.0,<5.9.0",
2929
"vtf2img==0.1.0",
30+
"toml==0.10.2",
31+
"pydantic==2.9.2",
3032
]
3133

3234
[project.optional-dependencies]

src/tagstudio/core/driver.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
from tagstudio.core.constants import TS_FOLDER_NAME
77
from tagstudio.core.enums import SettingItems
8+
from tagstudio.core.global_settings import GlobalSettings
89
from tagstudio.core.library.alchemy.library import LibraryStatus
910

1011
logger = structlog.get_logger(__name__)
1112

1213

1314
class DriverMixin:
14-
settings: QSettings
15+
cached_values: QSettings
16+
settings: GlobalSettings
1517

1618
def evaluate_path(self, open_path: str | None) -> LibraryStatus:
1719
"""Check if the path of library is valid."""
@@ -21,17 +23,17 @@ def evaluate_path(self, open_path: str | None) -> LibraryStatus:
2123
if not library_path.exists():
2224
logger.error("Path does not exist.", open_path=open_path)
2325
return LibraryStatus(success=False, message="Path does not exist.")
24-
elif self.settings.value(
25-
SettingItems.START_LOAD_LAST, defaultValue=True, type=bool
26-
) and self.settings.value(SettingItems.LAST_LIBRARY):
27-
library_path = Path(str(self.settings.value(SettingItems.LAST_LIBRARY)))
26+
elif self.settings.open_last_loaded_on_startup and self.cached_values.value(
27+
SettingItems.LAST_LIBRARY
28+
):
29+
library_path = Path(str(self.cached_values.value(SettingItems.LAST_LIBRARY)))
2830
if not (library_path / TS_FOLDER_NAME).exists():
2931
logger.error(
3032
"TagStudio folder does not exist.",
3133
library_path=library_path,
3234
ts_folder=TS_FOLDER_NAME,
3335
)
34-
self.settings.setValue(SettingItems.LAST_LIBRARY, "")
36+
self.cached_values.setValue(SettingItems.LAST_LIBRARY, "")
3537
# dont consider this a fatal error, just skip opening the library
3638
library_path = None
3739

src/tagstudio/core/enums.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,9 @@
1010
class SettingItems(str, enum.Enum):
1111
"""List of setting item names."""
1212

13-
START_LOAD_LAST = "start_load_last"
1413
LAST_LIBRARY = "last_library"
1514
LIBS_LIST = "libs_list"
16-
WINDOW_SHOW_LIBS = "window_show_libs"
17-
SHOW_FILENAMES = "show_filenames"
18-
SHOW_FILEPATH = "show_filepath"
19-
AUTOPLAY = "autoplay_videos"
2015
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
21-
LANGUAGE = "language"
2216

2317

2418
class ShowFilepathOption(int, enum.Enum):
@@ -81,5 +75,4 @@ class LibraryPrefs(DefaultEnum):
8175

8276
IS_EXCLUDE_LIST = True
8377
EXTENSION_LIST = [".json", ".xmp", ".aae"]
84-
PAGE_SIZE = 500
8578
DB_VERSION = 9
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Licensed under the GPL-3.0 License.
2+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
3+
4+
import platform
5+
from enum import Enum
6+
from pathlib import Path
7+
from typing import override
8+
9+
import structlog
10+
import toml
11+
from pydantic import BaseModel, Field
12+
13+
from tagstudio.core.enums import ShowFilepathOption
14+
15+
if platform.system() == "Windows":
16+
DEFAULT_GLOBAL_SETTINGS_PATH = (
17+
Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml"
18+
)
19+
else:
20+
DEFAULT_GLOBAL_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml"
21+
22+
logger = structlog.get_logger(__name__)
23+
24+
25+
class TomlEnumEncoder(toml.TomlEncoder):
26+
@override
27+
def dump_value(self, v):
28+
if isinstance(v, Enum):
29+
return super().dump_value(v.value)
30+
return super().dump_value(v)
31+
32+
33+
class Theme(Enum):
34+
DARK = 0
35+
LIGHT = 1
36+
SYSTEM = 2
37+
DEFAULT = SYSTEM
38+
39+
40+
# NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings
41+
# properties to be overwritten with environment variables. as tagstudio is not currently using
42+
# environment variables, i did not base it on that, but that may be useful in the future.
43+
class GlobalSettings(BaseModel):
44+
language: str = Field(default="en")
45+
open_last_loaded_on_startup: bool = Field(default=False)
46+
autoplay: bool = Field(default=False)
47+
show_filenames_in_grid: bool = Field(default=False)
48+
page_size: int = Field(default=500)
49+
show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT)
50+
theme: Theme = Field(default=Theme.SYSTEM)
51+
52+
@staticmethod
53+
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings":
54+
if path.exists():
55+
with open(path) as file:
56+
filecontents = file.read()
57+
if len(filecontents.strip()) != 0:
58+
logger.info("[Settings] Reading Global Settings File", path=path)
59+
settings_data = toml.loads(filecontents)
60+
settings = GlobalSettings(**settings_data)
61+
return settings
62+
63+
return GlobalSettings()
64+
65+
def save(self, path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> None:
66+
if not path.parent.exists():
67+
path.parent.mkdir(parents=True, exist_ok=True)
68+
69+
with open(path, "w") as f:
70+
toml.dump(dict(self), f, encoder=TomlEnumEncoder())

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

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@ class FilterState:
7676
"""Represent a state of the Library grid view."""
7777

7878
# these should remain
79-
page_index: int | None = 0
80-
page_size: int | None = 500
79+
page_size: int
80+
page_index: int = 0
8181
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
8282
ascending: bool = True
8383

8484
# these should be erased on update
8585
# Abstract Syntax Tree Of the current Search Query
86-
ast: AST = None
86+
ast: AST | None = None
8787

8888
@property
8989
def limit(self):
@@ -94,35 +94,32 @@ def offset(self):
9494
return self.page_size * self.page_index
9595

9696
@classmethod
97-
def show_all(cls) -> "FilterState":
98-
return FilterState()
97+
def show_all(cls, page_size: int) -> "FilterState":
98+
return FilterState(page_size=page_size)
9999

100100
@classmethod
101-
def from_search_query(cls, search_query: str) -> "FilterState":
102-
return cls(ast=Parser(search_query).parse())
101+
def from_search_query(cls, search_query: str, page_size: int) -> "FilterState":
102+
return cls(ast=Parser(search_query).parse(), page_size=page_size)
103103

104104
@classmethod
105-
def from_tag_id(cls, tag_id: int | str) -> "FilterState":
106-
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []))
105+
def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState":
106+
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size)
107107

108108
@classmethod
109-
def from_path(cls, path: Path | str) -> "FilterState":
110-
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []))
109+
def from_path(cls, path: Path | str, page_size: int) -> "FilterState":
110+
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size)
111111

112112
@classmethod
113-
def from_mediatype(cls, mediatype: str) -> "FilterState":
114-
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []))
113+
def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState":
114+
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size)
115115

116116
@classmethod
117-
def from_filetype(cls, filetype: str) -> "FilterState":
118-
return cls(ast=Constraint(ConstraintType.FileType, filetype, []))
117+
def from_filetype(cls, filetype: str, page_size: int) -> "FilterState":
118+
return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size)
119119

120120
@classmethod
121-
def from_tag_name(cls, tag_name: str) -> "FilterState":
122-
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []))
123-
124-
def with_page_size(self, page_size: int) -> "FilterState":
125-
return replace(self, page_size=page_size)
121+
def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState":
122+
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size)
126123

127124
def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState":
128125
return replace(self, sorting_mode=mode)

src/tagstudio/core/utils/dupe_files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def refresh_dupe_files(self, results_filepath: str | Path):
5252
continue
5353

5454
results = self.library.search_library(
55-
FilterState.from_path(path_relative),
55+
FilterState.from_path(path_relative, page_size=500),
5656
)
5757

5858
if not results:

src/tagstudio/main.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,19 @@ def main():
3232
type=str,
3333
help="Path to a TagStudio Library folder to open on start.",
3434
)
35+
parser.add_argument(
36+
"-s",
37+
"--settings-file",
38+
dest="settings_file",
39+
type=str,
40+
help="Path to a TagStudio .toml global settings file to use.",
41+
)
3542
parser.add_argument(
3643
"-c",
37-
"--config-file",
38-
dest="config_file",
44+
"--cache-file",
45+
dest="cache_file",
3946
type=str,
40-
help="Path to a TagStudio .ini or .plist config file to use.",
47+
help="Path to a TagStudio .ini or .plist cache file to use.",
4148
)
4249

4350
# parser.add_argument('--browse', dest='browse', action='store_true',
@@ -50,12 +57,6 @@ def main():
5057
action="store_true",
5158
help="Reveals additional internal data useful for debugging.",
5259
)
53-
parser.add_argument(
54-
"--ui",
55-
dest="ui",
56-
type=str,
57-
help="User interface option for TagStudio. Options: qt, cli (Default: qt)",
58-
)
5960
args = parser.parse_args()
6061

6162
driver = QtDriver(args)

src/tagstudio/qt/cache_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self):
3232
self.last_lib_path: Path | None = None
3333

3434
@staticmethod
35-
def clear_cache(library_dir: Path) -> bool:
35+
def clear_cache(library_dir: Path | None) -> bool:
3636
"""Clear all files and folders within the cached folder.
3737
3838
Returns:

0 commit comments

Comments
 (0)