mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-30 06:40:50 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c79086f715 | ||
|
|
9ce07bd369 | ||
|
|
33ee27a84f | ||
|
|
1204d2b7b5 | ||
|
|
501ab1f977 | ||
|
|
b3c01e180a | ||
|
|
4c6ebec529 | ||
|
|
5c25666e67 | ||
|
|
fae65bd9e9 | ||
|
|
8e065ca8ac | ||
|
|
82946cb0b8 | ||
|
|
aa2925cde0 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -6,6 +6,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Please add an appropriate title for this issue.*
|
||||
|
||||
Before reporting, read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/doc/index.md) and search existing [issues](https://github.com/TagStudioDev/TagStudio/issues).
|
||||
Validate that you are using an up-to-date version[^1], your issue might already be fixed!
|
||||
Questions, guidance, and usage goes in [discussions](https://github.com/TagStudioDev/TagStudio/discussions). Invalid issues will be closed.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -6,6 +6,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Please add an appropriate title for this feature request.*
|
||||
|
||||
Before suggesting, read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/doc/index.md) and search existing [issues](https://github.com/TagStudioDev/TagStudio/issues).
|
||||
Validate that you are using an up-to-date version[^1], your feature might already be implemented!
|
||||
Questions, guidance, and usage goes in [discussions](https://github.com/TagStudioDev/TagStudio/discussions). Invalid issues will be closed.
|
||||
|
||||
2
.github/workflows/mypy.yaml
vendored
2
.github/workflows/mypy.yaml
vendored
@@ -23,8 +23,6 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# pyside 6.6.3 has some issue in their .pyi files
|
||||
pip install PySide6==6.6.2
|
||||
pip install -r requirements.txt
|
||||
pip install mypy==1.10.0
|
||||
mkdir tagstudio/.mypy_cache
|
||||
|
||||
22
.github/workflows/pytest.yaml
vendored
Normal file
22
.github/workflows/pytest.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: pytest
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest tagstudio/tests/
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,6 +55,7 @@ coverage.xml
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
tagstudio/tests/fixtures/library/*
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
@@ -25,8 +25,9 @@ Thank you so much for showing interest in contributing to TagStudio! Here are a
|
||||
### Prerequisites
|
||||
|
||||
- [Python](https://www.python.org/downloads/) 3.12
|
||||
- [Ruff](https://github.com/astral-sh/ruff) _(Included in `requirements-dev.txt`)_
|
||||
- [Mypy](https://github.com/python/mypy) _(Included in `requirements-dev.txt`)_
|
||||
- [Ruff](https://github.com/astral-sh/ruff) (Included in `requirements-dev.txt`)
|
||||
- [Mypy](https://github.com/python/mypy) (Included in `requirements-dev.txt`)
|
||||
- [PyTest](https://docs.pytest.org) (Included in `requirements-dev.txt`)
|
||||
|
||||
### Creating a Python Virtual Environment
|
||||
|
||||
@@ -85,7 +86,7 @@ A Python linter and code formatter. Ruff uses the `pyproject.toml` as its config
|
||||
|
||||
#### Running Locally
|
||||
|
||||
- Lint code with by moving into the `/tagstudio` directory with `cd tagstudio` and running `ruff --config ../pyproject.toml `
|
||||
- Lint code with by moving into the `/tagstudio` directory with `cd tagstudio` and running `ruff --config ../pyproject.toml`.
|
||||
- Format code with `ruff format` inside the repository directory
|
||||
|
||||
Ruff is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff), PyCharm [plugin](https://plugins.jetbrains.com/plugin/20574-ruff), and [more](https://docs.astral.sh/ruff/integrations/).
|
||||
@@ -99,19 +100,16 @@ Mypy is a static type checker for Python. It sure has a lot to say sometimes, bu
|
||||
- **First time only:** Move into the `/tagstudio` directory with `cd tagstudio` and run the following:
|
||||
- `mkdir -p .mypy_cache`
|
||||
- `mypy --install-types --non-interactive`
|
||||
- Check code by moving into the `/tagstudio` directory with `cd tagstudio` _(if you aren't already inside)_ and running `mypy --config-file ../pyproject.toml .` _(Don't forget the `.` at the end!)_
|
||||
- Check code by moving into the `/tagstudio` directory with `cd tagstudio` _(if you aren't already inside)_ and running `mypy --config-file ../pyproject.toml .`. _(Don't forget the `.` at the end!)_
|
||||
|
||||
> [!CAUTION]
|
||||
> There's a known issue between PySide v6.6.3 and Mypy where Mypy will detect issues with the `.pyi` files inside of PySide and prematurely stop checking files. This issue is not present in PySide v6.6.2, which _should_ be compatible with everything else if you wish to try using that version in the meantime.
|
||||
|
||||
Mypy is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=matangover.mypy), PyCharm [plugin](https://plugins.jetbrains.com/plugin/11086-mypy), and [more](https://plugins.jetbrains.com/plugin/11086-mypy).
|
||||
|
||||
### PyTest _(Work in Progress)_
|
||||
### PyTest
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Tests are not currently run as part of any automated workflow.
|
||||
|
||||
To run all the tests use `python -m pytest tests/` from the `tagstudio` folder.
|
||||
- Run all tests by moving into the `/tagstudio` directory with `cd tagstudio` and running `pytest tests/`.
|
||||
|
||||
## Code Guidelines
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ As of writing (Alpha v9.3.0) the project is in a useable state, however it lacks
|
||||
### What Features Are You Planning on Adding?
|
||||
|
||||
> [!IMPORTANT]
|
||||
> See the [Planned Features](/doc/planned_features.md) documentation for the latest feature lists. The lists here are currently being migrated over there with individual pages for larger features.
|
||||
> See the [Planned Features](/doc/updates/planned_features.md) documentation for the latest feature lists. The lists here are currently being migrated over there with individual pages for larger features.
|
||||
|
||||
Of the several features I have planned for the project, these are broken up into “priority” features and “future” features. Priority features were originally intended for the first public release, however are currently absent from the Alpha v9.x.x builds.
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ strict_optional = false
|
||||
disable_error_code = ["union-attr", "annotation-unchecked", "import-untyped"]
|
||||
explicit_package_bases = true
|
||||
warn_unused_ignores = true
|
||||
exclude = ['tests']
|
||||
|
||||
@@ -3,3 +3,4 @@ pre-commit==3.7.0
|
||||
pytest==8.2.0
|
||||
Pyinstaller==6.6.0
|
||||
mypy==1.10.0
|
||||
syrupy==4.6.1
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
humanfriendly==10.0
|
||||
opencv_python>=4.8.0.74,<=4.9.0.80
|
||||
Pillow==10.3.0
|
||||
PySide6>=6.5.1.1,<=6.6.3.1
|
||||
PySide6_Addons>=6.5.1.1,<=6.6.3.1
|
||||
PySide6_Essentials>=6.5.1.1,<=6.6.3.1
|
||||
PySide6==6.7.1
|
||||
PySide6_Addons==6.7.1
|
||||
PySide6_Essentials==6.7.1
|
||||
typing_extensions>=3.10.0.0,<=4.11.0
|
||||
ujson>=5.8.0,<=5.9.0
|
||||
numpy==1.26.4
|
||||
rawpy==0.21.0
|
||||
pillow-heif==0.16.0
|
||||
chardet==5.2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
VERSION: str = "9.3.1" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "Pre-Release" # Usually "" or "Pre-Release"
|
||||
VERSION: str = "9.3.2" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
TS_FOLDER_NAME: str = ".TagStudio"
|
||||
@@ -163,3 +163,6 @@ TAG_COLORS = [
|
||||
"cool gray",
|
||||
"olive",
|
||||
]
|
||||
|
||||
TAG_FAVORITE = 1
|
||||
TAG_ARCHIVED = 0
|
||||
|
||||
@@ -24,3 +24,16 @@ class SearchMode(int, enum.Enum):
|
||||
|
||||
AND = 0
|
||||
OR = 1
|
||||
|
||||
|
||||
class FieldID(int, enum.Enum):
|
||||
TITLE = 0
|
||||
AUTHOR = 1
|
||||
ARTIST = 2
|
||||
DESCRIPTION = 4
|
||||
NOTES = 5
|
||||
TAGS = 6
|
||||
CONTENT_TAGS = 7
|
||||
META_TAGS = 8
|
||||
DATE_PUBLISHED = 14
|
||||
SOURCE = 21
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"""The Library object and related methods for TagStudio."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
@@ -18,6 +17,7 @@ from pathlib import Path
|
||||
from typing import cast, Generator
|
||||
from typing_extensions import Self
|
||||
|
||||
from src.core.enums import FieldID
|
||||
from src.core.json_typing import JsonCollation, JsonEntry, JsonLibary, JsonTag
|
||||
from src.core.utils.str import strip_punctuation
|
||||
from src.core.utils.web import strip_web_protocol
|
||||
@@ -80,7 +80,7 @@ class Entry:
|
||||
# self.word_count: int = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"\n{self.compressed_dict()}\n"
|
||||
return str(self.compressed_dict())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
@@ -843,24 +843,22 @@ class Library:
|
||||
|
||||
def clear_internal_vars(self):
|
||||
"""Clears the internal variables of the Library object."""
|
||||
self.library_dir = None
|
||||
self.is_legacy_library = False
|
||||
|
||||
# Reset Directory Data =================================================
|
||||
self.library_dir = None
|
||||
|
||||
# Reset Entries ========================================================
|
||||
self.entries.clear()
|
||||
self._next_entry_id = 0
|
||||
# self.filtered_entries.clear()
|
||||
self._entry_id_to_index_map.clear()
|
||||
|
||||
self._collation_id_to_index_map.clear()
|
||||
|
||||
self.missing_matches = {}
|
||||
self.dir_file_count = -1
|
||||
self.files_not_in_library.clear()
|
||||
self.missing_files.clear()
|
||||
self.fixed_files.clear()
|
||||
self.filename_to_entry_id_map: dict[Path, int] = {}
|
||||
self.ext_list = self.default_ext_exclude_list
|
||||
|
||||
# Reset Tags ===========================================================
|
||||
self.tags.clear()
|
||||
self._next_tag_id = 1000
|
||||
self._tag_strings_to_id_map = {}
|
||||
@@ -868,6 +866,13 @@ class Library:
|
||||
self._tag_id_to_index_map = {}
|
||||
self._tag_entry_ref_map.clear()
|
||||
|
||||
# Reset Collations =====================================================
|
||||
self.collations.clear()
|
||||
self._collation_id_to_index_map.clear()
|
||||
|
||||
# Reset Extension List =================================================
|
||||
self.ext_list = self.default_ext_exclude_list
|
||||
|
||||
def refresh_dir(self) -> Generator:
|
||||
"""Scans a directory for files, and adds those relative filenames to internal variables."""
|
||||
|
||||
@@ -889,12 +894,12 @@ class Library:
|
||||
and "tagstudio_thumbs" not in f.parts
|
||||
and not f.is_dir()
|
||||
):
|
||||
if f.suffix not in self.ext_list and self.is_exclude_list:
|
||||
if f.suffix.lower() not in self.ext_list and self.is_exclude_list:
|
||||
self.dir_file_count += 1
|
||||
file = f.relative_to(self.library_dir)
|
||||
if file not in self.filename_to_entry_id_map:
|
||||
self.files_not_in_library.append(file)
|
||||
elif f.suffix in self.ext_list and not self.is_exclude_list:
|
||||
elif f.suffix.lower() in self.ext_list and not self.is_exclude_list:
|
||||
self.dir_file_count += 1
|
||||
file = f.relative_to(self.library_dir)
|
||||
try:
|
||||
@@ -1382,7 +1387,7 @@ class Library:
|
||||
# non_entry_count = 0
|
||||
# Iterate over all Entries =============================================================
|
||||
for entry in self.entries:
|
||||
allowed_ext: bool = entry.filename.suffix not in self.ext_list
|
||||
allowed_ext: bool = entry.filename.suffix.lower() not in self.ext_list
|
||||
# try:
|
||||
# entry: Entry = self.entries[self.file_to_library_index_map[self._source_filenames[i]]]
|
||||
# print(f'{entry}')
|
||||
@@ -1539,7 +1544,7 @@ class Library:
|
||||
else:
|
||||
for entry in self.entries:
|
||||
added = False
|
||||
allowed_ext = entry.filename.suffix not in self.ext_list
|
||||
allowed_ext = entry.filename.suffix.lower() not in self.ext_list
|
||||
if allowed_ext == self.is_exclude_list:
|
||||
for f in entry.fields:
|
||||
if self.get_field_attr(f, "type") == "collation":
|
||||
@@ -1948,48 +1953,44 @@ class Library:
|
||||
if data:
|
||||
# Add a Title Field if the data doesn't already exist.
|
||||
if data.get("title"):
|
||||
field_id = 0 # Title Field ID
|
||||
if not self.does_field_content_exist(entry_id, field_id, data["title"]):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, FieldID.TITLE, data["title"]
|
||||
):
|
||||
self.add_field_to_entry(entry_id, FieldID.TITLE)
|
||||
self.update_entry_field(entry_id, -1, data["title"], "replace")
|
||||
|
||||
# Add an Author Field if the data doesn't already exist.
|
||||
if data.get("author"):
|
||||
field_id = 1 # Author Field ID
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, field_id, data["author"]
|
||||
entry_id, FieldID.AUTHOR, data["author"]
|
||||
):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
self.add_field_to_entry(entry_id, FieldID.AUTHOR)
|
||||
self.update_entry_field(entry_id, -1, data["author"], "replace")
|
||||
|
||||
# Add an Artist Field if the data doesn't already exist.
|
||||
if data.get("artist"):
|
||||
field_id = 2 # Artist Field ID
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, field_id, data["artist"]
|
||||
entry_id, FieldID.ARTIST, data["artist"]
|
||||
):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
self.add_field_to_entry(entry_id, FieldID.ARTIST)
|
||||
self.update_entry_field(entry_id, -1, data["artist"], "replace")
|
||||
|
||||
# Add a Date Published Field if the data doesn't already exist.
|
||||
if data.get("date_published"):
|
||||
field_id = 14 # Date Published Field ID
|
||||
date = str(
|
||||
datetime.datetime.strptime(
|
||||
data["date_published"], "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
)
|
||||
if not self.does_field_content_exist(entry_id, field_id, date):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, FieldID.DATE_PUBLISHED, date
|
||||
):
|
||||
self.add_field_to_entry(entry_id, FieldID.DATE_PUBLISHED)
|
||||
# entry = self.entries[entry_id]
|
||||
self.update_entry_field(entry_id, -1, date, "replace")
|
||||
|
||||
# Process String Tags if the data doesn't already exist.
|
||||
if data.get("tags"):
|
||||
tags_field_id = 6 # Tags Field ID
|
||||
content_tags_field_id = 7 # Content Tags Field ID
|
||||
meta_tags_field_id = 8 # Meta Tags Field ID
|
||||
notes_field_id = 5 # Notes Field ID
|
||||
tags: list[str] = data["tags"]
|
||||
# extra: list[str] = []
|
||||
# for tag in tags:
|
||||
@@ -2038,7 +2039,7 @@ class Library:
|
||||
# tag_field_indices = self.get_field_index_in_entry(
|
||||
# entry_index, tags_field_id)
|
||||
content_tags_field_indices = self.get_field_index_in_entry(
|
||||
self.get_entry(entry_id), content_tags_field_id
|
||||
self.get_entry(entry_id), FieldID.CONTENT_TAGS
|
||||
)
|
||||
# meta_tags_field_indices = self.get_field_index_in_entry(
|
||||
# entry_index, meta_tags_field_id)
|
||||
@@ -2055,45 +2056,40 @@ class Library:
|
||||
entry_id, priority_field_index, [matching[0]], "append"
|
||||
)
|
||||
else:
|
||||
self.add_field_to_entry(entry_id, content_tags_field_id)
|
||||
self.add_field_to_entry(entry_id, FieldID.CONTENT_TAGS)
|
||||
self.update_entry_field(
|
||||
entry_id, -1, [matching[0]], "append"
|
||||
)
|
||||
|
||||
# Add all original string tags as a note.
|
||||
str_tags = f"Original Tags: {tags}"
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, notes_field_id, str_tags
|
||||
):
|
||||
self.add_field_to_entry(entry_id, notes_field_id)
|
||||
if not self.does_field_content_exist(entry_id, FieldID.NOTES, str_tags):
|
||||
self.add_field_to_entry(entry_id, FieldID.NOTES)
|
||||
self.update_entry_field(entry_id, -1, str_tags, "replace")
|
||||
|
||||
# Add a Description Field if the data doesn't already exist.
|
||||
if "description" in data.keys() and data["description"]:
|
||||
field_id = 4 # Description Field ID
|
||||
if data.get("description"):
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, field_id, data["description"]
|
||||
entry_id, FieldID.DESCRIPTION, data["description"]
|
||||
):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
self.add_field_to_entry(entry_id, FieldID.DESCRIPTION)
|
||||
self.update_entry_field(
|
||||
entry_id, -1, data["description"], "replace"
|
||||
)
|
||||
if "content" in data.keys() and data["content"]:
|
||||
field_id = 4 # Description Field ID
|
||||
if data.get("content"):
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, field_id, data["content"]
|
||||
entry_id, FieldID.DESCRIPTION, data["content"]
|
||||
):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
self.add_field_to_entry(entry_id, FieldID.DESCRIPTION)
|
||||
self.update_entry_field(entry_id, -1, data["content"], "replace")
|
||||
if "source" in data.keys() and data["source"]:
|
||||
field_id = 21 # Source Field ID
|
||||
if data.get("source"):
|
||||
for source in data["source"].split(" "):
|
||||
if source and source != " ":
|
||||
source = strip_web_protocol(string=source)
|
||||
if not self.does_field_content_exist(
|
||||
entry_id, field_id, source
|
||||
entry_id, FieldID.SOURCE, source
|
||||
):
|
||||
self.add_field_to_entry(entry_id, field_id)
|
||||
self.add_field_to_entry(entry_id, FieldID.SOURCE)
|
||||
self.update_entry_field(entry_id, -1, source, "replace")
|
||||
|
||||
def add_field_to_entry(self, entry_id: int, field_id: int) -> None:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
from src.core.library import Entry, Library
|
||||
from src.core.constants import TS_FOLDER_NAME, TEXT_FIELDS
|
||||
|
||||
16
tagstudio/src/qt/helpers/qbutton_wrapper.py
Normal file
16
tagstudio/src/qt/helpers/qbutton_wrapper.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
|
||||
class QPushButtonWrapper(QPushButton):
|
||||
"""
|
||||
This is a customized implementation of the PySide6 QPushButton that allows to suppress the warning that is triggered
|
||||
by disconnecting a signal that is not currently connected.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_connected = False
|
||||
@@ -24,6 +24,7 @@ class AddFieldModal(QWidget):
|
||||
# - OR -
|
||||
# [Cancel] [Save]
|
||||
super().__init__()
|
||||
self.is_connected = False
|
||||
self.lib = library
|
||||
self.setWindowTitle(f"Add Field")
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
|
||||
@@ -110,4 +110,4 @@ class FileExtensionModal(PanelWidget):
|
||||
for i in range(self.table.rowCount()):
|
||||
ext = self.table.item(i, 0)
|
||||
if ext and ext.text():
|
||||
self.lib.ext_list.append(ext.text())
|
||||
self.lib.ext_list.append(ext.text().lower())
|
||||
|
||||
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
)
|
||||
|
||||
from src.core.enums import FieldID
|
||||
from src.core.library import Library, Tag
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
@@ -73,13 +74,13 @@ def folders_to_tags(library: Library):
|
||||
tag = add_folders_to_tree(folders)
|
||||
if tag:
|
||||
if not entry.has_tag(library, tag.id):
|
||||
entry.add_tag(library, tag.id, 6)
|
||||
entry.add_tag(library, tag.id, FieldID.TAGS)
|
||||
|
||||
logging.info("Done")
|
||||
|
||||
|
||||
def reverse_tag(library: Library, tag: Tag, list: list[Tag]) -> list[Tag]:
|
||||
if list != None:
|
||||
if list is not None:
|
||||
list.append(tag)
|
||||
else:
|
||||
list = [tag]
|
||||
@@ -144,7 +145,7 @@ def generate_preview_data(library: Library):
|
||||
if cut:
|
||||
branch["dirs"].pop(folder)
|
||||
|
||||
if not "tag" in branch:
|
||||
if "tag" not in branch:
|
||||
return
|
||||
if branch["tag"].id == -1 or len(branch["files"]) > 0: # Needs to be first
|
||||
return False
|
||||
@@ -289,7 +290,7 @@ class TreeItem(QWidget):
|
||||
self.children_layout.addWidget(item)
|
||||
for file in data["files"]:
|
||||
label = QLabel()
|
||||
label.setText(" -> " + file)
|
||||
label.setText(" -> " + str(file))
|
||||
self.children_layout.addWidget(label)
|
||||
|
||||
if len(data["files"]) == 0 and len(data["dirs"].values()) == 0:
|
||||
@@ -321,7 +322,7 @@ class ModifiedTagWidget(
|
||||
|
||||
self.bg_button = QPushButton(self)
|
||||
self.bg_button.setFlat(True)
|
||||
if parentTag != None:
|
||||
if parentTag is not None:
|
||||
text = f"{tag.name} ({parentTag.name})".replace("&", "&&")
|
||||
else:
|
||||
text = tag.name.replace("&", "&&")
|
||||
|
||||
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
|
||||
# class NumberEdit(QLineEdit):
|
||||
# def __init__(self, parent=None) -> None:
|
||||
@@ -50,13 +50,13 @@ class Pagination(QWidget, QObject):
|
||||
# self.setMinimumHeight(32)
|
||||
|
||||
# [<] ----------------------------------
|
||||
self.prev_button = QPushButton()
|
||||
self.prev_button = QPushButtonWrapper()
|
||||
self.prev_button.setText("<")
|
||||
self.prev_button.setMinimumSize(self.button_size)
|
||||
self.prev_button.setMaximumSize(self.button_size)
|
||||
|
||||
# --- [1] ------------------------------
|
||||
self.start_button = QPushButton()
|
||||
self.start_button = QPushButtonWrapper()
|
||||
self.start_button.setMinimumSize(self.button_size)
|
||||
self.start_button.setMaximumSize(self.button_size)
|
||||
# self.start_button.setStyleSheet('background:cyan;')
|
||||
@@ -104,14 +104,14 @@ class Pagination(QWidget, QObject):
|
||||
self.end_ellipses.setText(". . .")
|
||||
|
||||
# ----------------------------- [42] ---
|
||||
self.end_button = QPushButton()
|
||||
self.end_button = QPushButtonWrapper()
|
||||
self.end_button.setMinimumSize(self.button_size)
|
||||
self.end_button.setMaximumSize(self.button_size)
|
||||
# self.end_button.setMaximumHeight(self.button_size.height())
|
||||
# self.end_button.setStyleSheet('background:red;')
|
||||
|
||||
# ---------------------------------- [>]
|
||||
self.next_button = QPushButton()
|
||||
self.next_button = QPushButtonWrapper()
|
||||
self.next_button.setText(">")
|
||||
self.next_button.setMinimumSize(self.button_size)
|
||||
self.next_button.setMaximumSize(self.button_size)
|
||||
@@ -428,16 +428,15 @@ class Pagination(QWidget, QObject):
|
||||
# print(f'GOTO PAGE: {index}')
|
||||
self.update_buttons(self.page_count, index)
|
||||
|
||||
def _assign_click(self, button: QPushButton, index):
|
||||
try:
|
||||
def _assign_click(self, button: QPushButtonWrapper, index):
|
||||
if button.is_connected:
|
||||
button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
button.clicked.connect(lambda checked=False, i=index: self._goto_page(i))
|
||||
button.is_connected = True
|
||||
|
||||
def _populate_buffer_buttons(self):
|
||||
for i in range(max(self.buffer_page_count * 2, 5)):
|
||||
button = QPushButton()
|
||||
button = QPushButtonWrapper()
|
||||
button.setMinimumSize(self.button_size)
|
||||
button.setMaximumSize(self.button_size)
|
||||
button.setHidden(True)
|
||||
@@ -445,7 +444,7 @@ class Pagination(QWidget, QObject):
|
||||
self.start_buffer_layout.addWidget(button)
|
||||
|
||||
for i in range(max(self.buffer_page_count * 2, 5)):
|
||||
button = QPushButton()
|
||||
button = QPushButtonWrapper()
|
||||
button.setMinimumSize(self.button_size)
|
||||
button.setMaximumSize(self.button_size)
|
||||
button.setHidden(True)
|
||||
|
||||
@@ -64,6 +64,8 @@ from src.core.constants import (
|
||||
TS_FOLDER_NAME,
|
||||
VERSION_BRANCH,
|
||||
VERSION,
|
||||
TAG_FAVORITE,
|
||||
TAG_ARCHIVED,
|
||||
)
|
||||
from src.core.utils.web import strip_web_protocol
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
@@ -1252,8 +1254,8 @@ class QtDriver(QObject):
|
||||
filepath = self.lib.library_dir / entry.path / entry.filename
|
||||
|
||||
item_thumb.set_item_id(entry.id)
|
||||
item_thumb.assign_archived(entry.has_tag(self.lib, 0))
|
||||
item_thumb.assign_favorite(entry.has_tag(self.lib, 1))
|
||||
item_thumb.assign_archived(entry.has_tag(self.lib, TAG_ARCHIVED))
|
||||
item_thumb.assign_favorite(entry.has_tag(self.lib, TAG_FAVORITE))
|
||||
# ctrl_down = True if QGuiApplication.keyboardModifiers() else False
|
||||
# TODO: Change how this works. The click function
|
||||
# for collations a few lines down should NOT be allowed during modifier keys.
|
||||
|
||||
@@ -13,6 +13,7 @@ from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
from PySide6.QtGui import QPixmap, QEnterEvent
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
|
||||
|
||||
class FieldContainer(QWidget):
|
||||
@@ -81,7 +82,7 @@ class FieldContainer(QWidget):
|
||||
|
||||
self.title_layout.addStretch(2)
|
||||
|
||||
self.copy_button = QPushButton()
|
||||
self.copy_button = QPushButtonWrapper()
|
||||
self.copy_button.setMinimumSize(button_size, button_size)
|
||||
self.copy_button.setMaximumSize(button_size, button_size)
|
||||
self.copy_button.setFlat(True)
|
||||
@@ -92,7 +93,7 @@ class FieldContainer(QWidget):
|
||||
self.title_layout.addWidget(self.copy_button)
|
||||
self.copy_button.setHidden(True)
|
||||
|
||||
self.edit_button = QPushButton()
|
||||
self.edit_button = QPushButtonWrapper()
|
||||
self.edit_button.setMinimumSize(button_size, button_size)
|
||||
self.edit_button.setMaximumSize(button_size, button_size)
|
||||
self.edit_button.setFlat(True)
|
||||
@@ -101,7 +102,7 @@ class FieldContainer(QWidget):
|
||||
self.title_layout.addWidget(self.edit_button)
|
||||
self.edit_button.setHidden(True)
|
||||
|
||||
self.remove_button = QPushButton()
|
||||
self.remove_button = QPushButtonWrapper()
|
||||
self.remove_button.setMinimumSize(button_size, button_size)
|
||||
self.remove_button.setMaximumSize(button_size, button_size)
|
||||
self.remove_button.setFlat(True)
|
||||
@@ -124,31 +125,30 @@ class FieldContainer(QWidget):
|
||||
# self.set_inner_widget(mode)
|
||||
|
||||
def set_copy_callback(self, callback: Optional[MethodType]):
|
||||
try:
|
||||
if self.copy_button.is_connected:
|
||||
self.copy_button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.copy_callback = callback
|
||||
self.copy_button.clicked.connect(callback)
|
||||
if callback is not None:
|
||||
self.copy_button.is_connected = True
|
||||
|
||||
def set_edit_callback(self, callback: Optional[MethodType]):
|
||||
try:
|
||||
if self.edit_button.is_connected:
|
||||
self.edit_button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.edit_callback = callback
|
||||
self.edit_button.clicked.connect(callback)
|
||||
if callback is not None:
|
||||
self.edit_button.is_connected = True
|
||||
|
||||
def set_remove_callback(self, callback: Optional[Callable]):
|
||||
try:
|
||||
if self.remove_button.is_connected:
|
||||
self.remove_button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.remove_callback = callback
|
||||
self.remove_button.clicked.connect(callback)
|
||||
self.remove_button.is_connected = True
|
||||
|
||||
def set_inner_widget(self, widget: "FieldWidget"):
|
||||
# widget.setStyleSheet('background-color:green;')
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import typing
|
||||
from types import FunctionType
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -23,9 +21,15 @@ from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
)
|
||||
|
||||
|
||||
from src.core.enums import FieldID
|
||||
from src.core.library import ItemType, Library, Entry
|
||||
from src.core.constants import AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES
|
||||
from src.core.constants import (
|
||||
AUDIO_TYPES,
|
||||
VIDEO_TYPES,
|
||||
IMAGE_TYPES,
|
||||
TAG_FAVORITE,
|
||||
TAG_ARCHIVED,
|
||||
)
|
||||
from src.qt.flowlayout import FlowWidget
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
@@ -38,9 +42,6 @@ ERROR = f"[ERROR]"
|
||||
WARNING = f"[WARNING]"
|
||||
INFO = f"[INFO]"
|
||||
|
||||
DEFAULT_META_TAG_FIELD = 8
|
||||
TAG_FAVORITE = 1
|
||||
TAG_ARCHIVED = 0
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
@@ -394,19 +395,22 @@ class ItemThumb(FlowWidget):
|
||||
def update_clickable(self, clickable: typing.Callable):
|
||||
"""Updates attributes of a thumbnail element."""
|
||||
# logging.info(f'[GUI] Updating Click Event for element {id(element)}: {id(clickable) if clickable else None}')
|
||||
try:
|
||||
if self.thumb_button.is_connected:
|
||||
self.thumb_button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
if clickable:
|
||||
self.thumb_button.clicked.connect(clickable)
|
||||
self.thumb_button.is_connected = True
|
||||
|
||||
def update_badges(self):
|
||||
if self.mode == ItemType.ENTRY:
|
||||
# logging.info(f'[UPDATE BADGES] ENTRY: {self.lib.get_entry(self.item_id)}')
|
||||
# logging.info(f'[UPDATE BADGES] ARCH: {self.lib.get_entry(self.item_id).has_tag(self.lib, 0)}, FAV: {self.lib.get_entry(self.item_id).has_tag(self.lib, 1)}')
|
||||
self.assign_archived(self.lib.get_entry(self.item_id).has_tag(self.lib, 0))
|
||||
self.assign_favorite(self.lib.get_entry(self.item_id).has_tag(self.lib, 1))
|
||||
self.assign_archived(
|
||||
self.lib.get_entry(self.item_id).has_tag(self.lib, TAG_ARCHIVED)
|
||||
)
|
||||
self.assign_favorite(
|
||||
self.lib.get_entry(self.item_id).has_tag(self.lib, TAG_FAVORITE)
|
||||
)
|
||||
|
||||
def set_item_id(self, id: int):
|
||||
"""
|
||||
@@ -475,7 +479,7 @@ class ItemThumb(FlowWidget):
|
||||
entry.add_tag(
|
||||
self.panel.driver.lib,
|
||||
tag_id,
|
||||
field_id=DEFAULT_META_TAG_FIELD,
|
||||
field_id=FieldID.META_TAGS,
|
||||
field_index=-1,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -40,6 +40,7 @@ from src.qt.widgets.text import TextWidget
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
from src.qt.widgets.text_box_edit import EditTextBox
|
||||
from src.qt.widgets.text_line_edit import EditTextLine
|
||||
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
from src.qt.widgets.video_player import VideoPlayer
|
||||
|
||||
|
||||
@@ -61,6 +62,7 @@ class PreviewPanel(QWidget):
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
self.is_connected = False
|
||||
self.lib = library
|
||||
self.driver: QtDriver = driver
|
||||
self.initialized = False
|
||||
@@ -83,7 +85,7 @@ class PreviewPanel(QWidget):
|
||||
self.open_file_action = QAction("Open file", self)
|
||||
self.open_explorer_action = QAction("Open file in explorer", self)
|
||||
|
||||
self.preview_img = QPushButton()
|
||||
self.preview_img = QPushButtonWrapper()
|
||||
self.preview_img.setMinimumSize(*self.img_button_size)
|
||||
self.preview_img.setFlat(True)
|
||||
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
@@ -218,7 +220,7 @@ class PreviewPanel(QWidget):
|
||||
self.afb_layout = QVBoxLayout(self.afb_container)
|
||||
self.afb_layout.setContentsMargins(0, 12, 0, 0)
|
||||
|
||||
self.add_field_button = QPushButton()
|
||||
self.add_field_button = QPushButtonWrapper()
|
||||
self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.add_field_button.setMinimumSize(96, 28)
|
||||
self.add_field_button.setMaximumSize(96, 28)
|
||||
@@ -279,7 +281,9 @@ class PreviewPanel(QWidget):
|
||||
row_layout.addWidget(label)
|
||||
layout.addLayout(row_layout)
|
||||
|
||||
def set_button_style(btn: QPushButton, extras: list[str] | None = None):
|
||||
def set_button_style(
|
||||
btn: QPushButtonWrapper | QPushButton, extras: list[str] | None = None
|
||||
):
|
||||
base_style = [
|
||||
f"background-color:{Theme.COLOR_BG.value};",
|
||||
"border-radius:6px;",
|
||||
@@ -317,7 +321,6 @@ class PreviewPanel(QWidget):
|
||||
|
||||
button.clicked.connect(open_library_button_clicked(full_val))
|
||||
set_button_style(button)
|
||||
|
||||
button_remove = QPushButton("➖")
|
||||
button_remove.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button_remove.setFixedWidth(30)
|
||||
@@ -411,16 +414,16 @@ class PreviewPanel(QWidget):
|
||||
self.afb_container, Qt.AlignmentFlag.AlignHCenter
|
||||
)
|
||||
|
||||
try:
|
||||
if self.afm.is_connected:
|
||||
self.afm.done.disconnect()
|
||||
if self.add_field_button.is_connected:
|
||||
self.add_field_button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# self.afm.done.connect(lambda f: (self.lib.add_field_to_entry(self.selected[0][1], f), self.update_widgets()))
|
||||
self.afm.done.connect(
|
||||
lambda f: (self.add_field_to_selected(f), self.update_widgets())
|
||||
)
|
||||
self.afm.is_connected = True
|
||||
self.add_field_button.clicked.connect(self.afm.show)
|
||||
|
||||
def add_field_to_selected(self, field_id: int):
|
||||
@@ -466,10 +469,8 @@ class PreviewPanel(QWidget):
|
||||
True,
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
try:
|
||||
if self.preview_img.is_connected:
|
||||
self.preview_img.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
for i, c in enumerate(self.containers):
|
||||
c.setHidden(True)
|
||||
self.preview_img.show()
|
||||
@@ -588,14 +589,12 @@ class PreviewPanel(QWidget):
|
||||
f"[PreviewPanel][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})"
|
||||
)
|
||||
|
||||
try:
|
||||
if self.preview_img.is_connected:
|
||||
self.preview_img.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
self.preview_img.clicked.connect(
|
||||
lambda checked=False, filepath=filepath: open_file(filepath)
|
||||
)
|
||||
|
||||
self.preview_img.is_connected = True
|
||||
self.selected = list(self.driver.selected)
|
||||
for i, f in enumerate(item.fields):
|
||||
self.write_container(i, f)
|
||||
@@ -641,10 +640,9 @@ class PreviewPanel(QWidget):
|
||||
True,
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
try:
|
||||
if self.preview_img.is_connected:
|
||||
self.preview_img.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
self.preview_img.is_connected = False
|
||||
|
||||
self.common_fields = []
|
||||
self.mixed_fields = []
|
||||
@@ -773,12 +771,12 @@ class PreviewPanel(QWidget):
|
||||
"""
|
||||
Replacement for tag_callback.
|
||||
"""
|
||||
try:
|
||||
if self.is_connected:
|
||||
self.tags_updated.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
logging.info("[UPDATE CONTAINER] Setting tags updated slot")
|
||||
self.tags_updated.connect(slot)
|
||||
self.is_connected = True
|
||||
|
||||
# def write_container(self, item:Union[Entry, Collation, Tag], index, field):
|
||||
def write_container(self, index, field, mixed=False):
|
||||
@@ -1067,7 +1065,8 @@ class PreviewPanel(QWidget):
|
||||
)
|
||||
# remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel)
|
||||
remove_mb.setDefaultButton(cancel_button)
|
||||
remove_mb.setEscapeButton(cancel_button)
|
||||
result = remove_mb.exec_()
|
||||
# logging.info(result)
|
||||
if result == 1:
|
||||
if result == 3:
|
||||
callback()
|
||||
|
||||
@@ -10,6 +10,7 @@ import typing
|
||||
from PySide6.QtCore import Signal, Qt
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
from src.core.constants import TAG_FAVORITE, TAG_ARCHIVED
|
||||
from src.core.library import Library, Tag
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.widgets.fields import FieldWidget
|
||||
@@ -141,7 +142,7 @@ class TagBoxWidget(FieldWidget):
|
||||
# panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag))
|
||||
self.edit_modal.show()
|
||||
|
||||
def add_tag_callback(self, tag_id):
|
||||
def add_tag_callback(self, tag_id: int):
|
||||
# self.base_layout.addWidget(TagWidget(self.lib, self.lib.get_tag(tag), True))
|
||||
# self.tags.append(tag)
|
||||
logging.info(
|
||||
@@ -154,7 +155,7 @@ class TagBoxWidget(FieldWidget):
|
||||
self.driver.lib, tag_id, field_id=id, field_index=-1
|
||||
)
|
||||
self.updated.emit()
|
||||
if tag_id == 0 or tag_id == 1:
|
||||
if tag_id in (TAG_FAVORITE, TAG_ARCHIVED):
|
||||
self.driver.update_badges()
|
||||
|
||||
# if type((x[0]) == ThumbButton):
|
||||
@@ -180,7 +181,7 @@ class TagBoxWidget(FieldWidget):
|
||||
self.driver.lib, tag_id, field_index=index[0]
|
||||
)
|
||||
self.updated.emit()
|
||||
if tag_id == 0 or tag_id == 1:
|
||||
if tag_id in (TAG_FAVORITE, TAG_ARCHIVED):
|
||||
self.driver.update_badges()
|
||||
|
||||
# def show_add_button(self, value:bool):
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtCore import QEvent
|
||||
from PySide6.QtGui import QEnterEvent, QPainter, QColor, QPen, QPainterPath, QPaintEvent
|
||||
from PySide6.QtWidgets import QWidget, QPushButton
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
|
||||
|
||||
class ThumbButton(QPushButton):
|
||||
class ThumbButton(QPushButtonWrapper):
|
||||
def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None:
|
||||
super().__init__(parent)
|
||||
self.thumb_size: tuple[int, int] = thumb_size
|
||||
|
||||
42
tagstudio/tests/conftest.py
Normal file
42
tagstudio/tests/conftest.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
from syrupy.extensions.json import JSONSnapshotExtension
|
||||
|
||||
CWD = pathlib.Path(__file__).parent
|
||||
|
||||
sys.path.insert(0, str(CWD.parent))
|
||||
|
||||
from src.core.library import Tag, Library
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_tag():
|
||||
yield Tag(
|
||||
id=1,
|
||||
name="Tag Name",
|
||||
shorthand="TN",
|
||||
aliases=["First A", "Second A"],
|
||||
subtags_ids=[2, 3, 4],
|
||||
color="",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_library():
|
||||
lib_dir = CWD / "fixtures" / "library"
|
||||
|
||||
lib = Library()
|
||||
ret_code = lib.open_library(lib_dir)
|
||||
assert ret_code == 1
|
||||
# create files for the entries
|
||||
for entry in lib.entries:
|
||||
(lib_dir / entry.filename).touch()
|
||||
|
||||
yield lib
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def snapshot_json(snapshot):
|
||||
return snapshot.with_defaults(extension_class=JSONSnapshotExtension)
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
[
|
||||
"<ItemType.ENTRY: 0>",
|
||||
2
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
[
|
||||
"<ItemType.ENTRY: 0>",
|
||||
1
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"{'id': 1, 'filename': 'foo.txt', 'path': '.', 'fields': [{6: [1001]}]}",
|
||||
"{'id': 2, 'filename': 'bar.txt', 'path': '.', 'fields': [{6: [1000]}]}"
|
||||
]
|
||||
18
tagstudio/tests/core/test_lib.py
Normal file
18
tagstudio/tests/core/test_lib.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_open_library(test_library, snapshot_json):
|
||||
assert test_library.entries == snapshot_json
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["query"],
|
||||
[
|
||||
("First",),
|
||||
("Second",),
|
||||
("--nomatch--",),
|
||||
],
|
||||
)
|
||||
def test_library_search(test_library, query, snapshot_json):
|
||||
res = test_library.search_library(query)
|
||||
assert res == snapshot_json
|
||||
@@ -1,18 +1,8 @@
|
||||
from src.core.library import Tag
|
||||
def test_subtag(test_tag):
|
||||
test_tag.remove_subtag(2)
|
||||
test_tag.remove_subtag(2)
|
||||
|
||||
|
||||
def test_construction():
|
||||
tag = Tag(
|
||||
id=1,
|
||||
name="Tag Name",
|
||||
shorthand="TN",
|
||||
aliases=["First A", "Second A"],
|
||||
subtags_ids=[2, 3, 4],
|
||||
color="",
|
||||
)
|
||||
assert tag
|
||||
|
||||
|
||||
def test_empty_construction():
|
||||
tag = Tag(id=1, name="", shorthand="", aliases=[], subtags_ids=[], color="")
|
||||
assert tag
|
||||
test_tag.add_subtag(5)
|
||||
# repeated add should not add the subtag
|
||||
test_tag.add_subtag(5)
|
||||
assert test_tag.subtag_ids == [3, 4, 5]
|
||||
|
||||
69
tagstudio/tests/fixtures/library/.TagStudio/ts_library.json
vendored
Normal file
69
tagstudio/tests/fixtures/library/.TagStudio/ts_library.json
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"ts-version": "9.3.1",
|
||||
"ext_list": [
|
||||
".json",
|
||||
".xmp",
|
||||
".aae"
|
||||
],
|
||||
"is_exclude_list": true,
|
||||
"tags": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Archived",
|
||||
"aliases": [
|
||||
"Archive"
|
||||
],
|
||||
"color": "Red"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Favorite",
|
||||
"aliases": [
|
||||
"Favorited",
|
||||
"Favorites"
|
||||
],
|
||||
"color": "Yellow"
|
||||
},
|
||||
{
|
||||
"id": 1000,
|
||||
"name": "first",
|
||||
"shorthand": "first",
|
||||
"color": "magenta"
|
||||
},
|
||||
{
|
||||
"id": 1001,
|
||||
"name": "second",
|
||||
"shorthand": "second",
|
||||
"color": "blue"
|
||||
}
|
||||
],
|
||||
"collations": [],
|
||||
"fields": [],
|
||||
"macros": [],
|
||||
"entries": [
|
||||
{
|
||||
"id": 1,
|
||||
"filename": "foo.txt",
|
||||
"path": ".",
|
||||
"fields": [
|
||||
{
|
||||
"6": [
|
||||
1001
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"filename": "bar.txt",
|
||||
"path": ".",
|
||||
"fields": [
|
||||
{
|
||||
"6": [
|
||||
1000
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user