mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-08-19 02:22:39 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 291d4b7678 | |||
| 8ec26dea3c | |||
| bcca08556e | |||
| 6452664b2a | |||
| bc96ba9e52 | |||
| ea76910451 | |||
| c32f500f3c | |||
| f89f0a6c7f | |||
| c33455b7a0 | |||
| 12fd4933be | |||
| 7e7def5a52 | |||
| 17cf87a4fa | |||
| 3fe7922642 | |||
| 555dae50d4 | |||
| e6d67c26dd |
@@ -18,7 +18,7 @@ runs:
|
||||
if: inputs.skip-setup != 'true'
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Setup uv install
|
||||
if: inputs.skip-setup != 'true'
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: reuse
|
||||
name: reuse
|
||||
entry: reuse lint-file
|
||||
language: system
|
||||
|
||||
- id: pyright
|
||||
name: pyright
|
||||
entry: pyright
|
||||
|
||||
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
"name": "TagStudio",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/src/tagstudio/main.py",
|
||||
"program": "${workspaceRoot}/src/tagstudio/__main__.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": ["-o", "~/Documents/Example"]
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ toc_depth: 2
|
||||
|
||||
# :material-script-text: Changelog
|
||||
|
||||
### 9.6.3 <small>August 15th, 2026</small>
|
||||
## 9.6.3 <small>August 15th, 2026</small>
|
||||
|
||||
This update includes some critical library bugfixes along with a handful QoL tweaks and additions to the tag/field search bars. The [documentation](https://docs.tagstud.io/usage/#tagging) on this feature has been updated to include the new improvements.
|
||||
|
||||
@@ -17,8 +17,6 @@ This update includes some critical library bugfixes along with a handful QoL twe
|
||||
|
||||
- feat(ui): show library format version in "About" window by @CyanVoxel in 102cfdf4a2e12635f4b6e47259aa23ea95787421
|
||||
|
||||
### Changed
|
||||
|
||||
#### Tag and Field Bars
|
||||
|
||||
- feat(ui): keep tag/field search bars open by default by @CyanVoxel in #1472
|
||||
@@ -26,6 +24,8 @@ This update includes some critical library bugfixes along with a handful QoL twe
|
||||
- feat(ui): use tab and shift+tab to navigate tag/field search bars by @CyanVoxel in #1474
|
||||
- feat(ui): add hint icons to tag/field search bars by @CyanVoxel in #1475
|
||||
|
||||
### Changed
|
||||
|
||||
#### Internal Changes
|
||||
|
||||
- refactor: almost final migrations cleanup by @Computerdores in #1456
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ If you wish to develop for TagStudio, you'll need to create a development enviro
|
||||
|
||||
## Installing Python
|
||||
|
||||
Python [3.12](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.12.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
|
||||
Python [3.14](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.14.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! info "Python Aliases"
|
||||
@@ -35,8 +35,8 @@ python --version
|
||||
If you choose to install Python using pyenv, please refer to the following instructions:
|
||||
|
||||
1. Follow pyenv's [install instructions](https://github.com/pyenv/pyenv/?tab=readme-ov-file#installation) for your system.
|
||||
2. Install the appropriate Python version with pyenv by running `pyenv install 3.12` (This will **not** mess with your existing Python installation).
|
||||
3. Navigate to the repository root folder in your terminal and run `pyenv local 3.12`. You could alternatively use `pyenv shell 3.12` or `pyenv global 3.12` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.
|
||||
2. Install the appropriate Python version with pyenv by running `pyenv install 3.14` (This will **not** mess with your existing Python installation).
|
||||
3. Navigate to the repository root folder in your terminal and run `pyenv local 3.14`. You could alternatively use `pyenv shell 3.14` or `pyenv global 3.14` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -16,11 +16,11 @@ File entry data is stored within the `ts_library.sqlite` file inside each librar
|
||||
|
||||
## Appearance
|
||||
|
||||
File entries appear as thumbnails inside the grid display. The preview panel shows a more detailed preview of the file, along with extra file stats and all attached TagStudio tags and fields.
|
||||
File entries appear as thumbnails inside the grid display. The inspector shows a more detailed preview of the file, along with extra file stats and all attached TagStudio tags and fields.
|
||||
|
||||
## Unlinked Entries
|
||||
|
||||
If the file that an entry is referencing has been moved, renamed, or deleted on disk, then TagStudio will display its unlinked status with a red chain-link icon instead of its thumbnail image. Certain uncached stats such as the file size and image dimensions will also be unavailable to see in the preview panel.
|
||||
If the file that an entry is referencing has been moved, renamed, or deleted on disk, then TagStudio will display its unlinked status with a red chain-link icon instead of its thumbnail image. Certain uncached stats such as the file size and image dimensions will also be unavailable to see in the inspector.
|
||||
|
||||
To fix file entries that have become unlinked, select the "Fix Unlinked Entries" option from the Tools menu. From there, refresh the unlinked entry count and choose whether to search and relink you files, and/or delete the file entries from your library. This will NOT delete or modify any files on disk.
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Fields are extra pieces of information you can add to [file entries](./entries.m
|
||||
|
||||
Unlike tags, fields are based on [templates](#templates) that contain pre-filled information such as the field type and title, and that information is _copied_ to fields when adding them to entries. Editing field information on entries **does not** modify the template it was created from.
|
||||
|
||||
To add a field to an entry, click the "Add Field" button in the preview panel. From there you can search and/or select a [field template](#field-templates) to choose from, or create a new one from the search bar. Alternatively you can create new field templates from **Edit -> Manage Field Templates**.
|
||||
To add a field to an entry, click the "Add Field" button at the bottom of the inspector. From there you can search and/or select a [field template](#field-templates) to choose from, or create a new one from the search bar. Alternatively you can create new field templates from **Edit -> Manage Field Templates**.
|
||||
|
||||
<figure markdown="span">
|
||||

|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ TagStudio has builds for :fontawesome-brands-windows: **Windows**, :fontawesome-
|
||||
|
||||
### :fontawesome-brands-python: 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.
|
||||
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.14 and PIP installed if you choose to install using this method.
|
||||
|
||||
The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio).
|
||||
|
||||
|
||||
@@ -201,3 +201,20 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
| 95e2fe7b4449951c385e35a2e13f0c1925f1f98e | [v9.6.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.1) | SQLite |
|
||||
|
||||
- Applies repairs to the `tag_parents` table, removing rows that reference child tags that have been deleted.
|
||||
|
||||
#### Version 300
|
||||
|
||||
| Added in Commit | Introduced in Release | Format |
|
||||
| ---------------------------------------- |-------------------------------------------------------------------------| ------ |
|
||||
| 51a9c16f50ca785d810911d2d0c83fa33eb1c0ae | [v9.6.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.2) | SQLite |
|
||||
|
||||
- Drops `folder` columns from the `entries` table.
|
||||
- Drops the unused `folders` table.
|
||||
|
||||
#### Version 400
|
||||
|
||||
| Added in Commit | Introduced in Release | Format |
|
||||
|-----------------|-----------------------| ------ |
|
||||
| TBD | TBD | SQLite |
|
||||
|
||||
- Adds the `category_exclusion` table.
|
||||
|
||||
+2
-2
@@ -203,8 +203,8 @@ Discrete library objects representing [attributes](<https://en.wikipedia.org/wik
|
||||
- [ ] User-Defined Icons :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.x]**
|
||||
- [ ] Tint Icons with Text Color :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.x]**
|
||||
- [x] [Category Property](tags.md#is-category) **[[v9.5.0](changelog.md#950-march-3rd-2025)]**
|
||||
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title
|
||||
- [ ] Fine-tuned exclusion from categories :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
|
||||
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the inspector under a title
|
||||
- [x] Fine-tuned exclusion from categories :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
|
||||
- [x] Hidden Property **[[v9.5.7](changelog.md#957-may-5th-2026)]**
|
||||
- [x] Built-in "Archived" tag has this property by default **[[v9.5.7](changelog.md#957-may-5th-2026)]**
|
||||
- [x] Checkbox near search bar to show hidden tags in search **[[v9.5.7](changelog.md#957-may-5th-2026)]**
|
||||
|
||||
+4
-3
@@ -50,7 +50,7 @@ TagStudio provides an [EditorConfig](https://editorconfig.org/#example-file) fil
|
||||
### :material-tag: TagStudio Systems
|
||||
|
||||
- Translation keys can be accessed via bracket notation (e.g. `Translations["translation_key"]`) or with the `Translations.format()` method when a value needs to be passed to a placeholder in the translation.
|
||||
- Avoid passing around the `QtDriver` class where possible. Instead, pass only the necessary components such as the `Library` and `GlobalSettings` instances.
|
||||
- Avoid passing around the `QtDriver` class where possible. Instead, pass only the necessary components such as the `Library` and `AppSettings` instances.
|
||||
- Use HTML-like tags inside strings over explicit stylesheets where possible. The `Style` class provides several handy methods for formatting text with these.
|
||||
- Use the `format` method in the stylesheets class to format text headers.
|
||||
|
||||
@@ -138,7 +138,7 @@ qt/
|
||||
│ # Frontend classes that aren't related to widgets, like managers
|
||||
├── resource_manager.py
|
||||
├── cache_manager.py
|
||||
├── ts_qt.py # Qt Driver
|
||||
├── qt_driver.py
|
||||
└── ...
|
||||
```
|
||||
|
||||
@@ -176,7 +176,8 @@ Observe the following key aspects of the example below:
|
||||
def _connect_callbacks(self):
|
||||
self.layout().button.clicked.connect(self._button_click_callback)
|
||||
self.layout().color_dropdown.currentIndexChanged.connect(
|
||||
lambda idx: self._color_dropdown_callback(self.color_dropdown.itemData(idx)))
|
||||
lambda idx: self._color_dropdown_callback(self.color_dropdown.itemData(idx))
|
||||
)
|
||||
|
||||
def _button_click_callback(self):
|
||||
print("Button was clicked!")
|
||||
|
||||
+3
-1
@@ -100,12 +100,14 @@ Properties are special attributes of tags that change their behavior in some way
|
||||
|
||||
#### Is Category
|
||||
|
||||
The "Is Category" property of tags determines if a tag should be treated as a category itself when being organized inside the preview panel. If this tag or any tags inheriting from this tag (i.e. tags that have this tag as a "[Parent Tag](#parent-tags)"), then these tags will appear under a separated group that's named after this tag. Tags inheriting from multiple "category tags" will still show up under any applicable category.
|
||||
The "Is Category" property of tags determines if a tag should be treated as a category itself when being organized inside the inspector. If this tag or any tags inheriting from this tag (i.e. tags that have this tag as a "[Parent Tag](#parent-tags)"), then these tags will appear under a separated group that's named after this tag. Tags inheriting from multiple "category tags" will still show up under any applicable category.
|
||||
|
||||
This means that duplicates of tags can appear on entries if the tag inherits from multiple parent categories, however this is by design and reflects the nature of multiple inheritance. Any tags not inheriting from a category tag will simply show under a default "Tag" section.
|
||||
|
||||

|
||||
|
||||
If you don't want a tag to appear in one, more, or even all the applicable categories, simply uncheck the category in the "Edit Tag" panel.
|
||||
|
||||
### Built-In Tags and Categories
|
||||
|
||||
The built-in tags "Favorite" and "Archived" inherit from the built-in "Meta Tags" category which is marked as a category by default. This behavior of default tags can be fully customized by disabling the category option and/or by adding/removing the tags' Parent Tags.
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ TagStudio automatically scans for new or updated files when opening a library by
|
||||
|
||||
## :material-tag-text: Tagging
|
||||
|
||||
With one or more file entries selected, you can **search for** or **create a new tag** by clicking the "Add Tag" button at the bottom of the preview panel or by pressing <kbd>Ctrl</kbd>+<kbd>T</kbd>.
|
||||
With one or more file entries selected, you can **search for** or **create a new tag** by clicking the "Add Tag" button at the bottom of the inspector or by pressing <kbd>Ctrl</kbd>+<kbd>T</kbd>.
|
||||
|
||||
This will open the the **"Tag Bar"**, a combination search/creation bar that is designed to allow you to quickly create and apply tags that don't exist yet, find and apply existing tags from your library, and verify if certain tags already exist and/or are applied to your selection.
|
||||
|
||||
|
||||
@@ -29,35 +29,54 @@
|
||||
systems = import inputs.systems;
|
||||
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
{ pkgs, self', ... }:
|
||||
let
|
||||
python3 = pkgs.python313;
|
||||
python3 = builtins.head python3Versions;
|
||||
python3Versions = with pkgs; [
|
||||
python313
|
||||
python312
|
||||
];
|
||||
in
|
||||
{
|
||||
packages =
|
||||
let
|
||||
python3Packages = python3.pkgs;
|
||||
pythonDerivations = lib.genAttrs' python3Versions (
|
||||
python3:
|
||||
lib.nameValuePair python3.pythonAttr (
|
||||
let
|
||||
python3Packages = python3.pkgs;
|
||||
|
||||
pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix {
|
||||
inherit (pkgs) cmake;
|
||||
inherit pyexiv2;
|
||||
};
|
||||
pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
|
||||
tagstudio = pkgs.callPackage ./nix/package {
|
||||
inherit python3Packages;
|
||||
|
||||
inherit pillow-jxl-plugin;
|
||||
};
|
||||
|
||||
pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix {
|
||||
inherit (pkgs) cmake;
|
||||
inherit pyexiv2;
|
||||
};
|
||||
pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
|
||||
in
|
||||
{
|
||||
inherit tagstudio;
|
||||
tagstudio-jxl = tagstudio.override { withJXLSupport = true; };
|
||||
|
||||
inherit pillow-jxl-plugin pyexiv2;
|
||||
}
|
||||
)
|
||||
);
|
||||
in
|
||||
rec {
|
||||
default = tagstudio;
|
||||
tagstudio = pkgs.callPackage ./nix/package {
|
||||
inherit python3Packages;
|
||||
|
||||
inherit pillow-jxl-plugin;
|
||||
};
|
||||
tagstudio-jxl = tagstudio.override { withJXLSupport = true; };
|
||||
|
||||
inherit pillow-jxl-plugin pyexiv2;
|
||||
(lib.concatMapAttrs (
|
||||
pythonAttr: lib.mapAttrs' (name: lib.nameValuePair "${pythonAttr}Packages_${name}")
|
||||
) pythonDerivations)
|
||||
// pythonDerivations.${python3.pythonAttr}
|
||||
// {
|
||||
default = self'.packages.tagstudio;
|
||||
};
|
||||
|
||||
devShells = rec {
|
||||
default = tagstudio;
|
||||
devShells = {
|
||||
default = self'.devShells.tagstudio;
|
||||
tagstudio = import ./nix/shell.nix {
|
||||
inherit
|
||||
inputs
|
||||
|
||||
@@ -96,7 +96,6 @@ python3Packages.buildPythonApplication {
|
||||
dependencies =
|
||||
with python3Packages;
|
||||
[
|
||||
audioop-lts
|
||||
chardet_5
|
||||
ffmpeg-python
|
||||
humanfriendly
|
||||
@@ -121,6 +120,7 @@ python3Packages.buildPythonApplication {
|
||||
ujson
|
||||
wcmatch
|
||||
]
|
||||
++ lib.optional (pythonAtLeast "3.13") audioop-lts
|
||||
++ lib.optional withJXLSupport pillow-jxl-plugin;
|
||||
|
||||
# These tests require modifications to a library, which does not work
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ pkgs.mkShellNoCC {
|
||||
|
||||
if [ ! -f "''${venv}"/pyproject.toml ] || ! diff --brief pyproject.toml "''${venv}"/pyproject.toml >/dev/null; then
|
||||
printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2
|
||||
uv pip install --quiet --editable . --group docs --group extra --group test
|
||||
uv pip install --quiet --editable . --group docs --group extra --group reuse --group test
|
||||
cp pyproject.toml "''${venv}"/pyproject.toml
|
||||
fi
|
||||
|
||||
|
||||
+14
-9
@@ -9,12 +9,12 @@ build-backend = "hatchling.build"
|
||||
[project]
|
||||
name = "TagStudio"
|
||||
description = "A User-Focused Photo & File Management System."
|
||||
version = "9.6.3"
|
||||
version = "9.6.4"
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.14"
|
||||
requires-python = ">=3.14,<3.15"
|
||||
dependencies = [
|
||||
"audioop-lts; python_version >= '3.13'",
|
||||
"audioop-lts~=0.2.2",
|
||||
"chardet~=5.2",
|
||||
"ffmpeg-python~=0.2",
|
||||
"humanfriendly==10.*",
|
||||
@@ -22,12 +22,12 @@ dependencies = [
|
||||
"numpy~=2.2",
|
||||
"opencv_python~=4.11",
|
||||
"Pillow>=10.2,<12",
|
||||
"pillow-heif~=0.22",
|
||||
"pillow-heif~=1.5.0",
|
||||
"pillow-jxl-plugin~=1.3",
|
||||
"py7zr~=1.1.3",
|
||||
"pydantic~=2.10",
|
||||
"pydub~=0.25",
|
||||
"PySide6==6.8.0.*",
|
||||
"PySide6==6.11.2",
|
||||
"rarfile==4.2",
|
||||
"rawpy~=0.27",
|
||||
"Send2Trash>=1.8,<3",
|
||||
@@ -43,7 +43,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.gui-scripts]
|
||||
tagstudio = "tagstudio.main:main"
|
||||
tagstudio = "tagstudio.__main__:main"
|
||||
|
||||
[dependency-groups]
|
||||
all = [
|
||||
@@ -57,7 +57,11 @@ check = [{ include-group = "lint" }, { include-group = "test" }]
|
||||
build = [{ include-group = "pyinstaller" }]
|
||||
docs = [{ include-group = "mkdocs" }]
|
||||
extra = [{ include-group = "pre-commit" }]
|
||||
lint = [{ include-group = "pyright" }, { include-group = "ruff" }]
|
||||
lint = [
|
||||
{ include-group = "pyright" },
|
||||
{ include-group = "reuse" },
|
||||
{ include-group = "ruff" },
|
||||
]
|
||||
test = [{ include-group = "pytest" }]
|
||||
|
||||
mkdocs = [
|
||||
@@ -75,6 +79,7 @@ pytest = [
|
||||
"pytest-qt==4.4.0",
|
||||
"syrupy==5.1.0",
|
||||
]
|
||||
reuse = ["reuse==6.*"]
|
||||
ruff = ["ruff==0.15.17"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
@@ -92,7 +97,7 @@ filterwarnings = [
|
||||
ignore = [
|
||||
".venv/**",
|
||||
"src/tagstudio/core/library/json/",
|
||||
"src/tagstudio/renderers/vendored/pydub/",
|
||||
"src/tagstudio/previews/vendored/pydub/",
|
||||
]
|
||||
include = ["src/tagstudio", "tests"]
|
||||
# Reference for the settings here: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
|
||||
@@ -121,7 +126,7 @@ ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = ["D", "E402"]
|
||||
"src/tagstudio/renderers/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
|
||||
"src/tagstudio/previews/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
@@ -41,7 +41,7 @@ datafiles = [
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
[Path(project_root, "main.py")],
|
||||
[Path(project_root, "__main__.py")],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datafiles,
|
||||
|
||||
@@ -1,9 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from tagstudio.main import main
|
||||
"""TagStudio launcher."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.constants import BUILD_TYPE, VERSION
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--open",
|
||||
dest="open",
|
||||
type=str,
|
||||
help="Path to a TagStudio Library folder to open on start.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--settings-file",
|
||||
dest="settings_file",
|
||||
type=str,
|
||||
help="Path to a TagStudio .toml global settings file to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--cache-file",
|
||||
dest="cache_file",
|
||||
type=str,
|
||||
help="Path to a TagStudio .ini or .plist cache file to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help="Reveals additional internal data useful for debugging.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
help="Displays TagStudio version information.",
|
||||
version=f"TagStudio v{VERSION} {Translations[BUILD_TYPE] if BUILD_TYPE else ''}",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
driver = QtDriver(args)
|
||||
ui_name = "Qt"
|
||||
|
||||
# Run the chosen frontend driver.
|
||||
try:
|
||||
driver.start()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logger.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
|
||||
input()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
@@ -10,16 +10,16 @@ from PySide6.QtCore import QSettings
|
||||
from tagstudio.core.constants import TS_FOLDER_NAME
|
||||
from tagstudio.core.enums import AppCacheItems
|
||||
from tagstudio.core.library.alchemy.library import LibraryStatus
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.app_settings import AppSettings
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Turn into a BaseDriver class instead of a "Mixin".
|
||||
class DriverMixin:
|
||||
cached_values: QSettings
|
||||
# TODO: GlobalSettings has become closely tied to Qt.
|
||||
# Should there be a base Settings class?
|
||||
settings: GlobalSettings
|
||||
# TODO: AppSettings is Qt-specific and should not be in a base driver class.
|
||||
settings: AppSettings
|
||||
|
||||
def evaluate_path(self, open_path: str | None) -> LibraryStatus:
|
||||
"""Check if the path of library is valid."""
|
||||
|
||||
@@ -14,14 +14,14 @@ JSON_FILENAME: str = "ts_library.json"
|
||||
|
||||
DB_VERSION_CURRENT_KEY: str = "CURRENT"
|
||||
DB_VERSION_INITIAL_KEY: str = "INITIAL"
|
||||
DB_VERSION: int = 300
|
||||
DB_VERSION: int = 400
|
||||
|
||||
TAG_CHILDREN_QUERY = text("""
|
||||
WITH RECURSIVE ChildTags AS (
|
||||
SELECT :tag_id AS tag_id
|
||||
UNION
|
||||
SELECT tp.child_id AS tag_id
|
||||
FROM tag_parents tp
|
||||
FROM tag_parents tp
|
||||
INNER JOIN ChildTags c ON tp.parent_id = c.tag_id
|
||||
)
|
||||
SELECT * FROM ChildTags;
|
||||
|
||||
@@ -57,7 +57,7 @@ class TagColorEnum(enum.IntEnum):
|
||||
OLIVE = 37
|
||||
|
||||
@staticmethod
|
||||
def get_color_from_str(color_name: str) -> "TagColorEnum":
|
||||
def get_color_from_str(color_name: str) -> TagColorEnum:
|
||||
for color in TagColorEnum:
|
||||
if color.name == color_name.upper().replace(" ", "_"):
|
||||
return color
|
||||
@@ -99,17 +99,15 @@ class BrowsingState:
|
||||
return Parser(self.query).parse()
|
||||
|
||||
@classmethod
|
||||
def show_all(cls) -> "BrowsingState":
|
||||
def show_all(cls) -> BrowsingState:
|
||||
return BrowsingState()
|
||||
|
||||
@classmethod
|
||||
def from_search_query(cls, search_query: str) -> "BrowsingState":
|
||||
def from_search_query(cls, search_query: str) -> BrowsingState:
|
||||
return cls(query=search_query)
|
||||
|
||||
@classmethod
|
||||
def from_tag_id(
|
||||
cls, tag_id: int | str, state: "BrowsingState | None" = None
|
||||
) -> "BrowsingState":
|
||||
def from_tag_id(cls, tag_id: int | str, state: BrowsingState | None = None) -> BrowsingState:
|
||||
"""Create and return a BrowsingState object given a tag ID.
|
||||
|
||||
Args:
|
||||
@@ -124,35 +122,35 @@ class BrowsingState:
|
||||
return cls(query=f"tag_id:{str(tag_id)}")
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path | str) -> "BrowsingState":
|
||||
def from_path(cls, path: Path | str) -> BrowsingState:
|
||||
return cls(query=f'path:"{str(path).strip()}"')
|
||||
|
||||
@classmethod
|
||||
def from_mediatype(cls, mediatype: str) -> "BrowsingState":
|
||||
def from_mediatype(cls, mediatype: str) -> BrowsingState:
|
||||
return cls(query=f"mediatype:{mediatype}")
|
||||
|
||||
@classmethod
|
||||
def from_filetype(cls, filetype: str) -> "BrowsingState":
|
||||
def from_filetype(cls, filetype: str) -> BrowsingState:
|
||||
return cls(query=f"filetype:{filetype}")
|
||||
|
||||
@classmethod
|
||||
def from_tag_name(cls, tag_name: str) -> "BrowsingState":
|
||||
def from_tag_name(cls, tag_name: str) -> BrowsingState:
|
||||
return cls(query=f'tag:"{tag_name}"')
|
||||
|
||||
def with_page_index(self, index: int) -> "BrowsingState":
|
||||
def with_page_index(self, index: int) -> BrowsingState:
|
||||
return replace(self, page_index=index)
|
||||
|
||||
def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
|
||||
def with_sorting_mode(self, mode: SortingModeEnum) -> BrowsingState:
|
||||
seed = self.random_seed
|
||||
if mode == SortingModeEnum.RANDOM:
|
||||
seed = random.random()
|
||||
return replace(self, sorting_mode=mode, random_seed=seed)
|
||||
|
||||
def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
|
||||
def with_sorting_direction(self, ascending: bool) -> BrowsingState:
|
||||
return replace(self, ascending=ascending)
|
||||
|
||||
def with_search_query(self, search_query: str) -> "BrowsingState":
|
||||
def with_search_query(self, search_query: str) -> BrowsingState:
|
||||
return replace(self, query=search_query)
|
||||
|
||||
def with_show_hidden_entries(self, show_hidden_entries: bool) -> "BrowsingState":
|
||||
def with_show_hidden_entries(self, show_hidden_entries: bool) -> BrowsingState:
|
||||
return replace(self, show_hidden_entries=show_hidden_entries)
|
||||
|
||||
@@ -20,3 +20,10 @@ class TagEntry(Base):
|
||||
|
||||
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
entry_id: Mapped[int] = mapped_column(ForeignKey("entries.id"), primary_key=True)
|
||||
|
||||
|
||||
class CategoryExclusion(Base):
|
||||
__tablename__ = "category_exclusions"
|
||||
|
||||
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
category_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
|
||||
@@ -14,7 +14,6 @@ from os import makedirs
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import sqlalchemy
|
||||
import structlog
|
||||
from humanfriendly import format_timespan # pyright: ignore[reportUnknownVariableType]
|
||||
from sqlalchemy import (
|
||||
@@ -82,7 +81,7 @@ from tagstudio.core.library.alchemy.fields import (
|
||||
TextField,
|
||||
TextFieldTemplate,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.joins import TagEntry, TagParent
|
||||
from tagstudio.core.library.alchemy.joins import CategoryExclusion, TagEntry, TagParent
|
||||
from tagstudio.core.library.alchemy.migrations import DBMigrations, MigrationError
|
||||
from tagstudio.core.library.alchemy.models import (
|
||||
Entry,
|
||||
@@ -669,37 +668,32 @@ class Library:
|
||||
with Session(self.engine) as session:
|
||||
return unwrap(session.scalar(select(func.count(Entry.id))))
|
||||
|
||||
@staticmethod
|
||||
def _all_entries(session: Session, with_joins: bool = False) -> Iterator[Entry]:
|
||||
"""Load entries without joins."""
|
||||
stmt = select(Entry)
|
||||
if with_joins:
|
||||
# load Entry with all joins and all tags
|
||||
stmt = (
|
||||
stmt.outerjoin(Entry.text_fields)
|
||||
.outerjoin(Entry.datetime_fields)
|
||||
.outerjoin(Entry.tags)
|
||||
)
|
||||
stmt = stmt.options(
|
||||
contains_eager(Entry.text_fields),
|
||||
contains_eager(Entry.datetime_fields),
|
||||
contains_eager(Entry.tags),
|
||||
)
|
||||
|
||||
stmt = stmt.distinct()
|
||||
|
||||
entries = session.execute(stmt).scalars()
|
||||
if with_joins:
|
||||
entries = entries.unique()
|
||||
|
||||
for entry in entries:
|
||||
yield entry
|
||||
session.expunge(entry)
|
||||
|
||||
def all_entries(self, with_joins: bool = False) -> Iterator[Entry]:
|
||||
"""Load entries without joins."""
|
||||
with Session(self.engine) as session:
|
||||
yield from Library._all_entries(session, with_joins)
|
||||
stmt = select(Entry)
|
||||
if with_joins:
|
||||
# load Entry with all joins and all tags
|
||||
stmt = (
|
||||
stmt.outerjoin(Entry.text_fields)
|
||||
.outerjoin(Entry.datetime_fields)
|
||||
.outerjoin(Entry.tags)
|
||||
)
|
||||
stmt = stmt.options(
|
||||
contains_eager(Entry.text_fields),
|
||||
contains_eager(Entry.datetime_fields),
|
||||
contains_eager(Entry.tags),
|
||||
)
|
||||
|
||||
stmt = stmt.distinct()
|
||||
|
||||
entries = session.execute(stmt).scalars()
|
||||
if with_joins:
|
||||
entries = entries.unique()
|
||||
|
||||
for entry in entries:
|
||||
yield entry
|
||||
session.expunge(entry)
|
||||
|
||||
@property
|
||||
def tags(self) -> list[Tag]:
|
||||
@@ -1326,6 +1320,7 @@ class Library:
|
||||
tag: Tag,
|
||||
parent_ids: list[int] | set[int] | None = None,
|
||||
aliases: Iterable[TagAlias] | None = None,
|
||||
exclusion_ids: list[int] | set[int] | None = None,
|
||||
) -> Tag | None:
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
@@ -1342,6 +1337,9 @@ class Library:
|
||||
self.update_aliases(tag, aliases, session)
|
||||
session.flush()
|
||||
|
||||
if exclusion_ids is not None:
|
||||
self._update_category_exclusion(tag, exclusion_ids, session)
|
||||
|
||||
session.commit()
|
||||
session.expunge(tag)
|
||||
return tag
|
||||
@@ -1471,6 +1469,7 @@ class Library:
|
||||
selectinload(Tag.parent_tags),
|
||||
selectinload(Tag.aliases),
|
||||
joinedload(Tag.color),
|
||||
selectinload(Tag.category_exclusions),
|
||||
)
|
||||
tag = session.scalar(tags_query.where(Tag.id == tag_id))
|
||||
|
||||
@@ -1541,7 +1540,10 @@ class Library:
|
||||
|
||||
statement = select(Tag).where(Tag.id.in_(all_tag_ids))
|
||||
statement = statement.options(
|
||||
noload(Tag.parent_tags), selectinload(Tag.aliases), joinedload(Tag.color)
|
||||
noload(Tag.parent_tags),
|
||||
selectinload(Tag.aliases),
|
||||
selectinload(Tag.category_exclusions),
|
||||
joinedload(Tag.color),
|
||||
)
|
||||
tags = session.scalars(statement).fetchall()
|
||||
for tag in tags:
|
||||
@@ -1620,9 +1622,10 @@ class Library:
|
||||
tag: Tag,
|
||||
parent_ids: list[int] | set[int] | None = None,
|
||||
aliases: Iterable[TagAlias] | None = None,
|
||||
exclusion_ids: list[int] | set[int] | None = None,
|
||||
) -> None:
|
||||
"""Edit a Tag in the Library."""
|
||||
self.add_tag(tag, parent_ids, aliases)
|
||||
self.add_tag(tag, parent_ids, aliases, exclusion_ids)
|
||||
|
||||
def update_color(self, old_color_group: TagColorGroup, new_color_group: TagColorGroup) -> None:
|
||||
"""Update a TagColorGroup in the Library. If it doesn't already exist, create it."""
|
||||
@@ -1745,36 +1748,35 @@ class Library:
|
||||
)
|
||||
session.add(parent_tag)
|
||||
|
||||
def _update_category_exclusion(
|
||||
self, tag: Tag, exclusion_ids: list[int] | set[int], session: Session
|
||||
):
|
||||
prev_exclusions = session.scalars(
|
||||
select(CategoryExclusion).where(CategoryExclusion.tag_id == tag.id)
|
||||
).all()
|
||||
|
||||
for exclusion in prev_exclusions:
|
||||
if exclusion.category_id not in exclusion_ids:
|
||||
session.delete(exclusion)
|
||||
else:
|
||||
exclusion_ids.remove(exclusion.category_id)
|
||||
|
||||
for exclusion_id in exclusion_ids:
|
||||
exclusion = CategoryExclusion(tag_id=tag.id, category_id=exclusion_id)
|
||||
session.add(exclusion)
|
||||
|
||||
def get_version(self, key: str) -> int:
|
||||
"""Get a version value from the DB.
|
||||
|
||||
Args:
|
||||
key(str): The key for the name of the version type to set.
|
||||
"""
|
||||
return Library._get_version(self.engine, key)
|
||||
|
||||
@staticmethod
|
||||
def _get_version(engine, key: str) -> int:
|
||||
with Session(engine) as session:
|
||||
engine = sqlalchemy.inspect(engine)
|
||||
try:
|
||||
# "Version" table added in DB_VERSION 101
|
||||
if engine and engine.has_table("versions"):
|
||||
version = session.scalar(select(Version).where(Version.key == key))
|
||||
assert version
|
||||
return version.value
|
||||
# NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4
|
||||
# and is set to be removed in a future release.
|
||||
else:
|
||||
return int(
|
||||
unwrap(
|
||||
session.scalar(
|
||||
text("SELECT value FROM preferences WHERE key == 'DB_VERSION'")
|
||||
)
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
with Session(self.engine) as session:
|
||||
version = session.scalar(select(Version).where(Version.key == key))
|
||||
if version is None:
|
||||
logger.info(f"[Library] Couldn't get version of type '{key}'")
|
||||
return 0
|
||||
return version.value
|
||||
|
||||
def mirror_entry_fields(self, entries: list[Entry]) -> None:
|
||||
"""Mirror fields among multiple Entry items."""
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import sqlalchemy
|
||||
import structlog
|
||||
import ujson
|
||||
from sqlalchemy import Engine, and_, delete, select, text, update
|
||||
@@ -21,10 +22,10 @@ from tagstudio.core.library.alchemy.constants import (
|
||||
)
|
||||
from tagstudio.core.library.alchemy.fields import LEGACY_FIELD_MAP, DatetimeField, TextField
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup, Version
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag, TagColorGroup, Version
|
||||
from tagstudio.core.library.ignore import migrate_ext_list
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -46,15 +47,12 @@ class DBMigration:
|
||||
|
||||
class DBMigrations:
|
||||
def __init__(self, library_dir: Path, engine: Engine) -> None:
|
||||
# TODO: Remove local import and don't make calls to private methods.
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
self.library_dir = library_dir
|
||||
self.engine = engine
|
||||
|
||||
# Don't check DB version when creating new library
|
||||
self.loaded_db_version = Library._get_version(engine, DB_VERSION_CURRENT_KEY)
|
||||
self.initial_db_version = Library._get_version(engine, DB_VERSION_INITIAL_KEY)
|
||||
self.loaded_db_version = self._get_version(DB_VERSION_CURRENT_KEY)
|
||||
self.initial_db_version = self._get_version(DB_VERSION_INITIAL_KEY)
|
||||
|
||||
# ======================== Library Database Version Checking =======================
|
||||
# DB_VERSION 6 is the first supported SQLite DB version.
|
||||
@@ -84,6 +82,8 @@ class DBMigrations:
|
||||
return self.loaded_db_version < DB_VERSION
|
||||
|
||||
def run(self):
|
||||
if not self.required:
|
||||
return
|
||||
|
||||
# migrate DB step by step from one version to the next
|
||||
# (migration_method, db_version, initial_db_version)
|
||||
@@ -100,11 +100,9 @@ class DBMigrations:
|
||||
MigrationTo201, # changes: field tables
|
||||
MigrationTo202, # changes: tag_parents
|
||||
MigrationTo300, # changes: deletes folders
|
||||
MigrationTo400, # changes: add category_exclusions
|
||||
]
|
||||
with Session(self.engine) as session:
|
||||
if self.loaded_db_version > DB_VERSION:
|
||||
return
|
||||
|
||||
for migration in migrations:
|
||||
if self.loaded_db_version < migration.version and (
|
||||
migration.initial_version is None
|
||||
@@ -137,6 +135,27 @@ class DBMigrations:
|
||||
"Ran all migrations, but the DB is still not on the newest version"
|
||||
)
|
||||
|
||||
def _get_version(self, key: str) -> int:
|
||||
with Session(self.engine) as session:
|
||||
inspector = sqlalchemy.inspect(self.engine)
|
||||
try:
|
||||
# "Version" table added in DB_VERSION 101
|
||||
if inspector and inspector.has_table("versions"):
|
||||
version = session.scalar(select(Version).where(Version.key == key))
|
||||
assert version
|
||||
return version.value
|
||||
# "Preferences" table deprecated in TagStudio 9.5.4
|
||||
else:
|
||||
return int(
|
||||
unwrap(
|
||||
session.scalar(
|
||||
text("SELECT value FROM preferences WHERE key == 'DB_VERSION'")
|
||||
)
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def _set_version(self, session: Session, key: str, value: int) -> None:
|
||||
"""Set a version value to the DB.
|
||||
|
||||
@@ -237,11 +256,10 @@ class MigrationTo9(DBMigration):
|
||||
session.flush()
|
||||
logger.info(fmt_log("Added filename column to entries table"))
|
||||
|
||||
# TODO: Remove local import and don't make calls to private methods.
|
||||
# Populate the new filename column.
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
for entry in Library._all_entries(session):
|
||||
# TODO: this could still break in the future through changes to the definition of Entry
|
||||
entries = session.execute(select(Entry).distinct()).scalars()
|
||||
for entry in entries:
|
||||
entry.filename = entry.path.name
|
||||
session.merge(entry)
|
||||
session.flush()
|
||||
@@ -578,3 +596,23 @@ class MigrationTo300(DBMigration):
|
||||
## drop table "folders"
|
||||
session.execute(text("DROP TABLE folders"))
|
||||
session.flush()
|
||||
|
||||
|
||||
class MigrationTo400(DBMigration):
|
||||
version = 400
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
logger.info(fmt_log("Creating category_exclusions table..."))
|
||||
session.execute(
|
||||
text("""
|
||||
CREATE TABLE category_exclusions (
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
||||
category_id INTEGER NOT NULL REFERENCES tags(id),
|
||||
|
||||
PRIMARY KEY (tag_id, category_id)
|
||||
)
|
||||
""")
|
||||
)
|
||||
session.flush()
|
||||
|
||||
@@ -16,7 +16,7 @@ from tagstudio.core.library.alchemy.fields import (
|
||||
DatetimeField,
|
||||
TextField,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
from tagstudio.core.library.alchemy.joins import CategoryExclusion, TagParent
|
||||
|
||||
|
||||
class Namespace(Base):
|
||||
@@ -41,7 +41,7 @@ class TagAlias(Base):
|
||||
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")
|
||||
tag: Mapped[Tag] = relationship(back_populates="aliases")
|
||||
|
||||
def __init__(self, name: str, tag_id: int | None = None):
|
||||
self.name = name
|
||||
@@ -97,13 +97,19 @@ class Tag(Base):
|
||||
is_hidden: Mapped[bool]
|
||||
icon: Mapped[str | None]
|
||||
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
|
||||
parent_tags: Mapped[set["Tag"]] = relationship(
|
||||
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]
|
||||
category_exclusions: Mapped[set[Tag]] = relationship(
|
||||
secondary=CategoryExclusion.__tablename__,
|
||||
primaryjoin="Tag.id == CategoryExclusion.tag_id",
|
||||
secondaryjoin="Tag.id == CategoryExclusion.category_id",
|
||||
back_populates="category_exclusions",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
@@ -124,19 +130,24 @@ class Tag(Base):
|
||||
def alias_ids(self) -> list[int]:
|
||||
return [tag.id for tag in self.aliases]
|
||||
|
||||
@property
|
||||
def exclusion_ids(self) -> list[int]:
|
||||
return [tag.id for tag in self.category_exclusions]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
id: int | None = None,
|
||||
shorthand: str | None = None,
|
||||
aliases: set[TagAlias] | None = None,
|
||||
parent_tags: set["Tag"] | 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,
|
||||
is_hidden: bool = False,
|
||||
category_exclusions: set[Tag] | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.aliases = aliases or set()
|
||||
@@ -149,6 +160,7 @@ class Tag(Base):
|
||||
self.is_category = is_category
|
||||
self.is_hidden = is_hidden
|
||||
self.id = id # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.category_exclusions = category_exclusions or set()
|
||||
super().__init__()
|
||||
|
||||
@override
|
||||
@@ -169,16 +181,16 @@ class Tag(Base):
|
||||
return False
|
||||
return self.id == value.id
|
||||
|
||||
def __lt__(self, other: "Tag") -> bool:
|
||||
def __lt__(self, other: Tag) -> bool:
|
||||
return self.name < other.name
|
||||
|
||||
def __le__(self, other: "Tag") -> bool:
|
||||
def __le__(self, other: Tag) -> bool:
|
||||
return self.name <= other.name
|
||||
|
||||
def __gt__(self, other: "Tag") -> bool:
|
||||
def __gt__(self, other: Tag) -> bool:
|
||||
return self.name > other.name
|
||||
|
||||
def __ge__(self, other: "Tag") -> bool:
|
||||
def __ge__(self, other: Tag) -> bool:
|
||||
return self.name >= other.name
|
||||
|
||||
|
||||
|
||||
@@ -861,7 +861,7 @@ class Library:
|
||||
self.files_not_in_library,
|
||||
key=lambda t: -(self.library_dir / t).stat().st_ctime,
|
||||
)
|
||||
except (FileExistsError, FileNotFoundError):
|
||||
except FileExistsError, FileNotFoundError:
|
||||
print(
|
||||
"[LIBRARY] [ERROR] Couldn't sort files, some were moved during the scanning/sorting process."
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ class ConstraintType(Enum):
|
||||
Special = 5
|
||||
|
||||
@staticmethod
|
||||
def from_string(text: str) -> "ConstraintType | None":
|
||||
def from_string(text: str) -> ConstraintType | None:
|
||||
return {
|
||||
"tag": ConstraintType.Tag,
|
||||
"tag_id": ConstraintType.TagID,
|
||||
@@ -28,7 +28,7 @@ class ConstraintType(Enum):
|
||||
|
||||
|
||||
class AST:
|
||||
parent: "AST | None" = None
|
||||
parent: AST | None = None
|
||||
|
||||
@override
|
||||
def __str__(self):
|
||||
@@ -65,9 +65,9 @@ class ORList(AST):
|
||||
class Constraint(AST):
|
||||
type: ConstraintType
|
||||
value: str
|
||||
properties: list["Property"]
|
||||
properties: list[Property]
|
||||
|
||||
def __init__(self, type: ConstraintType, value: str, properties: list["Property"]) -> None:
|
||||
def __init__(self, type: ConstraintType, value: str, properties: list[Property]) -> None:
|
||||
super().__init__()
|
||||
for prop in properties:
|
||||
prop.parent = self
|
||||
|
||||
@@ -39,11 +39,11 @@ class Token:
|
||||
self.end = end
|
||||
|
||||
@staticmethod
|
||||
def from_type(type: TokenType, pos: int) -> "Token":
|
||||
def from_type(type: TokenType, pos: int) -> Token:
|
||||
return Token(type, None, pos, pos)
|
||||
|
||||
@staticmethod
|
||||
def EOF(pos: int) -> "Token": # noqa: N802
|
||||
def EOF(pos: int) -> Token: # noqa: N802
|
||||
return Token.from_type(TokenType.EOF, pos)
|
||||
|
||||
@override
|
||||
|
||||
@@ -58,5 +58,5 @@ def format_duration(duration: int | float) -> str:
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
return f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes}:{seconds:02}"
|
||||
except (OverflowError, ValueError):
|
||||
except OverflowError, ValueError:
|
||||
return "-:--"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
"""A collection of platform-dependant strings."""
|
||||
|
||||
import platform
|
||||
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
|
||||
|
||||
def open_file_str() -> str:
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -10,7 +10,7 @@ from typing import Any
|
||||
import structlog
|
||||
import ujson
|
||||
|
||||
from tagstudio.qt.mnemonics import remove_mnemonic_marker
|
||||
from tagstudio.qt.helpers.mnemonics import remove_mnemonic_marker
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -86,7 +86,7 @@ class Translator:
|
||||
def __format(self, text: str, **kwargs: ...) -> str:
|
||||
try:
|
||||
return text.format(**kwargs)
|
||||
except (KeyError, ValueError):
|
||||
except KeyError, ValueError:
|
||||
logger.error(
|
||||
"[Translations] Error while formatting translation.",
|
||||
text=text,
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
"""TagStudio launcher."""
|
||||
|
||||
import argparse
|
||||
import traceback
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.constants import BUILD_TYPE, VERSION
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
# appid = "cyanvoxel.tagstudio.9"
|
||||
# ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
|
||||
|
||||
# Parse arguments.
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--open",
|
||||
dest="open",
|
||||
type=str,
|
||||
help="Path to a TagStudio Library folder to open on start.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--settings-file",
|
||||
dest="settings_file",
|
||||
type=str,
|
||||
help="Path to a TagStudio .toml global settings file to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--cache-file",
|
||||
dest="cache_file",
|
||||
type=str,
|
||||
help="Path to a TagStudio .ini or .plist cache file to use.",
|
||||
)
|
||||
|
||||
# parser.add_argument('--browse', dest='browse', action='store_true',
|
||||
# help='Jumps to entry browsing on startup.')
|
||||
# parser.add_argument('--external_preview', dest='external_preview', action='store_true',
|
||||
# help='Outputs current preview thumbnail to a live-updating file.')
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help="Reveals additional internal data useful for debugging.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
help="Displays TagStudio version information.",
|
||||
version=f"TagStudio v{VERSION} {Translations[BUILD_TYPE] if BUILD_TYPE else ''}",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
driver = QtDriver(args)
|
||||
ui_name = "Qt"
|
||||
|
||||
# Run the chosen frontend driver.
|
||||
try:
|
||||
driver.start()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logger.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
|
||||
input()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -17,37 +17,41 @@ from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.media_types import MediaCategories, MediaType
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.cache_manager import CacheManager
|
||||
from tagstudio.qt.global_settings import (
|
||||
DEFAULT_CACHED_THUMB_RES,
|
||||
MAX_CACHED_THUMB_RES,
|
||||
MIN_CACHED_THUMB_RES,
|
||||
GlobalSettings,
|
||||
Theme,
|
||||
)
|
||||
from tagstudio.qt.helpers.gradients import four_corner_gradient
|
||||
from tagstudio.qt.models.palette import UI_COLORS, ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.renderers.archive import (
|
||||
from tagstudio.previews.gradients import four_corner_gradient
|
||||
from tagstudio.previews.renderers.archive import (
|
||||
apple_embedded_thumb,
|
||||
archive_thumb,
|
||||
krita_thumb,
|
||||
open_doc_thumb,
|
||||
powerpoint_thumb,
|
||||
)
|
||||
from tagstudio.renderers.audio import audio_album_thumb, audio_waveform_thumb
|
||||
from tagstudio.renderers.blender import blender_thumb
|
||||
from tagstudio.renderers.clip_studio import clip_studio_thumb
|
||||
from tagstudio.renderers.ebook import epub_thumb
|
||||
from tagstudio.renderers.font import font_full_preview, font_small_thumb
|
||||
from tagstudio.renderers.medibang_paint import medibang_paint_thumb
|
||||
from tagstudio.renderers.paint_dot_net import paint_dot_net_thumb
|
||||
from tagstudio.renderers.pdf import pdf_thumb
|
||||
from tagstudio.renderers.raster_image import exr_image_thumb, raster_image_thumb, raw_image_thumb
|
||||
from tagstudio.renderers.source_engine import vtf_thumb
|
||||
from tagstudio.renderers.text import text_thumb
|
||||
from tagstudio.renderers.vector_image import vector_image_thumb
|
||||
from tagstudio.renderers.video import video_thumb
|
||||
from tagstudio.previews.renderers.audio import audio_album_thumb, audio_waveform_thumb
|
||||
from tagstudio.previews.renderers.blender import blender_thumb
|
||||
from tagstudio.previews.renderers.clip_studio import clip_studio_thumb
|
||||
from tagstudio.previews.renderers.ebook import epub_thumb
|
||||
from tagstudio.previews.renderers.font import font_full_preview, font_small_thumb
|
||||
from tagstudio.previews.renderers.medibang_paint import medibang_paint_thumb
|
||||
from tagstudio.previews.renderers.paint_dot_net import paint_dot_net_thumb
|
||||
from tagstudio.previews.renderers.pdf import pdf_thumb
|
||||
from tagstudio.previews.renderers.raster_image import (
|
||||
exr_image_thumb,
|
||||
raster_image_thumb,
|
||||
raw_image_thumb,
|
||||
)
|
||||
from tagstudio.previews.renderers.source_engine import vtf_thumb
|
||||
from tagstudio.previews.renderers.text import text_thumb
|
||||
from tagstudio.previews.renderers.vector_image import vector_image_thumb
|
||||
from tagstudio.previews.renderers.video import video_thumb
|
||||
from tagstudio.qt.app_settings import (
|
||||
DEFAULT_CACHED_THUMB_RES,
|
||||
MAX_CACHED_THUMB_RES,
|
||||
MIN_CACHED_THUMB_RES,
|
||||
AppSettings,
|
||||
Theme,
|
||||
)
|
||||
from tagstudio.qt.cache_manager import CacheManager
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.styles.palette import UI_COLORS, ColorType, UiColor, get_ui_color
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
@@ -62,7 +66,7 @@ class FileRenderer:
|
||||
rm: ResourceManager = ResourceManager()
|
||||
cached_img_ext: str = ".webp"
|
||||
|
||||
def __init__(self, library: Library, settings: GlobalSettings) -> None:
|
||||
def __init__(self, library: Library, settings: AppSettings) -> None:
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.settings = settings
|
||||
@@ -16,7 +16,7 @@ from PIL import Image
|
||||
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.renderers.raster_image import image_from_bytes
|
||||
from tagstudio.previews.renderers.raster_image import image_from_bytes
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -52,7 +52,7 @@ class TarFile:
|
||||
def read(self, name: str) -> bytes:
|
||||
return unwrap(self.tar.extractfile(name)).read()
|
||||
|
||||
def __enter__(self) -> "TarFile":
|
||||
def __enter__(self) -> TarFile:
|
||||
self.tar = tarfile.open(name=self.filepath, mode=self.mode).__enter__()
|
||||
return self
|
||||
|
||||
@@ -13,7 +13,7 @@ from mutagen import flac, id3, mp4
|
||||
from mutagen._util import MutagenError
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from tagstudio.renderers.vendored.pydub.audio_segment import (
|
||||
from tagstudio.previews.vendored.pydub.audio_segment import (
|
||||
_AudioSegment as AudioSegment, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ def audio_album_thumb(filepath: Path, ext: str) -> Image.Image | None:
|
||||
image = artwork
|
||||
except (
|
||||
FileNotFoundError,
|
||||
id3.ID3NoHeaderError, # pyright: ignore[reportPrivateImportUsage]
|
||||
id3.ID3NoHeaderError,
|
||||
mp4.MP4MetadataError,
|
||||
mp4.MP4StreamInfoError,
|
||||
MutagenError,
|
||||
@@ -9,7 +9,7 @@ from PIL import Image
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
|
||||
from tagstudio.renderers.vendored.blender_thumbnailer import blend_thumb
|
||||
from tagstudio.previews.vendored.blender_thumbnailer import blend_thumb
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -12,8 +12,8 @@ from PIL import Image
|
||||
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.renderers.archive import Archive, first_image_in_archive, open_archive
|
||||
from tagstudio.renderers.raster_image import image_from_bytes
|
||||
from tagstudio.previews.renderers.archive import Archive, first_image_in_archive, open_archive
|
||||
from tagstudio.previews.renderers.raster_image import image_from_bytes
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -11,8 +11,8 @@ import structlog
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from tagstudio.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT
|
||||
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.helpers.text_wrapper import wrap_full_text
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -14,7 +14,7 @@ from PySide6.QtCore import QBuffer, QFile, QFileDevice, QIODeviceBase, QSizeF
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions
|
||||
|
||||
from tagstudio.qt.helpers.image_effects import replace_transparent_pixels
|
||||
from tagstudio.qt.views.styles.image_effects import replace_transparent_pixels
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -11,7 +11,7 @@ from cv2.typing import MatLike
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
|
||||
from tagstudio.qt.helpers.file_tester import is_readable_video
|
||||
from tagstudio.previews.video_tester import is_readable_video
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ from pydub.utils import (
|
||||
)
|
||||
|
||||
from tagstudio.core.utils.silent_subprocess import silent_popen
|
||||
from tagstudio.renderers.vendored.pydub.utils import _mediainfo_json
|
||||
from tagstudio.previews.vendored.pydub.utils import _mediainfo_json
|
||||
|
||||
basestring = str
|
||||
xrange = range
|
||||
+1
-5
@@ -8,11 +8,7 @@ import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from pydub.utils import (
|
||||
_fd_or_path_or_tempfile,
|
||||
fsdecode,
|
||||
get_extra_info,
|
||||
)
|
||||
from pydub.utils import _fd_or_path_or_tempfile, fsdecode, get_extra_info
|
||||
|
||||
from tagstudio.core.utils.ffmpeg_status import FfprobeStatus
|
||||
from tagstudio.core.utils.silent_subprocess import silent_popen
|
||||
@@ -1,12 +1,12 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import ffmpeg
|
||||
|
||||
from tagstudio.renderers.vendored.probe import probe
|
||||
from tagstudio.previews.vendored.probe import probe
|
||||
|
||||
|
||||
def is_readable_video(filepath: Path | str):
|
||||
@@ -59,7 +59,7 @@ class TomlEnumEncoder(toml.TomlEncoder):
|
||||
# NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings
|
||||
# properties to be overwritten with environment variables. As TagStudio is not currently using
|
||||
# environment variables, this was not based on that, but that may be useful in the future.
|
||||
class GlobalSettings(BaseModel):
|
||||
class AppSettings(BaseModel):
|
||||
language: str = Field(default="en")
|
||||
open_last_loaded_on_startup: bool = Field(default=True)
|
||||
generate_thumbs: bool = Field(default=True)
|
||||
@@ -88,17 +88,17 @@ class GlobalSettings(BaseModel):
|
||||
loaded_from: Path = Field(default=DEFAULT_GLOBAL_SETTINGS_PATH, exclude=True)
|
||||
|
||||
@staticmethod
|
||||
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings":
|
||||
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> AppSettings:
|
||||
if path.exists():
|
||||
with open(path) as file:
|
||||
filecontents = file.read()
|
||||
if len(filecontents.strip()) != 0:
|
||||
logger.info("[Settings] Reading Global Settings File", path=path)
|
||||
settings_data = toml.loads(filecontents)
|
||||
settings = GlobalSettings(**settings_data, loaded_from=path)
|
||||
settings = AppSettings(**settings_data, loaded_from=path)
|
||||
return settings
|
||||
|
||||
return GlobalSettings(loaded_from=path)
|
||||
return AppSettings(loaded_from=path)
|
||||
|
||||
def save(self, path: Path | None = None) -> None:
|
||||
if path is None:
|
||||
@@ -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_THUMB_QUALITY, DEFAULT_THUMB_CACHE_SIZE
|
||||
from tagstudio.qt.app_settings import DEFAULT_CACHED_THUMB_QUALITY, DEFAULT_THUMB_CACHE_SIZE
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@@ -6,13 +6,9 @@ from typing import override
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import QPoint, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QLineEdit, QMenu, QWidget
|
||||
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
autofill_scroll_top_focus_style,
|
||||
autofill_scroll_top_style,
|
||||
)
|
||||
@@ -81,4 +77,4 @@ class AutofillLineEdit(QLineEdit):
|
||||
# Filter out icon action(s)
|
||||
if action.text():
|
||||
menu.addAction(action)
|
||||
menu.exec(self.mapToGlobal(pos)) # pyright: ignore[reportArgumentType]
|
||||
menu.exec(self.mapToGlobal(pos))
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import override
|
||||
from typing import Any, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
@@ -18,7 +18,7 @@ class ClickableSlider(QSlider):
|
||||
based on click events.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any): # pyright: ignore[reportExplicitAny]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@override
|
||||
@@ -9,13 +9,14 @@ from tagstudio.core.library.alchemy.fields import (
|
||||
DatetimeFieldTemplate,
|
||||
TextFieldTemplate,
|
||||
)
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.views.edit_field_template_modal_view import EditFieldTemplateModalView
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import line_edit_style
|
||||
from tagstudio.qt.views.styles.stylesheets import line_edit_style
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class EditFieldTemplateModal(EditFieldTemplateModalView):
|
||||
field_type_map: dict[str, str] = {
|
||||
"TextFieldTemplate": Translations["field_type.text"],
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ from tagstudio.qt.views.edit_text_view import EditTextView
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class EditText(EditTextView):
|
||||
def __init__(self, name: str, text: str | None, is_multiline: bool = False):
|
||||
super().__init__()
|
||||
@@ -11,20 +11,21 @@ from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.app_settings import AppSettings
|
||||
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
from tagstudio.qt.controllers.field_template_widget import FieldTemplateWidget
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.suggest_box import SuggestBox
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
def __init__(self, library: Library, settings: GlobalSettings, placeholder_text: str = ""):
|
||||
def __init__(self, library: Library, settings: AppSettings, placeholder_text: str = ""):
|
||||
super().__init__(library, settings, placeholder_text)
|
||||
|
||||
# Context Menu Actions
|
||||
@@ -155,6 +156,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
widget.setHidden(True)
|
||||
self.layout().content_layout.addWidget(widget)
|
||||
|
||||
widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
|
||||
item = unwrap(self.layout().content_layout.itemAt(index))
|
||||
widget_: QWidget = unwrap(item.widget())
|
||||
assert isinstance(widget_, UnderlinedWidget)
|
||||
return widget_
|
||||
|
||||
+6
-4
@@ -11,12 +11,13 @@ from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
from tagstudio.qt.controllers.field_template_widget import FieldTemplateWidget
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.controllers.search_panel import SearchPanel
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -173,6 +174,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
pad_field_template_widget.setHidden(True)
|
||||
self.layout().scroll_layout.addWidget(pad_field_template_widget)
|
||||
|
||||
field_template_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
|
||||
item = unwrap(self.layout().scroll_layout.itemAt(index))
|
||||
field_template_widget: QWidget = unwrap(item.widget())
|
||||
assert isinstance(field_template_widget, FieldTemplateWidget)
|
||||
return field_template_widget
|
||||
+2
-1
@@ -7,10 +7,11 @@ from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QAction, QEnterEvent
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.field_template_widget_view import FieldTemplateWidgetView
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class FieldTemplateWidget(FieldTemplateWidgetView):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -0,0 +1,55 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import QLabel, QWidget
|
||||
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.utils.file_opener import FileOpenerHelper
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FileOpenerLabel(QLabel):
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
"""Initialize the FileOpenerLabel.
|
||||
|
||||
Args:
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
self.filepath: Path | None = None
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
def set_file_path(self, filepath: Path) -> None:
|
||||
"""Set the filepath to open.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path to the file to open.
|
||||
"""
|
||||
self.filepath = filepath
|
||||
|
||||
@override
|
||||
def mousePressEvent(self, ev: QMouseEvent) -> None:
|
||||
"""Handle mouse press events.
|
||||
|
||||
On a left click, open the file in the default file explorer.
|
||||
On a right click, show a context menu.
|
||||
|
||||
Args:
|
||||
ev (QMouseEvent): The mouse press event.
|
||||
"""
|
||||
if ev.button() == Qt.MouseButton.LeftButton:
|
||||
opener = FileOpenerHelper(unwrap(self.filepath))
|
||||
opener.open_explorer()
|
||||
elif ev.button() == Qt.MouseButton.RightButton:
|
||||
# Show context menu
|
||||
pass
|
||||
else:
|
||||
super().mousePressEvent(ev)
|
||||
+6
-5
@@ -9,21 +9,22 @@ 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.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.mixed.remove_ignored_modal import RemoveIgnoredModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.fix_ignored_modal_view import FixIgnoredEntriesModalView
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class FixIgnoredEntriesModal(FixIgnoredEntriesModalView):
|
||||
def __init__(self, library: "Library", driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__(library, driver)
|
||||
self.tracker = IgnoredRegistry(self.lib)
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ from tagstudio.qt.views.ignore_modal_view import IgnoreModalView
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class IgnoreModal(IgnoreModalView):
|
||||
on_edit = Signal(Tag)
|
||||
|
||||
+9
-9
@@ -25,16 +25,16 @@ from tagstudio.core.library.alchemy.fields import (
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.edit_text_controller import EditText
|
||||
from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.controllers.edit_text import EditText
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.mixed.field_containers import FieldContainers
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
|
||||
from tagstudio.qt.views.inspector_view import InspectorView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -44,8 +44,8 @@ class _ItemMode(IntEnum):
|
||||
FIELD = 2
|
||||
|
||||
|
||||
class PreviewPanel(QWidget):
|
||||
def __init__(self, driver: "QtDriver") -> None:
|
||||
class Inspector(QWidget):
|
||||
def __init__(self, driver: QtDriver) -> None:
|
||||
super().__init__()
|
||||
self._driver = driver
|
||||
self._lib = self._driver.lib
|
||||
@@ -68,7 +68,7 @@ class PreviewPanel(QWidget):
|
||||
self,
|
||||
)
|
||||
|
||||
self.setLayout(PreviewPanelView(driver=driver, pixel_ratio=self.devicePixelRatio()))
|
||||
self.setLayout(InspectorView(driver=driver, pixel_ratio=self.devicePixelRatio()))
|
||||
self._set_item_mode(None)
|
||||
self._connect_callbacks()
|
||||
|
||||
@@ -276,7 +276,7 @@ class PreviewPanel(QWidget):
|
||||
logger.error("[Preview Panel] Error updating selection", error=e)
|
||||
|
||||
def stop_media_playback(self) -> None:
|
||||
"""Stop any media playback in the preview panel."""
|
||||
"""Stop any media playback in the inspector."""
|
||||
self.layout().preview_thumb.media_player.stop()
|
||||
|
||||
@property
|
||||
@@ -284,5 +284,5 @@ class PreviewPanel(QWidget):
|
||||
return self.layout().containers
|
||||
|
||||
@override
|
||||
def layout(self) -> PreviewPanelView:
|
||||
def layout(self) -> InspectorView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
+5
-4
@@ -19,20 +19,21 @@ 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.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.utils import file_opener
|
||||
from tagstudio.qt.views.library_info_window_view import LibraryInfoWindowView
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class LibraryInfoWindow(LibraryInfoWindowView):
|
||||
def __init__(self, library: "Library", driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__(library, driver)
|
||||
|
||||
# Statistics Buttons
|
||||
+16
-15
@@ -1,6 +1,7 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# TODO: Split view and logic, split classes into individual files
|
||||
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
@@ -37,20 +38,20 @@ 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.helpers.color_overlay import auto_theme_overlay
|
||||
from tagstudio.i18n.platform_strings import trash_term
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.inspector import Inspector
|
||||
from tagstudio.qt.helpers.mnemonics import assign_mnemonics
|
||||
from tagstudio.qt.mixed.landing import LandingWidget
|
||||
from tagstudio.qt.mixed.pagination import Pagination
|
||||
from tagstudio.qt.mnemonics import assign_mnemonics
|
||||
from tagstudio.qt.platform_strings import trash_term
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.thumb_grid_layout import ThumbGridLayout
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style
|
||||
from tagstudio.qt.views.layouts.thumb_grid_layout import ThumbGridLayout
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.views.styles.stylesheets import checkbox_style
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -457,7 +458,7 @@ class MainWindow(QMainWindow):
|
||||
(Translations["home.thumbnail_size.mini"], 76),
|
||||
]
|
||||
|
||||
def __init__(self, driver: "QtDriver", parent: QWidget | None = None) -> None:
|
||||
def __init__(self, driver: QtDriver, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.rm = ResourceManager()
|
||||
|
||||
@@ -491,7 +492,7 @@ class MainWindow(QMainWindow):
|
||||
self.pagination: Pagination
|
||||
|
||||
# initialized in setup_preview_panel
|
||||
self.preview_panel: PreviewPanel
|
||||
self.preview_panel: Inspector
|
||||
# endregion
|
||||
|
||||
if not self.objectName():
|
||||
@@ -528,7 +529,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# endregion
|
||||
|
||||
def setup_central_widget(self, driver: "QtDriver"):
|
||||
def setup_central_widget(self, driver: QtDriver):
|
||||
self.central_widget = QWidget(self)
|
||||
self.central_widget.setObjectName("central_widget")
|
||||
self.central_layout = QGridLayout(self.central_widget)
|
||||
@@ -650,7 +651,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.central_layout.addLayout(self.extra_input_layout, 5, 0, 1, 1)
|
||||
|
||||
def setup_content(self, driver: "QtDriver"):
|
||||
def setup_content(self, driver: QtDriver):
|
||||
self.content_layout = QHBoxLayout()
|
||||
self.content_layout.setObjectName("content_layout")
|
||||
|
||||
@@ -666,7 +667,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.central_layout.addLayout(self.content_layout, 10, 0, 1, 1)
|
||||
|
||||
def setup_entry_list(self, driver: "QtDriver"):
|
||||
def setup_entry_list(self, driver: QtDriver):
|
||||
self.entry_list_container = QWidget()
|
||||
self.entry_list_layout = QVBoxLayout(self.entry_list_container)
|
||||
self.entry_list_layout.setSpacing(0)
|
||||
@@ -699,8 +700,8 @@ class MainWindow(QMainWindow):
|
||||
self.entry_list_layout.addWidget(self.pagination)
|
||||
self.content_splitter.addWidget(self.entry_list_container)
|
||||
|
||||
def setup_preview_panel(self, driver: "QtDriver"):
|
||||
self.preview_panel = PreviewPanel(driver)
|
||||
def setup_preview_panel(self, driver: QtDriver):
|
||||
self.preview_panel = Inspector(driver)
|
||||
self.content_splitter.addWidget(self.preview_panel)
|
||||
|
||||
def setup_status_bar(self):
|
||||
+5
-6
@@ -8,18 +8,17 @@ 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.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.progress_bar import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
|
||||
class MergeDuplicateEntries(QObject):
|
||||
class MergeDuplicateEntriesProgress(QObject):
|
||||
done = Signal()
|
||||
|
||||
def __init__(self, library: "Library", driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.driver = driver
|
||||
+4
-3
@@ -19,20 +19,21 @@ from rawpy import (
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.qt.helpers.file_tester import is_readable_video
|
||||
from tagstudio.previews.video_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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class PreviewThumb(PreviewThumbView):
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__(library, driver)
|
||||
|
||||
self.__driver: QtDriver = driver
|
||||
+5
-4
@@ -11,6 +11,7 @@ from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.utils.function_iterator import FunctionIterator
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class ProgressWidget(QWidget):
|
||||
"""Prebuilt thread-safe progress bar widget."""
|
||||
|
||||
@@ -26,10 +27,10 @@ class ProgressWidget(QWidget):
|
||||
super().__init__()
|
||||
self.root = QVBoxLayout(self)
|
||||
self.pb = QProgressDialog(
|
||||
labelText=label_text,
|
||||
minimum=minimum,
|
||||
cancelButtonText=cancel_button_text, # pyright: ignore[reportArgumentType]
|
||||
maximum=maximum,
|
||||
label_text,
|
||||
cancel_button_text or "",
|
||||
minimum,
|
||||
maximum,
|
||||
)
|
||||
self.root.addWidget(self.pb)
|
||||
self.setFixedSize(432, 112)
|
||||
+3
-3
@@ -5,11 +5,11 @@
|
||||
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.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.progress_bar import ProgressWidget
|
||||
|
||||
|
||||
class RelinkUnlinkedEntries(QObject):
|
||||
class RelinkUnlinkedEntriesProgress(QObject):
|
||||
done = Signal()
|
||||
|
||||
def __init__(self, tracker: UnlinkedRegistry):
|
||||
+4
-4
@@ -11,15 +11,15 @@ from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
|
||||
def _item_id(item: object) -> int:
|
||||
@@ -78,7 +78,7 @@ class SearchPanel[T](ModalContent):
|
||||
self.setMinimumSize(300, 400)
|
||||
self.connect_callbacks(self)
|
||||
|
||||
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
|
||||
def connect_callbacks(self, controller: SearchPanel[Any]) -> None: # pyright: ignore[reportExplicitAny]
|
||||
self.layout().limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
|
||||
self.layout().search_field.textChanged.connect(controller.on_search_query_changed)
|
||||
self.layout().search_field.returnPressed.connect(
|
||||
@@ -166,7 +166,7 @@ class SearchPanel[T](ModalContent):
|
||||
if not query:
|
||||
self.layout().search_field.setFocus()
|
||||
parent: QWidget | None = self.parentWidget()
|
||||
if parent is not None: # pyright: ignore[reportUnnecessaryComparison]
|
||||
if parent is not None:
|
||||
parent.hide()
|
||||
return
|
||||
|
||||
@@ -11,13 +11,14 @@ from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap
|
||||
from PySide6.QtWidgets import QSplashScreen, QWidget
|
||||
|
||||
from tagstudio.core.constants import BUILD_TYPE, COPYRIGHT, COPYRIGHT_COMPACT, VERSION
|
||||
from tagstudio.qt.global_settings import Splash
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.app_settings import Splash
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split view and logic.
|
||||
class SplashScreen:
|
||||
"""The custom splash screen widget for TagStudio."""
|
||||
|
||||
@@ -11,14 +11,15 @@ from PySide6.QtGui import QAction, QPixmap, QShowEvent, Qt
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.app_settings import AppSettings
|
||||
from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
autofill_line_edit_style,
|
||||
autofill_line_edit_top_style,
|
||||
)
|
||||
@@ -49,9 +50,7 @@ class SuggestBox[T](QWidget):
|
||||
item_chosen = Signal(object)
|
||||
done = Signal(str) # Query
|
||||
|
||||
def __init__(
|
||||
self, library: Library, settings: GlobalSettings, placeholder_text: str = ""
|
||||
) -> None:
|
||||
def __init__(self, library: Library, settings: AppSettings, placeholder_text: str = "") -> None:
|
||||
super().__init__()
|
||||
self._lib = library
|
||||
self._settings = settings
|
||||
@@ -112,7 +111,7 @@ class SuggestBox[T](QWidget):
|
||||
def _on_shift_held(self, held: bool) -> None:
|
||||
self._is_shift_held = held
|
||||
for i in range(0, self.layout().content_layout.count()):
|
||||
underlined_widget = self.layout().content_layout.itemAt(i).widget()
|
||||
underlined_widget = self.layout().content_layout.itemAt(i).widget() # pyright: ignore
|
||||
assert isinstance(underlined_widget, UnderlinedWidget)
|
||||
|
||||
if held and i == self._selection_index:
|
||||
@@ -130,7 +129,7 @@ class SuggestBox[T](QWidget):
|
||||
# Initialize the widget count (non-hidden)
|
||||
widget_count = 0
|
||||
for i in range(0, self.layout().content_layout.count()):
|
||||
widget = self.layout().content_layout.itemAt(i).widget()
|
||||
widget = unwrap(self.layout().content_layout.itemAt(i).widget()) # pyright: ignore
|
||||
if not widget.isHidden():
|
||||
widget_count += 1
|
||||
|
||||
@@ -151,7 +150,7 @@ class SuggestBox[T](QWidget):
|
||||
|
||||
# Draw the correct underline for the selected widget
|
||||
for i in range(0, widget_count):
|
||||
underlined_widget = self.layout().content_layout.itemAt(i).widget()
|
||||
underlined_widget = self.layout().content_layout.itemAt(i).widget() # pyright: ignore
|
||||
assert isinstance(underlined_widget, UnderlinedWidget)
|
||||
if i == self._selection_index:
|
||||
underlined_widget.toggle_underline(is_hidden=False)
|
||||
@@ -218,7 +217,9 @@ class SuggestBox[T](QWidget):
|
||||
self._selection_index = 0
|
||||
if self.layout().content_layout.count() > 0:
|
||||
self.layout().scroll_area.ensureWidgetVisible(
|
||||
self.layout().content_layout.itemAt(0).widget(), xmargin=16, ymargin=0
|
||||
self.layout().content_layout.itemAt(0).widget(), # pyright: ignore
|
||||
xmargin=16,
|
||||
ymargin=0,
|
||||
)
|
||||
|
||||
# Get results for the search query
|
||||
|
||||
+4
-2
@@ -17,17 +17,18 @@ from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.views.tag_box_view import TagBoxWidgetView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class TagBoxWidget(TagBoxWidgetView):
|
||||
on_update = Signal()
|
||||
|
||||
__entries: list[int] = []
|
||||
|
||||
def __init__(self, title: str, driver: "QtDriver"):
|
||||
def __init__(self, title: str, driver: QtDriver):
|
||||
super().__init__(title, driver)
|
||||
self.__driver = driver
|
||||
|
||||
@@ -88,6 +89,7 @@ class TagBoxWidget(TagBoxWidgetView):
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
aliases=set(build_tag_panel.aliases),
|
||||
exclusion_ids=set(build_tag_panel.exclusion_ids),
|
||||
)
|
||||
self.on_update.emit()
|
||||
|
||||
+10
-4
@@ -12,11 +12,12 @@ from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
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.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.controllers.search_panel import SearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -166,7 +167,10 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
if isinstance(edit_item_panel, BuildTagPanel):
|
||||
tag: Tag = edit_item_panel.build_tag()
|
||||
self._lib.add_tag(
|
||||
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
|
||||
tag,
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
aliases=edit_item_panel.aliases,
|
||||
exclusion_ids=edit_item_panel.exclusion_ids,
|
||||
)
|
||||
|
||||
if choose_item:
|
||||
@@ -188,6 +192,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
tag=edit_item_panel.build_tag(),
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
aliases=edit_item_panel.aliases,
|
||||
exclusion_ids=edit_item_panel.exclusion_ids,
|
||||
)
|
||||
self.update_items(self.layout().search_field.text())
|
||||
|
||||
@@ -203,6 +208,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
pad_tag_widget.setHidden(True)
|
||||
self.layout().scroll_layout.addWidget(pad_tag_widget)
|
||||
|
||||
tag_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
|
||||
item = unwrap(self.layout().scroll_layout.itemAt(index))
|
||||
tag_widget: QWidget = unwrap(item.widget())
|
||||
assert isinstance(tag_widget, TagWidget)
|
||||
return tag_widget
|
||||
@@ -12,14 +12,15 @@ from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.app_settings import AppSettings
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.suggest_box import SuggestBox
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -27,9 +28,7 @@ logger = structlog.get_logger(__name__)
|
||||
class TagSuggestBox(SuggestBox[Tag]):
|
||||
search_for_tag = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self, library: Library, settings: GlobalSettings, placeholder_text: str = ""
|
||||
) -> None:
|
||||
def __init__(self, library: Library, settings: AppSettings, placeholder_text: str = "") -> None:
|
||||
super().__init__(library, settings, placeholder_text)
|
||||
|
||||
# Context Menu Actions
|
||||
@@ -158,7 +157,10 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
if isinstance(edit_item_panel, BuildTagPanel):
|
||||
tag: Tag = edit_item_panel.build_tag()
|
||||
self._lib.add_tag(
|
||||
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
|
||||
tag,
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
aliases=edit_item_panel.aliases,
|
||||
exclusion_ids=edit_item_panel.exclusion_ids,
|
||||
)
|
||||
self._on_item_chosen(tag)
|
||||
|
||||
@@ -174,6 +176,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
tag=edit_item_panel.build_tag(),
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
aliases=edit_item_panel.aliases,
|
||||
exclusion_ids=edit_item_panel.exclusion_ids,
|
||||
)
|
||||
self._update_items(self.layout().search_field.text())
|
||||
|
||||
@@ -189,6 +192,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
widget.setHidden(True)
|
||||
self.layout().content_layout.addWidget(widget)
|
||||
|
||||
widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
|
||||
item = unwrap(self.layout().content_layout.itemAt(index))
|
||||
widget_: QWidget = unwrap(item.widget())
|
||||
assert isinstance(widget_, UnderlinedWidget)
|
||||
return widget_
|
||||
|
||||
@@ -6,10 +6,13 @@ from typing import override
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.views.underlined_widget_view import UnderlinedWidgetView
|
||||
|
||||
|
||||
class UnderlinedWidget(QWidget):
|
||||
"""A container for a widget and an underline indicator."""
|
||||
|
||||
def __init__(self, widget: QWidget) -> None:
|
||||
super().__init__()
|
||||
self.setLayout(UnderlinedWidgetView(widget))
|
||||
@@ -19,7 +22,7 @@ class UnderlinedWidget(QWidget):
|
||||
|
||||
@property
|
||||
def widget(self) -> QWidget:
|
||||
return self.layout().itemAt(0).widget()
|
||||
return unwrap(unwrap(self.layout().itemAt(0)).widget())
|
||||
|
||||
@override
|
||||
def layout(self) -> UnderlinedWidgetView:
|
||||
|
||||
@@ -14,15 +14,16 @@ from PySide6.QtWidgets import QMessageBox
|
||||
from tagstudio.core.constants import GITHUB_RELEASE_URL, VERSION
|
||||
from tagstudio.core.ts_core import TagStudioCore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.styles.palette import ColorType, UiColor, get_ui_color
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split view into UpdateAvailableMessageBoxView
|
||||
class UpdateAvailableMessageBox(QMessageBox):
|
||||
"""A warning dialog for if the TagStudio is not running under the latest release version."""
|
||||
"""A message box notifying the user if there is a new version of TagStudio available."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@@ -35,7 +35,7 @@ def get_wanted_mnemonics(text: str) -> list[str]:
|
||||
return matches
|
||||
|
||||
|
||||
def sanitise_mnemonics(actions: list[QAction]) -> None:
|
||||
def sanitize_mnemonics(actions: list[QAction]) -> None:
|
||||
previous = []
|
||||
for action in actions:
|
||||
text = action.text()
|
||||
@@ -127,8 +127,8 @@ def assign_mnemonics(menu: QMenu):
|
||||
# Collect actions
|
||||
actions = [a for a in menu.actions() if not a.isSeparator()]
|
||||
|
||||
# sanitise mnemonics to prevent deadlocks
|
||||
sanitise_mnemonics(actions)
|
||||
# sanitize mnemonics to prevent deadlocks
|
||||
sanitize_mnemonics(actions)
|
||||
|
||||
# Sequence map: mnemonic key -> QAction
|
||||
sequence_to_action: dict[str, QAction] = {}
|
||||
@@ -32,14 +32,15 @@ from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
|
||||
from tagstudio.core.utils.ripgrep_status import RipgrepStatus
|
||||
from tagstudio.core.utils.str_formatting import is_version_outdated
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.file_opener import open_file
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import form_content_style, header
|
||||
from tagstudio.qt.views.styles.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.views.styles.stylesheets import form_content_style, header
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class AboutModal(QWidget):
|
||||
"""Modal window showing information about the TagStudio application."""
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ 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.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
from tagstudio.qt.views.styles.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
checkbox_style,
|
||||
line_edit_style,
|
||||
list_button_style,
|
||||
@@ -37,6 +37,7 @@ from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class BuildColorPanel(ModalContent):
|
||||
on_edit = Signal(TagColorGroup)
|
||||
|
||||
|
||||
@@ -13,13 +13,14 @@ 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.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import line_edit_style
|
||||
from tagstudio.qt.views.styles.stylesheets import line_edit_style
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class BuildNamespacePanel(ModalContent):
|
||||
on_edit = Signal(Namespace)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
QFrame,
|
||||
QGraphicsOpacityEffect,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
@@ -28,16 +29,17 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagAlias, TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
|
||||
from tagstudio.qt.controllers.tag_search_panel import TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.mixed.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
checkbox_style,
|
||||
colored_checkbox_style,
|
||||
colored_radio_button_style,
|
||||
get_tag_border_color,
|
||||
get_tag_highlight_color,
|
||||
@@ -75,6 +77,7 @@ class CustomTableItem(QLineEdit):
|
||||
super().keyPressEvent(arg__1)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class BuildTagPanel(ModalContent):
|
||||
on_edit = Signal(Tag)
|
||||
|
||||
@@ -86,9 +89,10 @@ class BuildTagPanel(ModalContent):
|
||||
self.tag_color_slug: str | None
|
||||
self.disambiguation_id: int | None
|
||||
self.parent_ids: set[int] = set()
|
||||
self.exclusion_ids: set[int] = set()
|
||||
self.aliases: list[TagAlias] = []
|
||||
|
||||
self.setMinimumSize(300, 460)
|
||||
self.setMinimumSize(300, 640)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
@@ -96,7 +100,6 @@ class BuildTagPanel(ModalContent):
|
||||
# Name -----------------------------------------------------------------
|
||||
self.name_widget = QWidget()
|
||||
self.name_layout = QVBoxLayout(self.name_widget)
|
||||
self.name_layout.setStretch(1, 1)
|
||||
self.name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.name_layout.setSpacing(0)
|
||||
self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
@@ -111,7 +114,6 @@ class BuildTagPanel(ModalContent):
|
||||
# Shorthand ------------------------------------------------------------
|
||||
self.shorthand_widget = QWidget()
|
||||
self.shorthand_layout = QVBoxLayout(self.shorthand_widget)
|
||||
self.shorthand_layout.setStretch(1, 1)
|
||||
self.shorthand_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.shorthand_layout.setSpacing(0)
|
||||
self.shorthand_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
@@ -123,7 +125,6 @@ class BuildTagPanel(ModalContent):
|
||||
# Aliases --------------------------------------------------------------
|
||||
self.aliases_widget = QWidget()
|
||||
self.aliases_layout = QVBoxLayout(self.aliases_widget)
|
||||
self.aliases_layout.setStretch(1, 1)
|
||||
self.aliases_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.aliases_layout.setSpacing(0)
|
||||
self.aliases_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
@@ -144,16 +145,14 @@ class BuildTagPanel(ModalContent):
|
||||
|
||||
# Parent Tags ----------------------------------------------------------
|
||||
self.parent_tags_widget = QWidget()
|
||||
self.parent_tags_widget.setMinimumHeight(128)
|
||||
self.parent_tags_layout = QVBoxLayout(self.parent_tags_widget)
|
||||
self.parent_tags_layout.setStretch(1, 1)
|
||||
self.parent_tags_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.parent_tags_layout.setSpacing(0)
|
||||
self.parent_tags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.disam_button_group = QButtonGroup(self)
|
||||
self.disam_button_group.setExclusive(False)
|
||||
|
||||
self.parent_tags_title = QLabel(Translations["tag.parent_tags"])
|
||||
self.parent_tags_title = QLabel(header(Translations["tag.parent_tags"], 3))
|
||||
self.parent_tags_layout.addWidget(self.parent_tags_title)
|
||||
self.scroll_contents = QWidget()
|
||||
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
|
||||
@@ -184,14 +183,40 @@ class BuildTagPanel(ModalContent):
|
||||
|
||||
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
|
||||
|
||||
# Categories -----------------------------------------------------------
|
||||
self.category_widget = QWidget()
|
||||
self.category_layout = QVBoxLayout(self.category_widget)
|
||||
self.category_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.category_layout.setSpacing(0)
|
||||
self.category_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.category_layout.addWidget(QLabel(header(Translations["tag.categories"], 3)))
|
||||
|
||||
category_subtitle = QLabel(Translations["tag.categories.subtitle"])
|
||||
opacity_effect = QGraphicsOpacityEffect(self)
|
||||
opacity_effect.setOpacity(0.5)
|
||||
category_subtitle.setGraphicsEffect(opacity_effect)
|
||||
self.category_layout.addWidget(category_subtitle)
|
||||
|
||||
self.category_scroll_contents = QWidget()
|
||||
self.category_scroll_layout = QVBoxLayout(self.category_scroll_contents)
|
||||
self.category_scroll_layout.setContentsMargins(6, 6, 6, 0)
|
||||
self.category_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.category_scroll_area = QScrollArea()
|
||||
self.category_scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.category_scroll_area.setWidgetResizable(True)
|
||||
self.category_scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.category_scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.category_scroll_area.setWidget(self.category_scroll_contents)
|
||||
self.category_layout.addWidget(self.category_scroll_area)
|
||||
|
||||
# Color ----------------------------------------------------------------
|
||||
self.color_widget = QWidget()
|
||||
self.color_layout = QVBoxLayout(self.color_widget)
|
||||
self.color_layout.setStretch(1, 1)
|
||||
self.color_layout.setContentsMargins(0, 0, 0, 6)
|
||||
self.color_layout.setSpacing(6)
|
||||
self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.color_title = QLabel(Translations["tag.color"])
|
||||
self.color_title = QLabel(header(Translations["tag.color"], 3))
|
||||
self.color_layout.addWidget(self.color_title)
|
||||
self.color_button: TagColorPreview
|
||||
try:
|
||||
@@ -215,7 +240,6 @@ class BuildTagPanel(ModalContent):
|
||||
# Category -------------------------------------------------------------
|
||||
self.cat_widget = QWidget()
|
||||
self.cat_layout = QHBoxLayout(self.cat_widget)
|
||||
self.cat_layout.setStretch(1, 1)
|
||||
self.cat_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.cat_layout.setSpacing(6)
|
||||
self.cat_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
@@ -229,7 +253,6 @@ class BuildTagPanel(ModalContent):
|
||||
# Hidden ---------------------------------------------------------------
|
||||
self.hidden_widget = QWidget()
|
||||
self.hidden_layout = QHBoxLayout(self.hidden_widget)
|
||||
self.hidden_layout.setStretch(1, 1)
|
||||
self.hidden_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.hidden_layout.setSpacing(6)
|
||||
self.hidden_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
@@ -244,16 +267,33 @@ class BuildTagPanel(ModalContent):
|
||||
self.root_layout.addWidget(self.name_widget)
|
||||
self.root_layout.addWidget(self.shorthand_widget)
|
||||
self.root_layout.addWidget(self.aliases_widget)
|
||||
self.root_layout.addWidget(self.aliases_table)
|
||||
self.root_layout.addWidget(self.aliases_table, stretch=1)
|
||||
self.root_layout.addWidget(self.aliases_add_button)
|
||||
self.root_layout.addWidget(self.parent_tags_widget)
|
||||
self._add_spaced_separator()
|
||||
self.root_layout.addWidget(self.parent_tags_widget, stretch=1)
|
||||
self._add_spaced_separator()
|
||||
self.root_layout.addWidget(self.category_widget)
|
||||
self._add_spaced_separator()
|
||||
self.root_layout.addWidget(self.color_widget)
|
||||
self._add_spaced_separator()
|
||||
self.root_layout.addWidget(QLabel(header(Translations["tag.properties"], 3)))
|
||||
self.root_layout.addWidget(self.cat_widget)
|
||||
self.root_layout.addWidget(self.hidden_widget)
|
||||
|
||||
self.set_tag(tag or Tag(name=Translations["tag.new"]))
|
||||
|
||||
def _add_spaced_separator(self) -> None:
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setFrameShadow(QFrame.Shadow.Plain)
|
||||
opacity_effect = QGraphicsOpacityEffect(self)
|
||||
opacity_effect.setOpacity(0.1)
|
||||
sep.setGraphicsEffect(opacity_effect)
|
||||
|
||||
self.root_layout.addSpacing(6)
|
||||
self.root_layout.addWidget(sep)
|
||||
self.root_layout.addSpacing(6)
|
||||
|
||||
def backspace(self):
|
||||
focused_widget = QApplication.focusWidget()
|
||||
row = self.aliases_table.rowCount()
|
||||
@@ -285,10 +325,12 @@ class BuildTagPanel(ModalContent):
|
||||
def _add_parent_tag_callback(self, tag_id: int):
|
||||
self.parent_ids.add(tag_id)
|
||||
self.set_parent_tags()
|
||||
self.set_categories(added_parent_id=tag_id)
|
||||
|
||||
def _remove_parent_tag_callback(self, tag_id: int):
|
||||
self.parent_ids.remove(tag_id)
|
||||
self.set_parent_tags()
|
||||
self.set_categories(removed_parent=True)
|
||||
|
||||
def _create_alias_callback(self):
|
||||
alias = TagAlias("", tag_id=self.tag.id)
|
||||
@@ -315,9 +357,130 @@ class BuildTagPanel(ModalContent):
|
||||
self.tag_color_slug = None
|
||||
self.color_button.set_tag_color_group(tag_color_group)
|
||||
|
||||
def set_categories(self, added_parent_id: int | None = None, removed_parent: bool = False):
|
||||
while self.category_scroll_layout.itemAt(0):
|
||||
self.category_scroll_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
c = QWidget()
|
||||
layout = QVBoxLayout(c)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
|
||||
if removed_parent:
|
||||
tags_by_category: dict[Tag, set[Tag]] = {}
|
||||
hierarchy = set(self._lib.get_tag_hierarchy(self.parent_ids).values())
|
||||
for tag in hierarchy:
|
||||
if tag.is_category:
|
||||
tags_by_category[tag] = set()
|
||||
for tag in hierarchy:
|
||||
for parent in self._lib.get_tag_hierarchy([tag.id]).values():
|
||||
if parent in tags_by_category:
|
||||
if tag == parent and parent.id not in self.parent_ids:
|
||||
continue
|
||||
tags_by_category[parent].add(tag)
|
||||
|
||||
for category, tags in tags_by_category.items():
|
||||
if len(tags) == 0:
|
||||
continue
|
||||
|
||||
last_tab, next_tab, container = self._build_category_row_widget(category)
|
||||
layout.addWidget(container)
|
||||
self.setTabOrder(last_tab, next_tab)
|
||||
else:
|
||||
tag_ids = set(self.parent_ids)
|
||||
if added_parent_id is not None:
|
||||
tag_ids.add(added_parent_id)
|
||||
|
||||
for tag in self._lib.get_tag_hierarchy(tag_ids).values():
|
||||
if not tag.is_category or tag == self.tag:
|
||||
continue
|
||||
last_tab, next_tab, container = self._build_category_row_widget(tag)
|
||||
layout.addWidget(container)
|
||||
self.setTabOrder(last_tab, next_tab)
|
||||
self.category_scroll_layout.addWidget(c)
|
||||
|
||||
def _build_category_row_widget(self, category: Tag) -> tuple[QPushButton, QCheckBox, QWidget]:
|
||||
container = QWidget()
|
||||
row = QHBoxLayout(container)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(3)
|
||||
|
||||
def update_parent_tag_callback(build_tag_panel: BuildTagPanel):
|
||||
self._lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
aliases=set(build_tag_panel.aliases),
|
||||
exclusion_ids=set(build_tag_panel.exclusion_ids),
|
||||
)
|
||||
self.set_categories()
|
||||
|
||||
def on_category_edit(category_tag: Tag) -> None:
|
||||
build_tag_panel = BuildTagPanel(self._lib, tag=category_tag)
|
||||
edit_modal = Modal(
|
||||
build_tag_panel,
|
||||
self._lib.tag_display_name(category_tag),
|
||||
"Edit Tag",
|
||||
is_savable=True,
|
||||
)
|
||||
edit_modal.saved.connect(partial(update_parent_tag_callback, build_tag_panel))
|
||||
edit_modal.show()
|
||||
|
||||
def update_category_exclusion(category_tag: Tag, checked: bool) -> None:
|
||||
if checked:
|
||||
self.exclusion_ids.remove(category_tag.id)
|
||||
else:
|
||||
self.exclusion_ids.add(category_tag.id)
|
||||
|
||||
# Add Tag Widget
|
||||
tag_widget = TagWidget(
|
||||
category,
|
||||
library=self._lib,
|
||||
has_edit=True,
|
||||
has_remove=False,
|
||||
)
|
||||
tag_widget.on_edit.connect(partial(on_category_edit, category))
|
||||
row.addWidget(tag_widget)
|
||||
|
||||
# Add Category Exclusion Tag Button
|
||||
include_checkbox = QCheckBox()
|
||||
include_checkbox.setFixedSize(22, 22)
|
||||
include_checkbox.setToolTip(Translations["tag.categories.tooltip"])
|
||||
include_checkbox.setStyleSheet(colored_checkbox_style(*self._tag_colors(category)))
|
||||
|
||||
if category.id not in self.exclusion_ids:
|
||||
include_checkbox.setChecked(True)
|
||||
include_checkbox.toggled.connect(partial(update_category_exclusion, category))
|
||||
|
||||
row.addWidget(include_checkbox)
|
||||
|
||||
return tag_widget.bg_button, include_checkbox, container
|
||||
|
||||
def _tag_colors(self, tag: Tag) -> tuple[QColor, QColor, QColor, QColor]:
|
||||
primary_color = get_tag_primary_color(tag)
|
||||
|
||||
border_color = (
|
||||
get_tag_border_color(primary_color)
|
||||
if not (tag.color and tag.color.secondary and tag.color.color_border)
|
||||
else (QColor(tag.color.secondary))
|
||||
)
|
||||
|
||||
highlight_color = get_tag_highlight_color(
|
||||
primary_color
|
||||
if not (tag.color and tag.color.secondary)
|
||||
else QColor(tag.color.secondary)
|
||||
)
|
||||
|
||||
text_color: QColor
|
||||
if tag.color and tag.color.secondary:
|
||||
text_color = QColor(tag.color.secondary)
|
||||
else:
|
||||
text_color = get_tag_text_color(primary_color, highlight_color)
|
||||
|
||||
return primary_color, border_color, highlight_color, text_color
|
||||
|
||||
def set_parent_tags(self):
|
||||
while self.parent_tags_scroll_layout.itemAt(0):
|
||||
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
|
||||
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
c = QWidget()
|
||||
layout = QVBoxLayout(c)
|
||||
@@ -346,29 +509,12 @@ class BuildTagPanel(ModalContent):
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(3)
|
||||
|
||||
# Init Colors
|
||||
primary_color = get_tag_primary_color(tag)
|
||||
border_color = (
|
||||
get_tag_border_color(primary_color)
|
||||
if not (tag.color and tag.color.secondary and tag.color.color_border)
|
||||
else (QColor(tag.color.secondary))
|
||||
)
|
||||
highlight_color = get_tag_highlight_color(
|
||||
primary_color
|
||||
if not (tag.color and tag.color.secondary)
|
||||
else QColor(tag.color.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if tag.color and tag.color.secondary:
|
||||
text_color = QColor(tag.color.secondary)
|
||||
else:
|
||||
text_color = get_tag_text_color(primary_color, highlight_color)
|
||||
|
||||
def update_parent_tag_callback(build_tag_panel: BuildTagPanel):
|
||||
self._lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
aliases=set(build_tag_panel.aliases),
|
||||
exclusion_ids=set(build_tag_panel.exclusion_ids),
|
||||
)
|
||||
self.set_parent_tags()
|
||||
|
||||
@@ -395,9 +541,7 @@ class BuildTagPanel(ModalContent):
|
||||
disam_button.setObjectName(f"disambiguationButton.{parent_id}")
|
||||
disam_button.setFixedSize(22, 22)
|
||||
disam_button.setToolTip(Translations["tag.disambiguation.tooltip"])
|
||||
disam_button.setStyleSheet(
|
||||
colored_radio_button_style(primary_color, text_color, border_color, highlight_color)
|
||||
)
|
||||
disam_button.setStyleSheet(colored_radio_button_style(*self._tag_colors(tag)))
|
||||
|
||||
self.disam_button_group.addButton(disam_button)
|
||||
if is_disambiguation:
|
||||
@@ -478,6 +622,10 @@ class BuildTagPanel(ModalContent):
|
||||
self.parent_ids.add(parent_id)
|
||||
self.set_parent_tags()
|
||||
|
||||
for exclusion_id in tag.exclusion_ids:
|
||||
self.exclusion_ids.add(exclusion_id)
|
||||
self.set_categories()
|
||||
|
||||
try:
|
||||
self.tag_color_namespace = tag.color_namespace
|
||||
self.tag_color_slug = tag.color_slug
|
||||
|
||||
@@ -12,13 +12,13 @@ from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
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.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import add_button_style
|
||||
from tagstudio.qt.views.styles.stylesheets import add_button_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
@@ -26,14 +26,15 @@ if typing.TYPE_CHECKING:
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class ColorBoxWidget(FieldWidget):
|
||||
updated = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
group: str,
|
||||
colors: list["TagColorGroup"],
|
||||
library: "Library",
|
||||
colors: list[TagColorGroup],
|
||||
library: Library,
|
||||
) -> None:
|
||||
self.namespace = group
|
||||
self.colors: list[TagColorGroup] = colors
|
||||
@@ -59,7 +60,7 @@ class ColorBoxWidget(FieldWidget):
|
||||
color_widgets: list[TagColorLabel] = []
|
||||
|
||||
while self.base_layout.itemAt(0):
|
||||
unwrap(self.base_layout.takeAt(0)).widget().deleteLater()
|
||||
unwrap(self.base_layout.takeAt(0)).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
for color in colors_:
|
||||
color_widget = TagColorLabel(
|
||||
|
||||
@@ -10,10 +10,10 @@ from PySide6.QtCore import QDateTime
|
||||
from PySide6.QtWidgets import QDateTimeEdit, QLineEdit, QVBoxLayout
|
||||
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import title_line_edit_style
|
||||
from tagstudio.qt.views.styles.stylesheets import title_line_edit_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
@@ -39,8 +39,9 @@ def qdtf2dtf(dtf: str) -> str:
|
||||
return out
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class DatetimePicker(ModalContent):
|
||||
def __init__(self, driver: "QtDriver", name: str, datetime: dt | str):
|
||||
def __init__(self, driver: QtDriver, name: str, datetime: dt | str):
|
||||
super().__init__()
|
||||
self.setMinimumSize(300, 60)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
|
||||
@@ -11,21 +11,14 @@ import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListView,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.progress_bar import ProgressWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -37,10 +30,11 @@ class DuplicateChoice(enum.StrEnum):
|
||||
CANCEL = "Cancelled"
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class DropImportModal(QWidget):
|
||||
DUPE_NAME_LIMT: int = 5
|
||||
|
||||
def __init__(self, driver: "QtDriver"):
|
||||
def __init__(self, driver: QtDriver):
|
||||
super().__init__()
|
||||
|
||||
self.driver: QtDriver = driver
|
||||
|
||||
@@ -29,27 +29,28 @@ from tagstudio.core.library.alchemy.fields import (
|
||||
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.edit_text_controller import EditText
|
||||
from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.controllers.edit_text import EditText
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tag_box_controller import TagBoxWidget
|
||||
from tagstudio.qt.controllers.tag_box 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 TextContainerWidget
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import inset_container_style
|
||||
from tagstudio.qt.views.styles.stylesheets import inset_container_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FieldContainers(QWidget):
|
||||
"""Widget for the tag and field containers displayed inside the Preview Panel."""
|
||||
|
||||
on_tags_update = Signal()
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver") -> None:
|
||||
def __init__(self, library: Library, driver: QtDriver) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.lib = library
|
||||
@@ -185,7 +186,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
grandparent_tags: set[Tag] = set()
|
||||
for parent_tag in parent_tags:
|
||||
if parent_tag in categories:
|
||||
if parent_tag in categories and parent_tag.id not in tag.exclusion_ids:
|
||||
categories[parent_tag].add(tag)
|
||||
has_category_parent = True
|
||||
grandparent_tags.update(parent_tag.parent_tags)
|
||||
@@ -320,7 +321,7 @@ class FieldContainers(QWidget):
|
||||
text = self.driver.settings.format_datetime(
|
||||
DatetimePicker.string2dt(field.value)
|
||||
)
|
||||
except (ValueError, AssertionError):
|
||||
except ValueError, AssertionError:
|
||||
text = str(field.value)
|
||||
else:
|
||||
text = f"<i>{Translations['field.mixed_data']}</i>"
|
||||
|
||||
@@ -12,13 +12,15 @@ from PySide6.QtCore import QEvent, QSize, Qt
|
||||
from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import container_style, header
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.views.styles.stylesheets import container_style, header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FieldContainer(QWidget):
|
||||
rm: ResourceManager = ResourceManager()
|
||||
copy_icon = auto_theme_overlay(rm.copy, inverse=True)
|
||||
@@ -135,17 +137,19 @@ class FieldContainer(QWidget):
|
||||
if callback:
|
||||
self.remove_button.clicked.connect(callback)
|
||||
|
||||
def set_inner_widget(self, widget: "FieldWidget") -> None:
|
||||
if self.field_layout.itemAt(0):
|
||||
old: QWidget = self.field_layout.itemAt(0).widget()
|
||||
def set_inner_widget(self, widget: FieldWidget) -> None:
|
||||
item = self.field_layout.itemAt(0)
|
||||
if item:
|
||||
old: QWidget = unwrap(item.widget())
|
||||
self.field_layout.removeWidget(old)
|
||||
old.deleteLater()
|
||||
|
||||
self.field_layout.addWidget(widget)
|
||||
|
||||
def get_inner_widget(self) -> QWidget | None:
|
||||
if self.field_layout.itemAt(0):
|
||||
return self.field_layout.itemAt(0).widget()
|
||||
item = self.field_layout.itemAt(0)
|
||||
if item:
|
||||
return item.widget()
|
||||
return None
|
||||
|
||||
def set_title(self, title: str) -> None:
|
||||
|
||||
@@ -21,13 +21,14 @@ from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.utils.str_formatting import format_duration
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.file_opener import FileOpenerHelper, FileOpenerLabel
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import properties_style
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.file_opener_label import FileOpenerLabel
|
||||
from tagstudio.qt.utils.file_opener import FileOpenerHelper
|
||||
from tagstudio.qt.views.styles.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.views.styles.stylesheets import properties_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -39,10 +40,9 @@ class FileAttributeData:
|
||||
duration: int | None = None
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FileAttributes(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__()
|
||||
root_layout = QVBoxLayout(self)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -130,7 +130,7 @@ class FileAttributes(QWidget):
|
||||
stats = FileAttributeData()
|
||||
|
||||
if not filepath:
|
||||
self.layout().setSpacing(0)
|
||||
self.layout().setSpacing(0) # pyright: ignore[reportOptionalMemberAccess]
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.file_label.setText(f"<i>{Translations['preview.no_selection']}</i>")
|
||||
self.file_label.set_file_path(Path())
|
||||
@@ -147,7 +147,7 @@ class FileAttributes(QWidget):
|
||||
elif self.driver.settings.show_filepath == ShowFilepathOption.SHOW_FILENAMES_ONLY:
|
||||
display_path = Path(filepath.name)
|
||||
|
||||
self.layout().setSpacing(6)
|
||||
self.layout().setSpacing(6) # pyright: ignore[reportOptionalMemberAccess]
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.file_label.set_file_path(filepath)
|
||||
self.dimensions_label.setHidden(False)
|
||||
@@ -234,7 +234,7 @@ class FileAttributes(QWidget):
|
||||
|
||||
def update_multi_selection(self, count: int):
|
||||
"""Format attributes for multiple selected items."""
|
||||
self.layout().setSpacing(0)
|
||||
self.layout().setSpacing(0) # pyright: ignore[reportOptionalMemberAccess]
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.file_label.setText(Translations.format("preview.multiple_selection", count=count))
|
||||
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
|
||||
@@ -17,18 +17,18 @@ 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.i18n.translations import Translations
|
||||
from tagstudio.qt.mixed.mirror_entries_modal import MirrorEntriesModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
|
||||
# TODO: Break up into MVC classes, similar to fix_ignored_modal
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FixDupeFilesModal(QWidget):
|
||||
def __init__(self, library: "Library", driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.driver = driver
|
||||
|
||||
@@ -10,21 +10,21 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWi
|
||||
|
||||
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.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.merge_dupe_entries_progress import MergeDuplicateEntriesProgress
|
||||
from tagstudio.qt.controllers.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.controllers.relink_entries_progress import RelinkUnlinkedEntriesProgress
|
||||
from tagstudio.qt.mixed.remove_unlinked_modal import RemoveUnlinkedEntriesModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
|
||||
# TODO: Break up into MVC classes, similar to fix_ignored_modal
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FixUnlinkedEntriesModal(QWidget):
|
||||
def __init__(self, library: "Library", driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.driver = driver
|
||||
@@ -55,8 +55,8 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.refresh_unlinked_button = QPushButton(Translations["entries.generic.refresh_alt"])
|
||||
self.refresh_unlinked_button.clicked.connect(self.refresh_unlinked)
|
||||
|
||||
self.merge_class = MergeDuplicateEntries(self.lib, self.driver)
|
||||
self.relink_class = RelinkUnlinkedEntries(self.tracker)
|
||||
self.merge_class = MergeDuplicateEntriesProgress(self.lib, self.driver)
|
||||
self.relink_class = RelinkUnlinkedEntriesProgress(self.tracker)
|
||||
|
||||
self.search_button = QPushButton(Translations["entries.unlinked.search_and_relink"])
|
||||
self.relink_class.done.connect(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# pyright: reportOptionalMemberAccess=false
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, override
|
||||
from warnings import deprecated
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
@@ -19,27 +21,26 @@ 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.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
from tagstudio.qt.views.styles.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BranchData:
|
||||
dirs: dict[str, "BranchData"] = field(default_factory=dict)
|
||||
dirs: dict[str, BranchData] = field(default_factory=dict)
|
||||
files: list[str] = field(default_factory=list)
|
||||
tag: Tag | None = None
|
||||
|
||||
@@ -165,7 +166,7 @@ def generate_preview_data(library: Library) -> BranchData:
|
||||
|
||||
|
||||
class FoldersToTagsModal(QWidget):
|
||||
def __init__(self, library: "Library", driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: QtDriver):
|
||||
super().__init__()
|
||||
self.library = library
|
||||
self.driver = driver
|
||||
|
||||
@@ -17,15 +17,15 @@ 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.platform_strings import open_file_str, trash_term
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.platform_strings import open_file_str, trash_term
|
||||
from tagstudio.i18n.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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -60,6 +60,7 @@ def badge_update_lock(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class ItemThumb(FlowWidget):
|
||||
"""The thumbnail widget for a library item (Entry, Entry Group, etc.)."""
|
||||
|
||||
@@ -95,7 +96,7 @@ class ItemThumb(FlowWidget):
|
||||
self,
|
||||
mode: ItemType | None,
|
||||
library: Library,
|
||||
driver: "QtDriver",
|
||||
driver: QtDriver,
|
||||
thumb_size: tuple[int, int],
|
||||
show_filename_label: bool = False,
|
||||
):
|
||||
@@ -439,7 +440,7 @@ class ItemThumb(FlowWidget):
|
||||
self.thumb_button.setMinimumSize(size)
|
||||
self.thumb_button.setMaximumSize(size)
|
||||
|
||||
def set_item(self, entry: "Entry"):
|
||||
def set_item(self, entry: Entry):
|
||||
self.set_item_id(entry.id)
|
||||
path = unwrap(self.lib.library_dir) / entry.path
|
||||
self.set_item_path(path)
|
||||
|
||||
@@ -11,24 +11,25 @@ from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class LandingWidget(QWidget):
|
||||
rm: ResourceManager = ResourceManager()
|
||||
mono_logo: Image.Image = rm.ts_logo_text_mono
|
||||
color_logo: Image.Image = rm.ts_logo_text_color
|
||||
|
||||
def __init__(self, driver: "QtDriver", pixel_ratio: float):
|
||||
def __init__(self, driver: QtDriver, pixel_ratio: float):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.logo_label: ClickableLabel = ClickableLabel()
|
||||
|
||||
@@ -36,25 +36,26 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.qt.helpers.color_overlay import light_overlay
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.clickable_slider import ClickableSlider
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.clickable_slider import ClickableSlider
|
||||
from tagstudio.qt.views.styles.color_overlay import light_overlay
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class MediaPlayer(QGraphicsView):
|
||||
"""A basic media player widget.
|
||||
|
||||
Gives a basic control set to manage media playback.
|
||||
"""
|
||||
|
||||
video_preview: "VideoPreview | None" = None
|
||||
video_preview: VideoPreview | None = None
|
||||
|
||||
def __init__(self, driver: "QtDriver") -> None:
|
||||
def __init__(self, driver: QtDriver) -> None:
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.play_icon = QPixmap.fromImage(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from warnings import deprecated
|
||||
|
||||
import structlog
|
||||
import wcmatch.fnmatch as fnmatch
|
||||
@@ -42,17 +43,18 @@ from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glo
|
||||
from tagstudio.core.library.json.library import Library as JsonLibrary
|
||||
from tagstudio.core.library.json.library import Tag as JsonTag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.paged_panel_controller import PagedPanel
|
||||
from tagstudio.qt.controllers.paged_panel_state import PagedPanelState
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.mixed.paged_body_wrapper import PagedBodyWrapper
|
||||
from tagstudio.qt.mixed.paged_panel import PagedPanel
|
||||
from tagstudio.qt.mixed.paged_panel_state import PagedPanelState
|
||||
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.stylesheets.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@deprecated("This modal will be removed when legacy JSON library support is dropped.")
|
||||
class JsonMigrationModal(QObject):
|
||||
"""A modal for data migration from v9.4 JSON to v9.5+ SQLite."""
|
||||
|
||||
@@ -96,8 +98,8 @@ class JsonMigrationModal(QObject):
|
||||
body_label = QLabel(Translations["json_migration.info.description"])
|
||||
body_label.setWordWrap(True)
|
||||
body_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
body_wrapper.layout().addWidget(body_label)
|
||||
body_wrapper.layout().setContentsMargins(0, 36, 0, 0)
|
||||
body_wrapper.layout().addWidget(body_label) # pyright: ignore[reportOptionalMemberAccess]
|
||||
body_wrapper.layout().setContentsMargins(0, 36, 0, 0) # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
cancel_button = QPushButton(Translations["generic.cancel"])
|
||||
next_button = QPushButton(Translations["generic.continue"])
|
||||
@@ -285,9 +287,9 @@ class JsonMigrationModal(QObject):
|
||||
body_container_layout.addStretch(1)
|
||||
body_container_layout.addWidget(new_lib_container)
|
||||
body_container_layout.addStretch(2)
|
||||
self.body_wrapper_01.layout().addWidget(body_container)
|
||||
self.body_wrapper_01.layout().addWidget(desc_label)
|
||||
self.body_wrapper_01.layout().setSpacing(12)
|
||||
self.body_wrapper_01.layout().addWidget(body_container) # pyright: ignore[reportOptionalMemberAccess]
|
||||
self.body_wrapper_01.layout().addWidget(desc_label) # pyright: ignore[reportOptionalMemberAccess]
|
||||
self.body_wrapper_01.layout().setSpacing(12) # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
back_button = QPushButton(Translations["generic.navigation.back"])
|
||||
start_button = QPushButton(Translations["json_migration.start_and_preview"])
|
||||
@@ -350,31 +352,22 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
def migration_progress(self, skip_ui: bool = False):
|
||||
"""Initialize the progress bar and iterator for the library migration."""
|
||||
pb = QProgressDialog(
|
||||
labelText="",
|
||||
cancelButtonText="",
|
||||
minimum=0,
|
||||
maximum=0,
|
||||
)
|
||||
pb.setCancelButton(None) # pyright: ignore[reportArgumentType]
|
||||
self.body_wrapper_01.layout().addWidget(pb)
|
||||
pb = QProgressDialog("", "", 0, 0)
|
||||
pb.setCancelButton(None)
|
||||
self.body_wrapper_01.layout().addWidget(pb) # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
try:
|
||||
iterator = FunctionIterator(self.migration_iterator)
|
||||
iterator.value.connect(
|
||||
lambda x: (
|
||||
pb.setLabelText(header(x, 4)),
|
||||
self.update_sql_value_ui(show_msg_box=False)
|
||||
if x == Translations["json_migration.checking_for_parity"]
|
||||
else (),
|
||||
self.update_parity_ui()
|
||||
if x == Translations["json_migration.checking_for_parity"]
|
||||
else (),
|
||||
)
|
||||
)
|
||||
|
||||
if skip_ui:
|
||||
iterator.run()
|
||||
return
|
||||
|
||||
iterator.value.connect(lambda x: pb.setLabelText(header(x, 4)))
|
||||
r = CustomRunnable(iterator.run)
|
||||
r.done.connect(
|
||||
lambda: (
|
||||
self.update_parity_ui(),
|
||||
self.update_sql_value_ui(show_msg_box=not skip_ui),
|
||||
pb.setMinimum(1),
|
||||
pb.setValue(1),
|
||||
@@ -423,7 +416,6 @@ class JsonMigrationModal(QObject):
|
||||
yield Translations["json_migration.migration_complete"]
|
||||
else:
|
||||
yield Translations["json_migration.migration_complete_with_discrepancies"]
|
||||
self.update_parity_ui()
|
||||
QApplication.beep()
|
||||
QApplication.alert(self.paged_panel)
|
||||
self.done = True
|
||||
@@ -483,26 +475,26 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
def update_json_entry_count(self, value: int):
|
||||
self.old_entry_count = value
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.entries_row, 1).widget() # pyright: ignore[reportAssignmentType]
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.entries_row, 1).widget() # pyright: ignore
|
||||
label.setText(self.color_value_default(value))
|
||||
|
||||
def update_json_tag_count(self, value: int):
|
||||
self.old_tag_count = value
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # pyright: ignore[reportAssignmentType]
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # pyright: ignore
|
||||
label.setText(self.color_value_default(value))
|
||||
|
||||
def update_sql_value(self, row: int, value: int | bool, old_value: int | bool):
|
||||
label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore[reportAssignmentType]
|
||||
warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore[reportAssignmentType]
|
||||
label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore
|
||||
warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
|
||||
label.setText(self.color_value_conditional(old_value, value))
|
||||
warning_icon.setText("" if old_value == value else self.warning)
|
||||
|
||||
def update_parity_value(self, row: int, value: bool):
|
||||
result: str = self.match_text if value else self.differ_text
|
||||
old_label: QLabel = self.old_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore[reportAssignmentType]
|
||||
new_label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore[reportAssignmentType]
|
||||
old_warning_icon: QLabel = self.old_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore[reportAssignmentType]
|
||||
new_warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore[reportAssignmentType]
|
||||
old_label: QLabel = self.old_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore
|
||||
new_label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore
|
||||
old_warning_icon: QLabel = self.old_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
|
||||
new_warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
|
||||
old_label.setText(self.color_value_conditional(self.match_text, result))
|
||||
new_label.setText(self.color_value_conditional(self.match_text, result))
|
||||
old_warning_icon.setText("" if value else self.warning)
|
||||
|
||||
@@ -10,18 +10,19 @@ 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.qt.translations import Translations
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.progress_bar import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class MirrorEntriesModal(QWidget):
|
||||
done = Signal()
|
||||
|
||||
def __init__(self, driver: "QtDriver", tracker: DupeFilesRegistry):
|
||||
def __init__(self, driver: QtDriver, tracker: DupeFilesRegistry):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.setWindowTitle(Translations["entries.mirror.window_title"])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user