mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-01 07:39:10 +00:00
chore: merge main into pyright-alchemy
This commit is contained in:
@@ -12,7 +12,6 @@ class SettingItems(str, enum.Enum):
|
||||
|
||||
LAST_LIBRARY = "last_library"
|
||||
LIBS_LIST = "libs_list"
|
||||
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
|
||||
|
||||
|
||||
class ShowFilepathOption(int, enum.Enum):
|
||||
|
||||
@@ -13,21 +13,16 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
DEFAULT_GLOBAL_SETTINGS_PATH = (
|
||||
Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml"
|
||||
if platform.system() == "Windows"
|
||||
else Path.home() / ".config" / "TagStudio" / "settings.toml"
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TomlEnumEncoder(toml.TomlEncoder):
|
||||
@override
|
||||
def dump_value(self, v): # pyright: ignore[reportMissingParameterType]
|
||||
if isinstance(v, Enum):
|
||||
return super().dump_value(v.value)
|
||||
return super().dump_value(v)
|
||||
DEFAULT_THUMB_CACHE_SIZE = 500 # Number in MiB
|
||||
MIN_THUMB_CACHE_SIZE = 10 # Number in MiB
|
||||
|
||||
|
||||
class Theme(IntEnum):
|
||||
@@ -45,6 +40,14 @@ class Splash(StrEnum):
|
||||
NINETY_FIVE = "95"
|
||||
|
||||
|
||||
class TomlEnumEncoder(toml.TomlEncoder):
|
||||
@override
|
||||
def dump_value(self, v): # pyright: ignore[reportMissingParameterType]
|
||||
if isinstance(v, Enum):
|
||||
return super().dump_value(v.value)
|
||||
return super().dump_value(v)
|
||||
|
||||
|
||||
# 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, this was not based on that, but that may be useful in the future.
|
||||
@@ -52,6 +55,7 @@ class GlobalSettings(BaseModel):
|
||||
language: str = Field(default="en")
|
||||
open_last_loaded_on_startup: bool = Field(default=True)
|
||||
generate_thumbs: bool = Field(default=True)
|
||||
thumb_cache_size: float = Field(default=DEFAULT_THUMB_CACHE_SIZE)
|
||||
autoplay: bool = Field(default=True)
|
||||
loop: bool = Field(default=True)
|
||||
show_filenames_in_grid: bool = Field(default=True)
|
||||
@@ -60,6 +64,7 @@ class GlobalSettings(BaseModel):
|
||||
tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT)
|
||||
theme: Theme = Field(default=Theme.SYSTEM)
|
||||
splash: Splash = Field(default=Splash.DEFAULT)
|
||||
windows_start_command: bool = Field(default=False)
|
||||
|
||||
date_format: str = Field(default="%x")
|
||||
hour_format: bool = Field(default=True)
|
||||
|
||||
@@ -44,7 +44,9 @@ def ignore_to_glob(ignore_patterns: list[str]) -> list[str]:
|
||||
ignore_patterns (list[str]): The .gitignore-like patterns to convert.
|
||||
"""
|
||||
glob_patterns: list[str] = deepcopy(ignore_patterns)
|
||||
glob_patterns_remove: list[str] = []
|
||||
additional_patterns: list[str] = []
|
||||
root_patterns: list[str] = []
|
||||
|
||||
# Mimic implicit .gitignore syntax behavior for the SQLite GLOB function.
|
||||
for pattern in glob_patterns:
|
||||
@@ -66,6 +68,16 @@ def ignore_to_glob(ignore_patterns: list[str]) -> list[str]:
|
||||
gp = gp.removeprefix("**/").removeprefix("*/")
|
||||
additional_patterns.append(exclusion_char + gp)
|
||||
|
||||
elif gp.startswith("/"):
|
||||
# Matches "/file" case for .gitignore behavior where it should only match
|
||||
# a file or folder int the root directory, and nowhere else.
|
||||
glob_patterns_remove.append(gp)
|
||||
gp = gp.lstrip("/")
|
||||
root_patterns.append(exclusion_char + gp)
|
||||
|
||||
for gp in glob_patterns_remove:
|
||||
glob_patterns.remove(gp)
|
||||
|
||||
glob_patterns = glob_patterns + additional_patterns
|
||||
|
||||
# Add "/**" suffix to suffix-less patterns to match implicit .gitignore behavior.
|
||||
@@ -75,6 +87,7 @@ def ignore_to_glob(ignore_patterns: list[str]) -> list[str]:
|
||||
|
||||
glob_patterns.append(pattern.removesuffix("/*").removesuffix("/") + "/**")
|
||||
|
||||
glob_patterns = glob_patterns + root_patterns
|
||||
glob_patterns = list(set(glob_patterns))
|
||||
|
||||
logger.info("[Ignore]", glob_patterns=glob_patterns)
|
||||
|
||||
@@ -161,31 +161,34 @@ class RefreshDirTracker:
|
||||
|
||||
logger.info("[Refresh]: Falling back to wcmatch for scanning")
|
||||
|
||||
for f in pathlib.Path(str(library_dir)).glob(
|
||||
"***/*", flags=PATH_GLOB_FLAGS, exclude=ignore_patterns
|
||||
):
|
||||
end_time_loop = time()
|
||||
# Yield output every 1/30 of a second
|
||||
if (end_time_loop - start_time_loop) > 0.034:
|
||||
yield dir_file_count
|
||||
start_time_loop = time()
|
||||
try:
|
||||
for f in pathlib.Path(str(library_dir)).glob(
|
||||
"***/*", flags=PATH_GLOB_FLAGS, exclude=ignore_patterns
|
||||
):
|
||||
end_time_loop = time()
|
||||
# Yield output every 1/30 of a second
|
||||
if (end_time_loop - start_time_loop) > 0.034:
|
||||
yield dir_file_count
|
||||
start_time_loop = time()
|
||||
|
||||
# Skip if the file/path is already mapped in the Library
|
||||
if f in self.library.included_files:
|
||||
dir_file_count += 1
|
||||
continue
|
||||
|
||||
# Ignore if the file is a directory
|
||||
if f.is_dir():
|
||||
continue
|
||||
|
||||
# Skip if the file/path is already mapped in the Library
|
||||
if f in self.library.included_files:
|
||||
dir_file_count += 1
|
||||
continue
|
||||
self.library.included_files.add(f)
|
||||
|
||||
# Ignore if the file is a directory
|
||||
if f.is_dir():
|
||||
continue
|
||||
relative_path = f.relative_to(library_dir)
|
||||
|
||||
dir_file_count += 1
|
||||
self.library.included_files.add(f)
|
||||
|
||||
relative_path = f.relative_to(library_dir)
|
||||
|
||||
if not self.library.has_path_entry(relative_path):
|
||||
self.files_not_in_library.append(relative_path)
|
||||
if not self.library.has_path_entry(relative_path):
|
||||
self.files_not_in_library.append(relative_path)
|
||||
except ValueError:
|
||||
logger.info("[Refresh]: ValueError when refreshing directory with wcmatch!")
|
||||
|
||||
end_time_total = time()
|
||||
yield dir_file_count
|
||||
|
||||
@@ -12,58 +12,66 @@ import structlog
|
||||
from PIL import Image
|
||||
|
||||
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.core.global_settings import DEFAULT_THUMB_CACHE_SIZE
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class CacheEntry:
|
||||
class CacheFolder:
|
||||
def __init__(self, path: Path, size: int):
|
||||
self.path: Path = path
|
||||
self.size: int = size
|
||||
|
||||
|
||||
class CacheManager:
|
||||
DEFAULT_MAX_SIZE = 500_000_000
|
||||
DEFAULT_MAX_FOLDER_SIZE = 10_000_000
|
||||
MAX_FOLDER_SIZE = 10 # Number in MiB
|
||||
STAT_MULTIPLIER = 1_000_000 # Multiplier to apply to file stats (bytes) to get user units (MiB)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library_dir: Path,
|
||||
max_size: int = DEFAULT_MAX_SIZE,
|
||||
max_folder_size: int = DEFAULT_MAX_FOLDER_SIZE,
|
||||
max_size: int | float = DEFAULT_THUMB_CACHE_SIZE,
|
||||
):
|
||||
self._lock = RLock()
|
||||
self.cache_folder = library_dir / TS_FOLDER_NAME / THUMB_CACHE_NAME
|
||||
self.max_folder_size = max_folder_size
|
||||
self.max_size = max(max_size, max_folder_size)
|
||||
"""A class for managing frontend caches, such as for file thumbnails.
|
||||
|
||||
self.folders: list[CacheEntry] = []
|
||||
Args:
|
||||
library_dir(Path): The path of the folder containing the .TagStudio library folder.
|
||||
max_size: (int | float) The maximum size of the cache, in MiB.
|
||||
"""
|
||||
self._lock = RLock()
|
||||
self.cache_path = library_dir / TS_FOLDER_NAME / THUMB_CACHE_NAME
|
||||
self.max_size: int = max(
|
||||
math.floor(max_size * CacheManager.STAT_MULTIPLIER),
|
||||
math.floor(CacheManager.MAX_FOLDER_SIZE * CacheManager.STAT_MULTIPLIER),
|
||||
)
|
||||
|
||||
self.folders: list[CacheFolder] = []
|
||||
self.current_size = 0
|
||||
if self.cache_folder.exists():
|
||||
for folder in self.cache_folder.iterdir():
|
||||
if self.cache_path.exists():
|
||||
for folder in self.cache_path.iterdir():
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
folder_size = 0
|
||||
for file in folder.iterdir():
|
||||
folder_size += file.stat().st_size
|
||||
self.folders.append(CacheEntry(folder, folder_size))
|
||||
self.folders.append(CacheFolder(folder, folder_size))
|
||||
self.current_size += folder_size
|
||||
|
||||
def _set_mru(self, index: int):
|
||||
"""Move entry at index so it's considered the most recently used."""
|
||||
def _set_most_recent_folder(self, index: int):
|
||||
"""Move CacheFolder at index so it's considered the most recently used folder."""
|
||||
with self._lock as _lock:
|
||||
if index == (len(self.folders) - 1):
|
||||
return
|
||||
entry = self.folders.pop(index)
|
||||
self.folders.append(entry)
|
||||
cache_folder = self.folders.pop(index)
|
||||
self.folders.append(cache_folder)
|
||||
|
||||
def _mru(self) -> Iterable[int]:
|
||||
def _get_most_recent_folder(self) -> Iterable[int]:
|
||||
"""Get each folders index sorted most recently used first."""
|
||||
with self._lock as _lock:
|
||||
return reversed(range(len(self.folders)))
|
||||
|
||||
def _lru(self) -> Iterable[int]:
|
||||
"""Get each folders index sorted least recently used first."""
|
||||
def _least_recent_folder(self) -> Iterable[int]:
|
||||
"""Get each folder's index sorted least recently used first."""
|
||||
with self._lock as _lock:
|
||||
return range(len(self.folders))
|
||||
|
||||
@@ -78,14 +86,14 @@ class CacheManager:
|
||||
self.folders = folders
|
||||
logger.info("[CacheManager] Cleared cache!")
|
||||
|
||||
def _remove_folder(self, entry: CacheEntry) -> bool:
|
||||
def _remove_folder(self, cache_folder: CacheFolder) -> bool:
|
||||
with self._lock as _lock:
|
||||
self.current_size -= entry.size
|
||||
if not entry.path.is_dir():
|
||||
self.current_size -= cache_folder.size
|
||||
if not cache_folder.path.is_dir():
|
||||
return True
|
||||
|
||||
is_empty = True
|
||||
for file in entry.path.iterdir():
|
||||
for file in cache_folder.path.iterdir():
|
||||
assert file.is_file() and file.suffix == ".webp"
|
||||
try:
|
||||
file.unlink(missing_ok=True)
|
||||
@@ -94,61 +102,61 @@ class CacheManager:
|
||||
logger.warn("[CacheManager] Failed to remove file", file=file, error=e)
|
||||
|
||||
if is_empty:
|
||||
entry.path.rmdir()
|
||||
cache_folder.path.rmdir()
|
||||
return True
|
||||
else:
|
||||
size = 0
|
||||
for file in entry.path.iterdir():
|
||||
for file in cache_folder.path.iterdir():
|
||||
size += file.stat().st_size
|
||||
entry.size = size
|
||||
cache_folder.size = size
|
||||
self.current_size += size
|
||||
return False
|
||||
|
||||
def get_file_path(self, file_name: Path) -> Path | None:
|
||||
with self._lock as _lock:
|
||||
for i in self._mru():
|
||||
entry = self.folders[i]
|
||||
file_path = entry.path / file_name
|
||||
for i in self._get_most_recent_folder():
|
||||
cache_folder = self.folders[i]
|
||||
file_path = cache_folder.path / file_name
|
||||
if file_path.exists():
|
||||
self._set_mru(i)
|
||||
self._set_most_recent_folder(i)
|
||||
return file_path
|
||||
return None
|
||||
|
||||
def save_image(self, image: Image.Image, file_name: Path, mode: str = "RGBA"):
|
||||
"""Save an image to the cache."""
|
||||
with self._lock as _lock:
|
||||
entry = self._get_current_folder()
|
||||
file_path = entry.path / file_name
|
||||
cache_folder: CacheFolder = self._get_current_folder()
|
||||
file_path = cache_folder.path / file_name
|
||||
image.save(file_path, mode=mode)
|
||||
|
||||
size = file_path.stat().st_size
|
||||
entry.size += size
|
||||
cache_folder.size += size
|
||||
self.current_size += size
|
||||
self._cull_folders()
|
||||
|
||||
def _create_folder(self) -> CacheEntry:
|
||||
def _create_folder(self) -> CacheFolder:
|
||||
with self._lock as _lock:
|
||||
folder = self.cache_folder / Path(str(math.floor(dt.timestamp(dt.now()))))
|
||||
folder = self.cache_path / Path(str(math.floor(dt.timestamp(dt.now()))))
|
||||
try:
|
||||
folder.mkdir(parents=True)
|
||||
except FileExistsError:
|
||||
for entry in self.folders:
|
||||
if entry.path == folder:
|
||||
return entry
|
||||
entry = CacheEntry(folder, 0)
|
||||
self.folders.append(entry)
|
||||
return entry
|
||||
for cache_folder in self.folders:
|
||||
if cache_folder.path == folder:
|
||||
return cache_folder
|
||||
cache_folder = CacheFolder(folder, 0)
|
||||
self.folders.append(cache_folder)
|
||||
return cache_folder
|
||||
|
||||
def _get_current_folder(self) -> CacheEntry:
|
||||
def _get_current_folder(self) -> CacheFolder:
|
||||
with self._lock as _lock:
|
||||
if len(self.folders) == 0:
|
||||
return self._create_folder()
|
||||
|
||||
for i in self._mru():
|
||||
entry = self.folders[i]
|
||||
if entry.size < self.max_folder_size:
|
||||
self._set_mru(i)
|
||||
return entry
|
||||
for i in self._get_most_recent_folder():
|
||||
cache_folder: CacheFolder = self.folders[i]
|
||||
if cache_folder.size < CacheManager.MAX_FOLDER_SIZE * CacheManager.STAT_MULTIPLIER:
|
||||
self._set_most_recent_folder(i)
|
||||
return cache_folder
|
||||
|
||||
return self._create_folder()
|
||||
|
||||
@@ -159,10 +167,12 @@ class CacheManager:
|
||||
return
|
||||
|
||||
removed: list[int] = []
|
||||
for i in self._lru():
|
||||
entry = self.folders[i]
|
||||
logger.info("[CacheManager] Removing folder due to size limit", folder=entry.path)
|
||||
if self._remove_folder(entry):
|
||||
for i in self._least_recent_folder():
|
||||
cache_folder: CacheFolder = self.folders[i]
|
||||
logger.info(
|
||||
"[CacheManager] Removing folder due to size limit", folder=cache_folder.path
|
||||
)
|
||||
if self._remove_folder(cache_folder):
|
||||
removed.append(i)
|
||||
if self.current_size < self.max_size:
|
||||
break
|
||||
|
||||
@@ -144,7 +144,9 @@ class PreviewThumb(PreviewThumbView):
|
||||
return self.__get_image_stats(filepath)
|
||||
|
||||
def _open_file_action_callback(self):
|
||||
open_file(self.__current_file)
|
||||
open_file(
|
||||
self.__current_file, windows_start_command=self.__driver.settings.windows_start_command
|
||||
)
|
||||
|
||||
def _open_explorer_action_callback(self):
|
||||
open_file(self.__current_file, file_manager=True)
|
||||
@@ -154,4 +156,6 @@ class PreviewThumb(PreviewThumbView):
|
||||
self.__driver.delete_files_callback(self.__current_file)
|
||||
|
||||
def _button_wrapper_callback(self):
|
||||
open_file(self.__current_file)
|
||||
open_file(
|
||||
self.__current_file, windows_start_command=self.__driver.settings.windows_start_command
|
||||
)
|
||||
|
||||
@@ -20,13 +20,15 @@ from tagstudio.qt.helpers.silent_popen import silent_Popen
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def open_file(path: str | Path, file_manager: bool = False):
|
||||
def open_file(path: str | Path, file_manager: bool = False, windows_start_command: bool = False):
|
||||
"""Open a file in the default application or file explorer.
|
||||
|
||||
Args:
|
||||
path (str): The path to the file to open.
|
||||
file_manager (bool, optional): Whether to open the file in the file manager
|
||||
(e.g. Finder on macOS). Defaults to False.
|
||||
windows_start_command (bool): Flag to determine if the older 'start' command should be used
|
||||
on Windows for opening files. This fixes issues on some systems in niche cases.
|
||||
"""
|
||||
path = Path(path)
|
||||
logger.info("Opening file", path=path)
|
||||
@@ -51,14 +53,26 @@ def open_file(path: str | Path, file_manager: bool = False):
|
||||
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
|
||||
)
|
||||
else:
|
||||
command = f'"{normpath}"'
|
||||
silent_Popen(
|
||||
command,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
|
||||
)
|
||||
if windows_start_command:
|
||||
command_name = "start"
|
||||
# First parameter is for title, NOT filepath
|
||||
command_args = ["", normpath]
|
||||
subprocess.Popen(
|
||||
[command_name] + command_args,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
|
||||
)
|
||||
else:
|
||||
command = f'"{normpath}"'
|
||||
silent_Popen(
|
||||
command,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
|
||||
)
|
||||
else:
|
||||
if sys.platform == "darwin":
|
||||
command_name = "open"
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QDoubleValidator
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QTabWidget,
|
||||
@@ -18,13 +21,20 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
|
||||
from tagstudio.core.global_settings import Splash, Theme
|
||||
from tagstudio.core.global_settings import (
|
||||
DEFAULT_THUMB_CACHE_SIZE,
|
||||
MIN_THUMB_CACHE_SIZE,
|
||||
Splash,
|
||||
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
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class SettingsPanel(PanelWidget):
|
||||
driver: "QtDriver"
|
||||
@@ -142,6 +152,25 @@ class SettingsPanel(PanelWidget):
|
||||
self.generate_thumbs.setChecked(self.driver.settings.generate_thumbs)
|
||||
form_layout.addRow(Translations["settings.generate_thumbs"], self.generate_thumbs)
|
||||
|
||||
# Thumbnail Cache Size
|
||||
self.thumb_cache_size_container = QWidget()
|
||||
self.thumb_cache_size_layout = QHBoxLayout(self.thumb_cache_size_container)
|
||||
self.thumb_cache_size_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.thumb_cache_size_layout.setSpacing(6)
|
||||
self.thumb_cache_size = QLineEdit()
|
||||
self.thumb_cache_size.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.validator = QDoubleValidator(MIN_THUMB_CACHE_SIZE, 1_000_000_000, 2) # High limit
|
||||
self.thumb_cache_size.setValidator(self.validator)
|
||||
self.thumb_cache_size.setText(
|
||||
str(max(self.driver.settings.thumb_cache_size, MIN_THUMB_CACHE_SIZE)).removesuffix(".0")
|
||||
)
|
||||
self.thumb_cache_size_layout.addWidget(self.thumb_cache_size)
|
||||
self.thumb_cache_size_layout.setStretch(1, 2)
|
||||
self.thumb_cache_size_layout.addWidget(QLabel("MiB"))
|
||||
form_layout.addRow(
|
||||
Translations["settings.thumb_cache_size.label"], self.thumb_cache_size_container
|
||||
)
|
||||
|
||||
# Autoplay
|
||||
self.autoplay_checkbox = QCheckBox()
|
||||
self.autoplay_checkbox.setChecked(self.driver.settings.autoplay)
|
||||
@@ -252,6 +281,10 @@ class SettingsPanel(PanelWidget):
|
||||
"language": self.__get_language(),
|
||||
"open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(),
|
||||
"generate_thumbs": self.generate_thumbs.isChecked(),
|
||||
"thumb_cache_size": max(
|
||||
float(self.thumb_cache_size.text()) or DEFAULT_THUMB_CACHE_SIZE,
|
||||
MIN_THUMB_CACHE_SIZE,
|
||||
),
|
||||
"autoplay": self.autoplay_checkbox.isChecked(),
|
||||
"show_filenames_in_grid": self.show_filenames_checkbox.isChecked(),
|
||||
"page_size": int(self.page_size_line_edit.text()),
|
||||
@@ -271,6 +304,7 @@ class SettingsPanel(PanelWidget):
|
||||
driver.settings.open_last_loaded_on_startup = settings["open_last_loaded_on_startup"]
|
||||
driver.settings.autoplay = settings["autoplay"]
|
||||
driver.settings.generate_thumbs = settings["generate_thumbs"]
|
||||
driver.settings.thumb_cache_size = settings["thumb_cache_size"]
|
||||
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"]
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
"""A pagination widget created for TagStudio."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QObject, QSize, Signal
|
||||
from PySide6.QtGui import QIntValidator, QPixmap
|
||||
@@ -285,9 +287,10 @@ class Pagination(QWidget, QObject):
|
||||
|
||||
|
||||
class Validator(QIntValidator):
|
||||
def __init__(self, bottom: int, top: int, parent=None) -> None:
|
||||
super().__init__(bottom, top, parent)
|
||||
def __init__(self, bottom: int, top: int) -> None:
|
||||
super().__init__(bottom, top)
|
||||
|
||||
@override
|
||||
def fixup(self, input: str) -> str:
|
||||
input = input.strip("0")
|
||||
return super().fixup(str(self.top()) if input else "1")
|
||||
|
||||
@@ -13,10 +13,9 @@ logger = structlog.get_logger(__name__)
|
||||
DEFAULT_TRANSLATION = "en"
|
||||
|
||||
LANGUAGES = {
|
||||
# "Cantonese (Traditional)": "yue_Hant", # Empty
|
||||
"Chinese (Simplified)": "zh_Hans",
|
||||
"Chinese (Traditional)": "zh_Hant",
|
||||
# "Czech": "cs", # Minimal
|
||||
"Czech": "cs",
|
||||
# "Danish": "da", # Minimal
|
||||
"Dutch": "nl",
|
||||
"English": "en",
|
||||
@@ -29,7 +28,8 @@ LANGUAGES = {
|
||||
"Norwegian Bokmål": "nb_NO",
|
||||
"Polish": "pl",
|
||||
"Portuguese (Brazil)": "pt_BR",
|
||||
# "Portuguese (Portugal)": "pt", # Empty
|
||||
"Portuguese (Portugal)": "pt",
|
||||
"Romanian": "ro",
|
||||
"Russian": "ru",
|
||||
"Spanish": "es",
|
||||
"Swedish": "sv",
|
||||
|
||||
@@ -49,7 +49,11 @@ 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 MacroID, SettingItems, ShowFilepathOption
|
||||
from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
|
||||
from tagstudio.core.global_settings import (
|
||||
DEFAULT_GLOBAL_SETTINGS_PATH,
|
||||
GlobalSettings,
|
||||
Theme,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.enums import (
|
||||
BrowsingState,
|
||||
FieldTypeEnum,
|
||||
@@ -1691,15 +1695,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
open_status = LibraryStatus(
|
||||
success=False, library_path=path, message=type(e).__name__, msg_description=str(e)
|
||||
)
|
||||
|
||||
max_size: int = self.cached_values.value(
|
||||
SettingItems.THUMB_CACHE_SIZE_LIMIT,
|
||||
defaultValue=CacheManager.DEFAULT_MAX_SIZE,
|
||||
type=int,
|
||||
) # type: ignore
|
||||
self.cache_manager = CacheManager(path, max_size=max_size)
|
||||
self.cache_manager = CacheManager(path, max_size=self.settings.thumb_cache_size)
|
||||
logger.info(
|
||||
f"[Config] Thumbnail cache size limit: {format_size(max_size)}",
|
||||
f"[Config] Thumbnail Cache Size: {format_size(self.settings.thumb_cache_size)}",
|
||||
)
|
||||
|
||||
# Migration is required
|
||||
|
||||
@@ -30,6 +30,7 @@ from tagstudio.core.constants import (
|
||||
TS_FOLDER_NAME,
|
||||
)
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.core.library.alchemy.constants import SQL_FILENAME
|
||||
from tagstudio.core.library.alchemy.library import Entry, TagAlias, TagParent
|
||||
from tagstudio.core.library.alchemy.library import Library as SqliteLibrary
|
||||
from tagstudio.core.library.helpers.migration import json_to_sql_color
|
||||
@@ -491,7 +492,7 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
def finish_migration(self):
|
||||
"""Finish the migration upon user approval."""
|
||||
final_name = self.json_lib.library_dir / TS_FOLDER_NAME / SqliteLibrary.SQL_FILENAME
|
||||
final_name = self.json_lib.library_dir / TS_FOLDER_NAME / SQL_FILENAME
|
||||
if self.temp_path.exists():
|
||||
self.temp_path.rename(final_name)
|
||||
|
||||
|
||||
@@ -1524,8 +1524,23 @@ class ThumbRenderer(QObject):
|
||||
if _filepath and _filepath.is_file():
|
||||
try:
|
||||
ext: str = _filepath.suffix.lower() if _filepath.suffix else _filepath.stem.lower()
|
||||
# Images =======================================================
|
||||
# Ebooks =======================================================
|
||||
if MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.EBOOK_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._epub_cover(_filepath)
|
||||
# Krita ========================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.KRITA_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._krita_thumb(_filepath)
|
||||
# VTF ==========================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._vtf_thumb(_filepath)
|
||||
# Images =======================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_TYPES, mime_fallback=True
|
||||
):
|
||||
# Raw Images -----------------------------------------------
|
||||
@@ -1552,11 +1567,6 @@ class ThumbRenderer(QObject):
|
||||
# PowerPoint Slideshow
|
||||
elif ext in {".pptx"}:
|
||||
image = self._powerpoint_thumb(_filepath)
|
||||
# Krita ========================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.KRITA_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._krita_thumb(_filepath)
|
||||
# OpenDocument/OpenOffice ======================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True
|
||||
@@ -1590,11 +1600,6 @@ class ThumbRenderer(QObject):
|
||||
savable_media_type = False
|
||||
if image is not None:
|
||||
image = self._apply_overlay_color(image, UiColor.GREEN)
|
||||
# Ebooks =======================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.EBOOK_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._epub_cover(_filepath)
|
||||
# Blender ======================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.BLENDER_TYPES, mime_fallback=True
|
||||
@@ -1605,11 +1610,6 @@ class ThumbRenderer(QObject):
|
||||
ext, MediaCategories.PDF_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._pdf_thumb(_filepath, adj_size)
|
||||
# VTF ==========================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._vtf_thumb(_filepath)
|
||||
# No Rendered Thumbnail ========================================
|
||||
if not image:
|
||||
raise NoRendererError
|
||||
|
||||
@@ -287,6 +287,7 @@
|
||||
"settings.theme.label": "Theme:",
|
||||
"settings.theme.light": "Light",
|
||||
"settings.theme.system": "System",
|
||||
"settings.thumb_cache_size.label": "Thumbnail Cache Size",
|
||||
"settings.title": "Settings",
|
||||
"settings.zeropadding.label": "Date Zero-Padding",
|
||||
"sorting.direction.ascending": "Ascending",
|
||||
|
||||
Reference in New Issue
Block a user