Compare commits

..

3 Commits

Author SHA1 Message Date
Hosted Weblate 16e546f8b2 Translated using Weblate (French)
Currently translated at 100.0% (428 of 428 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Application Strings
2026-08-16 18:08:42 +02:00
Hosted Weblate 69377b5749 Translated using Weblate (Dutch)
Currently translated at 34.5% (148 of 428 strings)

Translated using Weblate (Dutch)

Currently translated at 34.5% (148 of 428 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lily H <flannyha@gmail.com>
Co-authored-by: Pheubel <luca.guagliardo@live.nl>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nl/
Translation: TagStudio/Application Strings
2026-08-16 18:08:42 +02:00
Hosted Weblate ce03c18b7d Translated using Weblate (Hungarian)
Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Application Strings
2026-08-16 18:08:42 +02:00
170 changed files with 968 additions and 1641 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ runs:
if: inputs.skip-setup != 'true'
uses: actions/setup-python@v6
with:
python-version: '3.14'
python-version: '3.12'
- name: Setup uv install
if: inputs.skip-setup != 'true'
-5
View File
@@ -4,11 +4,6 @@
repos:
- repo: local
hooks:
- id: reuse
name: reuse
entry: reuse lint-file
language: system
- id: pyright
name: pyright
entry: pyright
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -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.
+5 -5
View File
@@ -16,7 +16,7 @@ If you wish to develop for TagStudio, you'll need to create a development enviro
## Installing Python
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.
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.
<!-- 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.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. 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.
---
@@ -132,7 +132,7 @@ A reference `.envrc` is provided for use with [direnv](#direnv), see [`contrib/.
### Editor Integration
The entry point for TagStudio is `src/tagstudio/__main__.py`. You can target this file from your IDE to run or connect a debug session. The example(s) below show off example launch scripts for different IDEs. Here you can also take advantage of [launch arguments](./usage.md/#launch-arguments) to pass your own test [libraries](libraries.md) to use while developing. You can find more editor configurations in [`contrib`](https://github.com/TagStudioDev/TagStudio/tree/main/contrib).
The entry point for TagStudio is `src/tagstudio/main.py`. You can target this file from your IDE to run or connect a debug session. The example(s) below show off example launch scripts for different IDEs. Here you can also take advantage of [launch arguments](./usage.md/#launch-arguments) to pass your own test [libraries](libraries.md) to use while developing. You can find more editor configurations in [`contrib`](https://github.com/TagStudioDev/TagStudio/tree/main/contrib).
<!-- prettier-ignore -->
=== "VS Code"
@@ -144,7 +144,7 @@ The entry point for TagStudio is `src/tagstudio/__main__.py`. You can target thi
"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"]
+2 -2
View File
@@ -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 inspector 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 preview panel 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 inspector.
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.
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
View File
@@ -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 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**.
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**.
<figure markdown="span">
![Fields Example](assets/fields_example.png)
+1 -1
View File
@@ -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.14 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.12 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).
-17
View File
@@ -201,20 +201,3 @@ 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
View File
@@ -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 inspector under a title
- [x] 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 preview panel under a title
- [ ] 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)]**
+3 -4
View File
@@ -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 `AppSettings` instances.
- Avoid passing around the `QtDriver` class where possible. Instead, pass only the necessary components such as the `Library` and `GlobalSettings` 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
├── qt_driver.py
├── ts_qt.py # Qt Driver
└── ...
```
@@ -176,8 +176,7 @@ 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!")
+1 -3
View File
@@ -100,14 +100,12 @@ 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 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.
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.
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.
![Tag Category Example](assets/tag_categories_example.png)
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
View File
@@ -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 inspector 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 preview panel 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.
Generated
+6 -6
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1787559586,
"narHash": "sha256-onL0VLf9vPllmT0H/OlURIU5r5t5WIEl7t4tVNKT0Nw=",
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9d0d87172c374f89da73c1cfe6d81ae62feac1f1",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1787736819,
"narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=",
"lastModified": 1785571196,
"narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3",
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
"type": "github"
},
"original": {
+21 -38
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
description = "TagStudio";
@@ -29,52 +29,35 @@
systems = import inputs.systems;
perSystem =
{ pkgs, self', ... }:
{ pkgs, ... }:
let
python3 = builtins.head python3Versions;
python3Versions = with pkgs; [ python314 ];
python3 = pkgs.python313;
in
{
packages =
let
pythonDerivations = lib.genAttrs' python3Versions (
python3:
lib.nameValuePair python3.pythonAttr (
let
python3Packages = python3.pkgs;
python3Packages = python3.pkgs;
tagstudio = pkgs.callPackage ./nix/package {
inherit python3Packages;
inherit pillow-jxl-plugin;
};
openexr = python3Packages.callPackage ./nix/package/openexr.nix { inherit (pkgs) openexr; };
pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix {
inherit (pkgs) cmake;
inherit openexr pyexiv2;
};
pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
in
{
inherit tagstudio;
tagstudio-jxl = tagstudio.override { withJXLSupport = true; };
inherit openexr pillow-jxl-plugin pyexiv2;
}
)
);
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
(lib.concatMapAttrs (
pythonAttr: lib.mapAttrs' (name: lib.nameValuePair "${pythonAttr}Packages_${name}")
) pythonDerivations)
// pythonDerivations.${python3.pythonAttr}
// {
default = self'.packages.tagstudio;
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;
};
devShells = {
default = self'.devShells.tagstudio;
devShells = rec {
default = tagstudio;
tagstudio = import ./nix/shell.nix {
inherit
inputs
+40 -42
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
ffmpeg-headless,
@@ -40,36 +40,12 @@ python3Packages.buildPythonApplication {
qt6.qtmultimedia
];
build-system = with python3Packages; [ hatchling ];
dependencies =
with python3Packages;
[
chardet_5
ffmpeg-python
humanfriendly
mutagen
numpy
opencv-python
pillow
pillow-heif
py7zr
pydantic
pydub
pyside6
rarfile
rawpy
requests
semver
send2trash
sqlalchemy
srctools
structlog
toml
ujson
wcmatch
]
++ lib.optional (pythonAtLeast "3.13") audioop-lts
++ lib.optional withJXLSupport pillow-jxl-plugin;
nativeCheckInputs = with python3Packages; [
pytest-qt
pytest-xdist
pytestCheckHook
syrupy
];
# TODO: Install more icon resolutions when available.
preInstall = ''
@@ -114,19 +90,39 @@ python3Packages.buildPythonApplication {
"structlog"
"typing-extensions"
];
# HACK: All tests fail with Python: Aborted for unknown reasons.
doCheck = false;
nativeCheckInputs = with python3Packages; [
pytest-qt
pytest-xdist
pytestCheckHook
syrupy
];
pythonImportsCheck = [ "tagstudio" ];
build-system = with python3Packages; [ hatchling ];
dependencies =
with python3Packages;
[
audioop-lts
chardet_5
ffmpeg-python
humanfriendly
mutagen
numpy
opencv-python
pillow
pillow-heif
py7zr
pydantic
pydub
pyside6
rarfile
rawpy
requests
semver
send2trash
sqlalchemy
srctools
structlog
toml
ujson
wcmatch
]
++ lib.optional withJXLSupport pillow-jxl-plugin;
# These tests require modifications to a library, which does not work
# in a read-only environment.
disabledTests = [
@@ -144,8 +140,10 @@ python3Packages.buildPythonApplication {
"tests/qt/test_build_tag_panel.py"
"tests/qt/test_field_containers.py"
"tests/qt/test_file_path_options.py"
"tests/qt/test_preview_panel.py"
"tests/qt/test_tag_panel.py"
"tests/qt/test_tag_search_panel.py"
"tests/test_library.py"
];
meta = {
-38
View File
@@ -1,38 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
{
lib,
numpy,
openexr,
pybind11,
pytest,
python,
pythonImportsCheckHook,
toPythonModule,
}:
toPythonModule (
openexr.overrideAttrs (o: {
buildInputs = o.buildInputs or [ ] ++ [ pybind11 ];
cmakeFlags = o.cmakeFlags or [ ] ++ [ (lib.cmakeBool "OPENEXR_BUILD_PYTHON" true) ];
# `python.sitePackages` replacement can be removed once Python install path is inherited from sysconfig.
# Currently on main, but not part of a release.
# See: https://github.com/AcademySoftwareFoundation/openexr/commit/30345db72944b38926f13b5114b9a01b4b553890
postPatch = o.postPatch or "" + /* bash */ ''
substituteInPlace src/wrappers/python/CMakeLists.txt \
--replace-warn python/OpenEXR ${python.sitePackages} \
--replace-fail 'PYTHONPATH=''${CMAKE_CURRENT_BINARY_DIR}' 'PYTHONPATH=''${CMAKE_CURRENT_BINARY_DIR}:'"$PYTHONPATH"
'';
nativeCheckInputs = o.nativeCheckInputs or [ ] ++ [
numpy
pythonImportsCheckHook
];
checkInputs = o.checkInputs or [ ] ++ [ pytest ];
pythonImportsCheck = o.pythonImportsCheck or [ ] ++ [ "OpenEXR" ];
})
)
+13 -15
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
buildPythonPackage,
@@ -7,7 +7,6 @@
fetchPypi,
lib,
numpy,
openexr,
packaging,
pillow,
pyexiv2,
@@ -17,18 +16,18 @@
buildPythonPackage rec {
pname = "pillow-jxl-plugin";
version = "1.3.8";
version = "1.3.4";
pyproject = true;
src = fetchPypi {
pname = "pillow_jxl_plugin";
inherit version;
hash = "sha256-RDD9d1eJl0IHnFSKfSU31tY88PHTIxgAlbwPbwPZ1Po=";
hash = "sha256-jqWJ/FWep8XfzLQq9NgUj121CPX01FGDKLq1ox/LJo4=";
};
cargoDeps = rustPlatform.fetchCargoVendor {
inherit src;
hash = "sha256-IiVTlKtKkfZnRXme7QFA5MS8PPiL8+riOYOEoNaHHXc=";
hash = "sha256-7j+sCn+P6q6tsm2MJ/cM7hF2KEjILJNA6SDb35tecPg=";
};
nativeBuildInputs = [
@@ -37,22 +36,12 @@ buildPythonPackage rec {
rustPlatform.maturinBuildHook
];
dependencies = [
packaging
pillow
];
dontUseCmakeConfigure = true;
nativeCheckInputs = [
numpy
openexr
pyexiv2
pytestCheckHook
];
pythonImportsCheck = [ "pillow_jxl" ];
# Working directory takes precedence in the Python path. Remove
# `pillow_jxl` to prevent it from being loaded during pytest, rather than the
# built module, as it includes a `pillow_jxl.pillow_jxl.so` that is imported.
@@ -62,6 +51,15 @@ buildPythonPackage rec {
rm -r pillow_jxl
'';
dontUseCmakeConfigure = true;
pythonImportsCheck = [ "pillow_jxl" ];
dependencies = [
packaging
pillow
];
meta = {
description = "Pillow plugin for JPEG-XL, using Rust for bindings";
homepage = "https://github.com/Isotr0py/pillow-jpegxl-plugin";
+8 -16
View File
@@ -1,42 +1,34 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
autoPatchelfHook,
buildPythonPackage,
exiv2,
fetchFromGitHub,
lib,
pybind11,
python,
setuptools,
}:
buildPythonPackage rec {
pname = "pyexiv2";
version = "2.16.0";
version = "2.15.3";
pyproject = true;
src = fetchFromGitHub {
owner = "LeoHsiao1";
repo = "pyexiv2";
tag = "v${version}";
hash = "sha256-FH5nbbh0vaErJzBl6L2HPh0SQXkQ558abTBml7nSLU8=";
hash = "sha256-83bFMaoXncvhRJNcCgkkC7B29wR5pjuLO/EdkQdqxxo=";
};
buildInputs = [ exiv2.dev ];
build-system = [ setuptools ];
dependencies = [ pybind11 ];
postBuild = ''
lib_dir=$out/${python.sitePackages}/pyexiv2/lib
mkdir -p "$lib_dir"
cp -rT ${exiv2.lib}/lib "$lib_dir"
'';
nativeBuildInputs = [ autoPatchelfHook ];
buildInputs = [ exiv2.lib ];
pythonImportsCheck = [ "pyexiv2" ];
build-system = [ setuptools ];
meta = {
description = "Read and write image metadata, including EXIF, IPTC, XMP, ICC Profile";
homepage = "https://github.com/LeoHsiao1/pyexiv2";
+4 -4
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
lib,
@@ -21,7 +21,7 @@ let
stdenv.cc.cc
zstd
]
++ lib.optionals (!stdenv.hostPlatform.isDarwin) [
++ lib.optionals (!stdenv.isDarwin) [
dbus
libGL
libdrm
@@ -35,7 +35,7 @@ let
]
);
libraryPath = "${lib.optionalString pkgs.stdenv.hostPlatform.isDarwin "DY"}LD_LIBRARY_PATH";
libraryPath = "${lib.optionalString pkgs.stdenv.isDarwin "DY"}LD_LIBRARY_PATH";
python3Wrapped = pkgs.symlinkJoin {
inherit (python3)
@@ -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 reuse --group test
uv pip install --quiet --editable . --group docs --group extra --group test
cp pyproject.toml "''${venv}"/pyproject.toml
fi
+8 -13
View File
@@ -12,9 +12,9 @@ description = "A User-Focused Photo & File Management System."
version = "9.6.4"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.14,<3.15"
requires-python = ">=3.12,<3.14"
dependencies = [
"audioop-lts~=0.2.2",
"audioop-lts; python_version >= '3.13'",
"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~=1.5.0",
"pillow-heif~=0.22",
"pillow-jxl-plugin~=1.3",
"py7zr~=1.1.3",
"pydantic~=2.10",
"pydub~=0.25",
"PySide6==6.11.2",
"PySide6==6.8.0.*",
"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,11 +57,7 @@ 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 = "reuse" },
{ include-group = "ruff" },
]
lint = [{ include-group = "pyright" }, { include-group = "ruff" }]
test = [{ include-group = "pytest" }]
mkdocs = [
@@ -79,7 +75,6 @@ pytest = [
"pytest-qt==4.4.0",
"syrupy==5.1.0",
]
reuse = ["reuse==6.*"]
ruff = ["ruff==0.15.17"]
[tool.hatch.build.targets.wheel]
@@ -97,7 +92,7 @@ filterwarnings = [
ignore = [
".venv/**",
"src/tagstudio/core/library/json/",
"src/tagstudio/previews/vendored/pydub/",
"src/tagstudio/renderers/vendored/pydub/",
]
include = ["src/tagstudio", "tests"]
# Reference for the settings here: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
@@ -126,7 +121,7 @@ ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["D", "E402"]
"src/tagstudio/previews/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
"src/tagstudio/renderers/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
[tool.ruff.lint.pydocstyle]
convention = "google"
+1 -1
View File
@@ -41,7 +41,7 @@ datafiles = [
]
a = Analysis(
[Path(project_root, "__main__.py")],
[Path(project_root, "main.py")],
pathex=[],
binaries=[],
datas=datafiles,
+3 -66
View File
@@ -1,72 +1,9 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
"""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()
from tagstudio.main import main
if __name__ == "__main__":
sys.exit(main())
main()
+4 -4
View File
@@ -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.app_settings import AppSettings
from tagstudio.qt.global_settings import GlobalSettings
logger = structlog.get_logger(__name__)
# TODO: Turn into a BaseDriver class instead of a "Mixin".
class DriverMixin:
cached_values: QSettings
# TODO: AppSettings is Qt-specific and should not be in a base driver class.
settings: AppSettings
# TODO: GlobalSettings has become closely tied to Qt.
# Should there be a base Settings class?
settings: GlobalSettings
def evaluate_path(self, open_path: str | None) -> LibraryStatus:
"""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 = 400
DB_VERSION: int = 300
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;
+15 -13
View File
@@ -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,15 +99,17 @@ 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:
@@ -122,35 +124,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,10 +20,3 @@ 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)
+55 -57
View File
@@ -14,6 +14,7 @@ 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 (
@@ -81,7 +82,7 @@ from tagstudio.core.library.alchemy.fields import (
TextField,
TextFieldTemplate,
)
from tagstudio.core.library.alchemy.joins import CategoryExclusion, TagEntry, TagParent
from tagstudio.core.library.alchemy.joins import TagEntry, TagParent
from tagstudio.core.library.alchemy.migrations import DBMigrations, MigrationError
from tagstudio.core.library.alchemy.models import (
Entry,
@@ -668,32 +669,37 @@ 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:
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)
yield from Library._all_entries(session, with_joins)
@property
def tags(self) -> list[Tag]:
@@ -1320,7 +1326,6 @@ 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:
@@ -1337,9 +1342,6 @@ 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
@@ -1469,7 +1471,6 @@ 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))
@@ -1540,10 +1541,7 @@ class Library:
statement = select(Tag).where(Tag.id.in_(all_tag_ids))
statement = statement.options(
noload(Tag.parent_tags),
selectinload(Tag.aliases),
selectinload(Tag.category_exclusions),
joinedload(Tag.color),
noload(Tag.parent_tags), selectinload(Tag.aliases), joinedload(Tag.color)
)
tags = session.scalars(statement).fetchall()
for tag in tags:
@@ -1622,10 +1620,9 @@ 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, exclusion_ids)
self.add_tag(tag, parent_ids, aliases)
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."""
@@ -1748,35 +1745,36 @@ 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.
"""
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 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:
return 0
return version.value
def mirror_entry_fields(self, entries: list[Entry]) -> None:
"""Mirror fields among multiple Entry items."""
@@ -6,7 +6,6 @@ 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
@@ -22,10 +21,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 Entry, Tag, TagColorGroup, Version
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup, Version
from tagstudio.core.library.ignore import migrate_ext_list
from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.translations import Translations
logger = structlog.get_logger(__name__)
@@ -47,12 +46,15 @@ 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 = self._get_version(DB_VERSION_CURRENT_KEY)
self.initial_db_version = self._get_version(DB_VERSION_INITIAL_KEY)
self.loaded_db_version = Library._get_version(engine, DB_VERSION_CURRENT_KEY)
self.initial_db_version = Library._get_version(engine, DB_VERSION_INITIAL_KEY)
# ======================== Library Database Version Checking =======================
# DB_VERSION 6 is the first supported SQLite DB version.
@@ -82,8 +84,6 @@ 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,9 +100,11 @@ 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
@@ -135,27 +137,6 @@ 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.
@@ -256,10 +237,11 @@ 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.
# 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:
from tagstudio.core.library.alchemy.library import Library
for entry in Library._all_entries(session):
entry.filename = entry.path.name
session.merge(entry)
session.flush()
@@ -596,23 +578,3 @@ 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()
+8 -20
View File
@@ -16,7 +16,7 @@ from tagstudio.core.library.alchemy.fields import (
DatetimeField,
TextField,
)
from tagstudio.core.library.alchemy.joins import CategoryExclusion, TagParent
from tagstudio.core.library.alchemy.joins import 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,19 +97,13 @@ 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(
@@ -130,24 +124,19 @@ 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()
@@ -160,7 +149,6 @@ 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
@@ -181,16 +169,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
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
from pathlib import Path
import structlog
from wcmatch import glob, pathlib
from wcmatch import pathlib
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
@@ -54,7 +54,7 @@ class UnlinkedRegistry:
# NOTE: ignore_to_glob() is needed for wcmatch, not ripgrep.
ignore_patterns = ignore_to_glob(Ignore.get_patterns(library_dir))
for path in pathlib.Path(str(library_dir)).glob(
patterns=f"***/{glob.escape(match_entry.path.name)}",
f"***/{match_entry.path.name}",
flags=PATH_GLOB_FLAGS,
exclude=ignore_patterns,
):
+1 -1
View File
@@ -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."
)
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 "-:--"
+80
View File
@@ -0,0 +1,80 @@
#!/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()
+1 -1
View File
@@ -12,7 +12,7 @@ import structlog
from PIL import Image
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
from tagstudio.qt.app_settings import DEFAULT_CACHED_THUMB_QUALITY, DEFAULT_THUMB_CACHE_SIZE
from tagstudio.qt.global_settings import DEFAULT_CACHED_THUMB_QUALITY, DEFAULT_THUMB_CACHE_SIZE
logger = structlog.get_logger(__name__)
@@ -6,9 +6,13 @@ 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.styles.stylesheets import (
from tagstudio.qt.views.stylesheets.stylesheets import (
autofill_scroll_top_focus_style,
autofill_scroll_top_style,
)
@@ -77,4 +81,4 @@ class AutofillLineEdit(QLineEdit):
# Filter out icon action(s)
if action.text():
menu.addAction(action)
menu.exec(self.mapToGlobal(pos))
menu.exec(self.mapToGlobal(pos)) # pyright: ignore[reportArgumentType]
@@ -9,14 +9,13 @@ from tagstudio.core.library.alchemy.fields import (
DatetimeFieldTemplate,
TextFieldTemplate,
)
from tagstudio.i18n.translations import Translations
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.edit_field_template_modal_view import EditFieldTemplateModalView
from tagstudio.qt.views.styles.stylesheets import line_edit_style
from tagstudio.qt.views.stylesheets.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"],
@@ -11,7 +11,6 @@ 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,21 +11,20 @@ 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 import FieldTemplateWidget
from tagstudio.qt.controllers.field_template_widget_controller 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: AppSettings, placeholder_text: str = ""):
def __init__(self, library: Library, settings: GlobalSettings, placeholder_text: str = ""):
super().__init__(library, settings, placeholder_text)
# Context Menu Actions
@@ -156,7 +155,6 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
widget.setHidden(True)
self.layout().content_layout.addWidget(widget)
item = unwrap(self.layout().content_layout.itemAt(index))
widget_: QWidget = unwrap(item.widget())
widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
assert isinstance(widget_, UnderlinedWidget)
return widget_
@@ -11,13 +11,12 @@ 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 import FieldTemplateWidget
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.modal_content import ModalContent
from tagstudio.qt.controllers.search_panel import SearchPanel
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.search_panel_view import SearchPanelView
logger = structlog.get_logger(__name__)
@@ -174,7 +173,6 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
pad_field_template_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_field_template_widget)
item = unwrap(self.layout().scroll_layout.itemAt(index))
field_template_widget: QWidget = unwrap(item.widget())
field_template_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
assert isinstance(field_template_widget, FieldTemplateWidget)
return field_template_widget
@@ -7,11 +7,10 @@ from PySide6.QtCore import QEvent, Qt
from PySide6.QtGui import QAction, QEnterEvent
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.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__()
@@ -1,55 +0,0 @@
# 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)
@@ -9,22 +9,21 @@ from PySide6 import QtGui
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.registries.ignored_registry import IgnoredRegistry
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.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.styles.stylesheets import header
from tagstudio.qt.views.stylesheets.stylesheets import header
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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)
@@ -19,7 +19,6 @@ 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)
@@ -19,21 +19,20 @@ from tagstudio.core.library.alchemy.constants import (
)
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils import file_opener
from tagstudio.qt.views.library_info_window_view import LibraryInfoWindowView
from tagstudio.qt.views.styles.stylesheets import header
from tagstudio.qt.views.stylesheets.stylesheets import header
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# pyright: reportOptionalMemberAccess=false
from typing import override
import structlog
@@ -9,14 +9,12 @@ from PySide6 import QtCore, QtGui
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.mixed.paged_panel_state import PagedPanelState
from tagstudio.qt.views.styles.stylesheets import header
from tagstudio.qt.controllers.paged_panel_state import PagedPanelState
from tagstudio.qt.views.stylesheets.stylesheets import header
logger = structlog.get_logger(__name__)
# TODO: Split to use MVC guidelines, or remove the Paged[...] classes.
class PagedPanel(QWidget):
"""A paginated modal panel."""
@@ -97,7 +95,7 @@ class PagedPanel(QWidget):
# Update Body Widget
if self.body_layout.itemAt(0):
self.body_layout.itemAt(0).widget().setHidden(True)
self.body_layout.removeWidget(unwrap(self.body_layout.itemAt(0).widget()))
self.body_layout.removeWidget(self.body_layout.itemAt(0).widget())
self.body_layout.addWidget(frame.body_wrapper)
self.body_layout.itemAt(0).widget().setHidden(False)
@@ -4,7 +4,7 @@
from PySide6.QtWidgets import QPushButton
from tagstudio.qt.mixed.paged_body_wrapper import PagedBodyWrapper
from tagstudio.qt.views.paged_body_wrapper import PagedBodyWrapper
class PagedPanelState:
@@ -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.i18n.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.controllers.edit_text import EditText
from tagstudio.qt.controllers.edit_text_controller 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.views.inspector_view import InspectorView
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
@@ -44,8 +44,8 @@ class _ItemMode(IntEnum):
FIELD = 2
class Inspector(QWidget):
def __init__(self, driver: QtDriver) -> None:
class PreviewPanel(QWidget):
def __init__(self, driver: "QtDriver") -> None:
super().__init__()
self._driver = driver
self._lib = self._driver.lib
@@ -68,7 +68,7 @@ class Inspector(QWidget):
self,
)
self.setLayout(InspectorView(driver=driver, pixel_ratio=self.devicePixelRatio()))
self.setLayout(PreviewPanelView(driver=driver, pixel_ratio=self.devicePixelRatio()))
self._set_item_mode(None)
self._connect_callbacks()
@@ -276,7 +276,7 @@ class Inspector(QWidget):
logger.error("[Preview Panel] Error updating selection", error=e)
def stop_media_playback(self) -> None:
"""Stop any media playback in the inspector."""
"""Stop any media playback in the preview panel."""
self.layout().preview_thumb.media_player.stop()
@property
@@ -284,5 +284,5 @@ class Inspector(QWidget):
return self.layout().containers
@override
def layout(self) -> InspectorView:
def layout(self) -> PreviewPanelView:
return super().layout() # pyright: ignore[reportReturnType]
@@ -19,21 +19,20 @@ from rawpy import (
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.media_types import MediaCategories
from tagstudio.previews.video_tester import is_readable_video
from tagstudio.qt.helpers.file_tester import is_readable_video
from tagstudio.qt.mixed.file_attributes import FileAttributeData
from tagstudio.qt.utils.file_opener import open_file
from tagstudio.qt.views.preview_thumb_view import PreviewThumbView
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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
@@ -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.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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:
if parent is not None: # pyright: ignore[reportUnnecessaryComparison]
parent.hide()
return
+11 -12
View File
@@ -11,15 +11,14 @@ 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.views.styles.color_overlay import auto_theme_overlay
from tagstudio.qt.views.styles.stylesheets import (
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import (
autofill_line_edit_style,
autofill_line_edit_top_style,
)
@@ -50,7 +49,9 @@ class SuggestBox[T](QWidget):
item_chosen = Signal(object)
done = Signal(str) # Query
def __init__(self, library: Library, settings: AppSettings, placeholder_text: str = "") -> None:
def __init__(
self, library: Library, settings: GlobalSettings, placeholder_text: str = ""
) -> None:
super().__init__()
self._lib = library
self._settings = settings
@@ -111,7 +112,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() # pyright: ignore
underlined_widget = self.layout().content_layout.itemAt(i).widget()
assert isinstance(underlined_widget, UnderlinedWidget)
if held and i == self._selection_index:
@@ -129,7 +130,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 = unwrap(self.layout().content_layout.itemAt(i).widget()) # pyright: ignore
widget = self.layout().content_layout.itemAt(i).widget()
if not widget.isHidden():
widget_count += 1
@@ -150,7 +151,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() # pyright: ignore
underlined_widget = self.layout().content_layout.itemAt(i).widget()
assert isinstance(underlined_widget, UnderlinedWidget)
if i == self._selection_index:
underlined_widget.toggle_underline(is_hidden=False)
@@ -217,9 +218,7 @@ 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(), # pyright: ignore
xmargin=16,
ymargin=0,
self.layout().content_layout.itemAt(0).widget(), xmargin=16, ymargin=0
)
# Get results for the search query
@@ -17,18 +17,17 @@ from tagstudio.qt.mixed.build_tag import BuildTagPanel
from tagstudio.qt.views.tag_box_view import TagBoxWidgetView
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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
@@ -89,7 +88,6 @@ 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()
@@ -12,12 +12,11 @@ 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 import SearchPanel
from tagstudio.qt.controllers.search_panel_controller 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__)
@@ -167,10 +166,7 @@ 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,
exclusion_ids=edit_item_panel.exclusion_ids,
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
)
if choose_item:
@@ -192,7 +188,6 @@ 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())
@@ -208,7 +203,6 @@ class TagSearchPanel(SearchPanel[Tag]):
pad_tag_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_tag_widget)
item = unwrap(self.layout().scroll_layout.itemAt(index))
tag_widget: QWidget = unwrap(item.widget())
tag_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
assert isinstance(tag_widget, TagWidget)
return tag_widget
@@ -12,15 +12,14 @@ 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__)
@@ -28,7 +27,9 @@ logger = structlog.get_logger(__name__)
class TagSuggestBox(SuggestBox[Tag]):
search_for_tag = Signal(int)
def __init__(self, library: Library, settings: AppSettings, placeholder_text: str = "") -> None:
def __init__(
self, library: Library, settings: GlobalSettings, placeholder_text: str = ""
) -> None:
super().__init__(library, settings, placeholder_text)
# Context Menu Actions
@@ -157,10 +158,7 @@ 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,
exclusion_ids=edit_item_panel.exclusion_ids,
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
)
self._on_item_chosen(tag)
@@ -176,7 +174,6 @@ 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())
@@ -192,7 +189,6 @@ class TagSuggestBox(SuggestBox[Tag]):
widget.setHidden(True)
self.layout().content_layout.addWidget(widget)
item = unwrap(self.layout().content_layout.itemAt(index))
widget_: QWidget = unwrap(item.widget())
widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
assert isinstance(widget_, UnderlinedWidget)
return widget_
@@ -6,13 +6,10 @@ 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))
@@ -22,7 +19,7 @@ class UnderlinedWidget(QWidget):
@property
def widget(self) -> QWidget:
return unwrap(unwrap(self.layout().itemAt(0)).widget())
return self.layout().itemAt(0).widget()
@override
def layout(self) -> UnderlinedWidgetView:
@@ -14,16 +14,15 @@ 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.i18n.translations import Translations
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.views.styles.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
logger = structlog.get_logger(__name__)
# TODO: Split view into UpdateAvailableMessageBoxView
class UpdateAvailableMessageBox(QMessageBox):
"""A message box notifying the user if there is a new version of TagStudio available."""
"""A warning dialog for if the TagStudio is not running under the latest release version."""
def __init__(self):
super().__init__()
@@ -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 AppSettings(BaseModel):
class GlobalSettings(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 AppSettings(BaseModel):
loaded_from: Path = Field(default=DEFAULT_GLOBAL_SETTINGS_PATH, exclude=True)
@staticmethod
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> AppSettings:
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings":
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 = AppSettings(**settings_data, loaded_from=path)
settings = GlobalSettings(**settings_data, loaded_from=path)
return settings
return AppSettings(loaded_from=path)
return GlobalSettings(loaded_from=path)
def save(self, path: Path | None = None) -> None:
if path is None:
@@ -6,7 +6,7 @@ from PIL import Image
from PySide6.QtCore import Qt
from PySide6.QtGui import QGuiApplication
from tagstudio.previews.gradients import linear_gradient
from tagstudio.qt.helpers.gradients import linear_gradient
# TODO: Consolidate the built-in QT theme values with the values
# here, in enums.py, and in palette.py.
@@ -1,12 +1,12 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
from pathlib import Path
import ffmpeg
from tagstudio.previews.vendored.probe import probe
from tagstudio.renderers.vendored.probe import probe
def is_readable_video(filepath: Path | str):
+3 -4
View File
@@ -32,15 +32,14 @@ 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.styles.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.views.styles.stylesheets import form_content_style, header
from tagstudio.qt.views.stylesheets.stylesheets import form_content_style, header
# TODO: Split to use MVC guidelines.
class AboutModal(QWidget):
"""Modal window showing information about the TagStudio application."""
+3 -4
View File
@@ -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.views.styles.palette import ColorType, get_tag_color
from tagstudio.qt.views.styles.stylesheets import (
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import (
checkbox_style,
line_edit_style,
list_button_style,
@@ -37,7 +37,6 @@ from tagstudio.qt.views.styles.stylesheets import (
logger = structlog.get_logger(__name__)
# TODO: Split to use MVC guidelines.
class BuildColorPanel(ModalContent):
on_edit = Signal(TagColorGroup)
+2 -3
View File
@@ -13,14 +13,13 @@ 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.views.styles.stylesheets import line_edit_style
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import line_edit_style
logger = structlog.get_logger(__name__)
# TODO: Split to use MVC guidelines.
class BuildNamespacePanel(ModalContent):
on_edit = Signal(Namespace)
+38 -186
View File
@@ -14,7 +14,6 @@ from PySide6.QtWidgets import (
QButtonGroup,
QCheckBox,
QFrame,
QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QLineEdit,
@@ -29,17 +28,16 @@ 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 import TagSearchPanel
from tagstudio.qt.controllers.tag_search_panel_controller 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.styles.stylesheets import (
from tagstudio.qt.views.stylesheets.stylesheets import (
checkbox_style,
colored_checkbox_style,
colored_radio_button_style,
get_tag_border_color,
get_tag_highlight_color,
@@ -77,7 +75,6 @@ class CustomTableItem(QLineEdit):
super().keyPressEvent(arg__1)
# TODO: Split to use MVC guidelines.
class BuildTagPanel(ModalContent):
on_edit = Signal(Tag)
@@ -89,10 +86,9 @@ 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, 640)
self.setMinimumSize(300, 460)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
@@ -100,6 +96,7 @@ 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)
@@ -114,6 +111,7 @@ 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)
@@ -125,6 +123,7 @@ 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)
@@ -145,14 +144,16 @@ 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(header(Translations["tag.parent_tags"], 3))
self.parent_tags_title = QLabel(Translations["tag.parent_tags"])
self.parent_tags_layout.addWidget(self.parent_tags_title)
self.scroll_contents = QWidget()
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
@@ -183,40 +184,14 @@ 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(header(Translations["tag.color"], 3))
self.color_title = QLabel(Translations["tag.color"])
self.color_layout.addWidget(self.color_title)
self.color_button: TagColorPreview
try:
@@ -240,6 +215,7 @@ 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)
@@ -253,6 +229,7 @@ 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)
@@ -267,33 +244,16 @@ 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, stretch=1)
self.root_layout.addWidget(self.aliases_table)
self.root_layout.addWidget(self.aliases_add_button)
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.parent_tags_widget)
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()
@@ -325,12 +285,10 @@ 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)
@@ -357,130 +315,9 @@ 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() # pyright: ignore[reportOptionalMemberAccess]
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
c = QWidget()
layout = QVBoxLayout(c)
@@ -509,12 +346,29 @@ 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()
@@ -541,7 +395,9 @@ 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(*self._tag_colors(tag)))
disam_button.setStyleSheet(
colored_radio_button_style(primary_color, text_color, border_color, highlight_color)
)
self.disam_button_group.addButton(disam_button)
if is_disambiguation:
@@ -622,10 +478,6 @@ 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
+5 -6
View File
@@ -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.styles.stylesheets import add_button_style
from tagstudio.qt.views.stylesheets.stylesheets import add_button_style
if typing.TYPE_CHECKING:
from tagstudio.core.library.alchemy.library import Library
@@ -26,15 +26,14 @@ 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
@@ -60,7 +59,7 @@ class ColorBoxWidget(FieldWidget):
color_widgets: list[TagColorLabel] = []
while self.base_layout.itemAt(0):
unwrap(self.base_layout.takeAt(0)).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
unwrap(self.base_layout.takeAt(0)).widget().deleteLater()
for color in colors_:
color_widget = TagColorLabel(
+3 -4
View File
@@ -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.styles.stylesheets import title_line_edit_style
from tagstudio.qt.views.stylesheets.stylesheets import title_line_edit_style
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
@@ -39,9 +39,8 @@ 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)
+12 -6
View File
@@ -11,14 +11,21 @@ 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.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.translations import Translations
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
@@ -30,11 +37,10 @@ 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
+8 -9
View File
@@ -29,28 +29,27 @@ 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.i18n.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.controllers.edit_text import EditText
from tagstudio.qt.controllers.edit_text_controller import EditText
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.tag_box import TagBoxWidget
from tagstudio.qt.controllers.tag_box_controller import TagBoxWidget
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
from tagstudio.qt.mixed.field_widget import FieldContainer
from tagstudio.qt.mixed.text_field import TextContainerWidget
from tagstudio.qt.views.styles.stylesheets import inset_container_style
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.views.stylesheets.stylesheets import inset_container_style
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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
@@ -186,7 +185,7 @@ class FieldContainers(QWidget):
grandparent_tags: set[Tag] = set()
for parent_tag in parent_tags:
if parent_tag in categories and parent_tag.id not in tag.exclusion_ids:
if parent_tag in categories:
categories[parent_tag].add(tag)
has_category_parent = True
grandparent_tags.update(parent_tag.parent_tags)
@@ -321,7 +320,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>"
+7 -11
View File
@@ -12,15 +12,13 @@ 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.core.utils.types import unwrap
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
from tagstudio.qt.views.styles.stylesheets import container_style, header
from tagstudio.qt.views.stylesheets.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)
@@ -137,19 +135,17 @@ class FieldContainer(QWidget):
if callback:
self.remove_button.clicked.connect(callback)
def set_inner_widget(self, widget: FieldWidget) -> None:
item = self.field_layout.itemAt(0)
if item:
old: QWidget = unwrap(item.widget())
def set_inner_widget(self, widget: "FieldWidget") -> None:
if self.field_layout.itemAt(0):
old: QWidget = self.field_layout.itemAt(0).widget()
self.field_layout.removeWidget(old)
old.deleteLater()
self.field_layout.addWidget(widget)
def get_inner_widget(self) -> QWidget | None:
item = self.field_layout.itemAt(0)
if item:
return item.widget()
if self.field_layout.itemAt(0):
return self.field_layout.itemAt(0).widget()
return None
def set_title(self, title: str) -> None:
+11 -11
View File
@@ -21,14 +21,13 @@ 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.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
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
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
@@ -40,9 +39,10 @@ class FileAttributeData:
duration: int | None = None
# TODO: Split to use MVC guidelines.
class FileAttributes(QWidget):
def __init__(self, library: Library, driver: QtDriver):
"""The Preview Panel Widget."""
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) # pyright: ignore[reportOptionalMemberAccess]
self.layout().setSpacing(0)
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) # pyright: ignore[reportOptionalMemberAccess]
self.layout().setSpacing(6)
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) # pyright: ignore[reportOptionalMemberAccess]
self.layout().setSpacing(0)
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)
+5 -5
View File
@@ -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.views.styles.stylesheets import header
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import header
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
# TODO: Split to use MVC guidelines.
# TODO: Break up into MVC classes, similar to fix_ignored_modal
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 -10
View File
@@ -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.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.merge_dupe_entries import MergeDuplicateEntries
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.mixed.relink_entries_modal import RelinkUnlinkedEntries
from tagstudio.qt.mixed.remove_unlinked_modal import RemoveUnlinkedEntriesModal
from tagstudio.qt.views.styles.stylesheets import header
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import header
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
# TODO: Split to use MVC guidelines.
# TODO: Break up into MVC classes, similar to fix_ignored_modal
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 = MergeDuplicateEntriesProgress(self.lib, self.driver)
self.relink_class = RelinkUnlinkedEntriesProgress(self.tracker)
self.merge_class = MergeDuplicateEntries(self.lib, self.driver)
self.relink_class = RelinkUnlinkedEntries(self.tracker)
self.search_button = QPushButton(Translations["entries.unlinked.search_and_relink"])
self.relink_class.done.connect(
+7 -8
View File
@@ -1,13 +1,11 @@
# 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
@@ -21,26 +19,27 @@ 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.i18n.translations import Translations
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
from tagstudio.qt.views.styles.palette import ColorType, get_tag_color
from tagstudio.qt.views.styles.stylesheets import header
from tagstudio.qt.views.stylesheets.stylesheets import header
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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
@@ -166,7 +165,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
+16 -6
View File
@@ -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.i18n.platform_strings import open_file_str, trash_term
from tagstudio.i18n.translations import Translations
from tagstudio.qt.platform_strings import open_file_str, trash_term
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.file_opener import FileOpenerHelper
from tagstudio.qt.views.layouts.flow_layout import FlowWidget
from tagstudio.qt.views.thumb_button import ThumbButton
if TYPE_CHECKING:
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
@@ -60,7 +60,6 @@ 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.)."""
@@ -96,7 +95,7 @@ class ItemThumb(FlowWidget):
self,
mode: ItemType | None,
library: Library,
driver: QtDriver,
driver: "QtDriver",
thumb_size: tuple[int, int],
show_filename_label: bool = False,
):
@@ -311,6 +310,17 @@ class ItemThumb(FlowWidget):
else None
)
)
self.thumb_button.clicked.connect(
lambda: (
self.toggle_item_selection()
if (
QGuiApplication.keyboardModifiers() != Qt.KeyboardModifier.ControlModifier
and self.thumb_button.selected
)
else None
)
)
self.set_mode(mode)
@property
@@ -429,7 +439,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)
+4 -5
View File
@@ -11,25 +11,24 @@ 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.views.styles.color_overlay import auto_theme_overlay
from tagstudio.qt.translations import Translations
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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()
+6 -7
View File
@@ -36,26 +36,25 @@ from PySide6.QtWidgets import (
QWidget,
)
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.clickable_slider import ClickableSlider
from tagstudio.qt.views.styles.color_overlay import light_overlay
from tagstudio.qt.helpers.color_overlay import light_overlay
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.clickable_slider import ClickableSlider
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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(
@@ -8,17 +8,18 @@ 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.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.translations import Translations
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
class MergeDuplicateEntriesProgress(QObject):
class MergeDuplicateEntries(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
+38 -30
View File
@@ -5,7 +5,6 @@
import traceback
from pathlib import Path
from typing import cast
from warnings import deprecated
import structlog
import wcmatch.fnmatch as fnmatch
@@ -43,18 +42,17 @@ 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.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.controllers.paged_panel_controller import PagedPanel
from tagstudio.qt.controllers.paged_panel_state import PagedPanelState
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.custom_runnable import CustomRunnable
from tagstudio.qt.utils.function_iterator import FunctionIterator
from tagstudio.qt.views.styles.stylesheets import header
from tagstudio.qt.views.paged_body_wrapper import PagedBodyWrapper
from tagstudio.qt.views.stylesheets.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."""
@@ -98,8 +96,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) # pyright: ignore[reportOptionalMemberAccess]
body_wrapper.layout().setContentsMargins(0, 36, 0, 0) # pyright: ignore[reportOptionalMemberAccess]
body_wrapper.layout().addWidget(body_label)
body_wrapper.layout().setContentsMargins(0, 36, 0, 0)
cancel_button = QPushButton(Translations["generic.cancel"])
next_button = QPushButton(Translations["generic.continue"])
@@ -287,9 +285,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) # pyright: ignore[reportOptionalMemberAccess]
self.body_wrapper_01.layout().addWidget(desc_label) # pyright: ignore[reportOptionalMemberAccess]
self.body_wrapper_01.layout().setSpacing(12) # pyright: ignore[reportOptionalMemberAccess]
self.body_wrapper_01.layout().addWidget(body_container)
self.body_wrapper_01.layout().addWidget(desc_label)
self.body_wrapper_01.layout().setSpacing(12)
back_button = QPushButton(Translations["generic.navigation.back"])
start_button = QPushButton(Translations["json_migration.start_and_preview"])
@@ -352,22 +350,31 @@ class JsonMigrationModal(QObject):
def migration_progress(self, skip_ui: bool = False):
"""Initialize the progress bar and iterator for the library migration."""
pb = QProgressDialog("", "", 0, 0)
pb.setCancelButton(None)
self.body_wrapper_01.layout().addWidget(pb) # pyright: ignore[reportOptionalMemberAccess]
pb = QProgressDialog(
labelText="",
cancelButtonText="",
minimum=0,
maximum=0,
)
pb.setCancelButton(None) # pyright: ignore[reportArgumentType]
self.body_wrapper_01.layout().addWidget(pb)
try:
iterator = FunctionIterator(self.migration_iterator)
if skip_ui:
iterator.run()
return
iterator.value.connect(lambda x: pb.setLabelText(header(x, 4)))
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 (),
)
)
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),
@@ -416,6 +423,7 @@ 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
@@ -475,26 +483,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
label: QLabel = self.old_content_layout.itemAtPosition(self.entries_row, 1).widget() # pyright: ignore[reportAssignmentType]
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
label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # pyright: ignore[reportAssignmentType]
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
warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
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.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
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: 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.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,19 +10,18 @@ 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.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.translations import Translations
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt 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"])
+3 -3
View File
@@ -1,7 +1,8 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# pyright: reportOptionalMemberAccess=false
"""A pagination widget created for TagStudio."""
from typing import cast, override
from warnings import catch_warnings
@@ -11,11 +12,10 @@ from PySide6.QtCore import QSize, Signal
from PySide6.QtGui import QIntValidator, QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QWidget
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
# TODO: Split to use MVC guidelines.
class Pagination(QWidget):
"""Widget containing controls for navigating between pages of items."""
@@ -11,7 +11,6 @@ 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."""
@@ -27,10 +26,10 @@ class ProgressWidget(QWidget):
super().__init__()
self.root = QVBoxLayout(self)
self.pb = QProgressDialog(
label_text,
cancel_button_text, # pyright: ignore[reportArgumentType]
minimum,
maximum,
labelText=label_text,
minimum=minimum,
cancelButtonText=cancel_button_text, # pyright: ignore[reportArgumentType]
maximum=maximum,
)
self.root.addWidget(self.pb)
self.setFixedSize(432, 112)
@@ -5,11 +5,11 @@
from PySide6.QtCore import QObject, Signal
from tagstudio.core.library.alchemy.registries.unlinked_registry import UnlinkedRegistry
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.translations import Translations
class RelinkUnlinkedEntriesProgress(QObject):
class RelinkUnlinkedEntries(QObject):
done = Signal()
def __init__(self, tracker: UnlinkedRegistry):
+13 -6
View File
@@ -7,22 +7,29 @@ from typing import TYPE_CHECKING, override
from PySide6 import QtCore, QtGui
from PySide6.QtCore import Qt, QThreadPool, Signal
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.library.alchemy.registries.ignored_registry import IgnoredRegistry
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.custom_runnable import CustomRunnable
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
# TODO: Split to use MVC guidelines.
class RemoveIgnoredModal(QWidget):
done = Signal()
def __init__(self, driver: QtDriver, tracker: IgnoredRegistry):
def __init__(self, driver: "QtDriver", tracker: IgnoredRegistry):
super().__init__()
self.driver = driver
self.tracker = tracker
@@ -7,22 +7,29 @@ from typing import TYPE_CHECKING, override
from PySide6 import QtCore, QtGui
from PySide6.QtCore import Qt, QThreadPool, Signal
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.library.alchemy.registries.unlinked_registry import UnlinkedRegistry
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.mixed.progress_bar import ProgressWidget
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.custom_runnable import CustomRunnable
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
# TODO: Split to use MVC guidelines.
class RemoveUnlinkedEntriesModal(QWidget):
done = Signal()
def __init__(self, driver: QtDriver, tracker: UnlinkedRegistry):
def __init__(self, driver: "QtDriver", tracker: UnlinkedRegistry):
super().__init__()
self.driver = driver
self.tracker = tracker
+9 -10
View File
@@ -20,8 +20,9 @@ from PySide6.QtWidgets import (
)
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
from tagstudio.i18n.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations
from tagstudio.qt.app_settings import (
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.modal_content import ModalContent
from tagstudio.qt.global_settings import (
DEFAULT_CACHED_THUMB_RES,
DEFAULT_THUMB_CACHE_SIZE,
MAX_CACHED_THUMB_RES,
@@ -30,18 +31,16 @@ from tagstudio.qt.app_settings import (
Splash,
Theme,
)
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.modal_content import ModalContent
from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
# TODO: Split to use MVC guidelines.
class SettingsPanel(ModalContent):
driver: QtDriver
driver: "QtDriver"
filepath_option_map: dict[ShowFilepathOption, str] = {
ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"],
@@ -88,7 +87,7 @@ class SettingsPanel(ModalContent):
"%Y.%m.%d": "2024.08.21",
}
def __init__(self, driver: QtDriver):
def __init__(self, driver: "QtDriver"):
super().__init__()
# set these "constants" because language will be loaded from config shortly after startup
# and we want to use the current language for the dropdowns
@@ -400,7 +399,7 @@ class SettingsPanel(ModalContent):
"splash": self.splash_combobox.currentData(),
}
def update_settings(self, driver: QtDriver):
def update_settings(self, driver: "QtDriver"):
settings = self.get_settings()
driver.settings.language = settings["language"]
@@ -440,7 +439,7 @@ class SettingsPanel(ModalContent):
)
@classmethod
def build_modal(cls, driver: QtDriver) -> Modal:
def build_modal(cls, driver: "QtDriver") -> Modal:
settings_panel = cls(driver)
modal = Modal(
+4 -4
View File
@@ -10,9 +10,9 @@ from PySide6.QtGui import QAction, QColor, QEnterEvent
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
from tagstudio.core.library.alchemy.models import TagColorGroup
from tagstudio.i18n.translations import Translations
from tagstudio.qt.helpers.escape_text import escape_text
from tagstudio.qt.views.styles.stylesheets import (
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import (
get_tag_border_color,
get_tag_highlight_color,
get_tag_text_color,
@@ -22,11 +22,11 @@ from tagstudio.qt.views.styles.stylesheets import (
logger = structlog.get_logger(__name__)
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from tagstudio.core.library.alchemy.library import Library
# TODO: Split to use MVC guidelines.
class TagColorLabel(QWidget):
"""A widget for displaying a tag color's name.
@@ -41,7 +41,7 @@ class TagColorLabel(QWidget):
color: TagColorGroup | None,
has_edit: bool,
has_remove: bool,
library: Library | None = None,
library: "Library | None" = None,
) -> None:
super().__init__()
self.color = color
+6 -8
View File
@@ -23,27 +23,26 @@ from PySide6.QtWidgets import (
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
from tagstudio.core.enums import Theme
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_namespace import BuildNamespacePanel
from tagstudio.qt.mixed.color_box import ColorBoxWidget
from tagstudio.qt.mixed.field_widget import FieldContainer
from tagstudio.qt.views.styles.stylesheets import header
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import header
logger = structlog.get_logger(__name__)
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from tagstudio.qt.qt_driver import QtDriver
from tagstudio.qt.ts_qt import QtDriver
# TODO: Split to use MVC guidelines.
class TagColorManager(QWidget):
create_namespace_modal: Modal | None = None
def __init__(
self,
driver: QtDriver,
driver: "QtDriver",
):
super().__init__()
self.driver = driver
@@ -167,8 +166,7 @@ class TagColorManager(QWidget):
def reset(self):
while self.scroll_layout.count():
item = unwrap(self.scroll_layout.itemAt(0))
widget = unwrap(item.widget())
widget = self.scroll_layout.itemAt(0).widget()
self.scroll_layout.removeWidget(widget)
widget.deleteLater()
self.is_initialized = False
+4 -5
View File
@@ -11,9 +11,9 @@ from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget
from tagstudio.core.library.alchemy.enums import TagColorEnum
from tagstudio.core.library.alchemy.models import TagColorGroup
from tagstudio.i18n.translations import Translations
from tagstudio.qt.views.styles.palette import ColorType, get_tag_color
from tagstudio.qt.views.styles.stylesheets import (
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import (
get_tag_border_color,
get_tag_highlight_color,
get_tag_text_color,
@@ -26,13 +26,12 @@ if typing.TYPE_CHECKING:
logger = structlog.get_logger(__name__)
# TODO: Split to use MVC guidelines.
class TagColorPreview(QWidget):
on_click = Signal()
def __init__(
self,
library: Library,
library: "Library",
tag_color_group: TagColorGroup | None,
) -> None:
super().__init__()
@@ -20,11 +20,11 @@ from PySide6.QtWidgets import (
from tagstudio.core.library.alchemy.enums import TagColorEnum
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import TagColorGroup
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.modal_content import ModalContent
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
from tagstudio.qt.views.styles.palette import ColorType, get_tag_color
from tagstudio.qt.views.styles.stylesheets import (
from tagstudio.qt.views.stylesheets.stylesheets import (
color_swatch_style,
get_tag_border_color,
get_tag_highlight_color,
@@ -35,7 +35,6 @@ from tagstudio.qt.views.styles.stylesheets import (
logger = structlog.get_logger(__name__)
# TODO: Split to use MVC guidelines.
class TagColorSelection(ModalContent):
def __init__(self, library: Library):
super().__init__()
+4 -5
View File
@@ -12,10 +12,10 @@ from PySide6.QtWidgets import QHBoxLayout, QLineEdit, QPushButton, QSizePolicy,
from tagstudio.core.library.alchemy.enums import TagColorEnum
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.i18n.translations import Translations
from tagstudio.qt.helpers.escape_text import escape_text
from tagstudio.qt.views.styles.palette import ColorType, get_tag_color
from tagstudio.qt.views.styles.stylesheets import (
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import (
get_tag_border_color,
get_tag_highlight_color,
get_tag_primary_color,
@@ -31,7 +31,6 @@ if TYPE_CHECKING:
from tagstudio.core.library.alchemy.library import Library
# TODO: Split to use MVC guidelines.
class TagAliasWidget(QWidget):
on_remove = Signal()
@@ -111,7 +110,7 @@ class TagWidget(QWidget):
tag: Tag | None
def __init__(
self, tag: Tag | None, has_edit: bool, has_remove: bool, library: Library | None = None
self, tag: Tag | None, has_edit: bool, has_remove: bool, library: "Library | None" = None
) -> None:
super().__init__()
self.tag = tag
-1
View File
@@ -10,7 +10,6 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel
from tagstudio.qt.mixed.field_widget import FieldWidget
# TODO: Split to use MVC guidelines.
class TextContainerWidget(FieldWidget):
def __init__(self, title: str, text: str) -> None:
super().__init__(title)
@@ -35,7 +35,7 @@ def get_wanted_mnemonics(text: str) -> list[str]:
return matches
def sanitize_mnemonics(actions: list[QAction]) -> None:
def sanitise_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()]
# sanitize mnemonics to prevent deadlocks
sanitize_mnemonics(actions)
# sanitise mnemonics to prevent deadlocks
sanitise_mnemonics(actions)
# Sequence map: mnemonic key -> QAction
sequence_to_action: dict[str, QAction] = {}
@@ -1,12 +1,12 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
"""A collection of platform-dependant strings."""
import platform
from tagstudio.i18n.translations import Translations
from tagstudio.qt.translations import Translations
def open_file_str() -> str:
@@ -17,41 +17,37 @@ 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.previews.gradients import four_corner_gradient
from tagstudio.previews.renderers.archive import (
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 (
apple_embedded_thumb,
archive_thumb,
krita_thumb,
open_doc_thumb,
powerpoint_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
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
ImageFile.LOAD_TRUNCATED_IMAGES = True
Image.MAX_IMAGE_PIXELS = None
@@ -66,7 +62,7 @@ class FileRenderer:
rm: ResourceManager = ResourceManager()
cached_img_ext: str = ".webp"
def __init__(self, library: Library, settings: AppSettings) -> None:
def __init__(self, library: Library, settings: GlobalSettings) -> None:
super().__init__()
self.lib = library
self.settings = settings
+3 -3
View File
@@ -8,9 +8,9 @@ from PySide6.QtCore import QObject, QSize, Signal
from PySide6.QtGui import QGuiApplication, QPixmap, Qt
from tagstudio.core.library.alchemy.library import Library
from tagstudio.previews.file_renderer import FileRenderer
from tagstudio.qt.app_settings import AppSettings, Theme
from tagstudio.qt.cache_manager import CacheManager
from tagstudio.qt.global_settings import GlobalSettings, Theme
from tagstudio.qt.previews.renderer import FileRenderer
class QtFileRenderer(QObject):
@@ -19,7 +19,7 @@ class QtFileRenderer(QObject):
updated = Signal(float, QPixmap, QSize, Path)
updated_ratio = Signal(float)
def __init__(self, library: Library, settings: AppSettings) -> None:
def __init__(self, library: Library, settings: GlobalSettings) -> None:
super().__init__()
self.renderer = FileRenderer(library, settings)
self.theme = (
+1 -1
View File
@@ -20,7 +20,7 @@ class ResourceManager:
_map: dict[str, dict[str, str]] = {}
_cache: dict[str, bytes | str | Image.Image | QPixmap] = {}
_instance: ResourceManager | None = None
_instance: "ResourceManager | None" = None
def __new__(cls):
if ResourceManager._instance is None:

Some files were not shown because too many files have changed in this diff Show More