mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-05-21 08:15:09 +00:00
Compare commits
7 Commits
v9.5.5
...
pyright-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0bb5f44e0 | ||
|
|
bbb17285e7 | ||
|
|
46f7edf6e8 | ||
|
|
745fea6b85 | ||
|
|
668ac23a86 | ||
|
|
8e8f416246 | ||
|
|
218aa9e0d1 |
6
STYLE.md
6
STYLE.md
@@ -21,11 +21,11 @@ As of writing this section, the QT part of the code base is quite unstructured a
|
||||
The general structure of the QT code base should look like this:
|
||||
```
|
||||
qt
|
||||
├── controllers
|
||||
├── controller
|
||||
│ ├── widgets
|
||||
│ │ └── preview_panel_controller.py
|
||||
│ └── main_window_controller.py
|
||||
├── views
|
||||
├── view
|
||||
│ ├── widgets
|
||||
│ │ └── preview_panel_view.py
|
||||
│ └── main_window_view.py
|
||||
@@ -33,7 +33,7 @@ qt
|
||||
└── mixed.py
|
||||
```
|
||||
|
||||
In this structure there are the `views` and `controllers` sub-directories. They have the exact same structure and for every `<component>_view.py` there is a `<component>_controller.py` at the same location in the other subdirectory and vice versa.
|
||||
In this structure there are the `view` and `controller` sub-directories. They have the exact same structure and for every `<component>_view.py` there is a `<component>_controller.py` at the same location in the other subdirectory and vice versa.
|
||||
|
||||
Typically the classes should look like this:
|
||||
```py
|
||||
|
||||
@@ -10,14 +10,14 @@ TagStudio provides [releases](https://github.com/TagStudioDev/TagStudio/releases
|
||||
|
||||
To download executable builds of TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) page of the GitHub repository and download the latest release for your system under the "Assets" section at the bottom of the release.
|
||||
|
||||
TagStudio has builds for :fontawesome-brands-windows: **Windows**, :fontawesome-brands-apple: **macOS** _(Apple Silicon & Intel)_, and :material-penguin: **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around.
|
||||
TagStudio has builds for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! info "Third-Party Dependencies"
|
||||
You may need to install [third-party dependencies](#third-party-dependencies) such as [FFmpeg](https://ffmpeg.org/download.html) to use the full feature set of TagStudio.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! warning ":fontawesome-brands-apple: macOS "Privacy & Security" Popup"
|
||||
!!! warning "For macOS Users"
|
||||
On macOS, you may be met with a message saying "**"TagStudio" can't be opened because Apple cannot check it for malicious software.**" If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says "**"TagStudio" was blocked from use because it is not from an identified developer.**" Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
|
||||
|
||||
---
|
||||
@@ -30,7 +30,7 @@ TagStudio has builds for :fontawesome-brands-windows: **Windows**, :fontawesome-
|
||||
|
||||
Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk!
|
||||
|
||||
### :fontawesome-brands-python: Installing with PIP
|
||||
### Installing with PIP
|
||||
|
||||
TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.12 and PIP installed if you choose to install using this method.
|
||||
|
||||
@@ -58,7 +58,7 @@ TagStudio can now be launched via the `tagstudio` command in your terminal.
|
||||
|
||||
---
|
||||
|
||||
### :material-penguin: Linux
|
||||
### Linux
|
||||
|
||||
Some external dependencies are required for TagStudio to execute. Below is a table of known packages that will be necessary.
|
||||
|
||||
@@ -79,7 +79,7 @@ Some external dependencies are required for TagStudio to execute. Below is a tab
|
||||
| [qt-multimedia](https://repology.org/project/qt) | required |
|
||||
| [qt-wayland](https://repology.org/project/qt) | Wayland support |
|
||||
|
||||
### :material-nix: Nix(OS)
|
||||
### Nix(OS)
|
||||
|
||||
For [Nix(OS)](https://nixos.org/), the TagStudio repository includes a [flake](https://wiki.nixos.org/wiki/Flakes) that provides some outputs such as a development shell and package.
|
||||
|
||||
@@ -213,30 +213,12 @@ Don't forget to rebuild!
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! tip
|
||||
You can check to see if these dependencies are correctly located by launching TagStudio and going to "About TagStudio" in the menu bar.
|
||||
You can check to see if any of these dependencies are correctly located by launching TagStudio and going to "About TagStudio" in the menu bar.
|
||||
|
||||
### FFmpeg/FFprobe
|
||||
|
||||
For audio/video thumbnails and playback you'll need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide.
|
||||
|
||||
### RAR Extractor
|
||||
|
||||
To generate thumbnails for RAR-based files (like `.cbr`) you'll need an extractor capable of handling them.
|
||||
|
||||
- :material-penguin: On Linux you'll need to install either `unrar` (likely in you distro's non-free repository) or `unrar-free` from your package manager.
|
||||
|
||||
- :fontawesome-brands-apple: On macOS `unrar` can be installed through Homebrew's [`rar`](https://formulae.brew.sh/cask/rar) formula.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! warning ":fontawesome-brands-apple: macOS "Privacy & Security" Popup"
|
||||
On macOS, you may be met with a message similar to "**"unrar" Not Opened. Apple could not verify "unrar" is free of malware that may harm your Mac or compromise your privacy**" If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says "**"unrar" was blocked from use because it is not from an identified developer.**" Click the "Open Anyway" button to allow unrar to be used.
|
||||
|
||||
- :fontawesome-brands-windows: On Windows you'll need to install either [`WinRAR`](https://www.rarlab.com/download.htm) or [`7-zip`](https://www.7-zip.org/) and add their folder to you `PATH`.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! tip "WinRAR License"
|
||||
Both `unrar` and `WinRAR` require a license, but since the evaluation copy has no time limit you can simply dismiss the prompt.
|
||||
|
||||
### ripgrep
|
||||
|
||||
A recommended tool to improve the performance of directory scanning is [`ripgrep`](https://github.com/BurntSushi/ripgrep), a Rust-based directory walker that natively integrates with our [`.ts_ignore`](./utilities/ignore.md) (`.gitignore`-style) pattern matching system for excluding files and directories. Ripgrep is already pre-installed on some Linux distributions and also available from several package managers.
|
||||
|
||||
@@ -14,9 +14,9 @@ Legacy (JSON) library save format versions were tied to the release version of t
|
||||
|
||||
### Versions 1.0.0 - 9.4.2
|
||||
|
||||
| Used From | Format | Location |
|
||||
| --------- | ------ | --------------------------------------------- |
|
||||
| v1.0.0 | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| --------- | ----------------------------------------------------------------------- | ------ | --------------------------------------------- |
|
||||
| v1.0.0 | [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2) | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
|
||||
|
||||
The legacy database format for public TagStudio releases [v9.1](https://github.com/TagStudioDev/TagStudio/tree/Alpha-v9.1) through [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2). Variations of this format had been used privately since v1.0.0.
|
||||
|
||||
@@ -48,9 +48,9 @@ These versions were used while developing the new SQLite file format, outside an
|
||||
|
||||
### Version 6
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
The first public version of the SQLite save file format.
|
||||
|
||||
@@ -60,9 +60,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 7
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | [v9.5.0-pr3](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr3) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Repairs "Description" fields to use a TEXT_LINE key instead of a TEXT_BOX key.
|
||||
- Repairs tags that may have a disambiguation_id pointing towards a deleted tag.
|
||||
@@ -71,9 +71,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 8
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | [v9.5.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Adds the `color_border` column to the `tag_colors` table. Used for instructing the [secondary color](../library/tag_color.md#secondary-color) to apply to a tag's border as a new optional behavior.
|
||||
- Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)".
|
||||
@@ -83,9 +83,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 9
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | [v9.5.3](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.3) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Adds the `filename` column to the `entries` table. Used for sorting entries by filename in search results.
|
||||
|
||||
@@ -93,20 +93,20 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 100
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ---------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [74383e3](https://github.com/TagStudioDev/TagStudio/commit/74383e3c3c12f72be1481ab0b86c7360b95c2d85) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [74383e3](https://github.com/TagStudioDev/TagStudio/commit/74383e3c3c12f72be1481ab0b86c7360b95c2d85) | [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Introduces built-in minor versioning
|
||||
- The version number divided by 100 (and floored) constitutes the **major** version. Major version indicate breaking changes that prevent libraries from being opened in TagStudio versions older than the ones they were created in.
|
||||
- Values more precise than this ("ones" through "tens" columns) constitute the **minor** version. These indicate minor changes that don't prevent a newer library from being opened in an older version of TagStudio, as long as the major version is not also increased.
|
||||
- Swaps `parent_id` and `child_id` values in the `tag_parents` table
|
||||
- Swaps `parent_id` and `child_id` values in the `tag_parents` table, which have erroneously been flipped since the first SQLite DB version.
|
||||
|
||||
#### Version 101
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ---------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | _Current_ | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Deprecates the `preferences` table, set to be removed in a future TagStudio version.
|
||||
- Introduces the `versions` table
|
||||
@@ -115,11 +115,3 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
- `'INITIAL'` stores the database version number in which in was created
|
||||
- Pre-existing databases set this number to `100`
|
||||
- `'CURRENT'` stores the current database version number
|
||||
|
||||
#### Version 102
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted.
|
||||
|
||||
@@ -116,7 +116,6 @@ python3Packages.buildPythonApplication {
|
||||
"test_badge_visual_state"
|
||||
"test_browsing_state_update"
|
||||
"test_flow_layout_happy_path"
|
||||
"test_get" # TODO: Look further into, might be possible to run.
|
||||
"test_json_migration"
|
||||
"test_library_migrations"
|
||||
"test_update_tags"
|
||||
|
||||
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||
[project]
|
||||
name = "TagStudio"
|
||||
description = "A User-Focused Photo & File Management System."
|
||||
version = "9.5.5"
|
||||
version = "9.5.4"
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
@@ -23,7 +23,6 @@ dependencies = [
|
||||
"pydantic~=2.10",
|
||||
"pydub~=0.25",
|
||||
"PySide6==6.8.0.*",
|
||||
"rarfile==4.2",
|
||||
"rawpy~=0.24",
|
||||
"Send2Trash~=1.8",
|
||||
"SQLAlchemy~=2.0",
|
||||
@@ -89,7 +88,7 @@ qt_api = "pyside6"
|
||||
ignore = [
|
||||
".venv/**",
|
||||
"src/tagstudio/core/library/json/",
|
||||
"src/tagstudio/qt/previews/vendored/pydub/",
|
||||
"src/tagstudio/qt/helpers/vendored/pydub/",
|
||||
]
|
||||
include = ["src/tagstudio", "tests"]
|
||||
reportAny = false
|
||||
@@ -114,7 +113,7 @@ ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = ["D", "E402"]
|
||||
"src/tagstudio/qt/previews/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
|
||||
"src/tagstudio/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
VERSION: str = "9.5.5" # Major.Minor.Patch
|
||||
VERSION: str = "9.5.4" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
|
||||
@@ -5,16 +5,14 @@ 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
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class DriverMixin:
|
||||
cached_values: QSettings
|
||||
# TODO: GlobalSettings has become closely tied to Qt.
|
||||
# Should there be a base Settings class?
|
||||
settings: GlobalSettings
|
||||
|
||||
def evaluate_path(self, open_path: str | None) -> LibraryStatus:
|
||||
|
||||
28
src/tagstudio/core/field_template.py
Normal file
28
src/tagstudio/core/field_template.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
class FieldTemplate:
|
||||
"""A TagStudio Library Field Template object."""
|
||||
|
||||
def __init__(self, id: int, name: str, type: str) -> None:
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"\nID: {self.id}\nName: {self.name}\nType: {self.type}\n"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def to_compressed_obj(self) -> dict:
|
||||
"""An alternative to __dict__ that only includes fields containing non-default data."""
|
||||
obj: dict = {}
|
||||
# All Field fields (haha) are mandatory, so no value checks are done.
|
||||
obj["id"] = self.id
|
||||
obj["name"] = self.name
|
||||
obj["type"] = self.type
|
||||
|
||||
return obj
|
||||
@@ -24,10 +24,6 @@ DEFAULT_GLOBAL_SETTINGS_PATH = (
|
||||
DEFAULT_THUMB_CACHE_SIZE = 500 # Number in MiB
|
||||
MIN_THUMB_CACHE_SIZE = 10 # Number in MiB
|
||||
|
||||
# See: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#webp-saving
|
||||
DEFAULT_CACHED_IMAGE_QUALITY = 80
|
||||
DEFAULT_CACHED_IMAGE_RES = 256
|
||||
|
||||
|
||||
class Theme(IntEnum):
|
||||
DARK = 0
|
||||
@@ -60,8 +56,6 @@ class GlobalSettings(BaseModel):
|
||||
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)
|
||||
cached_thumb_quality: int = Field(default=DEFAULT_CACHED_IMAGE_QUALITY)
|
||||
cached_thumb_resolution: int = Field(default=DEFAULT_CACHED_IMAGE_RES)
|
||||
autoplay: bool = Field(default=True)
|
||||
loop: bool = Field(default=True)
|
||||
show_filenames_in_grid: bool = Field(default=True)
|
||||
@@ -11,7 +11,7 @@ JSON_FILENAME: str = "ts_library.json"
|
||||
DB_VERSION_LEGACY_KEY: str = "DB_VERSION"
|
||||
DB_VERSION_CURRENT_KEY: str = "CURRENT"
|
||||
DB_VERSION_INITIAL_KEY: str = "INITIAL"
|
||||
DB_VERSION: int = 102
|
||||
DB_VERSION: int = 101
|
||||
|
||||
TAG_CHILDREN_QUERY = text("""
|
||||
WITH RECURSIVE ChildTags AS (
|
||||
|
||||
@@ -1,572 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.models import Namespace, TagColorGroup
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def namespaces() -> list[Namespace]:
|
||||
tagstudio_standard = Namespace("tagstudio-standard", "TagStudio Standard")
|
||||
tagstudio_pastels = Namespace("tagstudio-pastels", "TagStudio Pastels")
|
||||
tagstudio_shades = Namespace("tagstudio-shades", "TagStudio Shades")
|
||||
tagstudio_earth_tones = Namespace("tagstudio-earth-tones", "TagStudio Earth Tones")
|
||||
tagstudio_grayscale = Namespace("tagstudio-grayscale", "TagStudio Grayscale")
|
||||
tagstudio_neon = Namespace("tagstudio-neon", "TagStudio Neon")
|
||||
return [
|
||||
tagstudio_standard,
|
||||
tagstudio_pastels,
|
||||
tagstudio_shades,
|
||||
tagstudio_earth_tones,
|
||||
tagstudio_grayscale,
|
||||
tagstudio_neon,
|
||||
]
|
||||
|
||||
|
||||
def json_to_sql_color(json_color: str) -> tuple[str | None, str | None]:
|
||||
"""Convert a color string from a <=9.4 JSON library to a 9.5+ (namespace, slug) tuple."""
|
||||
json_color_ = json_color.lower()
|
||||
match json_color_:
|
||||
case "black":
|
||||
return ("tagstudio-grayscale", "black")
|
||||
case "dark gray":
|
||||
return ("tagstudio-grayscale", "dark-gray")
|
||||
case "gray":
|
||||
return ("tagstudio-grayscale", "gray")
|
||||
case "light gray":
|
||||
return ("tagstudio-grayscale", "light-gray")
|
||||
case "white":
|
||||
return ("tagstudio-grayscale", "white")
|
||||
case "light pink":
|
||||
return ("tagstudio-pastels", "light-pink")
|
||||
case "pink":
|
||||
return ("tagstudio-standard", "pink")
|
||||
case "magenta":
|
||||
return ("tagstudio-standard", "magenta")
|
||||
case "red":
|
||||
return ("tagstudio-standard", "red")
|
||||
case "red orange":
|
||||
return ("tagstudio-standard", "red-orange")
|
||||
case "salmon":
|
||||
return ("tagstudio-pastels", "salmon")
|
||||
case "orange":
|
||||
return ("tagstudio-standard", "orange")
|
||||
case "yellow orange":
|
||||
return ("tagstudio-standard", "amber")
|
||||
case "yellow":
|
||||
return ("tagstudio-standard", "yellow")
|
||||
case "mint":
|
||||
return ("tagstudio-pastels", "mint")
|
||||
case "lime":
|
||||
return ("tagstudio-standard", "lime")
|
||||
case "light green":
|
||||
return ("tagstudio-pastels", "light-green")
|
||||
case "green":
|
||||
return ("tagstudio-standard", "green")
|
||||
case "teal":
|
||||
return ("tagstudio-standard", "teal")
|
||||
case "cyan":
|
||||
return ("tagstudio-standard", "cyan")
|
||||
case "light blue":
|
||||
return ("tagstudio-pastels", "light-blue")
|
||||
case "blue":
|
||||
return ("tagstudio-standard", "blue")
|
||||
case "blue violet":
|
||||
return ("tagstudio-shades", "navy")
|
||||
case "violet":
|
||||
return ("tagstudio-standard", "indigo")
|
||||
case "purple":
|
||||
return ("tagstudio-standard", "purple")
|
||||
case "peach":
|
||||
return ("tagstudio-earth-tones", "peach")
|
||||
case "brown":
|
||||
return ("tagstudio-earth-tones", "brown")
|
||||
case "lavender":
|
||||
return ("tagstudio-pastels", "lavender")
|
||||
case "blonde":
|
||||
return ("tagstudio-earth-tones", "blonde")
|
||||
case "auburn":
|
||||
return ("tagstudio-shades", "auburn")
|
||||
case "light brown":
|
||||
return ("tagstudio-earth-tones", "light-brown")
|
||||
case "dark brown":
|
||||
return ("tagstudio-earth-tones", "dark-brown")
|
||||
case "cool gray":
|
||||
return ("tagstudio-earth-tones", "cool-gray")
|
||||
case "warm gray":
|
||||
return ("tagstudio-earth-tones", "warm-gray")
|
||||
case "olive":
|
||||
return ("tagstudio-shades", "olive")
|
||||
case "berry":
|
||||
return ("tagstudio-shades", "berry")
|
||||
case _:
|
||||
return (None, None)
|
||||
|
||||
|
||||
def standard() -> list[TagColorGroup]:
|
||||
red = TagColorGroup(
|
||||
slug="red",
|
||||
namespace="tagstudio-standard",
|
||||
name="Red",
|
||||
primary="#E22C3C",
|
||||
)
|
||||
red_orange = TagColorGroup(
|
||||
slug="red-orange",
|
||||
namespace="tagstudio-standard",
|
||||
name="Red Orange",
|
||||
primary="#E83726",
|
||||
)
|
||||
orange = TagColorGroup(
|
||||
slug="orange",
|
||||
namespace="tagstudio-standard",
|
||||
name="Orange",
|
||||
primary="#ED6022",
|
||||
)
|
||||
amber = TagColorGroup(
|
||||
slug="amber",
|
||||
namespace="tagstudio-standard",
|
||||
name="Amber",
|
||||
primary="#FA9A2C",
|
||||
)
|
||||
yellow = TagColorGroup(
|
||||
slug="yellow",
|
||||
namespace="tagstudio-standard",
|
||||
name="Yellow",
|
||||
primary="#FFD63D",
|
||||
)
|
||||
lime = TagColorGroup(
|
||||
slug="lime",
|
||||
namespace="tagstudio-standard",
|
||||
name="Lime",
|
||||
primary="#92E649",
|
||||
)
|
||||
green = TagColorGroup(
|
||||
slug="green",
|
||||
namespace="tagstudio-standard",
|
||||
name="Green",
|
||||
primary="#45D649",
|
||||
)
|
||||
teal = TagColorGroup(
|
||||
slug="teal",
|
||||
namespace="tagstudio-standard",
|
||||
name="Teal",
|
||||
primary="#22D589",
|
||||
)
|
||||
cyan = TagColorGroup(
|
||||
slug="cyan",
|
||||
namespace="tagstudio-standard",
|
||||
name="Cyan",
|
||||
primary="#3DDBDB",
|
||||
)
|
||||
blue = TagColorGroup(
|
||||
slug="blue",
|
||||
namespace="tagstudio-standard",
|
||||
name="Blue",
|
||||
primary="#3B87F0",
|
||||
)
|
||||
indigo = TagColorGroup(
|
||||
slug="indigo",
|
||||
namespace="tagstudio-standard",
|
||||
name="Indigo",
|
||||
primary="#874FF5",
|
||||
)
|
||||
purple = TagColorGroup(
|
||||
slug="purple",
|
||||
namespace="tagstudio-standard",
|
||||
name="Purple",
|
||||
primary="#BB4FF0",
|
||||
)
|
||||
magenta = TagColorGroup(
|
||||
slug="magenta",
|
||||
namespace="tagstudio-standard",
|
||||
name="Magenta",
|
||||
primary="#F64680",
|
||||
)
|
||||
pink = TagColorGroup(
|
||||
slug="pink",
|
||||
namespace="tagstudio-standard",
|
||||
name="Pink",
|
||||
primary="#FF62AF",
|
||||
)
|
||||
return [
|
||||
red,
|
||||
red_orange,
|
||||
orange,
|
||||
amber,
|
||||
yellow,
|
||||
lime,
|
||||
green,
|
||||
teal,
|
||||
cyan,
|
||||
blue,
|
||||
indigo,
|
||||
purple,
|
||||
pink,
|
||||
magenta,
|
||||
]
|
||||
|
||||
|
||||
def pastels() -> list[TagColorGroup]:
|
||||
coral = TagColorGroup(
|
||||
slug="coral",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Coral",
|
||||
primary="#F2525F",
|
||||
)
|
||||
salmon = TagColorGroup(
|
||||
slug="salmon",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Salmon",
|
||||
primary="#F66348",
|
||||
)
|
||||
light_orange = TagColorGroup(
|
||||
slug="light-orange",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Orange",
|
||||
primary="#FF9450",
|
||||
)
|
||||
light_amber = TagColorGroup(
|
||||
slug="light-amber",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Amber",
|
||||
primary="#FFBA57",
|
||||
)
|
||||
light_yellow = TagColorGroup(
|
||||
slug="light-yellow",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Yellow",
|
||||
primary="#FFE173",
|
||||
)
|
||||
light_lime = TagColorGroup(
|
||||
slug="light-lime",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Lime",
|
||||
primary="#C9FF7A",
|
||||
)
|
||||
light_green = TagColorGroup(
|
||||
slug="light-green",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Green",
|
||||
primary="#81FF76",
|
||||
)
|
||||
mint = TagColorGroup(
|
||||
slug="mint",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Mint",
|
||||
primary="#68FFB4",
|
||||
)
|
||||
sky_blue = TagColorGroup(
|
||||
slug="sky-blue",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Sky Blue",
|
||||
primary="#8EFFF4",
|
||||
)
|
||||
light_blue = TagColorGroup(
|
||||
slug="light-blue",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Blue",
|
||||
primary="#64C6FF",
|
||||
)
|
||||
lavender = TagColorGroup(
|
||||
slug="lavender",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Lavender",
|
||||
primary="#908AF6",
|
||||
)
|
||||
lilac = TagColorGroup(
|
||||
slug="lilac",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Lilac",
|
||||
primary="#DF95FF",
|
||||
)
|
||||
light_pink = TagColorGroup(
|
||||
slug="light-pink",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Pink",
|
||||
primary="#FF87BA",
|
||||
)
|
||||
return [
|
||||
coral,
|
||||
salmon,
|
||||
light_orange,
|
||||
light_amber,
|
||||
light_yellow,
|
||||
light_lime,
|
||||
light_green,
|
||||
mint,
|
||||
sky_blue,
|
||||
light_blue,
|
||||
lavender,
|
||||
lilac,
|
||||
light_pink,
|
||||
]
|
||||
|
||||
|
||||
def shades() -> list[TagColorGroup]:
|
||||
burgundy = TagColorGroup(
|
||||
slug="burgundy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Burgundy",
|
||||
primary="#6E1C24",
|
||||
)
|
||||
auburn = TagColorGroup(
|
||||
slug="auburn",
|
||||
namespace="tagstudio-shades",
|
||||
name="Auburn",
|
||||
primary="#A13220",
|
||||
)
|
||||
olive = TagColorGroup(
|
||||
slug="olive",
|
||||
namespace="tagstudio-shades",
|
||||
name="Olive",
|
||||
primary="#4C652E",
|
||||
)
|
||||
dark_teal = TagColorGroup(
|
||||
slug="dark-teal",
|
||||
namespace="tagstudio-shades",
|
||||
name="Dark Teal",
|
||||
primary="#1F5E47",
|
||||
)
|
||||
navy = TagColorGroup(
|
||||
slug="navy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Navy",
|
||||
primary="#104B98",
|
||||
)
|
||||
dark_lavender = TagColorGroup(
|
||||
slug="dark_lavender",
|
||||
namespace="tagstudio-shades",
|
||||
name="Dark Lavender",
|
||||
primary="#3D3B6C",
|
||||
)
|
||||
berry = TagColorGroup(
|
||||
slug="berry",
|
||||
namespace="tagstudio-shades",
|
||||
name="Berry",
|
||||
primary="#9F2AA7",
|
||||
)
|
||||
return [burgundy, auburn, olive, dark_teal, navy, dark_lavender, berry]
|
||||
|
||||
|
||||
def earth_tones() -> list[TagColorGroup]:
|
||||
dark_brown = TagColorGroup(
|
||||
slug="dark-brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Dark Brown",
|
||||
primary="#4C2315",
|
||||
)
|
||||
brown = TagColorGroup(
|
||||
slug="brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Brown",
|
||||
primary="#823216",
|
||||
)
|
||||
light_brown = TagColorGroup(
|
||||
slug="light-brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Light Brown",
|
||||
primary="#BE5B2D",
|
||||
)
|
||||
blonde = TagColorGroup(
|
||||
slug="blonde",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Blonde",
|
||||
primary="#EFC664",
|
||||
)
|
||||
peach = TagColorGroup(
|
||||
slug="peach",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Peach",
|
||||
primary="#F1C69C",
|
||||
)
|
||||
warm_gray = TagColorGroup(
|
||||
slug="warm-gray",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Warm Gray",
|
||||
primary="#625550",
|
||||
)
|
||||
cool_gray = TagColorGroup(
|
||||
slug="cool-gray",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Cool Gray",
|
||||
primary="#515768",
|
||||
)
|
||||
return [dark_brown, brown, light_brown, blonde, peach, warm_gray, cool_gray]
|
||||
|
||||
|
||||
def grayscale() -> list[TagColorGroup]:
|
||||
black = TagColorGroup(
|
||||
slug="black",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Black",
|
||||
primary="#111018",
|
||||
)
|
||||
dark_gray = TagColorGroup(
|
||||
slug="dark-gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Dark Gray",
|
||||
primary="#242424",
|
||||
)
|
||||
gray = TagColorGroup(
|
||||
slug="gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Gray",
|
||||
primary="#53525A",
|
||||
)
|
||||
light_gray = TagColorGroup(
|
||||
slug="light-gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Light Gray",
|
||||
primary="#AAAAAA",
|
||||
)
|
||||
white = TagColorGroup(
|
||||
slug="white",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="White",
|
||||
primary="#F2F1F8",
|
||||
)
|
||||
return [black, dark_gray, gray, light_gray, white]
|
||||
|
||||
|
||||
def neon() -> list[TagColorGroup]:
|
||||
neon_red = TagColorGroup(
|
||||
slug="neon-red",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Red",
|
||||
primary="#180607",
|
||||
secondary="#E22C3C",
|
||||
color_border=True,
|
||||
)
|
||||
neon_red_orange = TagColorGroup(
|
||||
slug="neon-red-orange",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Red Orange",
|
||||
primary="#220905",
|
||||
secondary="#E83726",
|
||||
color_border=True,
|
||||
)
|
||||
neon_orange = TagColorGroup(
|
||||
slug="neon-orange",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Orange",
|
||||
primary="#1F0D05",
|
||||
secondary="#ED6022",
|
||||
color_border=True,
|
||||
)
|
||||
neon_amber = TagColorGroup(
|
||||
slug="neon-amber",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Amber",
|
||||
primary="#251507",
|
||||
secondary="#FA9A2C",
|
||||
color_border=True,
|
||||
)
|
||||
neon_yellow = TagColorGroup(
|
||||
slug="neon-yellow",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Yellow",
|
||||
primary="#2B1C0B",
|
||||
secondary="#FFD63D",
|
||||
color_border=True,
|
||||
)
|
||||
neon_lime = TagColorGroup(
|
||||
slug="neon-lime",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Lime",
|
||||
primary="#1B220C",
|
||||
secondary="#92E649",
|
||||
color_border=True,
|
||||
)
|
||||
neon_green = TagColorGroup(
|
||||
slug="neon-green",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Green",
|
||||
primary="#091610",
|
||||
secondary="#45D649",
|
||||
color_border=True,
|
||||
)
|
||||
neon_teal = TagColorGroup(
|
||||
slug="neon-teal",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Teal",
|
||||
primary="#09191D",
|
||||
secondary="#22D589",
|
||||
color_border=True,
|
||||
)
|
||||
neon_cyan = TagColorGroup(
|
||||
slug="neon-cyan",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Cyan",
|
||||
primary="#0B191C",
|
||||
secondary="#3DDBDB",
|
||||
color_border=True,
|
||||
)
|
||||
neon_blue = TagColorGroup(
|
||||
slug="neon-blue",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Blue",
|
||||
primary="#09101C",
|
||||
secondary="#3B87F0",
|
||||
color_border=True,
|
||||
)
|
||||
neon_indigo = TagColorGroup(
|
||||
slug="neon-indigo",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Indigo",
|
||||
primary="#150B24",
|
||||
secondary="#874FF5",
|
||||
color_border=True,
|
||||
)
|
||||
neon_purple = TagColorGroup(
|
||||
slug="neon-purple",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Purple",
|
||||
primary="#1E0B26",
|
||||
secondary="#BB4FF0",
|
||||
color_border=True,
|
||||
)
|
||||
neon_magenta = TagColorGroup(
|
||||
slug="neon-magenta",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Magenta",
|
||||
primary="#220A13",
|
||||
secondary="#F64680",
|
||||
color_border=True,
|
||||
)
|
||||
neon_pink = TagColorGroup(
|
||||
slug="neon-pink",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Pink",
|
||||
primary="#210E15",
|
||||
secondary="#FF62AF",
|
||||
color_border=True,
|
||||
)
|
||||
neon_white = TagColorGroup(
|
||||
slug="neon-white",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon White",
|
||||
primary="#131315",
|
||||
secondary="#F2F1F8",
|
||||
color_border=True,
|
||||
)
|
||||
return [
|
||||
neon_red,
|
||||
neon_red_orange,
|
||||
neon_orange,
|
||||
neon_amber,
|
||||
neon_yellow,
|
||||
neon_lime,
|
||||
neon_green,
|
||||
neon_teal,
|
||||
neon_cyan,
|
||||
neon_blue,
|
||||
neon_indigo,
|
||||
neon_purple,
|
||||
neon_pink,
|
||||
neon_magenta,
|
||||
neon_white,
|
||||
]
|
||||
@@ -1,144 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
|
||||
|
||||
from tagstudio.core.library.alchemy.db import Base
|
||||
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.models import Entry, ValueType
|
||||
|
||||
|
||||
class BaseField(Base):
|
||||
__abstract__ = True
|
||||
|
||||
@declared_attr
|
||||
def id(self) -> Mapped[int]:
|
||||
return mapped_column(primary_key=True, autoincrement=True)
|
||||
|
||||
@declared_attr
|
||||
def type_key(self) -> Mapped[str]:
|
||||
return mapped_column(ForeignKey("value_type.key"))
|
||||
|
||||
@declared_attr
|
||||
def type(self) -> Mapped[ValueType]:
|
||||
return relationship(foreign_keys=[self.type_key], lazy=False) # type: ignore # pyright: ignore[reportArgumentType]
|
||||
|
||||
@declared_attr
|
||||
def entry_id(self) -> Mapped[int]:
|
||||
return mapped_column(ForeignKey("entries.id"))
|
||||
|
||||
@declared_attr
|
||||
def entry(self) -> Mapped[Entry]:
|
||||
return relationship(foreign_keys=[self.entry_id]) # type: ignore # pyright: ignore[reportArgumentType]
|
||||
|
||||
@declared_attr
|
||||
def position(self) -> Mapped[int]:
|
||||
return mapped_column(default=0)
|
||||
|
||||
@override
|
||||
def __hash__(self):
|
||||
return hash(self.__key())
|
||||
|
||||
def __key(self): # pyright: ignore[reportUnknownParameterType]
|
||||
raise NotImplementedError
|
||||
|
||||
value: Any # pyright: ignore
|
||||
|
||||
|
||||
class BooleanField(BaseField):
|
||||
__tablename__ = "boolean_fields"
|
||||
|
||||
value: Mapped[bool]
|
||||
|
||||
def __key(self):
|
||||
return (self.type, self.value)
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object) -> bool:
|
||||
if isinstance(value, BooleanField):
|
||||
return self.__key() == value.__key()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TextField(BaseField):
|
||||
__tablename__ = "text_fields"
|
||||
|
||||
value: Mapped[str | None]
|
||||
|
||||
def __key(self) -> tuple[ValueType, str | None]:
|
||||
return self.type, self.value
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object) -> bool:
|
||||
if isinstance(value, TextField):
|
||||
return self.__key() == value.__key()
|
||||
elif isinstance(value, DatetimeField):
|
||||
return False
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DatetimeField(BaseField):
|
||||
__tablename__ = "datetime_fields"
|
||||
|
||||
value: Mapped[str | None]
|
||||
|
||||
def __key(self):
|
||||
return (self.type, self.value)
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object) -> bool:
|
||||
if isinstance(value, DatetimeField):
|
||||
return self.__key() == value.__key()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class DefaultField:
|
||||
id: int
|
||||
name: str
|
||||
type: FieldTypeEnum
|
||||
is_default: bool = field(default=False)
|
||||
|
||||
|
||||
class FieldID(Enum):
|
||||
"""Only for bootstrapping content of DB table."""
|
||||
|
||||
TITLE = DefaultField(id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True)
|
||||
AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE)
|
||||
ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE)
|
||||
URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE)
|
||||
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_BOX)
|
||||
NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX)
|
||||
COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE)
|
||||
DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME)
|
||||
DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME)
|
||||
DATE_MODIFIED = DefaultField(id=12, name="Date Modified", type=FieldTypeEnum.DATETIME)
|
||||
DATE_TAKEN = DefaultField(id=13, name="Date Taken", type=FieldTypeEnum.DATETIME)
|
||||
DATE_PUBLISHED = DefaultField(id=14, name="Date Published", type=FieldTypeEnum.DATETIME)
|
||||
# ARCHIVED = DefaultField(id=15, name="Archived", type=CheckboxField.checkbox)
|
||||
# FAVORITE = DefaultField(id=16, name="Favorite", type=CheckboxField.checkbox)
|
||||
BOOK = DefaultField(id=17, name="Book", type=FieldTypeEnum.TEXT_LINE)
|
||||
COMIC = DefaultField(id=18, name="Comic", type=FieldTypeEnum.TEXT_LINE)
|
||||
SERIES = DefaultField(id=19, name="Series", type=FieldTypeEnum.TEXT_LINE)
|
||||
MANGA = DefaultField(id=20, name="Manga", type=FieldTypeEnum.TEXT_LINE)
|
||||
SOURCE = DefaultField(id=21, name="Source", type=FieldTypeEnum.TEXT_LINE)
|
||||
DATE_UPLOADED = DefaultField(id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME)
|
||||
DATE_RELEASED = DefaultField(id=23, name="Date Released", type=FieldTypeEnum.DATETIME)
|
||||
VOLUME = DefaultField(id=24, name="Volume", type=FieldTypeEnum.TEXT_LINE)
|
||||
ANTHOLOGY = DefaultField(id=25, name="Anthology", type=FieldTypeEnum.TEXT_LINE)
|
||||
MAGAZINE = DefaultField(id=26, name="Magazine", type=FieldTypeEnum.TEXT_LINE)
|
||||
PUBLISHER = DefaultField(id=27, name="Publisher", type=FieldTypeEnum.TEXT_LINE)
|
||||
GUEST_ARTIST = DefaultField(id=28, name="Guest Artist", type=FieldTypeEnum.TEXT_LINE)
|
||||
COMPOSER = DefaultField(id=29, name="Composer", type=FieldTypeEnum.TEXT_LINE)
|
||||
COMMENTS = DefaultField(id=30, name="Comments", type=FieldTypeEnum.TEXT_LINE)
|
||||
@@ -1,23 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tagstudio.core.library.alchemy.db import Base
|
||||
|
||||
|
||||
class TagParent(Base):
|
||||
__tablename__ = "tag_parents"
|
||||
|
||||
parent_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
child_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
|
||||
|
||||
class TagEntry(Base):
|
||||
__tablename__ = "tag_entries"
|
||||
|
||||
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
entry_id: Mapped[int] = mapped_column(ForeignKey("entries.id"), primary_key=True)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,336 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from datetime import datetime as dt
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.library.alchemy.db import Base, PathType
|
||||
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
BaseField,
|
||||
BooleanField,
|
||||
DatetimeField,
|
||||
TextField,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
|
||||
|
||||
class Namespace(Base):
|
||||
__tablename__ = "namespaces"
|
||||
|
||||
namespace: Mapped[str] = mapped_column(primary_key=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
name: str,
|
||||
):
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
super().__init__()
|
||||
|
||||
|
||||
class TagAlias(Base):
|
||||
__tablename__ = "tag_aliases"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"))
|
||||
tag: Mapped["Tag"] = relationship(back_populates="aliases")
|
||||
|
||||
def __init__(self, name: str, tag_id: int | None = None):
|
||||
self.name = name
|
||||
|
||||
if tag_id is not None:
|
||||
self.tag_id = tag_id
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
class TagColorGroup(Base):
|
||||
__tablename__ = "tag_colors"
|
||||
|
||||
slug: Mapped[str] = mapped_column(primary_key=True, nullable=False)
|
||||
namespace: Mapped[str] = mapped_column(
|
||||
ForeignKey("namespaces.namespace"), primary_key=True, nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column()
|
||||
primary: Mapped[str] = mapped_column(nullable=False)
|
||||
secondary: Mapped[str | None]
|
||||
color_border: Mapped[bool] = mapped_column(nullable=False, default=False)
|
||||
|
||||
# TODO: Determine if slug and namespace can be optional and generated/added here if needed.
|
||||
def __init__(
|
||||
self,
|
||||
slug: str,
|
||||
namespace: str,
|
||||
name: str,
|
||||
primary: str,
|
||||
secondary: str | None = None,
|
||||
color_border: bool = False,
|
||||
):
|
||||
self.slug = slug
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
self.primary = primary
|
||||
if secondary:
|
||||
self.secondary = secondary
|
||||
self.color_border = color_border
|
||||
super().__init__()
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
__tablename__ = "tags"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str]
|
||||
shorthand: Mapped[str | None]
|
||||
color_namespace: Mapped[str | None] = mapped_column()
|
||||
color_slug: Mapped[str | None] = mapped_column()
|
||||
color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
|
||||
is_category: Mapped[bool]
|
||||
icon: Mapped[str | None]
|
||||
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
|
||||
parent_tags: Mapped[set["Tag"]] = relationship(
|
||||
secondary=TagParent.__tablename__,
|
||||
primaryjoin="Tag.id == TagParent.child_id",
|
||||
secondaryjoin="Tag.id == TagParent.parent_id",
|
||||
back_populates="parent_tags",
|
||||
)
|
||||
disambiguation_id: Mapped[int | None]
|
||||
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
[color_namespace, color_slug], [TagColorGroup.namespace, TagColorGroup.slug]
|
||||
),
|
||||
{"sqlite_autoincrement": True},
|
||||
)
|
||||
|
||||
@property
|
||||
def parent_ids(self) -> list[int]:
|
||||
return [tag.id for tag in self.parent_tags]
|
||||
|
||||
@property
|
||||
def alias_strings(self) -> list[str]:
|
||||
return [alias.name for alias in self.aliases]
|
||||
|
||||
@property
|
||||
def alias_ids(self) -> list[int]:
|
||||
return [tag.id for tag in self.aliases]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
id: int | None = None,
|
||||
shorthand: str | None = None,
|
||||
aliases: set[TagAlias] | None = None,
|
||||
parent_tags: set["Tag"] | None = None,
|
||||
icon: str | None = None,
|
||||
color_namespace: str | None = None,
|
||||
color_slug: str | None = None,
|
||||
disambiguation_id: int | None = None,
|
||||
is_category: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.aliases = aliases or set()
|
||||
self.parent_tags = parent_tags or set()
|
||||
self.color_namespace = color_namespace
|
||||
self.color_slug = color_slug
|
||||
self.icon = icon
|
||||
self.shorthand = shorthand
|
||||
self.disambiguation_id = disambiguation_id
|
||||
self.is_category = is_category
|
||||
self.id = id # pyright: ignore[reportAttributeAccessIssue]
|
||||
super().__init__()
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"<Tag ID: {self.id} Name: {self.name}>"
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __lt__(self, other: "Tag") -> bool:
|
||||
return self.name < other.name
|
||||
|
||||
def __le__(self, other: "Tag") -> bool:
|
||||
return self.name <= other.name
|
||||
|
||||
def __gt__(self, other: "Tag") -> bool:
|
||||
return self.name > other.name
|
||||
|
||||
def __ge__(self, other: "Tag") -> bool:
|
||||
return self.name >= other.name
|
||||
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = "folders"
|
||||
|
||||
# TODO - implement this
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
path: Mapped[Path] = mapped_column(PathType, unique=True)
|
||||
uuid: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
|
||||
class Entry(Base):
|
||||
__tablename__ = "entries"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
||||
folder_id: Mapped[int] = mapped_column(ForeignKey("folders.id"))
|
||||
folder: Mapped[Folder] = relationship("Folder")
|
||||
|
||||
path: Mapped[Path] = mapped_column(PathType, unique=True)
|
||||
filename: Mapped[str] = mapped_column()
|
||||
suffix: Mapped[str] = mapped_column()
|
||||
date_created: Mapped[dt | None]
|
||||
date_modified: Mapped[dt | None]
|
||||
date_added: Mapped[dt | None]
|
||||
|
||||
tags: Mapped[set[Tag]] = relationship(secondary="tag_entries")
|
||||
|
||||
text_fields: Mapped[list[TextField]] = relationship(
|
||||
back_populates="entry",
|
||||
cascade="all, delete",
|
||||
)
|
||||
datetime_fields: Mapped[list[DatetimeField]] = relationship(
|
||||
back_populates="entry",
|
||||
cascade="all, delete",
|
||||
)
|
||||
|
||||
@property
|
||||
def fields(self) -> list[BaseField]:
|
||||
fields: list[BaseField] = []
|
||||
fields.extend(self.text_fields)
|
||||
fields.extend(self.datetime_fields)
|
||||
fields = sorted(fields, key=lambda field: field.type.position)
|
||||
return fields
|
||||
|
||||
@property
|
||||
def is_favorite(self) -> bool:
|
||||
return any(tag.id == TAG_FAVORITE for tag in self.tags)
|
||||
|
||||
@property
|
||||
def is_archived(self) -> bool:
|
||||
return any(tag.id == TAG_ARCHIVED for tag in self.tags)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
folder: Folder,
|
||||
fields: list[BaseField],
|
||||
id: int | None = None,
|
||||
date_created: dt | None = None,
|
||||
date_modified: dt | None = None,
|
||||
date_added: dt | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.path = path
|
||||
self.folder = folder
|
||||
self.id = id # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.filename = path.name
|
||||
self.suffix = path.suffix.lstrip(".").lower()
|
||||
|
||||
# The date the file associated with this entry was created.
|
||||
# st_birthtime on Windows and Mac, st_ctime on Linux.
|
||||
self.date_created = date_created
|
||||
# The date the file associated with this entry was last modified: st_mtime.
|
||||
self.date_modified = date_modified
|
||||
# The date this entry was added to the library.
|
||||
self.date_added = date_added
|
||||
|
||||
for field in fields:
|
||||
if isinstance(field, TextField):
|
||||
self.text_fields.append(field)
|
||||
elif isinstance(field, DatetimeField):
|
||||
self.datetime_fields.append(field)
|
||||
else:
|
||||
raise ValueError(f"Invalid field type: {field}")
|
||||
|
||||
def has_tag(self, tag: Tag) -> bool:
|
||||
return tag in self.tags
|
||||
|
||||
def remove_tag(self, tag: Tag) -> None:
|
||||
"""Removes a Tag from the Entry."""
|
||||
self.tags.remove(tag)
|
||||
|
||||
|
||||
class ValueType(Base):
|
||||
"""Define Field Types in the Library.
|
||||
|
||||
Example:
|
||||
key: content_tags (this field is slugified `name`)
|
||||
name: Content Tags (this field is human readable name)
|
||||
kind: type of content (Text Line, Text Box, Tags, Datetime, Checkbox)
|
||||
is_default: Should the field be present in new Entry?
|
||||
order: position of the field widget in the Entry form
|
||||
|
||||
"""
|
||||
|
||||
__tablename__ = "value_type"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
type: Mapped[FieldTypeEnum] = mapped_column(default=FieldTypeEnum.TEXT_LINE)
|
||||
is_default: Mapped[bool] # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
position: Mapped[int] # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
|
||||
# add relations to other tables
|
||||
text_fields: Mapped[list[TextField]] = relationship("TextField", back_populates="type")
|
||||
datetime_fields: Mapped[list[DatetimeField]] = relationship(
|
||||
"DatetimeField", back_populates="type"
|
||||
)
|
||||
boolean_fields: Mapped[list[BooleanField]] = relationship("BooleanField", back_populates="type")
|
||||
|
||||
@property
|
||||
def as_field(self) -> BaseField:
|
||||
FieldClass = { # noqa: N806
|
||||
FieldTypeEnum.TEXT_LINE: TextField,
|
||||
FieldTypeEnum.TEXT_BOX: TextField,
|
||||
FieldTypeEnum.DATETIME: DatetimeField,
|
||||
FieldTypeEnum.BOOLEAN: BooleanField,
|
||||
}
|
||||
|
||||
return FieldClass[self.type](
|
||||
type_key=self.key,
|
||||
position=self.position,
|
||||
)
|
||||
|
||||
|
||||
@event.listens_for(ValueType, "before_insert")
|
||||
def slugify_field_key(mapper, connection, target): # pyright: ignore
|
||||
"""Slugify the field key before inserting into the database."""
|
||||
if not target.key:
|
||||
from tagstudio.core.library.alchemy.library import slugify
|
||||
|
||||
target.key = slugify(target.tag)
|
||||
|
||||
|
||||
# NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4
|
||||
# and is set to be removed in a future release.
|
||||
@deprecated("Use `Version` for storing version, and `ts_ignore` system for file exclusion.")
|
||||
class Preferences(Base):
|
||||
__tablename__ = "preferences"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
value: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
|
||||
|
||||
class Version(Base):
|
||||
__tablename__ = "versions"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
value: Mapped[int] = mapped_column(nullable=False, default=0)
|
||||
@@ -1,198 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import ColumnElement, and_, distinct, func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql.operators import ilike_op
|
||||
|
||||
from tagstudio.core.library.alchemy.constants import TAG_CHILDREN_ID_QUERY
|
||||
from tagstudio.core.library.alchemy.joins import TagEntry
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag, TagAlias
|
||||
from tagstudio.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories
|
||||
from tagstudio.core.query_lang.ast import (
|
||||
AST,
|
||||
ANDList,
|
||||
BaseVisitor,
|
||||
Constraint,
|
||||
ConstraintType,
|
||||
Not,
|
||||
ORList,
|
||||
Property,
|
||||
)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
else:
|
||||
Library = None # don't import library because of circular imports
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def get_filetype_equivalency_list(item: str) -> list[str] | set[str]:
|
||||
for s in FILETYPE_EQUIVALENTS:
|
||||
if item in s:
|
||||
return s
|
||||
return [item]
|
||||
|
||||
|
||||
class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
|
||||
def __init__(self, lib: Library) -> None:
|
||||
super().__init__()
|
||||
self.lib = lib
|
||||
|
||||
@override
|
||||
def visit_or_list(self, node: ORList) -> ColumnElement[bool]: # type: ignore
|
||||
tag_ids, bool_expressions = self.__separate_tags(node.elements, only_single=False)
|
||||
if len(tag_ids) > 0:
|
||||
bool_expressions.append(self.__entry_has_any_tags(tag_ids))
|
||||
return or_(*bool_expressions)
|
||||
|
||||
@override
|
||||
def visit_and_list(self, node: ANDList) -> ColumnElement[bool]: # type: ignore
|
||||
tag_ids, bool_expressions = self.__separate_tags(node.terms, only_single=True)
|
||||
if len(tag_ids) > 0:
|
||||
bool_expressions.append(self.__entry_has_all_tags(tag_ids))
|
||||
return and_(*bool_expressions)
|
||||
|
||||
@override
|
||||
def visit_constraint(self, node: Constraint) -> ColumnElement[bool]: # type: ignore
|
||||
"""Returns a Boolean Expression that is true, if the Entry satisfies the constraint."""
|
||||
if len(node.properties) != 0:
|
||||
raise NotImplementedError("Properties are not implemented yet") # TODO TSQLANG
|
||||
|
||||
if node.type == ConstraintType.Tag:
|
||||
return self.__entry_has_any_tags(self.__get_tag_ids(node.value))
|
||||
elif node.type == ConstraintType.TagID:
|
||||
return self.__entry_has_any_tags([int(node.value)])
|
||||
elif node.type == ConstraintType.Path:
|
||||
ilike = False
|
||||
glob = False
|
||||
|
||||
# Smartcase check
|
||||
if node.value == node.value.lower():
|
||||
ilike = True
|
||||
if node.value.startswith("*") or node.value.endswith("*"):
|
||||
glob = True
|
||||
|
||||
if ilike and glob:
|
||||
logger.info("ConstraintType.Path", ilike=True, glob=True)
|
||||
return func.lower(Entry.path).op("GLOB")(f"{node.value.lower()}")
|
||||
elif ilike:
|
||||
logger.info("ConstraintType.Path", ilike=True, glob=False)
|
||||
return ilike_op(Entry.path, f"%{node.value}%")
|
||||
elif glob:
|
||||
logger.info("ConstraintType.Path", ilike=False, glob=True)
|
||||
return Entry.path.op("GLOB")(node.value)
|
||||
else:
|
||||
logger.info(
|
||||
"ConstraintType.Path", ilike=False, glob=False, re=re.escape(node.value)
|
||||
)
|
||||
return Entry.path.regexp_match(re.escape(node.value))
|
||||
elif node.type == ConstraintType.MediaType:
|
||||
extensions: set[str] = set[str]()
|
||||
for media_cat in MediaCategories.ALL_CATEGORIES:
|
||||
if node.value == media_cat.name:
|
||||
extensions = extensions | media_cat.extensions
|
||||
break
|
||||
return Entry.suffix.in_(map(lambda x: x.replace(".", ""), extensions))
|
||||
elif node.type == ConstraintType.FileType:
|
||||
return or_(
|
||||
*[Entry.suffix.ilike(ft) for ft in get_filetype_equivalency_list(node.value)]
|
||||
)
|
||||
elif node.type == ConstraintType.Special: # noqa: SIM102 unnecessary once there is a second special constraint
|
||||
if node.value.lower() == "untagged":
|
||||
return ~Entry.id.in_(select(Entry.id).join(TagEntry))
|
||||
|
||||
# raise exception if Constraint stays unhandled
|
||||
raise NotImplementedError("This type of constraint is not implemented yet")
|
||||
|
||||
@override
|
||||
def visit_property(self, node: Property) -> ColumnElement[bool]: # type: ignore
|
||||
raise NotImplementedError("This should never be reached!")
|
||||
|
||||
@override
|
||||
def visit_not(self, node: Not) -> ColumnElement[bool]: # type: ignore
|
||||
return ~self.visit(node.child)
|
||||
|
||||
def __get_tag_ids(self, tag_name: str, include_children: bool = True) -> list[int]:
|
||||
"""Given a tag name find the ids of all tags that this name could refer to."""
|
||||
with Session(self.lib.engine) as session:
|
||||
tag_ids = list(
|
||||
session.scalars(
|
||||
select(Tag.id)
|
||||
.where(or_(Tag.name.ilike(tag_name), Tag.shorthand.ilike(tag_name)))
|
||||
.union(select(TagAlias.tag_id).where(TagAlias.name.ilike(tag_name)))
|
||||
)
|
||||
)
|
||||
if len(tag_ids) > 1:
|
||||
logger.debug(
|
||||
f'Tag Constraint "{tag_name}" is ambiguous, {len(tag_ids)} matching tags found',
|
||||
tag_ids=tag_ids,
|
||||
include_children=include_children,
|
||||
)
|
||||
if not include_children:
|
||||
return tag_ids
|
||||
outp: list[int] = []
|
||||
for tag_id in tag_ids:
|
||||
outp.extend(list(session.scalars(TAG_CHILDREN_ID_QUERY, {"tag_id": tag_id})))
|
||||
return outp
|
||||
|
||||
def __separate_tags(
|
||||
self, terms: list[AST], only_single: bool = True
|
||||
) -> tuple[list[int], list[ColumnElement[bool]]]:
|
||||
tag_ids: list[int] = []
|
||||
bool_expressions: list[ColumnElement[bool]] = []
|
||||
|
||||
for term in terms:
|
||||
if isinstance(term, Constraint) and len(term.properties) == 0:
|
||||
match term.type:
|
||||
case ConstraintType.TagID:
|
||||
try:
|
||||
tag_ids.append(int(term.value))
|
||||
except ValueError:
|
||||
logger.error(
|
||||
"[SQLBoolExpressionBuilder] Could not cast value to an int Tag ID",
|
||||
value=term.value,
|
||||
)
|
||||
continue
|
||||
case ConstraintType.Tag:
|
||||
ids = self.__get_tag_ids(term.value)
|
||||
if not only_single:
|
||||
tag_ids.extend(ids)
|
||||
continue
|
||||
elif len(ids) == 1:
|
||||
tag_ids.append(ids[0])
|
||||
continue
|
||||
case ConstraintType.FileType:
|
||||
pass
|
||||
case ConstraintType.Path:
|
||||
pass
|
||||
case ConstraintType.Special:
|
||||
pass
|
||||
case _:
|
||||
raise NotImplementedError(f"Unhandled constraint: '{term.type}'")
|
||||
|
||||
bool_expressions.append(self.visit(term))
|
||||
return tag_ids, bool_expressions
|
||||
|
||||
def __entry_has_all_tags(self, tag_ids: list[int]) -> ColumnElement[bool]:
|
||||
"""Returns Binary Expression that is true if the Entry has all provided tag ids."""
|
||||
# Relational Division Query
|
||||
return Entry.id.in_(
|
||||
select(TagEntry.entry_id)
|
||||
.where(TagEntry.tag_id.in_(tag_ids))
|
||||
.group_by(TagEntry.entry_id)
|
||||
.having(func.count(distinct(TagEntry.tag_id)) == len(tag_ids))
|
||||
)
|
||||
|
||||
def __entry_has_any_tags(self, tag_ids: list[int]) -> ColumnElement[bool]:
|
||||
"""Returns Binary Expression that is true if the Entry has any of the provided tag ids."""
|
||||
return Entry.id.in_(
|
||||
select(TagEntry.entry_id).where(TagEntry.tag_id.in_(tag_ids)).distinct()
|
||||
)
|
||||
83
src/tagstudio/core/library/helpers/migration.py
Normal file
83
src/tagstudio/core/library/helpers/migration.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
def json_to_sql_color(json_color: str) -> tuple[str | None, str | None]:
|
||||
"""Convert a color string from a <=9.4 JSON library to a 9.5+ (namespace, slug) tuple."""
|
||||
json_color_ = json_color.lower()
|
||||
match json_color_:
|
||||
case "black":
|
||||
return ("tagstudio-grayscale", "black")
|
||||
case "dark gray":
|
||||
return ("tagstudio-grayscale", "dark-gray")
|
||||
case "gray":
|
||||
return ("tagstudio-grayscale", "gray")
|
||||
case "light gray":
|
||||
return ("tagstudio-grayscale", "light-gray")
|
||||
case "white":
|
||||
return ("tagstudio-grayscale", "white")
|
||||
case "light pink":
|
||||
return ("tagstudio-pastels", "light-pink")
|
||||
case "pink":
|
||||
return ("tagstudio-standard", "pink")
|
||||
case "magenta":
|
||||
return ("tagstudio-standard", "magenta")
|
||||
case "red":
|
||||
return ("tagstudio-standard", "red")
|
||||
case "red orange":
|
||||
return ("tagstudio-standard", "red-orange")
|
||||
case "salmon":
|
||||
return ("tagstudio-pastels", "salmon")
|
||||
case "orange":
|
||||
return ("tagstudio-standard", "orange")
|
||||
case "yellow orange":
|
||||
return ("tagstudio-standard", "amber")
|
||||
case "yellow":
|
||||
return ("tagstudio-standard", "yellow")
|
||||
case "mint":
|
||||
return ("tagstudio-pastels", "mint")
|
||||
case "lime":
|
||||
return ("tagstudio-standard", "lime")
|
||||
case "light green":
|
||||
return ("tagstudio-pastels", "light-green")
|
||||
case "green":
|
||||
return ("tagstudio-standard", "green")
|
||||
case "teal":
|
||||
return ("tagstudio-standard", "teal")
|
||||
case "cyan":
|
||||
return ("tagstudio-standard", "cyan")
|
||||
case "light blue":
|
||||
return ("tagstudio-pastels", "light-blue")
|
||||
case "blue":
|
||||
return ("tagstudio-standard", "blue")
|
||||
case "blue violet":
|
||||
return ("tagstudio-shades", "navy")
|
||||
case "violet":
|
||||
return ("tagstudio-standard", "indigo")
|
||||
case "purple":
|
||||
return ("tagstudio-standard", "purple")
|
||||
case "peach":
|
||||
return ("tagstudio-earth-tones", "peach")
|
||||
case "brown":
|
||||
return ("tagstudio-earth-tones", "brown")
|
||||
case "lavender":
|
||||
return ("tagstudio-pastels", "lavender")
|
||||
case "blonde":
|
||||
return ("tagstudio-earth-tones", "blonde")
|
||||
case "auburn":
|
||||
return ("tagstudio-shades", "auburn")
|
||||
case "light brown":
|
||||
return ("tagstudio-earth-tones", "light-brown")
|
||||
case "dark brown":
|
||||
return ("tagstudio-earth-tones", "dark-brown")
|
||||
case "cool gray":
|
||||
return ("tagstudio-earth-tones", "cool-gray")
|
||||
case "warm gray":
|
||||
return ("tagstudio-earth-tones", "warm-gray")
|
||||
case "olive":
|
||||
return ("tagstudio-shades", "olive")
|
||||
case "berry":
|
||||
return ("tagstudio-shades", "berry")
|
||||
case _:
|
||||
return (None, None)
|
||||
@@ -10,7 +10,7 @@ import wcmatch.fnmatch as fnmatch
|
||||
from wcmatch import glob, pathlib
|
||||
|
||||
from tagstudio.core.constants import IGNORE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.core.utils.singleton import Singleton
|
||||
from tagstudio.core.singleton import Singleton
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ from tagstudio.core.constants import (
|
||||
)
|
||||
from tagstudio.core.enums import OpenStatus
|
||||
from tagstudio.core.library.json.fields import DEFAULT_FIELDS, TEXT_FIELDS
|
||||
from tagstudio.core.utils.str_formatting import strip_punctuation, strip_web_protocol
|
||||
from tagstudio.core.utils.str import strip_punctuation
|
||||
from tagstudio.core.utils.web import strip_web_protocol
|
||||
|
||||
TYPE = ["file", "meta", "alt", "mask"]
|
||||
|
||||
|
||||
@@ -7,14 +7,9 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.constants import TS_FOLDER_NAME
|
||||
from tagstudio.core.library.alchemy.fields import FieldID
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
from tagstudio.core.library.alchemy.library import Entry, FieldID, Library
|
||||
from tagstudio.core.utils.unlinked_registry import logger
|
||||
|
||||
|
||||
class TagStudioCore:
|
||||
|
||||
@@ -5,14 +5,13 @@ from pathlib import Path
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class DupeFilesRegistry:
|
||||
class DupeRegistry:
|
||||
"""State handler for DupeGuru results."""
|
||||
|
||||
library: Library
|
||||
@@ -9,8 +9,7 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
|
||||
@@ -13,16 +13,15 @@ from time import time
|
||||
import structlog
|
||||
from wcmatch import pathlib
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob
|
||||
from tagstudio.core.utils.silent_subprocess import silent_run # pyright: ignore
|
||||
from tagstudio.qt.helpers.silent_popen import silent_run # pyright: ignore
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefreshTracker:
|
||||
class RefreshDirTracker:
|
||||
library: Library
|
||||
files_not_in_library: list[Path] = field(default_factory=list)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
@@ -24,11 +24,3 @@ def strip_punctuation(string: str) -> str:
|
||||
.replace(" ", "")
|
||||
.replace(" ", "")
|
||||
)
|
||||
|
||||
|
||||
def strip_web_protocol(string: str) -> str:
|
||||
r"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
|
||||
prefixes = ["https://", "http://", "www.", "www2."]
|
||||
for prefix in prefixes:
|
||||
string = string.removeprefix(prefix)
|
||||
return string
|
||||
@@ -5,8 +5,7 @@ from pathlib import Path
|
||||
import structlog
|
||||
from wcmatch import pathlib
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
|
||||
11
src/tagstudio/core/utils/web.py
Normal file
11
src/tagstudio/core/utils/web.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
def strip_web_protocol(string: str) -> str:
|
||||
r"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
|
||||
prefixes = ["https://", "http://", "www.", "www2."]
|
||||
for prefix in prefixes:
|
||||
string = string.removeprefix(prefix)
|
||||
return string
|
||||
@@ -12,7 +12,7 @@ import structlog
|
||||
from PIL import Image
|
||||
|
||||
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.qt.global_settings import DEFAULT_CACHED_IMAGE_QUALITY, DEFAULT_THUMB_CACHE_SIZE
|
||||
from tagstudio.core.global_settings import DEFAULT_THUMB_CACHE_SIZE
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -24,21 +24,19 @@ class CacheFolder:
|
||||
|
||||
|
||||
class CacheManager:
|
||||
MAX_FOLDER_SIZE = 10 # Absolute maximum size of a folder, number in MiB
|
||||
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 | float = DEFAULT_THUMB_CACHE_SIZE,
|
||||
img_quality: int = DEFAULT_CACHED_IMAGE_QUALITY,
|
||||
):
|
||||
"""A class for managing frontend caches, such as for file thumbnails.
|
||||
|
||||
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.
|
||||
img_quality: (int) The image quality value to save PIL images (0-100, default=80)
|
||||
"""
|
||||
self._lock = RLock()
|
||||
self.cache_path = library_dir / TS_FOLDER_NAME / THUMB_CACHE_NAME
|
||||
@@ -46,9 +44,6 @@ class CacheManager:
|
||||
math.floor(max_size * CacheManager.STAT_MULTIPLIER),
|
||||
math.floor(CacheManager.MAX_FOLDER_SIZE * CacheManager.STAT_MULTIPLIER),
|
||||
)
|
||||
self.img_quality = (
|
||||
img_quality if img_quality >= 0 and img_quality <= 100 else DEFAULT_CACHED_IMAGE_QUALITY
|
||||
)
|
||||
|
||||
self.folders: list[CacheFolder] = []
|
||||
self.current_size = 0
|
||||
@@ -132,20 +127,12 @@ class CacheManager:
|
||||
with self._lock as _lock:
|
||||
cache_folder: CacheFolder = self._get_current_folder()
|
||||
file_path = cache_folder.path / file_name
|
||||
try:
|
||||
image.save(file_path, mode=mode, quality=self.img_quality)
|
||||
image.save(file_path, mode=mode)
|
||||
|
||||
size = file_path.stat().st_size
|
||||
cache_folder.size += size
|
||||
self.current_size += size
|
||||
self._cull_folders()
|
||||
except FileNotFoundError:
|
||||
logger.warn(
|
||||
"[CacheManager] Failed to save cached image, was the folder deleted on disk?",
|
||||
folder=file_path,
|
||||
)
|
||||
if not cache_folder.path.exists():
|
||||
self.folders.remove(cache_folder)
|
||||
size = file_path.stat().st_size
|
||||
cache_folder.size += size
|
||||
self.current_size += size
|
||||
self._cull_folders()
|
||||
|
||||
def _create_folder(self) -> CacheFolder:
|
||||
with self._lock as _lock:
|
||||
|
||||
@@ -9,11 +9,11 @@ from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.enums import TagClickActionOption
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.tag_box_view import TagBoxWidgetView
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.view.components.tag_box_view import TagBoxWidgetView
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -9,11 +9,11 @@ import structlog
|
||||
from PySide6 import QtGui
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.registries.ignored_registry import IgnoredRegistry
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.mixed.remove_ignored_modal import RemoveIgnoredModal
|
||||
from tagstudio.core.utils.ignored_registry import IgnoredRegistry
|
||||
from tagstudio.qt.modals.remove_ignored_modal import RemoveIgnoredModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.fix_ignored_modal_view import FixIgnoredEntriesModalView
|
||||
from tagstudio.qt.view.widgets.fix_ignored_modal_view import FixIgnoredEntriesModalView
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
@@ -7,15 +7,14 @@ from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtGui
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
|
||||
from tagstudio.core.constants import IGNORE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.qt.utils.file_opener import open_file
|
||||
from tagstudio.qt.views.ignore_modal_view import IgnoreModalView
|
||||
from tagstudio.qt.helpers import file_opener
|
||||
from tagstudio.qt.view.widgets.ignore_modal_view import IgnoreModalView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -37,7 +36,7 @@ class IgnoreModal(IgnoreModalView):
|
||||
if not self.lib.library_dir:
|
||||
return
|
||||
ts_ignore_path = Path(self.lib.library_dir / TS_FOLDER_NAME / IGNORE_NAME)
|
||||
open_file(ts_ignore_path, file_manager=True)
|
||||
file_opener.open_file(ts_ignore_path, file_manager=True)
|
||||
|
||||
def save(self):
|
||||
if not self.lib.library_dir:
|
||||
@@ -47,6 +46,6 @@ class IgnoreModal(IgnoreModalView):
|
||||
Ignore.write_ignore_file(self.lib.library_dir, lines)
|
||||
|
||||
@override
|
||||
def showEvent(self, event: QShowEvent) -> None: # type: ignore
|
||||
def showEvent(self, event: QtGui.QShowEvent) -> None: # type: ignore
|
||||
self.__load_file()
|
||||
return super().showEvent(event)
|
||||
@@ -18,9 +18,9 @@ from tagstudio.core.library.alchemy.constants import (
|
||||
)
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.helpers import file_opener
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils import file_opener
|
||||
from tagstudio.qt.views.library_info_window_view import LibraryInfoWindowView
|
||||
from tagstudio.qt.view.widgets.library_info_window_view import LibraryInfoWindowView
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
@@ -14,10 +14,10 @@ from PySide6.QtCore import QSize
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.qt.helpers.file_opener import open_file
|
||||
from tagstudio.qt.helpers.file_tester import is_readable_video
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.utils.file_opener import open_file
|
||||
from tagstudio.qt.views.preview_thumb_view import PreviewThumbView
|
||||
from tagstudio.qt.view.widgets.preview.preview_thumb_view import PreviewThumbView
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -7,9 +7,9 @@ from warnings import catch_warnings
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.mixed.add_field import AddFieldModal
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchModal
|
||||
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
|
||||
from tagstudio.qt.modals.add_field import AddFieldModal
|
||||
from tagstudio.qt.modals.tag_search import TagSearchModal
|
||||
from tagstudio.qt.view.widgets.preview_panel_view import PreviewPanelView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -7,7 +7,7 @@ from PIL import Image
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
|
||||
from tagstudio.qt.helpers.gradients import linear_gradient
|
||||
from tagstudio.qt.helpers.gradient import linear_gradient
|
||||
|
||||
# TODO: Consolidate the built-in QT theme values with the values
|
||||
# here, in enums.py, and in palette.py.
|
||||
|
||||
@@ -14,8 +14,8 @@ from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import QLabel, QWidget
|
||||
|
||||
from tagstudio.core.utils.silent_subprocess import silent_popen # pyright: ignore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.helpers.silent_popen import silent_Popen
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -45,7 +45,7 @@ def open_file(path: str | Path, file_manager: bool = False, windows_start_comman
|
||||
|
||||
# For some reason, if the args are passed in a list, this will error when the
|
||||
# path has spaces, even while surrounded in double quotes.
|
||||
silent_popen(
|
||||
silent_Popen(
|
||||
command_name + command_arg,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
@@ -66,7 +66,7 @@ def open_file(path: str | Path, file_manager: bool = False, windows_start_comman
|
||||
)
|
||||
else:
|
||||
command = f'"{normpath}"'
|
||||
silent_popen(
|
||||
silent_Popen(
|
||||
command,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
@@ -98,7 +98,7 @@ def open_file(path: str | Path, file_manager: bool = False, windows_start_comman
|
||||
command_args = [str(path)]
|
||||
command = shutil.which(command_name)
|
||||
if command is not None:
|
||||
silent_popen([command] + command_args, close_fds=True)
|
||||
silent_Popen([command] + command_args, close_fds=True)
|
||||
else:
|
||||
logger.info("Could not find command on system PATH", command=command_name)
|
||||
except Exception:
|
||||
@@ -7,9 +7,7 @@ from pathlib import Path
|
||||
|
||||
import ffmpeg
|
||||
|
||||
from tagstudio.qt.previews.vendored.ffmpeg import (
|
||||
probe, # pyright: ignore[reportUnknownVariableType]
|
||||
)
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import probe
|
||||
|
||||
|
||||
def is_readable_video(filepath: Path | str):
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
|
||||
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
@deprecated("Use QPushButton with 'catch warnings' to silence disconnect warnings instead.")
|
||||
class QPushButtonWrapper(QPushButton):
|
||||
"""Custom QPushButton wrapper.
|
||||
|
||||
@@ -11,8 +11,8 @@ from PySide6.QtWidgets import QSlider, QStyle, QStyleOptionSlider
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class ClickableSlider(QSlider):
|
||||
"""A clickable QSlider wrapper.
|
||||
class QClickSlider(QSlider):
|
||||
"""Custom QSlider wrapper.
|
||||
|
||||
The purpose of this wrapper is to allow us to set slider positions
|
||||
based on click events.
|
||||
@@ -5,22 +5,16 @@
|
||||
# Modified for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
from PySide6.QtCore import QRect
|
||||
from PySide6.QtGui import QBrush, QColor, QImage, QPainter, QPixmap
|
||||
from PySide6.QtGui import QBrush, QColor, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import QProxyStyle
|
||||
|
||||
|
||||
class RoundedPixmapStyle(QProxyStyle):
|
||||
def __init__(self, radius: int = 8):
|
||||
def __init__(self, radius=8):
|
||||
super().__init__()
|
||||
self._radius = radius
|
||||
|
||||
@override
|
||||
def drawItemPixmap(
|
||||
self, painter: QPainter, rect: QRect, alignment: int, pixmap: QPixmap | QImage
|
||||
):
|
||||
def drawItemPixmap(self, painter, rectangle, alignment, pixmap): # noqa: N802
|
||||
painter.save()
|
||||
pix = QPixmap(pixmap.size())
|
||||
pix.fill(QColor("transparent"))
|
||||
@@ -30,5 +24,5 @@ class RoundedPixmapStyle(QProxyStyle):
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
p.drawRoundedRect(pixmap.rect(), self._radius, self._radius)
|
||||
p.end()
|
||||
super().drawItemPixmap(painter, rect, alignment, pix)
|
||||
super().drawItemPixmap(painter, rectangle, alignment, pix)
|
||||
painter.restore()
|
||||
@@ -1,51 +1,46 @@
|
||||
# Copyright (C) 2025
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
# pyright: reportExplicitAny=false
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable, Collection, Iterable
|
||||
from typing import Any
|
||||
|
||||
"""Implementation of subprocess.Popen that does not spawn console windows or log output
|
||||
and sanitizes pyinstaller environment variables."""
|
||||
and sanitizes pyinstall environment variables."""
|
||||
|
||||
|
||||
def silent_popen(
|
||||
def silent_Popen( # noqa: N802
|
||||
args,
|
||||
bufsize: int = -1,
|
||||
bufsize=-1,
|
||||
executable=None,
|
||||
stdin=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
preexec_fn: Callable[[], Any] | None = None,
|
||||
close_fds: bool = True,
|
||||
shell: bool = False,
|
||||
preexec_fn=None,
|
||||
close_fds=True,
|
||||
shell=False,
|
||||
cwd=None,
|
||||
env=None,
|
||||
universal_newlines: bool | None = None,
|
||||
startupinfo: Any | None = None,
|
||||
creationflags: int = 0,
|
||||
restore_signals: bool = True,
|
||||
start_new_session: bool = False,
|
||||
pass_fds: Collection[int] = (),
|
||||
universal_newlines=None,
|
||||
startupinfo=None,
|
||||
creationflags=0,
|
||||
restore_signals=True,
|
||||
start_new_session=False,
|
||||
pass_fds=(),
|
||||
*,
|
||||
text: bool | None = None,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
user: str | int | None = None,
|
||||
group: str | int | None = None,
|
||||
extra_groups: Iterable[str | int] | None = None,
|
||||
umask: int = -1,
|
||||
pipesize: int = -1,
|
||||
process_group: int | None = None,
|
||||
group=None,
|
||||
extra_groups=None,
|
||||
user=None,
|
||||
umask=-1,
|
||||
encoding=None,
|
||||
errors=None,
|
||||
text=None,
|
||||
pipesize=-1,
|
||||
process_group=None,
|
||||
):
|
||||
"""Call subprocess.Popen without creating a console window."""
|
||||
current_env = env
|
||||
|
||||
if sys.platform == "win32":
|
||||
creationflags |= subprocess.CREATE_NO_WINDOW
|
||||
import ctypes
|
||||
@@ -57,9 +52,9 @@ def silent_popen(
|
||||
or sys.platform.startswith("openbsd")
|
||||
):
|
||||
# pass clean environment to the subprocess
|
||||
current_env = os.environ
|
||||
original_env = current_env.get("LD_LIBRARY_PATH_ORIG")
|
||||
current_env["LD_LIBRARY_PATH"] = original_env if original_env else ""
|
||||
env = os.environ
|
||||
original_env = env.get("LD_LIBRARY_PATH_ORIG")
|
||||
env["LD_LIBRARY_PATH"] = original_env if original_env else ""
|
||||
|
||||
return subprocess.Popen(
|
||||
args=args,
|
||||
@@ -72,20 +67,20 @@ def silent_popen(
|
||||
close_fds=close_fds,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
env=current_env,
|
||||
env=env,
|
||||
universal_newlines=universal_newlines,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
restore_signals=restore_signals,
|
||||
start_new_session=start_new_session,
|
||||
pass_fds=pass_fds,
|
||||
text=text,
|
||||
encoding=encoding,
|
||||
errors=errors,
|
||||
user=user,
|
||||
group=group,
|
||||
extra_groups=extra_groups,
|
||||
user=user,
|
||||
umask=umask,
|
||||
encoding=encoding,
|
||||
errors=errors,
|
||||
text=text,
|
||||
pipesize=pipesize,
|
||||
process_group=process_group,
|
||||
)
|
||||
@@ -12,7 +12,7 @@ from shutil import which
|
||||
import ffmpeg
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.utils.silent_subprocess import silent_popen, silent_run
|
||||
from tagstudio.qt.helpers.silent_popen import silent_Popen, silent_run
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -69,7 +69,7 @@ def probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
|
||||
args += [filename]
|
||||
|
||||
# PATCHED
|
||||
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = silent_Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
communicate_kwargs = {}
|
||||
if timeout is not None:
|
||||
communicate_kwargs["timeout"] = timeout
|
||||
@@ -18,7 +18,7 @@ from tempfile import NamedTemporaryFile
|
||||
from pydub.logging_utils import log_conversion, log_subprocess_output
|
||||
from pydub.utils import fsdecode
|
||||
|
||||
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD
|
||||
|
||||
try:
|
||||
from itertools import izip
|
||||
@@ -42,8 +42,8 @@ from pydub.utils import (
|
||||
ratio_to_db,
|
||||
)
|
||||
|
||||
from tagstudio.core.utils.silent_subprocess import silent_popen
|
||||
from tagstudio.qt.previews.vendored.pydub.utils import _mediainfo_json
|
||||
from tagstudio.qt.helpers.silent_popen import silent_Popen
|
||||
from tagstudio.qt.helpers.vendored.pydub.utils import _mediainfo_json
|
||||
|
||||
basestring = str
|
||||
xrange = range
|
||||
@@ -608,7 +608,7 @@ class _AudioSegment:
|
||||
|
||||
with open(os.devnull, "rb") as devnull:
|
||||
# PATCHED
|
||||
p = silent_popen(
|
||||
p = silent_Popen(
|
||||
conversion_command, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
p_out, p_err = p.communicate()
|
||||
@@ -781,7 +781,7 @@ class _AudioSegment:
|
||||
log_conversion(conversion_command)
|
||||
|
||||
# PATCHED
|
||||
p = silent_popen(
|
||||
p = silent_Popen(
|
||||
conversion_command,
|
||||
stdin=stdin_parameter,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -1008,7 +1008,7 @@ class _AudioSegment:
|
||||
# read stdin / write stdout
|
||||
with open(os.devnull, "rb") as devnull:
|
||||
# PATCHED
|
||||
p = silent_popen(
|
||||
p = silent_Popen(
|
||||
conversion_command, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
p_out, p_err = p.communicate()
|
||||
@@ -8,8 +8,8 @@ from pydub.utils import (
|
||||
get_extra_info,
|
||||
)
|
||||
|
||||
from tagstudio.core.utils.silent_subprocess import silent_popen
|
||||
from tagstudio.qt.previews.vendored.ffmpeg import FFPROBE_CMD
|
||||
from tagstudio.qt.helpers.silent_popen import silent_Popen
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import FFPROBE_CMD
|
||||
|
||||
|
||||
def _mediainfo_json(filepath, read_ahead_limit=-1):
|
||||
@@ -39,7 +39,7 @@ def _mediainfo_json(filepath, read_ahead_limit=-1):
|
||||
|
||||
command = [prober, "-of", "json"] + command_args
|
||||
# PATCHED
|
||||
res = silent_popen(
|
||||
res = silent_Popen(
|
||||
command, stdin=stdin_parameter, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
output, stderr = res.communicate(input=stdin_data)
|
||||
@@ -35,15 +35,15 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption
|
||||
from tagstudio.core.library.alchemy.enums import SortingModeEnum
|
||||
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
|
||||
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
|
||||
from tagstudio.qt.mixed.landing import LandingWidget
|
||||
from tagstudio.qt.mixed.pagination import Pagination
|
||||
from tagstudio.qt.mnemonics import assign_mnemonics
|
||||
from tagstudio.qt.pagination import Pagination
|
||||
from tagstudio.qt.platform_strings import trash_term
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.widgets.landing import LandingWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -17,7 +17,7 @@ def remove_mnemonic_marker(label: str) -> str:
|
||||
if ch == "&":
|
||||
# escaped ampersand "&&"
|
||||
if i + 1 < len(label) and label[i + 1] == "&":
|
||||
result += "&&"
|
||||
result += "&"
|
||||
skip = True
|
||||
# otherwise skip this '&'
|
||||
continue
|
||||
|
||||
@@ -20,8 +20,8 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import VERSION, VERSION_BRANCH
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.previews.vendored import ffmpeg
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.helpers.vendored import ffmpeg
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -20,18 +20,18 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core import palette
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library, slugify
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.mixed.tag_widget import (
|
||||
from tagstudio.core.library.alchemy.library import Library, TagColorGroup, slugify
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_text_color,
|
||||
)
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.widgets.tag_color_preview import TagColorPreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -264,7 +264,7 @@ class BuildColorPanel(PanelWidget):
|
||||
def update_secondary(self, color: QColor | None = None, color_border: bool = False):
|
||||
logger.info("[BuildColorPanel] Updating Secondary", color=color)
|
||||
|
||||
color_ = color or QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
|
||||
color_ = color or QColor(palette.get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
|
||||
|
||||
highlight_color = get_highlight_color(color_)
|
||||
text_color = get_text_color(color_, highlight_color)
|
||||
@@ -11,11 +11,15 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QLabel, QLineEdit, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.library import Library, ReservedNamespaceError, slugify
|
||||
from tagstudio.core.library.alchemy.models import Namespace
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.core.library.alchemy.library import (
|
||||
Library,
|
||||
Namespace,
|
||||
ReservedNamespaceError,
|
||||
slugify,
|
||||
)
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -26,21 +26,20 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.mixed.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchModal, TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import (
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag, TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.modals.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.modals.tag_search import TagSearchModal, TagSearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
TagWidget,
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_primary_color,
|
||||
get_text_color,
|
||||
)
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.tag_color_preview import TagColorPreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -21,8 +21,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -5,14 +5,14 @@ from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FfmpegMissingMessageBox(QMessageBox):
|
||||
class FfmpegChecker(QMessageBox):
|
||||
"""A warning dialog for if FFmpeg is missing."""
|
||||
|
||||
HELP_URL = "https://docs.tagstud.io/help/ffmpeg/"
|
||||
@@ -17,8 +17,8 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.registries.dupe_files_registry import DupeFilesRegistry
|
||||
from tagstudio.qt.mixed.mirror_entries_modal import MirrorEntriesModal
|
||||
from tagstudio.core.utils.dupe_files import DupeRegistry
|
||||
from tagstudio.qt.modals.mirror_entries_modal import MirrorEntriesModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
@@ -40,7 +40,7 @@ class FixDupeFilesModal(QWidget):
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.tracker = DupeFilesRegistry(library=self.lib)
|
||||
self.tracker = DupeRegistry(library=self.lib)
|
||||
|
||||
self.desc_widget = QLabel(Translations["file.duplicates.description"])
|
||||
self.desc_widget.setObjectName("descriptionLabel")
|
||||
@@ -10,12 +10,12 @@ from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.registries.unlinked_registry import UnlinkedRegistry
|
||||
from tagstudio.qt.mixed.merge_dupe_entries import MergeDuplicateEntries
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.mixed.relink_entries_modal import RelinkUnlinkedEntries
|
||||
from tagstudio.qt.mixed.remove_unlinked_modal import RemoveUnlinkedEntriesModal
|
||||
from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry
|
||||
from tagstudio.qt.modals.merge_dupe_entries import MergeDuplicateEntries
|
||||
from tagstudio.qt.modals.relink_entries_modal import RelinkUnlinkedEntries
|
||||
from tagstudio.qt.modals.remove_unlinked_modal import RemoveUnlinkedEntriesModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
@@ -20,16 +20,14 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -57,7 +55,6 @@ def add_folders_to_tree(library: Library, tree: BranchData, items: tuple[str, ..
|
||||
return branch
|
||||
|
||||
|
||||
@deprecated("Will be replaced with upcoming 'Macros' feature before v9.6")
|
||||
def folders_to_tags(library: Library):
|
||||
logger.info("Converting folders to Tags")
|
||||
tree = BranchData()
|
||||
@@ -8,9 +8,9 @@ import typing
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.registries.dupe_files_registry import DupeFilesRegistry
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.core.utils.dupe_files import DupeRegistry
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -24,7 +24,7 @@ class MergeDuplicateEntries(QObject):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.driver = driver
|
||||
self.tracker = DupeFilesRegistry(library=self.lib)
|
||||
self.tracker = DupeRegistry(library=self.lib)
|
||||
|
||||
def merge_entries(self):
|
||||
pw = ProgressWidget(
|
||||
@@ -10,9 +10,9 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.registries.dupe_files_registry import DupeFilesRegistry
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.core.utils.dupe_files import DupeRegistry
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -22,7 +22,7 @@ if typing.TYPE_CHECKING:
|
||||
class MirrorEntriesModal(QWidget):
|
||||
done = Signal()
|
||||
|
||||
def __init__(self, driver: "QtDriver", tracker: DupeFilesRegistry):
|
||||
def __init__(self, driver: "QtDriver", tracker: DupeRegistry):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.setWindowTitle(Translations["entries.mirror.window_title"])
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from tagstudio.core.library.alchemy.registries.unlinked_registry import UnlinkedRegistry
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
|
||||
class RelinkUnlinkedEntries(QObject):
|
||||
@@ -17,10 +17,10 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.registries.ignored_registry import IgnoredRegistry
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.core.utils.ignored_registry import IgnoredRegistry
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
@@ -17,10 +17,10 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.registries.unlinked_registry import UnlinkedRegistry
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
@@ -21,14 +21,14 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
|
||||
from tagstudio.qt.global_settings import (
|
||||
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.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -23,11 +23,11 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.qt.mixed.build_namespace import BuildNamespacePanel
|
||||
from tagstudio.qt.mixed.color_box import ColorBoxWidget
|
||||
from tagstudio.qt.mixed.field_widget import FieldContainer
|
||||
from tagstudio.qt.modals.build_namespace import BuildNamespacePanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.widgets.color_box import ColorBoxWidget
|
||||
from tagstudio.qt.widgets.fields import FieldContainer
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -19,13 +19,16 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.core.library.alchemy.library import Library, TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_text_color,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -7,12 +7,11 @@ import structlog
|
||||
from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchPanel
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.modals.tag_search import TagSearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -71,5 +70,5 @@ class TagDatabasePanel(TagSearchPanel):
|
||||
if result != QMessageBox.Ok: # type: ignore
|
||||
return
|
||||
|
||||
self.lib.remove_tag(tag.id)
|
||||
self.lib.remove_tag(tag)
|
||||
self.update_tags()
|
||||
@@ -25,12 +25,11 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.tag import TagWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -197,8 +196,7 @@ class TagSearchPanel(PanelWidget):
|
||||
self.search_field.setFocus()
|
||||
self.update_tags()
|
||||
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
self.build_tag_modal: BuildTagPanel = BuildTagPanel(self.lib)
|
||||
self.add_tag_modal: PanelModal = PanelModal(
|
||||
@@ -376,8 +374,7 @@ class TagSearchPanel(PanelWidget):
|
||||
pass
|
||||
|
||||
def edit_tag(self, tag: Tag):
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
|
||||
def callback(btp: BuildTagPanel):
|
||||
self.lib.update_tag(
|
||||
@@ -13,8 +13,8 @@ from PySide6.QtGui import QIntValidator, QPixmap
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSizePolicy, QWidget
|
||||
|
||||
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
|
||||
from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.qbutton_wrapper import QPushButtonWrapper
|
||||
|
||||
|
||||
class Pagination(QWidget, QObject):
|
||||
@@ -12,7 +12,7 @@ from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap
|
||||
from PySide6.QtWidgets import QSplashScreen, QWidget
|
||||
|
||||
from tagstudio.core.constants import VERSION, VERSION_BRANCH
|
||||
from tagstudio.qt.global_settings import Splash
|
||||
from tagstudio.core.global_settings import Splash
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -49,59 +49,57 @@ 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.library.alchemy.enums import (
|
||||
BrowsingState,
|
||||
FieldTypeEnum,
|
||||
ItemType,
|
||||
SortingModeEnum,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.fields import FieldID
|
||||
from tagstudio.core.library.alchemy.library import Library, LibraryStatus
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, FieldID, Library, LibraryStatus
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.library.refresh import RefreshTracker
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.core.query_lang.util import ParsingError
|
||||
from tagstudio.core.ts_core import TagStudioCore
|
||||
from tagstudio.core.utils.str_formatting import strip_web_protocol
|
||||
from tagstudio.core.utils.refresh_dir import RefreshDirTracker
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.core.utils.web import strip_web_protocol
|
||||
from tagstudio.qt.cache_manager import CacheManager
|
||||
from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
|
||||
|
||||
# this import has side-effect of import PySide resources
|
||||
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
|
||||
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
|
||||
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
|
||||
from tagstudio.qt.global_settings import (
|
||||
DEFAULT_GLOBAL_SETTINGS_PATH,
|
||||
GlobalSettings,
|
||||
Theme,
|
||||
)
|
||||
from tagstudio.qt.mixed.about_modal import AboutModal
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.mixed.drop_import_modal import DropImportModal
|
||||
from tagstudio.qt.mixed.fix_dupe_files import FixDupeFilesModal
|
||||
from tagstudio.qt.mixed.fix_unlinked import FixUnlinkedEntriesModal
|
||||
from tagstudio.qt.mixed.folders_to_tags import FoldersToTagsModal
|
||||
from tagstudio.qt.mixed.item_thumb import BadgeType, ItemThumb
|
||||
from tagstudio.qt.mixed.migration_modal import JsonMigrationModal
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.mixed.settings_panel import SettingsPanel
|
||||
from tagstudio.qt.mixed.tag_color_manager import TagColorManager
|
||||
from tagstudio.qt.mixed.tag_database import TagDatabasePanel
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchModal
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.controller.fix_ignored_modal_controller import FixIgnoredEntriesModal
|
||||
from tagstudio.qt.controller.widgets.ignore_modal_controller import IgnoreModal
|
||||
from tagstudio.qt.controller.widgets.library_info_window_controller import LibraryInfoWindow
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.helpers.file_deleter import delete_file
|
||||
from tagstudio.qt.helpers.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
from tagstudio.qt.main_window import MainWindow
|
||||
from tagstudio.qt.modals.about import AboutModal
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.modals.drop_import import DropImportModal
|
||||
from tagstudio.qt.modals.ffmpeg_checker import FfmpegChecker
|
||||
from tagstudio.qt.modals.fix_dupes import FixDupeFilesModal
|
||||
from tagstudio.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
|
||||
from tagstudio.qt.modals.folders_to_tags import FoldersToTagsModal
|
||||
from tagstudio.qt.modals.settings_panel import SettingsPanel
|
||||
from tagstudio.qt.modals.tag_color_manager import TagColorManager
|
||||
from tagstudio.qt.modals.tag_database import TagDatabasePanel
|
||||
from tagstudio.qt.modals.tag_search import TagSearchModal
|
||||
from tagstudio.qt.platform_strings import trash_term
|
||||
from tagstudio.qt.previews.renderer import ThumbRenderer
|
||||
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.splash import SplashScreen
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.utils.file_deleter import delete_file
|
||||
from tagstudio.qt.utils.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.views.main_window import MainWindow
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.splash import SplashScreen
|
||||
from tagstudio.qt.widgets.item_thumb import BadgeType, ItemThumb
|
||||
from tagstudio.qt.widgets.migration_modal import JsonMigrationModal
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
from tagstudio.qt.widgets.progress import ProgressWidget
|
||||
from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
|
||||
BADGE_TAGS = {
|
||||
BadgeType.FAVORITE: TAG_FAVORITE,
|
||||
@@ -590,7 +588,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
# Check if FFmpeg or FFprobe are missing and show warning if so
|
||||
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
|
||||
FfmpegMissingMessageBox().show()
|
||||
FfmpegChecker().show()
|
||||
|
||||
self.app.exec()
|
||||
self.shutdown()
|
||||
@@ -1005,7 +1003,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
def add_new_files_callback(self):
|
||||
"""Run when user initiates adding new files to the Library."""
|
||||
tracker = RefreshTracker(self.lib)
|
||||
tracker = RefreshDirTracker(self.lib)
|
||||
|
||||
pw = ProgressWidget(
|
||||
cancel_button_text=None,
|
||||
@@ -1043,7 +1041,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
|
||||
def add_new_files_runnable(self, tracker: RefreshTracker):
|
||||
def add_new_files_runnable(self, tracker: RefreshDirTracker):
|
||||
"""Adds any known new files to the library and run default macros on them.
|
||||
|
||||
Threaded method.
|
||||
@@ -1697,14 +1695,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
open_status = LibraryStatus(
|
||||
success=False, library_path=path, message=type(e).__name__, msg_description=str(e)
|
||||
)
|
||||
self.cache_manager = CacheManager(
|
||||
path,
|
||||
max_size=self.settings.thumb_cache_size,
|
||||
img_quality=self.settings.cached_thumb_quality,
|
||||
)
|
||||
cache_size = self.settings.thumb_cache_size * self.cache_manager.STAT_MULTIPLIER
|
||||
self.cache_manager = CacheManager(path, max_size=self.settings.thumb_cache_size)
|
||||
logger.info(
|
||||
f"[Config] Thumbnail Cache Size: {format_size(cache_size)}",
|
||||
f"[Config] Thumbnail Cache Size: {format_size(self.settings.thumb_cache_size)}",
|
||||
)
|
||||
|
||||
# Migration is required
|
||||
|
||||
@@ -7,11 +7,10 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.mixed.field_widget import FieldWidget
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.widgets.fields import FieldWidget
|
||||
from tagstudio.qt.widgets.tag import TagWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -13,10 +13,9 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.constants import IGNORE_NAME
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -13,12 +13,12 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QStackedLayout,
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaType
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.mixed.media_player import MediaPlayer
|
||||
from tagstudio.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle
|
||||
from tagstudio.qt.platform_strings import open_file_str, trash_term
|
||||
from tagstudio.qt.previews.renderer import ThumbRenderer
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.styles.rounded_pixmap_style import RoundedPixmapStyle
|
||||
from tagstudio.qt.widgets.media_player import MediaPlayer
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -16,14 +16,13 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.preview_thumb_controller import PreviewThumb
|
||||
from tagstudio.qt.mixed.field_containers import FieldContainers
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributeData, FileAttributes
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.controller.widgets.preview.preview_thumb_controller import PreviewThumb
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.preview.field_containers import FieldContainers
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData, FileAttributes
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -3,21 +3,17 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
"""A clickable QLabel widget."""
|
||||
"""A clickable Label widget."""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@override
|
||||
def mousePressEvent(self, ev: QMouseEvent):
|
||||
def mousePressEvent(self, event): # noqa: N802
|
||||
self.clicked.emit()
|
||||
@@ -12,17 +12,17 @@ from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.qt.mixed.build_color import BuildColorPanel
|
||||
from tagstudio.qt.mixed.field_widget import FieldWidget
|
||||
from tagstudio.qt.mixed.tag_color_label import TagColorLabel
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.core.library.alchemy.library import TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.modals.build_color import BuildColorPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.widgets.fields import FieldWidget
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
from tagstudio.qt.widgets.tag_color_label import TagColorLabel
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library import Library
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import cast
|
||||
from PySide6.QtCore import QDateTime
|
||||
from PySide6.QtWidgets import QDateTimeEdit, QVBoxLayout
|
||||
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -13,19 +13,26 @@ import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QEvent, QMimeData, QSize, Qt, QUrl
|
||||
from PySide6.QtGui import QAction, QDrag, QEnterEvent, QGuiApplication, QMouseEvent, QPixmap
|
||||
from PySide6.QtWidgets import QBoxLayout, QCheckBox, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
from PySide6.QtWidgets import (
|
||||
QBoxLayout,
|
||||
QCheckBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.library.alchemy.enums import ItemType
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaCategories, MediaType
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.flowlayout import FlowWidget
|
||||
from tagstudio.qt.helpers.file_opener import FileOpenerHelper
|
||||
from tagstudio.qt.platform_strings import open_file_str, trash_term
|
||||
from tagstudio.qt.previews.renderer import ThumbRenderer
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.file_opener import FileOpenerHelper
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowWidget
|
||||
from tagstudio.qt.views.thumb_button import ThumbButton
|
||||
from tagstudio.qt.widgets.thumb_button import ThumbButton
|
||||
from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.widgets.clickable_label import ClickableLabel
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -35,8 +35,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.qt.helpers.qslider_wrapper import QClickSlider
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.clickable_slider import ClickableSlider
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -147,7 +147,7 @@ class MediaPlayer(QGraphicsView):
|
||||
self.controls.setStyleSheet("background: transparent;")
|
||||
self.controls.setMinimumHeight(48)
|
||||
|
||||
self.timeline_slider = ClickableSlider()
|
||||
self.timeline_slider = QClickSlider()
|
||||
self.timeline_slider.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.timeline_slider.setTickPosition(QSlider.TickPosition.NoTicks)
|
||||
self.timeline_slider.setSingleStep(1)
|
||||
@@ -196,7 +196,7 @@ class MediaPlayer(QGraphicsView):
|
||||
sub_layout.addWidget(self.mute_unmute)
|
||||
sub_layout.setAlignment(self.mute_unmute, Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
self.volume_slider = ClickableSlider()
|
||||
self.volume_slider = QClickSlider()
|
||||
self.volume_slider.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.volume_slider.setValue(int(self.player.audioOutput().volume() * 100))
|
||||
self.volume_slider.valueChanged.connect(self.volume_slider_changed)
|
||||
@@ -404,12 +404,12 @@ class MediaPlayer(QGraphicsView):
|
||||
if not self.is_paused:
|
||||
self.player.stop()
|
||||
self.player.setSource(QUrl.fromLocalFile(self.filepath))
|
||||
|
||||
if self.autoplay.isChecked():
|
||||
self.player.play()
|
||||
else:
|
||||
self.player.setSource(QUrl.fromLocalFile(self.filepath))
|
||||
|
||||
if self.autoplay.isChecked():
|
||||
self.player.play()
|
||||
|
||||
def load_toggle_play_icon(self, playing: bool) -> None:
|
||||
icon = self.driver.rm.pause_icon if playing else self.driver.rm.play_icon
|
||||
self.play_pause.load(icon)
|
||||
@@ -454,10 +454,6 @@ class MediaPlayer(QGraphicsView):
|
||||
duration = self.format_time(self.player.duration())
|
||||
self.position_label.setText(f"{current} / {duration}")
|
||||
|
||||
# Ensures first frame of non-autoplay videos are loaded
|
||||
if not self.autoplay.isChecked():
|
||||
self.player.pause()
|
||||
|
||||
def _update_controls(self, size: QSize) -> None:
|
||||
self.scene().setSceneRect(0, 0, size.width(), size.height())
|
||||
|
||||
@@ -30,20 +30,19 @@ from tagstudio.core.constants import (
|
||||
TS_FOLDER_NAME,
|
||||
)
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.core.library.alchemy import default_color_groups
|
||||
from tagstudio.core.library.alchemy.constants import SQL_FILENAME
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
from tagstudio.core.library.alchemy.library import Entry, TagAlias, TagParent
|
||||
from tagstudio.core.library.alchemy.library import Library as SqliteLibrary
|
||||
from tagstudio.core.library.alchemy.models import Entry, TagAlias
|
||||
from tagstudio.core.library.helpers.migration import json_to_sql_color
|
||||
from tagstudio.core.library.json.library import Library as JsonLibrary
|
||||
from tagstudio.core.library.json.library import Tag as JsonTag
|
||||
from tagstudio.qt.controllers.paged_panel_controller import PagedPanel
|
||||
from tagstudio.qt.controllers.paged_panel_state import PagedPanelState
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.helpers.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.utils.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.views.paged_body_wrapper import PagedBodyWrapper
|
||||
from tagstudio.qt.views.qbutton_wrapper import QPushButtonWrapper
|
||||
from tagstudio.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper
|
||||
from tagstudio.qt.widgets.paged_panel.paged_panel import PagedPanel
|
||||
from tagstudio.qt.widgets.paged_panel.paged_panel_state import PagedPanelState
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -780,7 +779,7 @@ class JsonMigrationModal(QObject):
|
||||
for tag in self.sql_lib.tags:
|
||||
tag_id = tag.id # Tag IDs start at 0
|
||||
sql_color = (tag.color_namespace, tag.color_slug)
|
||||
json_color = default_color_groups.json_to_sql_color(self.json_lib.get_tag(tag_id).color)
|
||||
json_color = json_to_sql_color(self.json_lib.get_tag(tag_id).color)
|
||||
|
||||
logger.info(
|
||||
"[Color Parity]",
|
||||
@@ -10,7 +10,7 @@ from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.controllers.paged_panel_state import PagedPanelState
|
||||
from tagstudio.qt.widgets.paged_panel.paged_panel_state import PagedPanelState
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
from tagstudio.qt.views.paged_body_wrapper import PagedBodyWrapper
|
||||
from tagstudio.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper
|
||||
|
||||
|
||||
class PagedPanelState:
|
||||
@@ -25,22 +25,23 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
from tagstudio.core.library.alchemy.library import (
|
||||
BaseField,
|
||||
DatetimeField,
|
||||
Entry,
|
||||
Library,
|
||||
Tag,
|
||||
TextField,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.tag_box_controller import TagBoxWidget
|
||||
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.mixed.field_widget import FieldContainer
|
||||
from tagstudio.qt.mixed.text_field import TextWidget
|
||||
from tagstudio.qt.controller.components.tag_box_controller import TagBoxWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.edit_text_box_modal import EditTextBox
|
||||
from tagstudio.qt.views.edit_text_line_modal import EditTextLine
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.widgets.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.widgets.fields import FieldContainer
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
from tagstudio.qt.widgets.text import TextWidget
|
||||
from tagstudio.qt.widgets.text_box_edit import EditTextBox
|
||||
from tagstudio.qt.widgets.text_line_edit import EditTextLine
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -22,10 +22,10 @@ from tagstudio.core.enums import ShowFilepathOption, Theme
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.file_opener import FileOpenerHelper, FileOpenerLabel
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -8,8 +8,8 @@ from collections.abc import Callable
|
||||
from PySide6.QtCore import Qt, QThreadPool
|
||||
from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.utils.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.helpers.function_iterator import FunctionIterator
|
||||
|
||||
|
||||
class ProgressWidget(QWidget):
|
||||
@@ -12,9 +12,9 @@ from PySide6.QtGui import QAction, QColor, QEnterEvent, QFontMetrics
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.helpers.escape_text import escape_text
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -10,14 +10,14 @@ from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QAction, QColor, QEnterEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import TagColorGroup
|
||||
from tagstudio.qt.helpers.escape_text import escape_text
|
||||
from tagstudio.qt.mixed.tag_widget import (
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_text_color,
|
||||
)
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user