ci(ruff)!: update ruff linter config, refactor to comply (#499)

* ci: update ruff linter config

- Set line length to 100
- Enforce Google-style docstrings
- Lint docstrings and imports

* ci(ruff): exclude missing docstring warnings

* ci(ruff): exclude docstring checks from tests dir

* fix(ruff): change top level linter setting

Fix Ruff warning: `warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `pyproject.toml`:
  - 'per-file-ignores' -> 'lint.per-file-ignores'`.

* chore: format with ruff

* add `.git-blame-ignore-revs`

* ci(ruff): add E402 and F541 checks

* ci(ruff): add FBT003 check (#500)

* ci(ruff): add T20 check

* ci(ruff)!: add N check, refactor method names

* ci(ruff): add E501 check, refactor strings

Much commented-out code is removed in this commit.

* update `.git-blame-ignore.revs`

---------

Co-authored-by: yed <yedpodtrzitko@users.noreply.github.com>
This commit is contained in:
Travis Abendshien
2024-09-13 00:28:00 -07:00
committed by GitHub
parent c15963868e
commit b6e2167605
67 changed files with 727 additions and 1405 deletions

27
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,27 @@
# Date: Thu, 12 Sep 2024 19:38:58 -0700
# chore: format with ruff
308be42bd21c3483098088ceb2ebc89208f5f26c
# Date: Thu, 12 Sep 2024 21:43:01 -0700
# ci(ruff): add E402 and F541 checks
f52c1fc541fef0044edf5463a0818e17601e5fab
# Date: Fri, 13 Sep 2024 11:43:08 +0700
# ci(ruff): add FBT003 check
16dd6346e4d7e86212d4df1ac91c19d9cb11c572
# Date: Fri, 13 Sep 2024 11:43:08 +0700
# Merge #500 into #499
3baa07bd33decad5f048ff9ad7a36188bb2956b7
# Date: Thu, 12 Sep 2024 21:50:00 -0700
# ci(ruff): add T20 check
162bcf5a6c7b2e4a39ec7e571375998c9859a42a
# Date: Thu, 12 Sep 2024 22:28:24 -0700
# ci(ruff)!: add N check, refactor method names
5bc815cfba5b92bca8fc646b92cb4a12222f3a0e
# Date: Thu, 12 Sep 2024 23:40:24 -0700
# ci(ruff): add E501 check, refactor strings
836bb0155d0eae3448a752e3f2690061803707b4

View File

@@ -1,9 +1,36 @@
[tool.ruff]
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"]
line-length = 100
[tool.ruff.lint.per-file-ignores]
"tagstudio/tests/**" = ["D", "E402"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint]
select = ["E", "F", "UP", "B", 'SIM']
ignore = ["E402", "E501", "F541"]
select = [
"B",
"D",
"E",
"F",
"FBT003",
"I",
"N",
"SIM",
"T20",
"UP",
]
ignore = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
]
[tool.mypy]
strict_optional = false

View File

@@ -1,6 +1,5 @@
from .models import Entry
from .library import Library
from .models import Tag
from .enums import ItemType
from .library import Library
from .models import Entry, Tag
__all__ = ["Entry", "Library", "Tag", "ItemType"]

View File

@@ -104,9 +104,7 @@ class FilterState:
else:
self.tag = self.tag and self.tag.strip()
self.tag_id = (
int(self.tag_id) if str(self.tag_id).isnumeric() else self.tag_id
)
self.tag_id = int(self.tag_id) if str(self.tag_id).isnumeric() else self.tag_id
self.path = self.path and str(self.path).strip()
self.name = self.name and self.name.strip()
self.id = int(self.id) if str(self.id).isnumeric() else self.id
@@ -118,10 +116,8 @@ class FilterState:
@property
def summary(self):
"""Show query summary"""
return (
self.query or self.tag or self.name or self.tag_id or self.path or self.id
)
"""Show query summary."""
return self.query or self.tag or self.name or self.tag_id or self.path or self.id
@property
def limit(self):

View File

@@ -2,10 +2,10 @@ from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
from .db import Base
from .enums import FieldTypeEnum
@@ -18,27 +18,27 @@ class BaseField(Base):
__abstract__ = True
@declared_attr
def id(cls) -> Mapped[int]:
def id(cls) -> Mapped[int]: # noqa: N805
return mapped_column(primary_key=True, autoincrement=True)
@declared_attr
def type_key(cls) -> Mapped[str]:
def type_key(cls) -> Mapped[str]: # noqa: N805
return mapped_column(ForeignKey("value_type.key"))
@declared_attr
def type(cls) -> Mapped[ValueType]:
def type(cls) -> Mapped[ValueType]: # noqa: N805
return relationship(foreign_keys=[cls.type_key], lazy=False) # type: ignore
@declared_attr
def entry_id(cls) -> Mapped[int]:
def entry_id(cls) -> Mapped[int]: # noqa: N805
return mapped_column(ForeignKey("entries.id"))
@declared_attr
def entry(cls) -> Mapped[Entry]:
def entry(cls) -> Mapped[Entry]: # noqa: N805
return relationship(foreign_keys=[cls.entry_id]) # type: ignore
@declared_attr
def position(cls) -> Mapped[int]:
def position(cls) -> Mapped[int]: # noqa: N805
return mapped_column(default=0)
def __hash__(self):
@@ -125,33 +125,23 @@ class DefaultField:
class _FieldID(Enum):
"""Only for bootstrapping content of DB table"""
"""Only for bootstrapping content of DB table."""
TITLE = DefaultField(
id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True
)
TITLE = DefaultField(id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True)
AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE)
ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE)
URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE)
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_LINE)
NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX)
TAGS = DefaultField(id=6, name="Tags", type=FieldTypeEnum.TAGS)
TAGS_CONTENT = DefaultField(
id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True
)
TAGS_META = DefaultField(
id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True
)
TAGS_CONTENT = DefaultField(id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True)
TAGS_META = DefaultField(id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True)
COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE)
DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME)
DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME)
DATE_MODIFIED = DefaultField(
id=12, name="Date Modified", type=FieldTypeEnum.DATETIME
)
DATE_MODIFIED = DefaultField(id=12, name="Date Modified", type=FieldTypeEnum.DATETIME)
DATE_TAKEN = DefaultField(id=13, name="Date Taken", type=FieldTypeEnum.DATETIME)
DATE_PUBLISHED = DefaultField(
id=14, name="Date Published", type=FieldTypeEnum.DATETIME
)
DATE_PUBLISHED = DefaultField(id=14, name="Date Published", type=FieldTypeEnum.DATETIME)
# ARCHIVED = DefaultField(id=15, name="Archived", type=CheckboxField.checkbox)
# FAVORITE = DefaultField(id=16, name="Favorite", type=CheckboxField.checkbox)
BOOK = DefaultField(id=17, name="Book", type=FieldTypeEnum.TEXT_LINE)
@@ -159,18 +149,12 @@ class _FieldID(Enum):
SERIES = DefaultField(id=19, name="Series", type=FieldTypeEnum.TEXT_LINE)
MANGA = DefaultField(id=20, name="Manga", type=FieldTypeEnum.TEXT_LINE)
SOURCE = DefaultField(id=21, name="Source", type=FieldTypeEnum.TEXT_LINE)
DATE_UPLOADED = DefaultField(
id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME
)
DATE_RELEASED = DefaultField(
id=23, name="Date Released", type=FieldTypeEnum.DATETIME
)
DATE_UPLOADED = DefaultField(id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME)
DATE_RELEASED = DefaultField(id=23, name="Date Released", type=FieldTypeEnum.DATETIME)
VOLUME = DefaultField(id=24, name="Volume", type=FieldTypeEnum.TEXT_LINE)
ANTHOLOGY = DefaultField(id=25, name="Anthology", type=FieldTypeEnum.TEXT_LINE)
MAGAZINE = DefaultField(id=26, name="Magazine", type=FieldTypeEnum.TEXT_LINE)
PUBLISHER = DefaultField(id=27, name="Publisher", type=FieldTypeEnum.TEXT_LINE)
GUEST_ARTIST = DefaultField(
id=28, name="Guest Artist", type=FieldTypeEnum.TEXT_LINE
)
GUEST_ARTIST = DefaultField(id=28, name="Guest Artist", type=FieldTypeEnum.TEXT_LINE)
COMPOSER = DefaultField(id=29, name="Composer", type=FieldTypeEnum.TEXT_LINE)
COMMENTS = DefaultField(id=30, name="Comments", type=FieldTypeEnum.TEXT_LINE)

View File

@@ -14,7 +14,5 @@ class TagSubtag(Base):
class TagField(Base):
__tablename__ = "tag_fields"
field_id: Mapped[int] = mapped_column(
ForeignKey("tag_box_fields.id"), primary_key=True
)
field_id: Mapped[int] = mapped_column(ForeignKey("tag_box_fields.id"), primary_key=True)
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)

View File

@@ -1,58 +1,57 @@
from dataclasses import dataclass
from datetime import datetime, UTC
import re
import shutil
import unicodedata
from dataclasses import dataclass
from datetime import UTC, datetime
from os import makedirs
from pathlib import Path
from typing import Iterator, Any, Type
from typing import Any, Iterator, Type
from uuid import uuid4
import structlog
from sqlalchemy import (
URL,
Engine,
and_,
create_engine,
delete,
exists,
func,
or_,
select,
create_engine,
Engine,
func,
update,
URL,
exists,
delete,
)
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import (
Session,
contains_eager,
selectinload,
make_transient,
selectinload,
)
from ...constants import (
BACKUP_FOLDER_NAME,
TAG_ARCHIVED,
TAG_FAVORITE,
TS_FOLDER_NAME,
LibraryPrefs,
)
from .db import make_tables
from .enums import TagColor, FilterState, FieldTypeEnum
from .enums import FieldTypeEnum, FilterState, TagColor
from .fields import (
BaseField,
DatetimeField,
TagBoxField,
TextField,
_FieldID,
BaseField,
)
from .joins import TagSubtag, TagField
from .models import Entry, Preferences, Tag, TagAlias, ValueType, Folder
from ...constants import (
LibraryPrefs,
TS_FOLDER_NAME,
TAG_ARCHIVED,
TAG_FAVORITE,
BACKUP_FOLDER_NAME,
)
from .joins import TagField, TagSubtag
from .models import Entry, Folder, Preferences, Tag, TagAlias, ValueType
LIBRARY_FILENAME: str = "ts_library.sqlite"
logger = structlog.get_logger(__name__)
import re
import unicodedata
def slugify(input_string: str) -> str:
# Convert to lowercase and normalize unicode characters
@@ -92,8 +91,9 @@ def get_default_tags() -> tuple[Tag, ...]:
class SearchResult:
"""Wrapper for search results.
:param total_count: total number of items for given query, might be different than len(items)
:param items: items for current page (size matches filter.page_size)
Attributes:
total_count(int): total number of items for given query, might be different than len(items).
items(list[Entry]): for current page (size matches filter.page_size).
"""
total_count: int
@@ -130,9 +130,7 @@ class Library:
self.storage_path = None
self.folder = None
def open_library(
self, library_dir: Path | str, storage_path: str | None = None
) -> None:
def open_library(self, library_dir: Path | str, storage_path: str | None = None) -> None:
if isinstance(library_dir, str):
library_dir = Path(library_dir)
@@ -186,9 +184,7 @@ class Library:
session.rollback()
# check if folder matching current path exists already
self.folder = session.scalar(
select(Folder).where(Folder.path == self.library_dir)
)
self.folder = session.scalar(select(Folder).where(Folder.path == self.library_dir))
if not self.folder:
folder = Folder(
path=self.library_dir,
@@ -400,13 +396,9 @@ class Library:
if not search.id: # if `id` is set, we don't need to filter by extensions
if extensions and is_exclude_list:
statement = statement.where(
Entry.path.notilike(f"%.{','.join(extensions)}")
)
statement = statement.where(Entry.path.notilike(f"%.{','.join(extensions)}"))
elif extensions:
statement = statement.where(
Entry.path.ilike(f"%.{','.join(extensions)}")
)
statement = statement.where(Entry.path.ilike(f"%.{','.join(extensions)}"))
statement = statement.options(
selectinload(Entry.text_fields),
@@ -424,9 +416,7 @@ class Library:
logger.info(
"searching library",
filter=search,
query_full=str(
statement.compile(compile_kwargs={"literal_binds": True})
),
query_full=str(statement.compile(compile_kwargs={"literal_binds": True})),
)
res = SearchResult(
@@ -443,7 +433,6 @@ class Library:
search: FilterState,
) -> list[Tag]:
"""Return a list of Tag records matching the query."""
with Session(self.engine) as session:
query = select(Tag)
query = query.options(
@@ -475,7 +464,6 @@ class Library:
def get_all_child_tag_ids(self, tag_id: int) -> list[int]:
"""Recursively traverse a Tag's subtags and return a list of all children tags."""
all_subtags: set[int] = {tag_id}
with Session(self.engine) as session:
@@ -512,9 +500,7 @@ class Library:
def remove_tag_from_field(self, tag: Tag, field: TagBoxField) -> None:
with Session(self.engine) as session:
field_ = session.scalars(
select(TagBoxField).where(TagBoxField.id == field.id)
).one()
field_ = session.scalars(select(TagBoxField).where(TagBoxField.id == field.id)).one()
tag = session.scalars(select(Tag).where(Tag.id == tag.id)).one()
@@ -559,7 +545,7 @@ class Library:
field: BaseField,
entry_ids: list[int],
) -> None:
FieldClass = type(field)
FieldClass = type(field) # noqa: N806
logger.info(
"remove_entry_field",
@@ -596,7 +582,7 @@ class Library:
if isinstance(entry_ids, int):
entry_ids = [entry_ids]
FieldClass = type(field)
FieldClass = type(field) # noqa: N806
with Session(self.engine) as session:
update_stmt = (
@@ -766,9 +752,7 @@ class Library:
session.add(tag_field)
session.commit()
logger.info(
"tag added to field", tag=tag, field=field, entry_id=entry.id
)
logger.info("tag added to field", tag=tag, field=field, entry_id=entry.id)
return True
except IntegrityError as e:
@@ -779,13 +763,9 @@ class Library:
def save_library_backup_to_disk(self) -> Path:
assert isinstance(self.library_dir, Path)
makedirs(
str(self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True
)
makedirs(str(self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
filename = (
f'ts_library_backup_{datetime.now(UTC).strftime("%Y_%m_%d_%H%M%S")}.sqlite'
)
filename = f'ts_library_backup_{datetime.now(UTC).strftime("%Y_%m_%d_%H%M%S")}.sqlite'
target_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
@@ -825,9 +805,7 @@ class Library:
return False
def update_tag(self, tag: Tag, subtag_ids: list[int]) -> None:
"""
Edit a Tag in the Library.
"""
"""Edit a Tag in the Library."""
# TODO - maybe merge this with add_tag?
if tag.shorthand:
@@ -873,17 +851,13 @@ class Library:
def prefs(self, key: LibraryPrefs) -> Any:
# load given item from Preferences table
with Session(self.engine) as session:
return session.scalar(
select(Preferences).where(Preferences.key == key.name)
).value
return session.scalar(select(Preferences).where(Preferences.key == key.name)).value
def set_prefs(self, key: LibraryPrefs, value: Any) -> None:
# set given item in Preferences table
with Session(self.engine) as session:
# load existing preference and update value
pref = session.scalar(
select(Preferences).where(Preferences.key == key.name)
)
pref = session.scalar(select(Preferences).where(Preferences.key == key.name))
pref.value = value
session.add(pref)
session.commit()

View File

@@ -4,19 +4,19 @@ from typing import Optional
from sqlalchemy import JSON, ForeignKey, Integer, event
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ...constants import TAG_ARCHIVED, TAG_FAVORITE
from .db import Base, PathType
from .enums import TagColor
from .fields import (
DatetimeField,
TagBoxField,
TextField,
FieldTypeEnum,
_FieldID,
BaseField,
BooleanField,
DatetimeField,
FieldTypeEnum,
TagBoxField,
TextField,
_FieldID,
)
from .joins import TagSubtag
from ...constants import TAG_FAVORITE, TAG_ARCHIVED
class TagAlias(Base):
@@ -191,8 +191,9 @@ class Entry(Base):
return tag in self.tags
def remove_tag(self, tag: Tag, field: TagBoxField | None = None) -> None:
"""
Removes a Tag from the Entry. If given a field index, the given Tag will
"""Removes a Tag from the Entry.
If given a field index, the given Tag will
only be removed from that index. If left blank, all instances of that
Tag will be removed from the Entry.
"""
@@ -225,22 +226,16 @@ class ValueType(Base):
position: Mapped[int]
# add relations to other tables
text_fields: Mapped[list[TextField]] = relationship(
"TextField", back_populates="type"
)
text_fields: Mapped[list[TextField]] = relationship("TextField", back_populates="type")
datetime_fields: Mapped[list[DatetimeField]] = relationship(
"DatetimeField", back_populates="type"
)
tag_box_fields: Mapped[list[TagBoxField]] = relationship(
"TagBoxField", back_populates="type"
)
boolean_fields: Mapped[list[BooleanField]] = relationship(
"BooleanField", back_populates="type"
)
tag_box_fields: Mapped[list[TagBoxField]] = relationship("TagBoxField", back_populates="type")
boolean_fields: Mapped[list[BooleanField]] = relationship("BooleanField", back_populates="type")
@property
def as_field(self) -> BaseField:
FieldClass = {
FieldClass = { # noqa: N806
FieldTypeEnum.TEXT_LINE: TextField,
FieldTypeEnum.TEXT_BOX: TextField,
FieldTypeEnum.TAGS: TagBoxField,

View File

@@ -47,9 +47,7 @@ logger = structlog.get_logger(__name__)
class Entry:
"""A Library Entry Object. Referenced by ID."""
def __init__(
self, id: int, filename: str | Path, path: str | Path, fields: list[dict]
) -> None:
def __init__(self, id: int, filename: str | Path, path: str | Path, fields: list[dict]) -> None:
# Required Fields ======================================================
self.id = int(id)
self.filename = Path(filename)
@@ -138,9 +136,7 @@ class Entry:
while tag_id in t:
t.remove(tag_id)
def add_tag(
self, library: "Library", tag_id: int, field_id: int, field_index: int = -1
):
def add_tag(self, library: "Library", tag_id: int, field_id: int, field_index: int = -1):
# if self.fields:
# if field_index != -1:
# logger.info(f'[LIBRARY] ADD TAG to E:{self.id}, F-DI:{field_id}, F-INDEX:{field_index}')
@@ -212,9 +208,7 @@ class Tag:
"""Returns a formatted tag name intended for displaying."""
if self.subtag_ids:
if library.get_tag(self.subtag_ids[0]).shorthand:
return (
f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).shorthand})"
)
return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).shorthand})"
else:
return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).name})"
else:
@@ -473,9 +467,7 @@ class Library:
"ignored_extensions", self.default_ext_exclude_list
)
else:
self.ext_list = json_dump.get(
"ext_list", self.default_ext_exclude_list
)
self.ext_list = json_dump.get("ext_list", self.default_ext_exclude_list)
# Sanitizes older lists (v9.2.1) that don't use leading periods.
# Without this, existing lists (including default lists)
@@ -535,9 +527,7 @@ class Library:
self._map_tag_id_to_index(t, -1)
self._map_tag_strings_to_tag_id(t)
else:
logger.info(
f"[LIBRARY]Skipping Tag with duplicate ID: {tag}"
)
logger.info(f"[LIBRARY]Skipping Tag with duplicate ID: {tag}")
# Step 3: Map each Tag's subtags together now that all Tag objects in it.
for t in self.tags:
@@ -586,19 +576,14 @@ class Library:
matched = False
collation_id = -1
for c in self.collations:
if (
c.title
== self.get_field_attr(f, "content")[
"name"
]
):
if c.title == self.get_field_attr(f, "content")["name"]:
c.e_ids_and_pages.append(
(
id,
int(
self.get_field_attr(
f, "content"
)["page"]
self.get_field_attr(f, "content")[
"page"
]
),
)
)
@@ -607,9 +592,7 @@ class Library:
if not matched:
c = Collation(
id=self._next_collation_id,
title=self.get_field_attr(f, "content")[
"name"
],
title=self.get_field_attr(f, "content")["name"],
e_ids_and_pages=[],
sort_order="",
)
@@ -618,11 +601,7 @@ class Library:
c.e_ids_and_pages.append(
(
id,
int(
self.get_field_attr(
f, "content"
)["page"]
),
int(self.get_field_attr(f, "content")["page"]),
)
)
self.collations.append(c)
@@ -645,9 +624,7 @@ class Library:
self._map_entry_id_to_index(e, -1)
end_time = time.time()
logger.info(
f"[LIBRARY] Entries loaded", load_time=end_time - start_time
)
logger.info(f"[LIBRARY] Entries loaded", load_time=end_time - start_time)
# Parse Collations -----------------------------------------
if "collations" in json_dump.keys():
@@ -761,9 +738,7 @@ class Library:
self.verify_ts_folders()
with open(
self.library_dir / TS_FOLDER_NAME / filename, "w", encoding="utf-8"
) as outfile:
with open(self.library_dir / TS_FOLDER_NAME / filename, "w", encoding="utf-8") as outfile:
outfile.flush()
ujson.dump(
self.to_json(),
@@ -773,9 +748,7 @@ class Library:
)
# , indent=4 <-- How to prettyprint dump
end_time = time.time()
logger.info(
f"[LIBRARY] Library saved to disk in {(end_time - start_time):.3f} seconds"
)
logger.info(f"[LIBRARY] Library saved to disk in {(end_time - start_time):.3f} seconds")
def save_library_backup_to_disk(self) -> str:
"""
@@ -1053,9 +1026,7 @@ class Library:
file_1.resolve in self.filename_to_entry_id_map.keys()
and file_2 in self.filename_to_entry_id_map.keys()
):
self.dupe_files.append(
(files[match[0]], files[match[1]], match[2])
)
self.dupe_files.append((files[match[0]], files[match[1]], match[2]))
print("")
for dupe in self.dupe_files:
@@ -1287,9 +1258,7 @@ class Library:
"""Returns an Entry ID given the full filepath it points to."""
try:
if self.entries:
return self.filename_to_entry_id_map[
Path(filename).relative_to(self.library_dir)
]
return self.filename_to_entry_id_map[Path(filename).relative_to(self.library_dir)]
except KeyError:
return -1
@@ -1334,8 +1303,7 @@ class Library:
for j, term in enumerate(query_words):
if (
query_words[i : j + 1]
and " ".join(query_words[i : j + 1])
in self._tag_strings_to_id_map
and " ".join(query_words[i : j + 1]) in self._tag_strings_to_id_map
):
all_tag_terms.append(" ".join(query_words[i : j + 1]))
# print(all_tag_terms)
@@ -1404,9 +1372,7 @@ class Library:
if allow_adv:
if [q for q in query_words if (q in str(entry.path).lower())]:
results.append((ItemType.ENTRY, entry.id))
elif [
q for q in query_words if (q in str(entry.filename).lower())
]:
elif [q for q in query_words if (q in str(entry.filename).lower())]:
results.append((ItemType.ENTRY, entry.id))
elif tag_only:
if entry.has_tag(self, int(query_words[0])):
@@ -1423,19 +1389,14 @@ class Library:
added = False
for f in entry.fields:
if self.get_field_attr(f, "type") == "collation":
if (
self.get_field_attr(f, "content")
not in collations_added
):
if self.get_field_attr(f, "content") not in collations_added:
results.append(
(
ItemType.COLLATION,
self.get_field_attr(f, "content"),
)
)
collations_added.append(
self.get_field_attr(f, "content")
)
collations_added.append(self.get_field_attr(f, "content"))
added = True
if not added:
@@ -1453,9 +1414,7 @@ class Library:
# (You're 99.9999999% likely to just get 1 item)
for id in self._tag_strings_to_id_map[term]:
cluster.add(id)
cluster = cluster.union(
set(self.get_tag_cluster(id))
)
cluster = cluster.union(set(self.get_tag_cluster(id)))
# print(f'Full Cluster: {cluster}')
# For each of the Tag IDs in the term's ID cluster:
for t in cluster:
@@ -1516,19 +1475,14 @@ class Library:
if allowed_ext == self.is_exclude_list:
for f in entry.fields:
if self.get_field_attr(f, "type") == "collation":
if (
self.get_field_attr(f, "content")
not in collations_added
):
if self.get_field_attr(f, "content") not in collations_added:
results.append(
(
ItemType.COLLATION,
self.get_field_attr(f, "content"),
)
)
collations_added.append(
self.get_field_attr(f, "content")
)
collations_added.append(self.get_field_attr(f, "content"))
added = True
if not added:
@@ -1603,9 +1557,7 @@ class Library:
if query == string:
exact_match = True
elif string.startswith(query):
if len(query) >= (
len(string) // (len(string) if threshold == 1 else threshold)
):
if len(query) >= (len(string) // (len(string) if threshold == 1 else threshold)):
partial_match = True
if exact_match or partial_match:
@@ -1640,9 +1592,7 @@ class Library:
id_weights.append((id, 0))
# Contextual Weighing
if context and (
(len(id_weights) > 1 and len(priority_ids) > 1) or (len(priority_ids) > 1)
):
if context and ((len(id_weights) > 1 and len(priority_ids) > 1) or (len(priority_ids) > 1)):
context_strings: list[str] = [
s.replace(" ", "")
.replace("_", "")
@@ -1691,11 +1641,7 @@ class Library:
split += ts.split(" ")
tag_strings += split
tag_strings = [
s.replace(" ", "")
.replace("_", "")
.replace("-", "")
.replace("'", "")
.lower()
s.replace(" ", "").replace("_", "").replace("-", "").replace("'", "").lower()
for s in tag_strings
]
while "" in tag_strings:
@@ -1921,38 +1867,26 @@ class Library:
if data:
# Add a Title Field if the data doesn't already exist.
if data.get("title"):
if not self.does_field_content_exist(
entry_id, FieldID.TITLE, data["title"]
):
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"):
if not self.does_field_content_exist(
entry_id, FieldID.AUTHOR, data["author"]
):
if not self.does_field_content_exist(entry_id, FieldID.AUTHOR, data["author"]):
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"):
if not self.does_field_content_exist(
entry_id, FieldID.ARTIST, data["artist"]
):
if not self.does_field_content_exist(entry_id, FieldID.ARTIST, data["artist"]):
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"):
date = str(
datetime.datetime.strptime(
data["date_published"], "%Y-%m-%d %H:%M:%S"
)
)
if not self.does_field_content_exist(
entry_id, FieldID.DATE_PUBLISHED, date
):
date = str(datetime.datetime.strptime(data["date_published"], "%Y-%m-%d %H:%M:%S"))
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")
@@ -2025,9 +1959,7 @@ class Library:
)
else:
self.add_field_to_entry(entry_id, FieldID.CONTENT_TAGS)
self.update_entry_field(
entry_id, -1, [matching[0]], "append"
)
self.update_entry_field(entry_id, -1, [matching[0]], "append")
# Add all original string tags as a note.
str_tags = f"Original Tags: {tags}"
@@ -2041,9 +1973,7 @@ class Library:
entry_id, FieldID.DESCRIPTION, data["description"]
):
self.add_field_to_entry(entry_id, FieldID.DESCRIPTION)
self.update_entry_field(
entry_id, -1, data["description"], "replace"
)
self.update_entry_field(entry_id, -1, data["description"], "replace")
if data.get("content"):
if not self.does_field_content_exist(
entry_id, FieldID.DESCRIPTION, data["content"]
@@ -2054,9 +1984,7 @@ class Library:
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, FieldID.SOURCE, source
):
if not self.does_field_content_exist(entry_id, FieldID.SOURCE, source):
self.add_field_to_entry(entry_id, FieldID.SOURCE)
self.update_entry_field(entry_id, -1, source, "replace")
@@ -2140,9 +2068,7 @@ class Library:
elif attribute.lower() == "content":
return entry_field[self.get_field_attr(entry_field, "id")]
else:
return self.get_field_obj(self.get_field_attr(entry_field, "id"))[
attribute.lower()
]
return self.get_field_obj(self.get_field_attr(entry_field, "id"))[attribute.lower()]
def get_field_obj(self, field_id: int) -> dict:
"""
@@ -2215,11 +2141,7 @@ class Library:
if subtag.subtag_ids:
self._map_tag_id_to_cluster(
tag,
[
self.get_tag(sub_id)
for sub_id in subtag.subtag_ids
if sub_id != tag.id
],
[self.get_tag(sub_id) for sub_id in subtag.subtag_ids if sub_id != tag.id],
)
def _map_tag_id_to_index(self, tag: Tag, index: int) -> None:
@@ -2285,6 +2207,4 @@ class Library:
def sort_fields(self, entry_id: int, order: list[int]) -> None:
"""Sorts an Entry's Fields given an ordered list of Field IDs."""
entry = self.get_entry(entry_id)
entry.fields = sorted(
entry.fields, key=lambda x: order.index(self.get_field_attr(x, "id"))
)
entry.fields = sorted(entry.fields, key=lambda x: order.index(self.get_field_attr(x, "id")))

View File

@@ -6,7 +6,6 @@ from enum import IntEnum
from typing import Any
import structlog
from src.core.library.alchemy.enums import TagColor
logger = structlog.get_logger(__name__)

View File

@@ -7,8 +7,8 @@
import json
from pathlib import Path
from src.core.library import Entry, Library
from src.core.constants import TS_FOLDER_NAME
from src.core.library import Entry, Library
from src.core.library.alchemy.fields import _FieldID
from src.core.utils.missing_files import logger
@@ -19,8 +19,7 @@ class TagStudioCore:
@classmethod
def get_gdl_sidecar(cls, filepath: Path, source: str = "") -> dict:
"""
Attempt to open and dump a Gallery-DL Sidecar file for the filepath.
"""Attempt to open and dump a Gallery-DL Sidecar file for the filepath.
Return a formatted object with notable values or an empty object if none is found.
"""
@@ -34,9 +33,7 @@ class TagStudioCore:
newstem = _filepath.stem[:-16] + "1" + _filepath.stem[-15:]
_filepath = _filepath.parent / (newstem + ".json")
logger.info(
"get_gdl_sidecar", filepath=filepath, source=source, sidecar=_filepath
)
logger.info("get_gdl_sidecar", filepath=filepath, source=source, sidecar=_filepath)
try:
with open(_filepath, encoding="utf8") as f:
@@ -66,9 +63,6 @@ class TagStudioCore:
info[_FieldID.ARTIST] = json_dump["user"].strip()
info[_FieldID.DESCRIPTION] = json_dump["description"].strip()
info[_FieldID.SOURCE] = json_dump["post_url"].strip()
# else:
# print(
# f'[INFO]: TagStudio does not currently support sidecar files for "{source}"')
except Exception:
logger.exception("Error handling sidecar file.", path=_filepath)
@@ -101,7 +95,6 @@ class TagStudioCore:
@classmethod
def match_conditions(cls, lib: Library, entry_id: int) -> bool:
"""Match defined conditions against a file to add Entry data."""
# TODO - what even is this file format?
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
cond_file = lib.library_dir / TS_FOLDER_NAME / "conditions.json"
@@ -127,17 +120,13 @@ class TagStudioCore:
return False
fields = c["fields"]
entry_field_types = {
field.type_key: field for field in entry.fields
}
entry_field_types = {field.type_key: field for field in entry.fields}
for field in fields:
is_new = field["id"] not in entry_field_types
field_key = field["id"]
if is_new:
lib.add_entry_field_type(
entry.id, field_key, field["value"]
)
lib.add_entry_field_type(entry.id, field_key, field["value"])
else:
lib.update_entry_field(entry.id, field_key, field["value"])
@@ -149,7 +138,6 @@ class TagStudioCore:
@classmethod
def build_url(cls, entry: Entry, source: str):
"""Try to rebuild a source URL given a specific filename structure."""
source = source.lower().replace("-", " ").replace("_", " ")
if "twitter" in source:
return cls._build_twitter_url(entry)
@@ -158,8 +146,8 @@ class TagStudioCore:
@classmethod
def _build_twitter_url(cls, entry: Entry):
"""
Build a Twitter URL given a specific filename structure.
"""Build a Twitter URL given a specific filename structure.
Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD'
"""
try:
@@ -172,8 +160,8 @@ class TagStudioCore:
@classmethod
def _build_instagram_url(cls, entry: Entry):
"""
Build an Instagram URL given a specific filename structure.
"""Build an Instagram URL given a specific filename structure.
Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD'
"""
try:

View File

@@ -1,10 +1,9 @@
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from pathlib import Path
import xml.etree.ElementTree as ET
import structlog
from src.core.library import Library, Entry
from src.core.library import Entry, Library
from src.core.library.alchemy.enums import FilterState
logger = structlog.get_logger()
@@ -22,8 +21,8 @@ class DupeRegistry:
return len(self.groups)
def refresh_dupe_files(self, results_filepath: str | Path):
"""
Refresh the list of duplicate files.
"""Refresh the list of duplicate files.
A duplicate file is defined as an identical or near-identical file as determined
by a DupeGuru results file.
"""
@@ -67,9 +66,10 @@ class DupeRegistry:
self.groups.append(files)
def merge_dupe_entries(self):
"""
Merge the duplicate Entry items.
A duplicate Entry is defined as an Entry pointing to a file that one or more other Entries are also pointing to
"""Merge the duplicate Entry items.
A duplicate Entry is defined as an Entry pointing to a file
that one or more other Entries are also pointing to
"""
logger.info(
"Consolidating Entries... (This may take a while for larger libraries)",

View File

@@ -2,13 +2,13 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from chardet.universaldetector import UniversalDetector
from pathlib import Path
from chardet.universaldetector import UniversalDetector
def detect_char_encoding(filepath: Path) -> str | None:
"""
Attempts to detect the character encoding of a text file.
"""Attempts to detect the character encoding of a text file.
Args:
filepath (Path): The path of the text file to analyze.
@@ -16,7 +16,6 @@ def detect_char_encoding(filepath: Path) -> str | None:
Returns:
str | None: The detected character encoding, if any.
"""
detector = UniversalDetector()
with open(filepath, "rb") as text_file:
for line in text_file.readlines():

View File

@@ -1,10 +1,9 @@
from collections.abc import Iterator
from dataclasses import field, dataclass
from dataclasses import dataclass, field
from pathlib import Path
import structlog
from src.core.library import Library, Entry
from src.core.library import Entry, Library
IGNORE_ITEMS = [
"$recycle.bin",
@@ -36,11 +35,10 @@ class MissingRegistry:
yield i
def match_missing_file(self, match_item: Entry) -> list[Path]:
"""
Try to find missing entry files within the library directory.
"""Try to find missing entry files within the library directory.
Works if files were just moved to different subfolders and don't have duplicate names.
"""
matches = []
for item in self.library.library_dir.glob(f"**/{match_item.path.name}"):
if item.name == match_item.path.name: # TODO - implement IGNORE_ITEMS

View File

@@ -1,12 +1,11 @@
from time import time
from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import Path
from time import time
import structlog
from src.core.constants import TS_FOLDER_NAME
from src.core.library import Library, Entry
from src.core.library import Entry, Library
logger = structlog.get_logger(__name__)

View File

@@ -4,7 +4,7 @@
def strip_web_protocol(string: str) -> str:
"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
r"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
prefixes = ["https://", "http://", "www.", "www2."]
for prefix in prefixes:
string = string.removeprefix(prefix)

View File

@@ -2,26 +2,12 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x"""
"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x."""
from PySide6.QtCore import Qt, QMargins, QPoint, QRect, QSize
from PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt
from PySide6.QtWidgets import QLayout, QSizePolicy, QWidget
# class Window(QWidget):
# def __init__(self):
# super().__init__()
# flow_layout = FlowLayout(self)
# flow_layout.addWidget(QPushButton("Short"))
# flow_layout.addWidget(QPushButton("Longer"))
# flow_layout.addWidget(QPushButton("Different text"))
# flow_layout.addWidget(QPushButton("More text"))
# flow_layout.addWidget(QPushButton("Even longer button text"))
# self.setWindowTitle("Flow Layout")
class FlowWidget(QWidget):
def __init__(self, parent=None) -> None:
super().__init__(parent)
@@ -43,46 +29,46 @@ class FlowLayout(QLayout):
while item:
item = self.takeAt(0)
def addItem(self, item):
def addItem(self, item): # noqa: N802
self._item_list.append(item)
def count(self):
return len(self._item_list)
def itemAt(self, index):
def itemAt(self, index): # noqa: N802
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
def takeAt(self, index):
def takeAt(self, index): # noqa: N802
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
return None
def expandingDirections(self):
def expandingDirections(self): # noqa: N802
return Qt.Orientation(0)
def hasHeightForWidth(self):
def hasHeightForWidth(self): # noqa: N802
return True
def heightForWidth(self, width):
height = self._do_layout(QRect(0, 0, width, 0), True)
def heightForWidth(self, width): # noqa: N802
height = self._do_layout(QRect(0, 0, width, 0), test_only=True)
return height
def setGeometry(self, rect):
def setGeometry(self, rect): # noqa: N802
super().setGeometry(rect)
self._do_layout(rect, False)
self._do_layout(rect, test_only=False)
def setGridEfficiency(self, value: bool):
def enable_grid_optimizations(self, value: bool):
"""Enable or Disable efficiencies when all objects are equally sized."""
self.grid_efficiency = value
def sizeHint(self):
def sizeHint(self): # noqa: N802
return self.minimumSize()
def minimumSize(self):
def minimumSize(self): # noqa: N802
if self.grid_efficiency:
if self._item_list:
return self._item_list[0].minimumSize()
@@ -94,9 +80,7 @@ class FlowLayout(QLayout):
for item in self._item_list:
size = size.expandedTo(item.minimumSize())
size += QSize(
2 * self.contentsMargins().top(), 2 * self.contentsMargins().top()
)
size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
return size
def _do_layout(self, rect: QRect, test_only: bool) -> float:
@@ -122,20 +106,13 @@ class FlowLayout(QLayout):
)
for item in self._item_list:
# print(issubclass(type(item.widget()), FlowWidget))
# print(item.widget().ignore_size)
skip_count = 0
if (
issubclass(type(item.widget()), FlowWidget)
and item.widget().ignore_size
):
if issubclass(type(item.widget()), FlowWidget) and item.widget().ignore_size:
skip_count += 1
if (
issubclass(type(item.widget()), FlowWidget)
and not item.widget().ignore_size
) or (not issubclass(type(item.widget()), FlowWidget)):
# print(f'Item {i}')
if (issubclass(type(item.widget()), FlowWidget) and not item.widget().ignore_size) or (
not issubclass(type(item.widget()), FlowWidget)
):
if not self.grid_efficiency:
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
@@ -163,15 +140,4 @@ class FlowLayout(QLayout):
x = next_x
line_height = max(line_height, item.sizeHint().height())
# print(y + line_height - rect.y() * ((len(self._item_list) - skip_count) / len(self._item_list)))
# print(y + line_height - rect.y()) * ((len(self._item_list) - skip_count) / len(self._item_list))
return (
y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list))
)
# if __name__ == "__main__":
# app = QApplication(sys.argv)
# main_win = Window()
# main_win.show()
# sys.exit(app.exec())
return y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list))

View File

@@ -15,13 +15,11 @@ _THEME_LIGHT_FG: str = "#000000DD"
def theme_fg_overlay(image: Image.Image) -> Image.Image:
"""
Overlay the foreground theme color onto an image.
"""Overlay the foreground theme color onto an image.
Args:
image (Image): The PIL Image object to apply an overlay to.
"""
overlay_color = (
_THEME_DARK_FG
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
@@ -32,23 +30,19 @@ def theme_fg_overlay(image: Image.Image) -> Image.Image:
def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image:
"""
Overlay a color gradient onto an image.
"""Overlay a color gradient onto an image.
Args:
image (Image): The PIL Image object to apply an overlay to.
gradient (list[str): A list of string hex color codes for use as
the colors of the gradient.
"""
im: Image.Image = _apply_overlay(image, linear_gradient(image.size, gradient))
return im
def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image:
"""
Internal method to apply an overlay on top of an image, using
the image's alpha channel as a mask.
"""Apply an overlay on top of an image, using the image's alpha channel as a mask.
Args:
image (Image): The PIL Image object to apply an overlay to.

View File

@@ -3,7 +3,7 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Signal, QRunnable, QObject
from PySide6.QtCore import QObject, QRunnable, Signal
class CustomRunnable(QRunnable, QObject):

View File

@@ -2,16 +2,15 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import subprocess
import shutil
import subprocess
import sys
import traceback
from pathlib import Path
import structlog
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel
logger = structlog.get_logger(__name__)
@@ -21,8 +20,8 @@ def open_file(path: str | Path, file_manager: bool = False):
Args:
path (str): The path to the file to open.
file_manager (bool, optional): Whether to open the file in the file manager (e.g. Finder on macOS).
Defaults to False.
file_manager (bool, optional): Whether to open the file in the file manager
(e.g. Finder on macOS). Defaults to False.
"""
path = Path(path)
logger.info("Opening file", path=path)
@@ -36,7 +35,8 @@ def open_file(path: str | Path, file_manager: bool = False):
if file_manager:
command_name = "explorer"
command_args = '/select,"' + normpath + '"'
# For some reason, if the args are passed in a list, this will error when the path has spaces, even while surrounded in double quotes
# For some reason, if the args are passed in a list, this will error when the
# path has spaces, even while surrounded in double quotes.
subprocess.Popen(
command_name + command_args,
shell=True,
@@ -82,9 +82,7 @@ def open_file(path: str | Path, file_manager: bool = False):
if command is not None:
subprocess.Popen([command] + command_args, close_fds=True)
else:
logger.info(
"Could not find command on system PATH", command=command_name
)
logger.info("Could not find command on system PATH", command=command_name)
except Exception:
traceback.print_exc()
@@ -125,7 +123,7 @@ class FileOpenerLabel(QLabel):
"""
super().__init__(text, parent)
def setFilePath(self, filepath):
def set_file_path(self, filepath):
"""Set the filepath to open.
Args:
@@ -133,10 +131,11 @@ class FileOpenerLabel(QLabel):
"""
self.filepath = filepath
def mousePressEvent(self, event):
def mousePressEvent(self, event): # noqa: N802
"""Handle mouse press events.
On a left click, open the file in the default file explorer. On a right click, show a context menu.
On a left click, open the file in the default file explorer.
On a right click, show a context menu.
Args:
event (QMouseEvent): The mouse press event.

View File

@@ -3,7 +3,7 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from collections.abc import Callable
from PySide6.QtCore import Signal, QObject
from PySide6.QtCore import QObject, Signal
class FunctionIterator(QObject):

View File

@@ -2,12 +2,10 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PIL import Image, ImageEnhance, ImageChops
from PIL import Image, ImageChops, ImageEnhance
def four_corner_gradient_background(
image: Image.Image, adj_size, mask, hl
) -> Image.Image:
def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl) -> Image.Image:
if image.size != (adj_size, adj_size):
# Old 1 color method.
# bg_col = image.copy().resize((1, 1)).getpixel((0,0))
@@ -16,7 +14,11 @@ def four_corner_gradient_background(
# bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
# Small gradient background. Looks decent, and is only a one-liner.
# bg = image.copy().resize((2, 2), resample=Image.Resampling.BILINEAR).resize((adj_size,adj_size),resample=Image.Resampling.BILINEAR)
# bg = (
# image.copy()
# .resize((2, 2), resample=Image.Resampling.BILINEAR)
# .resize((adj_size, adj_size), resample=Image.Resampling.BILINEAR)
# )
# Four-Corner Gradient Background.
# Not exactly a one-liner, but it's (subjectively) really cool.

View File

@@ -6,9 +6,10 @@ 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.
"""Custom QPushButton wrapper.
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):

View File

@@ -3,17 +3,16 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Signal, Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QListWidget,
QListWidgetItem,
QPushButton,
QVBoxLayout,
QWidget,
)
from src.core.library import Library

View File

@@ -4,26 +4,24 @@
import structlog
from PySide6.QtCore import Signal, Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QLabel,
QPushButton,
QLineEdit,
QScrollArea,
QFrame,
QTextEdit,
QComboBox,
QFrame,
QLabel,
QLineEdit,
QPushButton,
QScrollArea,
QTextEdit,
QVBoxLayout,
QWidget,
)
from src.core.library import Tag, Library
from src.core.library import Library, Tag
from src.core.library.alchemy.enums import TagColor
from src.core.palette import ColorType, get_tag_color
from src.qt.widgets.panel import PanelWidget, PanelModal
from src.qt.widgets.tag import TagWidget
from src.qt.modals.tag_search import TagSearchPanel
from src.qt.widgets.panel import PanelModal, PanelWidget
from src.qt.widgets.tag import TagWidget
logger = structlog.get_logger(__name__)
@@ -145,7 +143,9 @@ class BuildTagPanel(PanelWidget):
"combobox-popup:0;"
"font-weight:600;"
f"color:{get_tag_color(ColorType.TEXT, self.color_field.currentData())};"
f"background-color:{get_tag_color(ColorType.PRIMARY, self.color_field.currentData())};"
f"background-color:{get_tag_color(
ColorType.PRIMARY,
self.color_field.currentData())};"
)
)
)
@@ -183,7 +183,7 @@ class BuildTagPanel(PanelWidget):
layout.setSpacing(3)
for tag_id in self.subtags:
tag = self.lib.get_tag(tag_id)
tw = TagWidget(tag, False, True)
tw = TagWidget(tag, has_edit=False, has_remove=True)
tw.on_remove.connect(lambda t=tag_id: self.remove_subtag_callback(t))
layout.addWidget(tw)
self.scroll_layout.addWidget(c)

View File

@@ -4,17 +4,16 @@
import typing
from PySide6.QtCore import Signal, Qt, QThreadPool
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtCore import Qt, QThreadPool, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QListView,
QPushButton,
QVBoxLayout,
QWidget,
)
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator

View File

@@ -3,32 +3,27 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Signal, Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QVBoxLayout,
QComboBox,
QHBoxLayout,
QWidget,
QLabel,
QLineEdit,
QPushButton,
QStyledItemDelegate,
QTableWidget,
QTableWidgetItem,
QStyledItemDelegate,
QLineEdit,
QComboBox,
QLabel,
QVBoxLayout,
QWidget,
)
from src.core.constants import LibraryPrefs
from src.core.library import Library
from src.qt.widgets.panel import PanelWidget
from src.core.constants import LibraryPrefs
class FileExtensionItemDelegate(QStyledItemDelegate):
def setModelData(self, editor, model, index):
if (
isinstance(editor, QLineEdit)
and editor.text()
and not editor.text().startswith(".")
):
def setModelData(self, editor, model, index): # noqa: N802
if isinstance(editor, QLineEdit) and editor.text() and not editor.text().startswith("."):
editor.setText(f".{editor.text()}")
super().setModelData(editor, model, index)
@@ -75,9 +70,7 @@ class FileExtensionModal(PanelWidget):
is_exclude_list = int(bool(self.lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST)))
self.mode_combobox.setCurrentIndex(is_exclude_list)
self.mode_combobox.currentIndexChanged.connect(
lambda i: self.update_list_mode(i)
)
self.mode_combobox.currentIndexChanged.connect(lambda i: self.update_list_mode(i))
self.mode_layout.addWidget(self.mode_label)
self.mode_layout.addWidget(self.mode_combobox)
self.mode_layout.setStretch(1, 1)
@@ -85,16 +78,13 @@ class FileExtensionModal(PanelWidget):
# Add Widgets To Layout ------------------------------------------------
self.root_layout.addWidget(self.mode_widget)
self.root_layout.addWidget(self.table)
self.root_layout.addWidget(
self.add_button, alignment=Qt.AlignmentFlag.AlignCenter
)
self.root_layout.addWidget(self.add_button, alignment=Qt.AlignmentFlag.AlignCenter)
# Finalize Modal -------------------------------------------------------
self.refresh_list()
def update_list_mode(self, mode: int):
"""
Update the mode of the extension list: "Exclude" or "Include".
"""Update the mode of the extension list: "Exclude" or "Include".
Args:
mode (int): The list mode, given by the index of the mode inside

View File

@@ -7,14 +7,13 @@ import typing
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QFileDialog,
QHBoxLayout,
QLabel,
QPushButton,
QFileDialog,
QVBoxLayout,
QWidget,
)
from src.core.library import Library
from src.core.utils.dupe_files import DupeRegistry
from src.qt.modals.mirror_entities import MirrorEntriesModal
@@ -25,7 +24,6 @@ if typing.TYPE_CHECKING:
class FixDupeFilesModal(QWidget):
# done = Signal(int)
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
@@ -43,41 +41,19 @@ class FixDupeFilesModal(QWidget):
self.desc_widget = QLabel()
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setStyleSheet(
# 'background:blue;'
"text-align:left;"
# 'font-weight:bold;'
# 'font-size:14px;'
# 'padding-top: 6px'
""
)
self.desc_widget.setStyleSheet("text-align:left;")
self.desc_widget.setText(
"""TagStudio supports importing DupeGuru results to manage duplicate files."""
"TagStudio supports importing DupeGuru results to manage duplicate files."
)
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.dupe_count = QLabel()
self.dupe_count.setObjectName("dupeCountLabel")
self.dupe_count.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
"font-weight:bold;"
"font-size:14px;"
# 'padding-top: 6px'
""
)
self.dupe_count.setStyleSheet("font-weight:bold;" "font-size:14px;" "")
self.dupe_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.file_label = QLabel()
self.file_label.setObjectName("fileLabel")
# self.file_label.setStyleSheet(
# # 'background:blue;'
# # 'text-align:center;'
# 'font-weight:bold;'
# 'font-size:14px;'
# # 'padding-top: 6px'
# '')
# self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.file_label.setText("No DupeGuru File Selected")
self.open_button = QPushButton()
@@ -93,17 +69,19 @@ class FixDupeFilesModal(QWidget):
self.mirror_desc = QLabel()
self.mirror_desc.setWordWrap(True)
self.mirror_desc.setText(
"""Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data."""
"Mirror the Entry data across each duplicate match set, combining all data while not "
"removing or duplicating fields. This operation will not delete any files or data."
)
# self.mirror_delete_button = QPushButton()
# self.mirror_delete_button.setText('Mirror && Delete')
self.advice_label = QLabel()
self.advice_label.setWordWrap(True)
# fmt: off
self.advice_label.setText(
"""After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's "Fix Unlinked Entries" feature in the Tools menu in order to delete the unlinked Entries."""
"After mirroring, you're free to use DupeGuru to delete the unwanted files. "
"Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the "
"Tools menu in order to delete the unlinked Entries."
)
# fmt: on
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
@@ -112,28 +90,18 @@ class FixDupeFilesModal(QWidget):
self.done_button = QPushButton()
self.done_button.setText("&Done")
# self.save_button.setAutoDefault(True)
self.done_button.setDefault(True)
self.done_button.clicked.connect(self.hide)
# self.done_button.clicked.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.done_button)
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.done.connect(lambda x: callback(x))
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.dupe_count)
self.root_layout.addWidget(self.file_label)
self.root_layout.addWidget(self.open_button)
# self.mirror_delete_button.setHidden(True)
self.root_layout.addWidget(self.mirror_button)
self.root_layout.addWidget(self.mirror_desc)
# self.root_layout.addWidget(self.mirror_delete_button)
self.root_layout.addWidget(self.advice_label)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)

View File

@@ -6,15 +6,14 @@
import typing
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from src.core.library import Library
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal
from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries
from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries
from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@@ -43,7 +42,11 @@ class FixUnlinkedEntriesModal(QWidget):
self.unlinked_desc_widget.setWordWrap(True)
self.unlinked_desc_widget.setStyleSheet("text-align:left;")
self.unlinked_desc_widget.setText(
"""Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked. Unlinked entries may be automatically relinked via searching your directories, manually relinked by the user, or deleted if desired."""
"Each library entry is linked to a file in one of your directories. "
"If a file linked to an entry is moved or deleted outside of TagStudio, "
"it is then considered unlinked.\n\n"
"Unlinked entries may be automatically relinked via searching your directories, "
"manually relinked by the user, or deleted if desired."
)
self.missing_count_label = QLabel()

View File

@@ -10,17 +10,16 @@ from dataclasses import dataclass, field
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QFrame,
QVBoxLayout,
QWidget,
)
from src.core.constants import TAG_FAVORITE, TAG_ARCHIVED
from src.core.library import Tag, Library
from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from src.core.library import Library, Tag
from src.core.library.alchemy.fields import _FieldID
from src.core.palette import ColorType, get_tag_color
from src.qt.flowlayout import FlowLayout
@@ -38,9 +37,7 @@ class BranchData:
tag: Tag | None = None
def add_folders_to_tree(
library: Library, tree: BranchData, items: tuple[str, ...]
) -> BranchData:
def add_folders_to_tree(library: Library, tree: BranchData, items: tuple[str, ...]) -> BranchData:
branch = tree
for folder in items:
if folder not in branch.dirs:
@@ -160,7 +157,6 @@ def generate_preview_data(library: Library) -> BranchData:
class FoldersToTagsModal(QWidget):
# done = Signal(int)
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.library = library
@@ -177,9 +173,7 @@ class FoldersToTagsModal(QWidget):
self.title_widget = QLabel()
self.title_widget.setObjectName("title")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet(
"font-weight:bold;" "font-size:14px;" "padding-top: 6px"
)
self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px")
self.title_widget.setText("Create Tags From Folders")
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -187,7 +181,8 @@ class FoldersToTagsModal(QWidget):
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setText(
"""Creates tags based on your folder structure and applies them to your entries.\n The structure below shows all the tags that will be created and what entries they will be applied to."""
"""Creates tags based on your folder structure and applies them to your entries.
This tree shows all tags to be created and which entries they will be applied to."""
)
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -210,9 +205,7 @@ class FoldersToTagsModal(QWidget):
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
self.scroll_area.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOn
)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
@@ -229,9 +222,7 @@ class FoldersToTagsModal(QWidget):
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.open_close_button_w)
self.root_layout.addWidget(self.scroll_area)
self.root_layout.addWidget(
self.apply_button, alignment=Qt.AlignmentFlag.AlignCenter
)
self.root_layout.addWidget(self.apply_button, alignment=Qt.AlignmentFlag.AlignCenter)
def on_apply(self, event):
folders_to_tags(self.library)
@@ -310,10 +301,10 @@ class TreeItem(QWidget):
self.label.setText(">" if self.children_widget.isHidden() else "v")
class ModifiedTagWidget(
QWidget
): # Needed to be modified because the original searched the display name in the library where it wasn't added yet
def __init__(self, tag: Tag, parentTag: Tag) -> None:
class ModifiedTagWidget(QWidget):
"""Modified TagWidget that does not search for the Tag's display name in the Library."""
def __init__(self, tag: Tag, parent_tag: Tag) -> None:
super().__init__()
self.tag = tag
@@ -324,8 +315,8 @@ class ModifiedTagWidget(
self.bg_button = QPushButton(self)
self.bg_button.setFlat(True)
if parentTag is not None:
text = f"{tag.name} ({parentTag.name})".replace("&", "&&")
if parent_tag is not None:
text = f"{tag.name} ({parent_tag.name})".replace("&", "&&")
else:
text = tag.name.replace("&", "&&")
self.bg_button.setText(text)

View File

@@ -4,12 +4,11 @@
import typing
from PySide6.QtCore import QObject, Signal, QThreadPool
from PySide6.QtCore import QObject, QThreadPool, Signal
from src.core.library import Library
from src.core.utils.dupe_files import DupeRegistry
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@@ -39,9 +38,7 @@ class MergeDuplicateEntries(QObject):
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x))
iterator.value.connect(
lambda: (pw.update_label("Merging Duplicate Entries..."))
)
iterator.value.connect(lambda: (pw.update_label("Merging Duplicate Entries...")))
r = CustomRunnable(iterator.run)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))

View File

@@ -3,23 +3,22 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from time import sleep
import typing
from time import sleep
from PySide6.QtCore import Signal, Qt, QThreadPool
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtCore import Qt, QThreadPool, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QListView,
QPushButton,
QVBoxLayout,
QWidget,
)
from src.core.utils.dupe_files import DupeRegistry
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@@ -95,9 +94,7 @@ class MirrorEntriesModal(QWidget):
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x + 1))
iterator.value.connect(
lambda x: pw.update_label(
f"Mirroring {x + 1}/{self.tracker.groups_count} Entries..."
)
lambda x: pw.update_label(f"Mirroring {x + 1}/{self.tracker.groups_count} Entries...")
)
r = CustomRunnable(iterator.run)
QThreadPool.globalInstance().start(r)

View File

@@ -3,11 +3,10 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import QObject, Signal, QThreadPool
from PySide6.QtCore import QObject, QThreadPool, Signal
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget

View File

@@ -2,21 +2,20 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Signal, Qt, QSize
from PySide6.QtCore import QSize, Qt, Signal
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QFrame,
QHBoxLayout,
QLineEdit,
QScrollArea,
QFrame,
QVBoxLayout,
QWidget,
)
from src.core.library import Library, Tag
from src.core.library.alchemy.enums import FilterState
from src.qt.widgets.panel import PanelWidget, PanelModal
from src.qt.widgets.tag import TagWidget
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.widgets.panel import PanelModal, PanelWidget
from src.qt.widgets.tag import TagWidget
class TagDatabasePanel(PanelWidget):
@@ -25,10 +24,8 @@ class TagDatabasePanel(PanelWidget):
def __init__(self, library: Library):
super().__init__()
self.lib: Library = library
# self.callback = callback
self.first_tag_id = -1
self.tag_limit = 30
# self.selected_tag: int = 0
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
@@ -38,49 +35,27 @@ class TagDatabasePanel(PanelWidget):
self.search_field.setObjectName("searchField")
self.search_field.setMinimumSize(QSize(0, 32))
self.search_field.setPlaceholderText("Search Tags")
self.search_field.textEdited.connect(
lambda: self.update_tags(self.search_field.text())
)
self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text()))
self.search_field.returnPressed.connect(
lambda checked=False: self.on_return(self.search_field.text())
)
# self.content_container = QWidget()
# self.content_layout = QHBoxLayout(self.content_container)
self.scroll_contents = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_contents)
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
# self.scroll_area.setStyleSheet('background: #000000;')
self.scroll_area.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOn
)
# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
# sa.setMaximumWidth(self.preview_size[0])
self.scroll_area.setWidget(self.scroll_contents)
# self.add_button = QPushButton()
# self.root_layout.addWidget(self.add_button)
# self.add_button.setText('Add Tag')
# # self.done_button.clicked.connect(lambda checked=False, x=1101: (callback(x), self.hide()))
# self.add_button.clicked.connect(lambda checked=False, x=1101: callback(x))
# # self.setLayout(self.root_layout)
self.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
self.update_tags()
# def reset(self):
# self.search_field.setText('')
# self.update_tags('')
# self.search_field.setFocus()
def on_return(self, text: str):
if text and self.first_tag_id >= 0:
# callback(self.first_tag_id)
@@ -91,7 +66,7 @@ class TagDatabasePanel(PanelWidget):
self.parentWidget().hide()
def update_tags(self, query: str | None = None):
# TODO: Look at recycling rather than deleting and reinitializing
# TODO: Look at recycling rather than deleting and re-initializing
while self.scroll_layout.itemAt(0):
self.scroll_layout.takeAt(0).widget().deleteLater()
@@ -102,7 +77,7 @@ class TagDatabasePanel(PanelWidget):
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(3)
tag_widget = TagWidget(tag, True, False)
tag_widget = TagWidget(tag, has_edit=True, has_remove=False)
tag_widget.on_edit.connect(lambda checked=False, t=tag: self.edit_tag(t))
row.addWidget(tag_widget)
self.scroll_layout.addWidget(container)
@@ -119,7 +94,6 @@ class TagDatabasePanel(PanelWidget):
done_callback=(self.update_tags(self.search_field.text())),
has_save=True,
)
# self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t))
# TODO Check Warning: Expected type 'BuildTagPanel', got 'PanelWidget' instead
self.edit_modal.saved.connect(lambda: self.edit_tag_callback(build_tag_panel))
self.edit_modal.show()
@@ -127,8 +101,3 @@ class TagDatabasePanel(PanelWidget):
def edit_tag_callback(self, btp: BuildTagPanel):
self.lib.add_tag(btp.build_tag())
self.update_tags(self.search_field.text())
# def enterEvent(self, event: QEnterEvent) -> None:
# self.search_field.setFocus()
# return super().enterEvent(event)
# self.focusOutEvent

View File

@@ -6,17 +6,16 @@
import math
import structlog
from PySide6.QtCore import Signal, Qt, QSize
from PySide6.QtCore import QSize, Qt, Signal
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QLineEdit,
QScrollArea,
QFrame,
QHBoxLayout,
QLineEdit,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from src.core.library import Library
from src.core.library.alchemy.enums import FilterState
from src.core.palette import ColorType, get_tag_color
@@ -32,10 +31,8 @@ class TagSearchPanel(PanelWidget):
def __init__(self, library: Library):
super().__init__()
self.lib = library
# self.callback = callback
self.first_tag_id = None
self.tag_limit = 100
# self.selected_tag: int = 0
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
@@ -44,49 +41,27 @@ class TagSearchPanel(PanelWidget):
self.search_field.setObjectName("searchField")
self.search_field.setMinimumSize(QSize(0, 32))
self.search_field.setPlaceholderText("Search Tags")
self.search_field.textEdited.connect(
lambda: self.update_tags(self.search_field.text())
)
self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text()))
self.search_field.returnPressed.connect(
lambda checked=False: self.on_return(self.search_field.text())
)
# self.content_container = QWidget()
# self.content_layout = QHBoxLayout(self.content_container)
self.scroll_contents = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_contents)
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
# self.scroll_area.setStyleSheet('background: #000000;')
self.scroll_area.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOn
)
# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
# sa.setMaximumWidth(self.preview_size[0])
self.scroll_area.setWidget(self.scroll_contents)
# self.add_button = QPushButton()
# self.root_layout.addWidget(self.add_button)
# self.add_button.setText('Add Tag')
# # self.done_button.clicked.connect(lambda checked=False, x=1101: (callback(x), self.hide()))
# self.add_button.clicked.connect(lambda checked=False, x=1101: callback(x))
# # self.setLayout(self.root_layout)
self.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
self.update_tags()
# def reset(self):
# self.search_field.setText('')
# self.update_tags('')
# self.search_field.setFocus()
def on_return(self, text: str):
if text and self.first_tag_id is not None:
# callback(self.first_tag_id)
@@ -113,7 +88,7 @@ class TagSearchPanel(PanelWidget):
layout = QHBoxLayout(c)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(3)
tw = TagWidget(tag, False, False)
tw = TagWidget(tag, has_edit=False, has_remove=False)
ab = QPushButton()
ab.setMinimumSize(23, 23)
ab.setMaximumSize(23, 23)
@@ -145,8 +120,3 @@ class TagSearchPanel(PanelWidget):
self.scroll_layout.addWidget(c)
self.search_field.setFocus()
# def enterEvent(self, event: QEnterEvent) -> None:
# self.search_field.setFocus()
# return super().enterEvent(event)
# self.focusOutEvent

View File

@@ -3,24 +3,18 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
"""A pagination widget created for TagStudio."""
# I never want to see this code again.
from PySide6.QtCore import QObject, Signal, QSize
from PySide6.QtCore import QObject, QSize, Signal
from PySide6.QtGui import QIntValidator
from PySide6.QtWidgets import (
QWidget,
QHBoxLayout,
QLabel,
QLineEdit,
QSizePolicy,
QWidget,
)
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
# class NumberEdit(QLineEdit):
# def __init__(self, parent=None) -> None:
# super().__init__(parent)
# self.textChanged
class Pagination(QWidget, QObject):
"""Widget containing controls for navigating between pages of items."""
@@ -46,7 +40,6 @@ class Pagination(QWidget, QObject):
self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
self.root_layout.setContentsMargins(0, 6, 0, 0)
self.root_layout.setSpacing(3)
# self.setMinimumHeight(32)
# [<] ----------------------------------
self.prev_button = QPushButtonWrapper()
@@ -58,14 +51,11 @@ class Pagination(QWidget, QObject):
self.start_button = QPushButtonWrapper()
self.start_button.setMinimumSize(self.button_size)
self.start_button.setMaximumSize(self.button_size)
# self.start_button.setStyleSheet('background:cyan;')
# self.start_button.setMaximumHeight(self.button_size.height())
# ------ ... ---------------------------
self.start_ellipses = QLabel()
self.start_ellipses.setMinimumSize(self.button_size)
self.start_ellipses.setMaximumSize(self.button_size)
# self.start_ellipses.setMaximumHeight(self.button_size.height())
self.start_ellipses.setText(". . .")
# --------- [3][4] ---------------------
@@ -73,7 +63,6 @@ class Pagination(QWidget, QObject):
self.start_buffer_layout = QHBoxLayout(self.start_buffer_container)
self.start_buffer_layout.setContentsMargins(0, 0, 0, 0)
self.start_buffer_layout.setSpacing(3)
# self.start_buffer_container.setStyleSheet('background:blue;')
# ---------------- [5] -----------------
self.current_page_field = QLineEdit()
@@ -84,30 +73,23 @@ class Pagination(QWidget, QObject):
self.current_page_field.returnPressed.connect(
lambda: self._goto_page(int(self.current_page_field.text()) - 1)
)
# self.current_page_field.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
# self.current_page_field.setMaximumHeight(self.button_size.height())
# self.current_page_field.setMaximumWidth(self.button_size.width())
# -------------------- [6][7] ----------
self.end_buffer_container = QWidget()
self.end_buffer_layout = QHBoxLayout(self.end_buffer_container)
self.end_buffer_layout.setContentsMargins(0, 0, 0, 0)
self.end_buffer_layout.setSpacing(3)
# self.end_buffer_container.setStyleSheet('background:orange;')
# -------------------------- ... -------
self.end_ellipses = QLabel()
self.end_ellipses.setMinimumSize(self.button_size)
self.end_ellipses.setMaximumSize(self.button_size)
# self.end_ellipses.setMaximumHeight(self.button_size.height())
self.end_ellipses.setText(". . .")
# ----------------------------- [42] ---
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 = QPushButtonWrapper()
@@ -129,14 +111,12 @@ class Pagination(QWidget, QObject):
self.root_layout.addStretch(1)
self._populate_buffer_buttons()
# self.update_buttons(page_count=9, index=0)
def update_buttons(self, page_count: int, index: int, emit: bool = True):
# Guard
if index < 0:
raise ValueError("Negative index detected")
# Screw it
for i in range(0, 10):
if self.start_buffer_layout.itemAt(i):
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
@@ -148,46 +128,22 @@ class Pagination(QWidget, QObject):
# Hide everything if there are only one or less pages.
# [-------------- HIDDEN --------------]
self.setHidden(True)
# elif page_count > 1 and page_count < 7:
# # Only show Next/Prev, current index field, and both start and end
# # buffers (the end may be odd).
# # [<] [1][2][3][4][5][6] [>]
# self.start_button.setHidden(True)
# self.start_ellipses.setHidden(True)
# self.end_ellipses.setHidden(True)
# self.end_button.setHidden(True)
# elif page_count > 1:
# self.start_button.setHidden(False)
# self.start_ellipses.setHidden(False)
# self.end_ellipses.setHidden(False)
# self.end_button.setHidden(False)
# self.start_button.setText('1')
# self.assign_click(self.start_button, 0)
# self.end_button.setText(str(page_count))
# self.assign_click(self.end_button, page_count-1)
elif page_count > 1:
# Enable/Disable Next+Prev Buttons
if index == 0:
self.prev_button.setDisabled(True)
# self.start_buffer_layout.setContentsMargins(0,0,0,0)
else:
# self.start_buffer_layout.setContentsMargins(3,0,3,0)
self._assign_click(self.prev_button, index - 1)
self.prev_button.setDisabled(False)
if index == end_page:
self.next_button.setDisabled(True)
# self.end_buffer_layout.setContentsMargins(0,0,0,0)
else:
# self.end_buffer_layout.setContentsMargins(3,0,3,0)
self._assign_click(self.next_button, index + 1)
self.next_button.setDisabled(False)
# Set Ellipses Sizes
# I do not know where these magic values were derived from, but
# this is better than the chain elif's that were here before
if 8 <= page_count <= 11:
end_scale = max(1, page_count - index - 6)
srt_scale = max(1, index - 5)
@@ -204,42 +160,29 @@ class Pagination(QWidget, QObject):
self.start_ellipses.setMaximumWidth(srt_size)
# Enable/Disable Ellipses
# if index <= max(self.buffer_page_count, 5)+1:
if index <= self.buffer_page_count + 1:
self.start_ellipses.setHidden(True)
# self.start_button.setHidden(True)
else:
self.start_ellipses.setHidden(False)
# self.start_button.setHidden(False)
# self.start_button.setText('1')
self._assign_click(self.start_button, 0)
# if index >=(page_count-max(self.buffer_page_count, 5)-2):
if index >= (page_count - self.buffer_page_count - 2):
self.end_ellipses.setHidden(True)
# self.end_button.setHidden(True)
else:
self.end_ellipses.setHidden(False)
# self.end_button.setHidden(False)
# self.end_button.setText(str(page_count))
# self.assign_click(self.end_button, page_count-1)
# Hide/Unhide Start+End Buttons
if index != 0:
self.start_button.setText("1")
self._assign_click(self.start_button, 0)
self.start_button.setHidden(False)
# self.start_buffer_layout.setContentsMargins(3,0,0,0)
else:
self.start_button.setHidden(True)
# self.start_buffer_layout.setContentsMargins(0,0,0,0)
if index != page_count - 1:
self.end_button.setText(str(page_count))
self._assign_click(self.end_button, page_count - 1)
self.end_button.setHidden(False)
# self.end_buffer_layout.setContentsMargins(0,0,3,0)
else:
self.end_button.setHidden(True)
# self.end_buffer_layout.setContentsMargins(0,0,0,0)
if index == 0 or index == 1:
self.start_buffer_container.setHidden(True)
@@ -251,47 +194,26 @@ class Pagination(QWidget, QObject):
else:
self.end_buffer_container.setHidden(False)
# for i in range(0, self.buffer_page_count):
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
# Current Field and Buffer Pages
sbc = 0
# for i in range(0, max(self.buffer_page_count*2, 11)):
for i in range(0, page_count):
# for j in range(0, self.buffer_page_count+1):
# self.start_buffer_layout.itemAt(j).widget().setHidden(True)
# if i == 1:
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
# elif i == page_count-2:
# self.end_buffer_layout.itemAt(i).widget().setHidden(True)
# Set Field
if i == index:
# print(f'Current Index: {i}')
if self.start_buffer_layout.itemAt(i):
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
if self.end_buffer_layout.itemAt(i):
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
sbc += 1
self.current_page_field.setText(str(i + 1))
# elif index == page_count-1:
# self.start_button.setText(str(page_count))
start_offset = max(0, (index - 4) - 4)
end_offset = min(page_count - 1, (index + 4) - 4)
if i < index:
# if i != 0 and ((i-self.buffer_page_count) >= 0 or i <= self.buffer_page_count):
if (i != 0) and i >= index - 4:
# print(f' Start i: {i}')
# print(f'Start Offset: {start_offset}')
# print(f' Requested i: {i-start_offset}')
# print(f'Setting Text "{str(i+1)}" for Local Start i:{i-start_offset}, Global i:{i}')
self.start_buffer_layout.itemAt(
i - start_offset
).widget().setHidden(False)
self.start_buffer_layout.itemAt(
i - start_offset
).widget().setText(str(i + 1)) # type: ignore
self.start_buffer_layout.itemAt(i - start_offset).widget().setHidden(False)
self.start_buffer_layout.itemAt(i - start_offset).widget().setText( # type: ignore
str(i + 1)
)
self._assign_click(
self.start_buffer_layout.itemAt(i - start_offset).widget(), # type: ignore
i,
@@ -299,25 +221,12 @@ class Pagination(QWidget, QObject):
sbc += 1
else:
if self.start_buffer_layout.itemAt(i):
# print(f'Removing S-Start {i}')
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
if self.end_buffer_layout.itemAt(i):
# print(f'Removing S-End {i}')
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
elif i > index:
# if i != page_count-1:
if i != page_count - 1 and i <= index + 4:
# print(f'End Buffer: {i}')
# print(f' End i: {i}')
# print(f' End Offset: {end_offset}')
# print(f'Requested i: {i-end_offset}')
# print(f'Requested i: {end_offset-sbc-i}')
# if self.start_buffer_layout.itemAt(i):
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
# print(f'Setting Text "{str(i+1)}" for Local End i:{i-end_offset}, Global i:{i}')
self.end_buffer_layout.itemAt(
i - end_offset
).widget().setHidden(False)
self.end_buffer_layout.itemAt(i - end_offset).widget().setHidden(False)
self.end_buffer_layout.itemAt(i - end_offset).widget().setText( # type: ignore
str(i + 1)
)
@@ -326,104 +235,26 @@ class Pagination(QWidget, QObject):
i,
)
else:
# if self.start_buffer_layout.itemAt(i-1):
# print(f'Removing E-Start {i-1}')
# self.start_buffer_layout.itemAt(i-1).widget().setHidden(True)
# if self.start_buffer_layout.itemAt(i-start_offset):
# print(f'Removing E-Start Offset {i-end_offset}')
# self.start_buffer_layout.itemAt(i-end_offset).widget().setHidden(True)
if self.end_buffer_layout.itemAt(i):
# print(f'Removing E-End {i}')
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
for j in range(0, self.buffer_page_count):
if self.end_buffer_layout.itemAt(i - end_offset + j):
# print(f'Removing E-End-Offset {i-end_offset+j}')
self.end_buffer_layout.itemAt(
i - end_offset + j
).widget().setHidden(True)
# if self.end_buffer_layout.itemAt(i+1):
# print(f'Removing T-End {i+1}')
# self.end_buffer_layout.itemAt(i+1).widget().setHidden(True)
if self.start_buffer_layout.itemAt(i - 1):
# print(f'Removing T-Start {i-1}')
self.start_buffer_layout.itemAt(i - 1).widget().setHidden(True)
# if index == 0 or index == 1:
# print(f'Removing Start i: {i}')
# if self.start_buffer_layout.itemAt(i):
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
# elif index == page_count-1 or index == page_count-2 or index == page_count-3 or index == page_count-4:
# print(f' Removing End i: {i}')
# if self.end_buffer_layout.itemAt(i):
# self.end_buffer_layout.itemAt(i).widget().setHidden(True)
# else:
# print(f'Truncate: {i}')
# if self.start_buffer_layout.itemAt(i):
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
# if self.end_buffer_layout.itemAt(i):
# self.end_buffer_layout.itemAt(i).widget().setHidden(True)
# if i < self.buffer_page_count:
# print(f'start {i}')
# if i == 0:
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
# self.current_page_field.setText((str(i+1)))
# else:
# self.start_buffer_layout.itemAt(i).widget().setHidden(False)
# self.start_buffer_layout.itemAt(i).widget().setText(str(i+1))
# elif i >= self.buffer_page_count and i < count:
# print(f'end {i}')
# self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setHidden(False)
# self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setText(str(i+1))
# else:
# self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setHidden(True)
self.setHidden(False)
# elif page_count >= 7:
# # Show everything, except truncate the buffers as needed.
# # [<] [1]...[3] [4] [5]...[7] [>]
# self.start_button.setHidden(False)
# self.start_ellipses.setHidden(False)
# self.end_ellipses.setHidden(False)
# self.end_button.setHidden(False)
# if index == 0:
# self.prev_button.setDisabled(True)
# self.start_buffer_layout.setContentsMargins(0,0,3,0)
# else:
# self.start_buffer_layout.setContentsMargins(3,0,3,0)
# self.assign_click(self.prev_button, index-1)
# self.prev_button.setDisabled(False)
# if index == page_count-1:
# self.next_button.setDisabled(True)
# self.end_buffer_layout.setContentsMargins(3,0,0,0)
# else:
# self.end_buffer_layout.setContentsMargins(3,0,3,0)
# self.assign_click(self.next_button, index+1)
# self.next_button.setDisabled(False)
# self.start_button.setText('1')
# self.assign_click(self.start_button, 0)
# self.end_button.setText(str(page_count))
# self.assign_click(self.end_button, page_count-1)
# self.setHidden(False)
self.validator.setTop(page_count)
# if self.current_page_index != index:
if emit:
self.index.emit(index)
self.current_page_index = index
self.page_count = page_count
def _goto_page(self, index: int):
# print(f'GOTO PAGE: {index}')
self.update_buttons(self.page_count, index)
def _assign_click(self, button: QPushButtonWrapper, index):
@@ -438,14 +269,12 @@ class Pagination(QWidget, QObject):
button.setMinimumSize(self.button_size)
button.setMaximumSize(self.button_size)
button.setHidden(True)
# button.setMaximumHeight(self.button_size.height())
self.start_buffer_layout.addWidget(button)
end_button = QPushButtonWrapper()
end_button.setMinimumSize(self.button_size)
end_button.setMaximumSize(self.button_size)
end_button.setHidden(True)
# button.setMaximumHeight(self.button_size.height())
self.end_buffer_layout.addWidget(end_button)
@@ -454,7 +283,5 @@ class Validator(QIntValidator):
super().__init__(bottom, top, parent)
def fixup(self, input: str) -> str:
# print(input)
input = input.strip("0")
print(input)
return super().fixup(str(self.top()) if input else "1")

View File

@@ -23,13 +23,12 @@ class ResourceManager:
if not ResourceManager._initialized:
with open(Path(__file__).parent / "resources.json", encoding="utf-8") as f:
ResourceManager._map = ujson.load(f)
logger.info(
"resources registered", count=len(ResourceManager._map.items())
)
logger.info("resources registered", count=len(ResourceManager._map.items()))
ResourceManager._initialized = True
def get(self, id: str) -> Any:
"""Get a resource from the ResourceManager.
This can include resources inside and outside of QResources, and will return
theme-respecting variations of resources if available.

View File

@@ -19,88 +19,85 @@ from itertools import zip_longest
from pathlib import Path
from queue import Queue
# this import has side-effect of import PySide resources
import src.qt.resources_rc # noqa: F401
import structlog
from humanfriendly import format_timespan
from PySide6 import QtCore
from PySide6.QtCore import (
QObject,
QThread,
Signal,
QSettings,
Qt,
QThread,
QThreadPool,
QTimer,
QSettings,
Signal,
)
from PySide6.QtGui import (
QGuiApplication,
QPixmap,
QMouseEvent,
QColor,
QAction,
QColor,
QFontDatabase,
QGuiApplication,
QIcon,
QMouseEvent,
QPixmap,
)
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import (
QApplication,
QWidget,
QPushButton,
QLineEdit,
QScrollArea,
QComboBox,
QFileDialog,
QSplashScreen,
QLineEdit,
QMenu,
QMenuBar,
QComboBox,
QPushButton,
QScrollArea,
QSplashScreen,
QWidget,
)
from humanfriendly import format_timespan
from src.core.enums import SettingItems, MacroID
from src.core.constants import (
TS_FOLDER_NAME,
VERSION_BRANCH,
VERSION,
LibraryPrefs,
TAG_ARCHIVED,
TAG_FAVORITE,
TS_FOLDER_NAME,
VERSION,
VERSION_BRANCH,
LibraryPrefs,
)
from src.core.enums import MacroID, SettingItems
from src.core.library.alchemy.enums import (
SearchMode,
FieldTypeEnum,
FilterState,
ItemType,
FieldTypeEnum,
SearchMode,
)
from src.core.library.alchemy.fields import _FieldID
from src.core.ts_core import TagStudioCore
from src.core.utils.refresh_dir import RefreshDirTracker
from src.core.utils.web import strip_web_protocol
from src.qt.flowlayout import FlowLayout
from src.qt.main_window import Ui_MainWindow
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.resource_manager import ResourceManager
from src.qt.widgets.panel import PanelModal
from src.qt.widgets.thumb_renderer import ThumbRenderer
from src.qt.widgets.progress import ProgressWidget
from src.qt.widgets.preview_panel import PreviewPanel
from src.qt.widgets.item_thumb import ItemThumb, BadgeType
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.main_window import Ui_MainWindow
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.modals.tag_database import TagDatabasePanel
from src.qt.modals.file_extension import FileExtensionModal
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
from src.qt.modals.fix_dupes import FixDupeFilesModal
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
from src.qt.modals.folders_to_tags import FoldersToTagsModal
# this import has side-effect of import PySide resources
import src.qt.resources_rc # noqa: F401
from src.qt.modals.tag_database import TagDatabasePanel
from src.qt.resource_manager import ResourceManager
from src.qt.widgets.item_thumb import BadgeType, ItemThumb
from src.qt.widgets.panel import PanelModal
from src.qt.widgets.preview_panel import PreviewPanel
from src.qt.widgets.progress import ProgressWidget
from src.qt.widgets.thumb_renderer import ThumbRenderer
# SIGQUIT is not defined on Windows
if sys.platform == "win32":
from signal import signal, SIGINT, SIGTERM
from signal import SIGINT, SIGTERM, signal
SIGQUIT = SIGTERM
else:
from signal import signal, SIGINT, SIGTERM, SIGQUIT
from signal import SIGINT, SIGQUIT, SIGTERM, signal
logger = structlog.get_logger(__name__)
@@ -156,7 +153,7 @@ class QtDriver(QObject):
# grid indexes of selected items
self.selected: list[int] = []
self.SIGTERM.connect(self.handleSIGTERM)
self.SIGTERM.connect(self.handle_sigterm)
if self.args.config_file:
path = Path(self.args.config_file)
@@ -178,7 +175,9 @@ class QtDriver(QObject):
max_threads = os.cpu_count()
for i in range(max_threads):
# thread = threading.Thread(target=self.consumer, name=f'ThumbRenderer_{i}',args=(), daemon=True)
# thread = threading.Thread(
# target=self.consumer, name=f"ThumbRenderer_{i}", args=(), daemon=True
# )
# thread.start()
thread = Consumer(self.thumb_job_queue)
thread.setObjectName(f"ThumbRenderer_{i}")
@@ -206,7 +205,6 @@ class QtDriver(QObject):
def start(self) -> None:
"""Launch the main Qt window."""
_ = QUiLoader()
if os.name == "nt":
sys.argv += ["-platform", "windows:darkmode=2"]
@@ -370,12 +368,10 @@ class QtDriver(QObject):
check_action = QAction("Open library on start", self)
check_action.setCheckable(True)
check_action.setChecked(
bool(self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool))
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
)
check_action.triggered.connect(
lambda checked: self.settings.setValue(
SettingItems.START_LOAD_LAST, checked
)
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
)
window_menu.addAction(check_action)
@@ -415,7 +411,7 @@ class QtDriver(QObject):
show_libs_list_action = QAction("Show Recent Libraries", menu_bar)
show_libs_list_action.setCheckable(True)
show_libs_list_action.setChecked(
bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, True, type=bool))
bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool))
)
show_libs_list_action.triggered.connect(
lambda checked: (
@@ -467,14 +463,12 @@ class QtDriver(QObject):
lib: str | None = None
if self.args.open:
lib = self.args.open
elif self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool):
elif self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool):
lib = str(self.settings.value(SettingItems.LAST_LIBRARY))
# TODO: Remove this check if the library is no longer saved with files
if lib and not (Path(lib) / TS_FOLDER_NAME).exists():
logger.error(
f"[QT DRIVER] {TS_FOLDER_NAME} folder in {lib} does not exist."
)
logger.error(f"[QT DRIVER] {TS_FOLDER_NAME} folder in {lib} does not exist.")
self.settings.setValue(SettingItems.LAST_LIBRARY, "")
lib = None
@@ -501,22 +495,16 @@ class QtDriver(QObject):
search_button: QPushButton = self.main_window.searchButton
search_button.clicked.connect(
lambda: self.filter_items(
FilterState(query=self.main_window.searchField.text())
)
lambda: self.filter_items(FilterState(query=self.main_window.searchField.text()))
)
search_field: QLineEdit = self.main_window.searchField
search_field.returnPressed.connect(
# TODO - parse search field for filters
lambda: self.filter_items(
FilterState(query=self.main_window.searchField.text())
)
lambda: self.filter_items(FilterState(query=self.main_window.searchField.text()))
)
search_type_selector: QComboBox = self.main_window.comboBox_2
search_type_selector.currentIndexChanged.connect(
lambda: self.set_search_type(
SearchMode(search_type_selector.currentIndex())
)
lambda: self.set_search_type(SearchMode(search_type_selector.currentIndex()))
)
back_button: QPushButton = self.main_window.backButton
@@ -529,7 +517,7 @@ class QtDriver(QObject):
# or implementing some clever loading tricks.
self.main_window.show()
self.main_window.activateWindow()
self.main_window.toggle_landing_page(True)
self.main_window.toggle_landing_page(enabled=True)
self.main_window.pagination.index.connect(lambda i: self.page_move(page_id=i))
@@ -544,15 +532,15 @@ class QtDriver(QObject):
self.preview_panel.update()
def callback_library_needed_check(self, func):
"""Check if loaded library has valid path before executing the button function"""
"""Check if loaded library has valid path before executing the button function."""
if self.lib.library_dir:
func()
def handleSIGTERM(self):
def handle_sigterm(self):
self.shutdown()
def shutdown(self):
"""Save Library on Application Exit"""
"""Save Library on Application Exit."""
self.close_library(is_shutdown=True)
logger.info("[SHUTDOWN] Ending Thumbnail Threads...")
for _ in self.thumb_threads:
@@ -590,7 +578,7 @@ class QtDriver(QObject):
[x.set_mode(None) for x in self.item_thumbs]
self.preview_panel.update_widgets()
self.main_window.toggle_landing_page(True)
self.main_window.toggle_landing_page(enabled=True)
self.main_window.pagination.setHidden(True)
@@ -663,7 +651,6 @@ class QtDriver(QObject):
def add_new_files_callback(self):
"""Run when user initiates adding new files to the Library."""
tracker = RefreshDirTracker(self.lib)
pw = ProgressWidget(
@@ -680,7 +667,9 @@ class QtDriver(QObject):
lambda x: (
pw.update_progress(x + 1),
pw.update_label(
f'Scanning Directories for New Files...\n{x + 1} File{"s" if x + 1 != 1 else ""} Searched, {tracker.files_count} New Files Found'
f"Scanning Directories for New Files...\n{x + 1}"
f" File{"s" if x + 1 != 1 else ""} Searched,"
f" {tracker.files_count} New Files Found"
),
)
)
@@ -695,11 +684,13 @@ class QtDriver(QObject):
QThreadPool.globalInstance().start(r)
def add_new_files_runnable(self, tracker: RefreshDirTracker):
"""Adds any known new files to the library and run default macros on them.
Threaded method.
"""
Threaded method that adds any known new files to the library and
initiates running default macros on them.
"""
# pb = QProgressDialog(f'Running Configured Macros on 1/{len(new_ids)} New Entries', None, 0,len(new_ids))
# pb = QProgressDialog(
# f"Running Configured Macros on 1/{len(new_ids)} New Entries", None, 0, len(new_ids)
# )
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Running Macros')
@@ -711,13 +702,14 @@ class QtDriver(QObject):
# r.run()
# # QThreadPool.globalInstance().start(r)
# # self.main_window.statusbar.showMessage(f'Running configured Macros on {len(new_ids)} new Entries...', 3)
# # self.main_window.statusbar.showMessage(
# # f"Running configured Macros on {len(new_ids)} new Entries...", 3
# # )
# # pb.hide()
files_count = tracker.files_count
# iterator = FunctionIterator(lambda: self.new_file_macros_runnable(tracker.files_not_in_library))
iterator = FunctionIterator(tracker.save_new_files)
pw = ProgressWidget(
window_title="Running Macros on New Entries",
@@ -730,9 +722,7 @@ class QtDriver(QObject):
iterator.value.connect(
lambda x: (
pw.update_progress(x + 1),
pw.update_label(
f"Running Configured Macros on {x + 1}/{files_count} New Entries"
),
pw.update_label(f"Running Configured Macros on {x + 1}/{files_count} New Entries"),
)
)
r = CustomRunnable(iterator.run)
@@ -846,7 +836,7 @@ class QtDriver(QObject):
def _init_thumb_grid(self):
layout = FlowLayout()
layout.setGridEfficiency(True)
layout.enable_grid_optimizations(value=True)
layout.setSpacing(min(self.thumb_size // 10, 12))
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -868,9 +858,7 @@ class QtDriver(QObject):
def select_item(self, grid_index: int, append: bool, bridge: bool):
"""Select one or more items in the Thumbnail Grid."""
logger.info(
"selecting item", grid_index=grid_index, append=append, bridge=bridge
)
logger.info("selecting item", grid_index=grid_index, append=append, bridge=bridge)
if append:
if grid_index not in self.selected:
self.selected.append(grid_index)
@@ -967,8 +955,7 @@ class QtDriver(QObject):
== Qt.KeyboardModifier.ControlModifier
),
bridge=(
QGuiApplication.keyboardModifiers()
== Qt.KeyboardModifier.ShiftModifier
QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier
),
)
)
@@ -1003,9 +990,7 @@ class QtDriver(QObject):
if filter:
self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter))
self.main_window.statusbar.showMessage(
f'Searching Library: "{self.filter.summary}"'
)
self.main_window.statusbar.showMessage(f'Searching Library: "{self.filter.summary}"')
self.main_window.statusbar.repaint()
start_time = time.time()
@@ -1015,9 +1000,12 @@ class QtDriver(QObject):
end_time = time.time()
if self.filter.summary:
# fmt: off
self.main_window.statusbar.showMessage(
f'{results.total_count} Results Found for "{self.filter.summary}" ({format_timespan(end_time - start_time)})'
f"{results.total_count} Results Found for \"{self.filter.summary}\""
f" ({format_timespan(end_time - start_time)})"
)
# fmt: on
else:
self.main_window.statusbar.showMessage(
f"{results.total_count} Results ({format_timespan(end_time - start_time)})"
@@ -1048,8 +1036,8 @@ class QtDriver(QObject):
self.settings.sync()
def update_libs_list(self, path: Path | str):
"""add library to list in SettingItems.LIBS_LIST"""
ITEMS_LIMIT = 5
"""Add library to list in SettingItems.LIBS_LIST."""
item_limit: int = 5
path = Path(path)
self.settings.beginGroup(SettingItems.LIBS_LIST)
@@ -1067,7 +1055,7 @@ class QtDriver(QObject):
# remove previously saved items
self.settings.clear()
for item_key, item_value in all_libs_list[:ITEMS_LIMIT]:
for item_key, item_value in all_libs_list[:item_limit]:
self.settings.setValue(item_key, item_value)
self.settings.endGroup()
@@ -1097,4 +1085,4 @@ class QtDriver(QObject):
# page (re)rendering, extract eventually
self.filter_items()
self.main_window.toggle_landing_page(False)
self.main_window.toggle_landing_page(enabled=False)

View File

@@ -14,5 +14,5 @@ class ClickableLabel(QLabel):
def __init__(self):
super().__init__()
def mousePressEvent(self, event):
def mousePressEvent(self, event): # noqa: N802
self.clicked.emit()

View File

@@ -12,8 +12,7 @@ from PySide6.QtCore import (
QObject,
Signal,
)
from src.core.constants import DOC_TYPES, VIDEO_TYPES, IMAGE_TYPES
from src.core.constants import DOC_TYPES, IMAGE_TYPES, VIDEO_TYPES
from src.core.library import Library
from src.core.library.alchemy.fields import _FieldID
@@ -84,15 +83,11 @@ class CollageIconRenderer(QObject):
pic = pic.resize(size)
if data_tint_mode and color:
pic = pic.convert(mode="RGB")
pic = ImageChops.hard_light(
pic, Image.new("RGB", size, color)
)
pic = ImageChops.hard_light(pic, Image.new("RGB", size, color))
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
except DecompressionBombError:
logger.exception(
"One of the images was too big", entry=entry.path
)
logger.exception("One of the images was too big", entry=entry.path)
elif filepath.suffix.lower() in VIDEO_TYPES:
video = cv2.VideoCapture(str(filepath))
video.set(
@@ -113,18 +108,13 @@ class CollageIconRenderer(QObject):
else:
pic = pic.resize(size)
if data_tint_mode and color:
pic = ImageChops.hard_light(
pic, Image.new("RGB", size, color)
)
pic = ImageChops.hard_light(pic, Image.new("RGB", size, color))
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
except (UnidentifiedImageError, FileNotFoundError):
logger.error("Couldn't read entry", entry=entry.path)
with Image.open(
str(
Path(__file__).parents[2]
/ "resources/qt/images/thumb_broken_512.png"
)
str(Path(__file__).parents[2] / "resources/qt/images/thumb_broken_512.png")
) as pic:
pic.thumbnail(size)
if data_tint_mode and color:

View File

@@ -4,14 +4,14 @@
import math
from types import MethodType
from pathlib import Path
from typing import Optional, Callable
from types import MethodType
from typing import Callable, Optional
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
from PySide6.QtCore import QEvent, Qt
from PySide6.QtGui import QEnterEvent, QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
@@ -34,22 +34,17 @@ class FieldContainer(QWidget):
def __init__(self, title: str = "Field", inline: bool = True) -> None:
super().__init__()
# self.mode:str = mode
self.setObjectName("fieldContainer")
# self.item = item
self.title: str = title
self.inline: bool = inline
# self.editable:bool = editable
self.copy_callback: Callable = None
self.edit_callback: Callable = None
self.remove_callback: Callable = None
button_size = 24
# self.setStyleSheet('border-style:solid;border-color:#1e1a33;border-radius:8px;border-width:2px;')
self.root_layout = QVBoxLayout(self)
self.root_layout.setObjectName("baseLayout")
self.root_layout.setContentsMargins(0, 0, 0, 0)
# self.setStyleSheet('background-color:red;')
self.inner_layout = QVBoxLayout()
self.inner_layout.setObjectName("innerLayout")
@@ -61,7 +56,6 @@ class FieldContainer(QWidget):
self.root_layout.addWidget(self.inner_container)
self.title_container = QWidget()
# self.title_container.setStyleSheet('background:black;')
self.title_layout = QHBoxLayout(self.title_container)
self.title_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.title_layout.setObjectName("fieldLayout")
@@ -74,20 +68,15 @@ class FieldContainer(QWidget):
self.title_widget.setObjectName("fieldTitle")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet("font-weight: bold; font-size: 14px;")
# self.title_widget.setStyleSheet('background-color:orange;')
self.title_widget.setText(title)
# self.inner_layout.addWidget(self.title_widget)
self.title_layout.addWidget(self.title_widget)
self.title_layout.addStretch(2)
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)
self.copy_button.setIcon(
QPixmap.fromImage(ImageQt.ImageQt(self.clipboard_icon_128))
)
self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.clipboard_icon_128)))
self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.copy_button)
self.copy_button.setHidden(True)
@@ -105,9 +94,7 @@ class FieldContainer(QWidget):
self.remove_button.setMinimumSize(button_size, button_size)
self.remove_button.setMaximumSize(button_size, button_size)
self.remove_button.setFlat(True)
self.remove_button.setIcon(
QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon_128))
)
self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon_128)))
self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.remove_button)
self.remove_button.setHidden(True)
@@ -118,11 +105,8 @@ class FieldContainer(QWidget):
self.field_layout.setObjectName("fieldLayout")
self.field_layout.setContentsMargins(0, 0, 0, 0)
self.field_container.setLayout(self.field_layout)
# self.field_container.setStyleSheet('background-color:#666600;')
self.inner_layout.addWidget(self.field_container)
# self.set_inner_widget(mode)
def set_copy_callback(self, callback: Optional[MethodType]):
if self.copy_button.is_connected:
self.copy_button.clicked.disconnect()
@@ -150,12 +134,7 @@ class FieldContainer(QWidget):
self.remove_button.is_connected = True
def set_inner_widget(self, widget: "FieldWidget"):
# widget.setStyleSheet('background-color:green;')
# self.inner_container.dumpObjectTree()
# logging.info('')
if self.field_layout.itemAt(0):
# logging.info(f'Removing {self.field_layout.itemAt(0)}')
# self.field_layout.removeItem(self.field_layout.itemAt(0))
self.field_layout.itemAt(0).widget().deleteLater()
self.field_layout.addWidget(widget)
@@ -171,12 +150,7 @@ class FieldContainer(QWidget):
def set_inline(self, inline: bool):
self.inline = inline
# def set_editable(self, editable:bool):
# self.editable = editable
def enterEvent(self, event: QEnterEvent) -> None:
# if self.field_layout.itemAt(1):
# self.field_layout.itemAt(1).
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
# NOTE: You could pass the hover event to the FieldWidget if needed.
if self.copy_callback:
self.copy_button.setHidden(False)
@@ -186,7 +160,7 @@ class FieldContainer(QWidget):
self.remove_button.setHidden(False)
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
if self.copy_callback:
self.copy_button.setHidden(True)
if self.edit_callback:
@@ -199,5 +173,4 @@ class FieldContainer(QWidget):
class FieldWidget(QWidget):
def __init__(self, title) -> None:
super().__init__()
# self.item = item
self.title = title

View File

@@ -10,32 +10,30 @@ from typing import TYPE_CHECKING
import structlog
from PIL import Image, ImageQt
from PySide6.QtCore import Qt, QSize, QEvent
from PySide6.QtGui import QPixmap, QEnterEvent, QAction
from PySide6.QtCore import QEvent, QSize, Qt
from PySide6.QtGui import QAction, QEnterEvent, QPixmap
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QBoxLayout,
QCheckBox,
QHBoxLayout,
QLabel,
QVBoxLayout,
QWidget,
)
from src.core.constants import (
AUDIO_TYPES,
VIDEO_TYPES,
IMAGE_TYPES,
TAG_FAVORITE,
TAG_ARCHIVED,
TAG_FAVORITE,
VIDEO_TYPES,
)
from src.core.library import ItemType, Entry, Library
from src.core.library import Entry, ItemType, Library
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.fields import _FieldID
from src.qt.flowlayout import FlowWidget
from src.qt.helpers.file_opener import FileOpenerHelper
from src.qt.widgets.thumb_renderer import ThumbRenderer
from src.qt.widgets.thumb_button import ThumbButton
from src.qt.widgets.thumb_renderer import ThumbRenderer
if TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
@@ -74,9 +72,7 @@ def badge_update_lock(func):
class ItemThumb(FlowWidget):
"""
The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.).
"""
"""The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.)."""
update_cutoff: float = time.time()
@@ -91,27 +87,27 @@ class ItemThumb(FlowWidget):
tag_group_icon_128.load()
small_text_style = (
f"background-color:rgba(0, 0, 0, 192);"
f"font-family:Oxanium;"
f"font-weight:bold;"
f"font-size:12px;"
f"border-radius:3px;"
f"padding-top: 4px;"
f"padding-right: 1px;"
f"padding-bottom: 1px;"
f"padding-left: 1px;"
"background-color:rgba(0, 0, 0, 192);"
"font-family:Oxanium;"
"font-weight:bold;"
"font-size:12px;"
"border-radius:3px;"
"padding-top: 4px;"
"padding-right: 1px;"
"padding-bottom: 1px;"
"padding-left: 1px;"
)
med_text_style = (
f"background-color:rgba(0, 0, 0, 192);"
f"font-family:Oxanium;"
f"font-weight:bold;"
f"font-size:18px;"
f"border-radius:3px;"
f"padding-top: 4px;"
f"padding-right: 1px;"
f"padding-bottom: 1px;"
f"padding-left: 1px;"
"background-color:rgba(0, 0, 0, 192);"
"font-family:Oxanium;"
"font-weight:bold;"
"font-size:18px;"
"border-radius:3px;"
"padding-top: 4px;"
"padding-right: 1px;"
"padding-bottom: 1px;"
"padding-left: 1px;"
)
def __init__(
@@ -132,7 +128,6 @@ class ItemThumb(FlowWidget):
self.setMinimumSize(*thumb_size)
self.setMaximumSize(*thumb_size)
check_size = 24
# self.setStyleSheet('background-color:red;')
# +----------+
# | ARC FAV| Top Right: Favorite & Archived Badges
@@ -151,7 +146,6 @@ class ItemThumb(FlowWidget):
# +----------+
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName("baseLayout")
# self.base_layout.setRowStretch(1, 2)
self.base_layout.setContentsMargins(0, 0, 0, 0)
# +----------+
@@ -162,8 +156,6 @@ class ItemThumb(FlowWidget):
# +----------+
self.top_layout = QHBoxLayout()
self.top_layout.setObjectName("topLayout")
# self.top_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# self.top_layout.setColumnStretch(1, 2)
self.top_layout.setContentsMargins(6, 6, 6, 6)
self.top_container = QWidget()
self.top_container.setLayout(self.top_layout)
@@ -185,20 +177,11 @@ class ItemThumb(FlowWidget):
# +----------+
self.bottom_layout = QHBoxLayout()
self.bottom_layout.setObjectName("bottomLayout")
# self.bottom_container.setAlignment(Qt.AlignmentFlag.AlignBottom)
# self.bottom_layout.setColumnStretch(1, 2)
self.bottom_layout.setContentsMargins(6, 6, 6, 6)
self.bottom_container = QWidget()
self.bottom_container.setLayout(self.bottom_layout)
self.base_layout.addWidget(self.bottom_container)
# self.root_layout = QGridLayout(self)
# self.root_layout.setObjectName('rootLayout')
# self.root_layout.setColumnStretch(1, 2)
# self.root_layout.setRowStretch(1, 2)
# self.root_layout.setContentsMargins(6,6,6,6)
# # root_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.thumb_button = ThumbButton(self, thumb_size)
self.renderer = ThumbRenderer()
self.renderer.updated.connect(
@@ -209,14 +192,9 @@ class ItemThumb(FlowWidget):
)
)
self.thumb_button.setFlat(True)
# self.bg_button.setStyleSheet('background-color:blue;')
# self.bg_button.setLayout(self.root_layout)
self.thumb_button.setLayout(self.base_layout)
# self.bg_button.setMinimumSize(*thumb_size)
# self.bg_button.setMaximumSize(*thumb_size)
self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.opener = FileOpenerHelper("")
open_file_action = QAction("Open file", self)
open_file_action.triggered.connect(self.opener.open_file)
@@ -243,50 +221,32 @@ class ItemThumb(FlowWidget):
)
self.item_type_badge.setMinimumSize(check_size, check_size)
self.item_type_badge.setMaximumSize(check_size, check_size)
# self.root_layout.addWidget(self.item_type_badge, 2, 0)
self.bottom_layout.addWidget(self.item_type_badge)
# File Extension Badge -------------------------------------------------
# Mutually exclusive with the File Extension Badge.
self.ext_badge = QLabel()
self.ext_badge.setObjectName("extBadge")
# self.ext_badge.setText('MP4')
# self.ext_badge.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.ext_badge.setStyleSheet(ItemThumb.small_text_style)
# self.type_badge.setAlignment(Qt.AlignmentFlag.AlignRight)
# self.root_layout.addWidget(self.ext_badge, 2, 0)
self.bottom_layout.addWidget(self.ext_badge)
# self.type_badge.setHidden(True)
# bl_layout.addWidget(self.type_badge)
self.bottom_layout.addStretch(2)
# Count Badge ----------------------------------------------------------
# Used for Tag Group + Collation counts, video length, word count, etc.
self.count_badge = QLabel()
self.count_badge.setObjectName("countBadge")
# self.count_badge.setMaximumHeight(17)
self.count_badge.setText("-:--")
# self.count_badge.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.count_badge.setStyleSheet(ItemThumb.small_text_style)
# self.count_badge.setAlignment(Qt.AlignmentFlag.AlignBottom)
# self.root_layout.addWidget(self.count_badge, 2, 2)
self.bottom_layout.addWidget(
self.count_badge, alignment=Qt.AlignmentFlag.AlignBottom
)
self.bottom_layout.addWidget(self.count_badge, alignment=Qt.AlignmentFlag.AlignBottom)
self.top_layout.addStretch(2)
# Intractable Badges ===================================================
self.cb_container = QWidget()
# check_badges.setStyleSheet('background-color:cyan;')
self.cb_layout = QHBoxLayout()
self.cb_layout.setDirection(QBoxLayout.Direction.RightToLeft)
self.cb_layout.setContentsMargins(0, 0, 0, 0)
self.cb_layout.setSpacing(6)
self.cb_container.setLayout(self.cb_layout)
# self.cb_container.setHidden(True)
# self.root_layout.addWidget(self.check_badges, 0, 2)
self.top_layout.addWidget(self.cb_container)
self.badge_active: dict[BadgeType, bool] = {
@@ -336,14 +296,11 @@ class ItemThumb(FlowWidget):
def set_mode(self, mode: ItemType | None) -> None:
if mode is None:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=True)
self.unsetCursor()
self.thumb_button.setHidden(True)
# self.check_badges.setHidden(True)
# self.ext_badge.setHidden(True)
# self.item_type_badge.setHidden(True)
elif mode == ItemType.ENTRY and self.mode != ItemType.ENTRY:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(False)
@@ -353,7 +310,7 @@ class ItemThumb(FlowWidget):
self.count_badge.setHidden(True)
self.ext_badge.setHidden(True)
elif mode == ItemType.COLLATION and self.mode != ItemType.COLLATION:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(True)
@@ -362,15 +319,13 @@ class ItemThumb(FlowWidget):
self.count_badge.setHidden(False)
self.item_type_badge.setHidden(False)
elif mode == ItemType.TAG_GROUP and self.mode != ItemType.TAG_GROUP:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
# self.cb_container.setHidden(True)
self.ext_badge.setHidden(True)
self.count_badge.setHidden(False)
self.item_type_badge.setHidden(False)
self.mode = mode
# logging.info(f'Set Mode To: {self.mode}')
def set_extension(self, ext: str) -> None:
if ext and ext.startswith(".") is False:
@@ -396,14 +351,11 @@ class ItemThumb(FlowWidget):
def update_thumb(self, timestamp: float, image: QPixmap | None = None):
"""Update attributes of a thumbnail element."""
# logging.info(f'[GUI] Updating Thumbnail for element {id(element)}: {id(image) if image else None}')
if timestamp > ItemThumb.update_cutoff:
self.thumb_button.setIcon(image if image else QPixmap())
# element.repaint()
def update_size(self, timestamp: float, size: QSize):
"""Updates attributes of a thumbnail element."""
# logging.info(f'[GUI] Updating size for element {id(element)}: {size.__str__()}')
if timestamp > ItemThumb.update_cutoff and self.thumb_button.iconSize != size:
self.thumb_button.setIconSize(size)
self.thumb_button.setMinimumSize(size)
@@ -411,7 +363,6 @@ 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}')
if self.thumb_button.is_connected:
self.thumb_button.clicked.disconnect()
if clickable:
@@ -455,12 +406,12 @@ class ItemThumb(FlowWidget):
is_hidden = not (show or self.badge_active[badge_type])
badge.setHidden(is_hidden)
def enterEvent(self, event: QEnterEvent) -> None:
self.show_check_badges(True)
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
self.show_check_badges(show=True)
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
self.show_check_badges(False)
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
self.show_check_badges(show=False)
return super().leaveEvent(event)
@badge_update_lock
@@ -482,7 +433,7 @@ class ItemThumb(FlowWidget):
for idx in update_items:
entry = self.driver.frame_content[idx]
self.toggle_item_tag(
entry, toggle_value, tag_id, _FieldID.TAGS_META.name, True
entry, toggle_value, tag_id, _FieldID.TAGS_META.name, create_field=True
)
# update the entry
self.driver.frame_content[idx] = self.lib.search_library(

View File

@@ -7,12 +7,13 @@ import logging
import sys
import typing
from pathlib import Path
from PIL import Image, ImageQt
from PySide6.QtCore import Qt, QPropertyAnimation, QPoint, QEasingCurve
from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton
from src.qt.widgets.clickable_label import ClickableLabel
from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
from src.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay
from src.qt.widgets.clickable_label import ClickableLabel
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
@@ -39,8 +40,7 @@ class LandingWidget(QWidget):
# Create landing logo --------------------------------------------------
# self.landing_logo_pixmap = QPixmap(":/images/tagstudio_logo_text_mono.png")
self.logo_raw: Image.Image = Image.open(
Path(__file__).parents[3]
/ "resources/qt/images/tagstudio_logo_text_mono.png"
Path(__file__).parents[3] / "resources/qt/images/tagstudio_logo_text_mono.png"
)
self.landing_pixmap: QPixmap = QPixmap()
self.update_logo_color()
@@ -78,21 +78,15 @@ class LandingWidget(QWidget):
# Add widgets to layout ------------------------------------------------
self.landing_layout.addWidget(self.logo_label)
self.landing_layout.addWidget(
self.open_button, alignment=Qt.AlignmentFlag.AlignCenter
)
self.landing_layout.addWidget(
self.status_label, alignment=Qt.AlignmentFlag.AlignCenter
)
self.landing_layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignCenter)
self.landing_layout.addWidget(self.status_label, alignment=Qt.AlignmentFlag.AlignCenter)
def update_logo_color(self, style: str = "mono"):
"""
Update the color of the TagStudio logo.
"""Update the color of the TagStudio logo.
Args:
style (str): = The style of the logo. Either "mono" or "gradient".
"""
logo_im: Image.Image = None
if style == "mono":
logo_im = theme_fg_overlay(self.logo_raw)
@@ -100,9 +94,7 @@ class LandingWidget(QWidget):
gradient_colors: list[str] = ["#d27bf4", "#7992f5", "#63c6e3", "#63f5cf"]
logo_im = gradient_overlay(self.logo_raw, gradient_colors)
logo_final: Image.Image = Image.new(
mode="RGBA", size=self.logo_raw.size, color="#00000000"
)
logo_final: Image.Image = Image.new(mode="RGBA", size=self.logo_raw.size, color="#00000000")
logo_final.paste(logo_im, (0, 0), mask=self.logo_raw)
@@ -118,9 +110,9 @@ class LandingWidget(QWidget):
self.logo_label.setPixmap(self.landing_pixmap)
def _update_special_click(self):
"""
Increment the click count for the logo easter egg if it has not
been triggered. If it reaches the click threshold, this triggers it
"""Increment the click count for the logo easter egg if it has not been triggered.
If it reaches the click threshold, this triggers it
and prevents it from triggering again.
"""
if self._special_click_count >= 0:
@@ -137,9 +129,7 @@ class LandingWidget(QWidget):
# the cause of this is, so I've just done this workaround to disable
# the animation if the y position is too incorrect.
if self.logo_label.y() > 50:
self.logo_pos_anim.setStartValue(
QPoint(self.logo_label.x(), self.logo_label.y() - 100)
)
self.logo_pos_anim.setStartValue(QPoint(self.logo_label.x(), self.logo_label.y() - 100))
self.logo_pos_anim.setEndValue(self.logo_label.pos())
self.logo_pos_anim.start()
@@ -169,8 +159,7 @@ class LandingWidget(QWidget):
# self.status_pos_anim.start()
def set_status_label(self, text=str):
"""
Set the text of the status label.
"""Set the text of the status label.
Args:
text (str): Text of the status to set.

View File

@@ -4,8 +4,8 @@
import logging
from typing import Callable
from PySide6.QtCore import Signal, Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
class PanelModal(QWidget):
@@ -35,9 +35,7 @@ class PanelModal(QWidget):
self.title_widget = QLabel()
self.title_widget.setObjectName("fieldTitle")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet(
"font-weight:bold;" "font-size:14px;" "padding-top: 6px"
)
self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px")
self.title_widget.setText(title)
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -76,9 +74,7 @@ class PanelModal(QWidget):
self.save_button.clicked.connect(done_callback)
if save_callback:
self.save_button.clicked.connect(
lambda: save_callback(widget.get_content())
)
self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.save_button)
@@ -94,9 +90,7 @@ class PanelModal(QWidget):
class PanelWidget(QWidget):
"""
Used for widgets that go in a modal panel, ex. for editing or searching.
"""
"""Used for widgets that go in a modal panel, ex. for editing or searching."""
done = Signal()

View File

@@ -2,56 +2,55 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import sys
from collections.abc import Callable
from pathlib import Path
import time
import typing
from collections.abc import Callable
from datetime import datetime as dt
from pathlib import Path
import cv2
import rawpy
import structlog
from humanfriendly import format_size
from PIL import Image, UnidentifiedImageError
from PIL.Image import DecompressionBombError
from PySide6.QtCore import Signal, Qt, QSize
from PySide6.QtGui import QResizeEvent, QAction
from PySide6.QtCore import QSize, Qt, Signal
from PySide6.QtGui import QAction, QResizeEvent
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QFrame,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QScrollArea,
QFrame,
QSplitter,
QSizePolicy,
QMessageBox,
QSplitter,
QVBoxLayout,
QWidget,
)
from humanfriendly import format_size
from src.core.constants import IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME, VIDEO_TYPES
from src.core.enums import SettingItems, Theme
from src.core.constants import VIDEO_TYPES, IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.fields import (
TagBoxField,
BaseField,
DatetimeField,
FieldTypeEnum,
_FieldID,
TagBoxField,
TextField,
BaseField,
_FieldID,
)
from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file
from src.core.library.alchemy.library import Library
from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel, open_file
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
from src.qt.modals.add_field import AddFieldModal
from src.qt.widgets.thumb_renderer import ThumbRenderer
from src.qt.widgets.fields import FieldContainer
from src.qt.widgets.panel import PanelModal
from src.qt.widgets.tag_box import TagBoxWidget
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.thumb_renderer import ThumbRenderer
from src.qt.widgets.video_player import VideoPlayer
from src.core.library.alchemy.library import Library
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
@@ -64,9 +63,7 @@ def update_selected_entry(driver: "QtDriver"):
entry = driver.frame_content[grid_idx]
# reload entry
results = driver.lib.search_library(FilterState(id=entry.id))
logger.info(
"found item", entries=len(results), grid_idx=grid_idx, lookup_id=entry.id
)
logger.info("found item", entries=len(results), grid_idx=grid_idx, lookup_id=entry.id)
assert results, f"Entry not found: {entry.id}"
driver.frame_content[grid_idx] = next(results)
@@ -111,9 +108,7 @@ class PreviewPanel(QWidget):
self.preview_vid = VideoPlayer(driver)
self.preview_vid.hide()
self.thumb_renderer = ThumbRenderer()
self.thumb_renderer.updated.connect(
lambda ts, i, s: (self.preview_img.setIcon(i))
)
self.thumb_renderer.updated.connect(lambda ts, i, s: (self.preview_img.setIcon(i)))
self.thumb_renderer.updated_ratio.connect(
lambda ratio: (
self.set_image_ratio(ratio),
@@ -134,9 +129,7 @@ class PreviewPanel(QWidget):
self.image_container.setMinimumSize(*self.img_button_size)
self.file_label = FileOpenerLabel("Filename")
self.file_label.setWordWrap(True)
self.file_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.file_label.setStyleSheet("font-weight: bold; font-size: 12px")
self.dimensions_label = QLabel("Dimensions")
@@ -173,9 +166,7 @@ class PreviewPanel(QWidget):
scroll_area = QScrollArea()
scroll_area.setObjectName("entryScrollArea")
scroll_area.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShadow(QFrame.Shadow.Plain)
@@ -211,7 +202,7 @@ class PreviewPanel(QWidget):
# set initial visibility based on settings
if not self.driver.settings.value(
SettingItems.WINDOW_SHOW_LIBS, True, type=bool
SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool
):
self.libs_flow_container.hide()
@@ -279,9 +270,7 @@ class PreviewPanel(QWidget):
self.render_libs = new_keys
self._fill_libs_widget(libs_sorted, layout)
def _fill_libs_widget(
self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout
):
def _fill_libs_widget(self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout):
def clear_layout(layout_item: QVBoxLayout):
for i in reversed(range(layout_item.count())):
child = layout_item.itemAt(i)
@@ -357,7 +346,7 @@ class PreviewPanel(QWidget):
layout.addLayout(row_layout)
def resizeEvent(self, event: QResizeEvent) -> None:
def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
self.update_image_size(
(self.image_container.size().width(), self.image_container.size().height())
)
@@ -370,7 +359,6 @@ class PreviewPanel(QWidget):
)
def set_image_ratio(self, ratio: float):
# logging.info(f'Updating Ratio to: {ratio} #####################################################')
self.image_ratio = ratio
def update_image_size(self, size: tuple[int, int], ratio: float = None):
@@ -407,23 +395,20 @@ class PreviewPanel(QWidget):
self.img_button_size = (int(adj_width), int(adj_height))
self.preview_img.setMaximumSize(adj_size)
self.preview_img.setIconSize(adj_size)
self.preview_vid.resizeVideo(adj_size)
self.preview_vid.resize_video(adj_size)
self.preview_vid.setMaximumSize(adj_size)
self.preview_vid.setMinimumSize(adj_size)
# self.preview_img.setMinimumSize(adj_size)
def place_add_field_button(self):
self.scroll_layout.addWidget(self.afb_container)
self.scroll_layout.setAlignment(
self.afb_container, Qt.AlignmentFlag.AlignHCenter
)
self.scroll_layout.setAlignment(self.afb_container, Qt.AlignmentFlag.AlignHCenter)
if self.add_field_modal.is_connected:
self.add_field_modal.done.disconnect()
if self.add_field_button.is_connected:
self.add_field_button.clicked.disconnect()
# self.afm.done.connect(lambda f: (self.lib.add_field_to_entry(self.selected[0][1], f), self.update_widgets()))
self.add_field_modal.done.connect(
lambda items: (
self.add_field_to_selected(items),
@@ -446,9 +431,7 @@ class PreviewPanel(QWidget):
)
def update_widgets(self) -> bool:
"""
Render the panel widgets with the newest data from the Library.
"""
"""Render the panel widgets with the newest data from the Library."""
logger.info("update_widgets", selected=self.driver.selected)
self.is_open = True
# self.tag_callback = tag_callback if tag_callback else None
@@ -460,13 +443,11 @@ class PreviewPanel(QWidget):
if not self.driver.selected:
if self.selected or not self.initialized:
self.file_label.setText("No Items Selected")
self.file_label.setFilePath("")
self.file_label.set_file_path("")
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
self.dimensions_label.setText("")
self.preview_img.setContextMenuPolicy(
Qt.ContextMenuPolicy.NoContextMenu
)
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
ratio = self.devicePixelRatio()
@@ -475,7 +456,7 @@ class PreviewPanel(QWidget):
"",
(512, 512),
ratio,
True,
is_loading=True,
update_on_ratio_change=True,
)
if self.preview_img.is_connected:
@@ -520,7 +501,7 @@ class PreviewPanel(QWidget):
# If a new selection is made, update the thumbnail and filepath.
if not self.selected or self.selected != self.driver.selected:
filepath = self.lib.library_dir / item.path
self.file_label.setFilePath(filepath)
self.file_label.set_file_path(filepath)
ratio = self.devicePixelRatio()
self.thumb_renderer.render(
time.time(),
@@ -532,9 +513,7 @@ class PreviewPanel(QWidget):
self.file_label.setText("\u200b".join(str(filepath)))
self.file_label.setCursor(Qt.CursorShape.PointingHandCursor)
self.preview_img.setContextMenuPolicy(
Qt.ContextMenuPolicy.ActionsContextMenu
)
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
self.opener = FileOpenerHelper(filepath)
@@ -550,9 +529,7 @@ class PreviewPanel(QWidget):
try:
with rawpy.imread(str(filepath)) as raw:
rgb = raw.postprocess()
image = Image.new(
"L", (rgb.shape[1], rgb.shape[0]), color="black"
)
image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
except (
rawpy._rawpy.LibRawIOError,
rawpy._rawpy.LibRawFileUnsupportedError,
@@ -568,9 +545,7 @@ class PreviewPanel(QWidget):
image = Image.fromarray(frame)
if success:
self.preview_img.hide()
self.preview_vid.play(
filepath, QSize(image.width, image.height)
)
self.preview_vid.play(filepath, QSize(image.width, image.height))
self.resizeEvent(
QResizeEvent(
QSize(image.width, image.height),
@@ -584,11 +559,14 @@ class PreviewPanel(QWidget):
IMAGE_TYPES + VIDEO_TYPES + RAW_IMAGE_TYPES
):
self.dimensions_label.setText(
f"{filepath.suffix.upper()[1:]}{format_size(filepath.stat().st_size)}\n{image.width} x {image.height} px"
f"{filepath.suffix.upper()[1:]}"
f"{format_size(filepath.stat().st_size)}\n{image.width} "
f"x {image.height} px"
)
else:
self.dimensions_label.setText(
f"{filepath.suffix.upper()[1:]}{format_size(filepath.stat().st_size)}"
f"{filepath.suffix.upper()[1:]}"
f"{format_size(filepath.stat().st_size)}"
)
if not filepath.is_file():
@@ -596,9 +574,7 @@ class PreviewPanel(QWidget):
except (FileNotFoundError, cv2.error) as e:
self.dimensions_label.setText(f"{filepath.suffix.upper()}")
logger.error(
"Couldn't Render thumbnail", filepath=filepath, error=e
)
logger.error("Couldn't Render thumbnail", filepath=filepath, error=e)
except (
UnidentifiedImageError,
@@ -607,15 +583,11 @@ class PreviewPanel(QWidget):
self.dimensions_label.setText(
f"{filepath.suffix.upper()[1:]}{format_size(filepath.stat().st_size)}"
)
logger.error(
"Couldn't Render thumbnail", filepath=filepath, error=e
)
logger.error("Couldn't Render thumbnail", filepath=filepath, error=e)
if self.preview_img.is_connected:
self.preview_img.clicked.disconnect()
self.preview_img.clicked.connect(
lambda checked=False, pth=filepath: open_file(pth)
)
self.preview_img.clicked.connect(lambda checked=False, pth=filepath: open_file(pth))
self.preview_img.is_connected = True
self.selected = self.driver.selected
@@ -643,12 +615,10 @@ class PreviewPanel(QWidget):
if self.selected != self.driver.selected:
self.file_label.setText(f"{len(self.driver.selected)} Items Selected")
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
self.file_label.setFilePath("")
self.file_label.set_file_path("")
self.dimensions_label.setText("")
self.preview_img.setContextMenuPolicy(
Qt.ContextMenuPolicy.NoContextMenu
)
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
ratio = self.devicePixelRatio()
@@ -657,7 +627,7 @@ class PreviewPanel(QWidget):
"",
(512, 512),
ratio,
True,
is_loading=True,
update_on_ratio_change=True,
)
if self.preview_img.is_connected:
@@ -712,9 +682,7 @@ class PreviewPanel(QWidget):
return True
def set_tags_updated_slot(self, slot: object):
"""
Replacement for tag_callback.
"""
"""Replacement for tag_callback."""
if self.is_connected:
self.tags_updated.disconnect()
@@ -725,7 +693,11 @@ class PreviewPanel(QWidget):
def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
"""Update/Create data for a FieldContainer.
:param is_mixed: Relevant when multiple items are selected. If True, field is not present in all selected items
Args:
index(int): The container index.
field(BaseField): The type of field to write to.
is_mixed(bool): Relevant when multiple items are selected.
If True, field is not present in all selected items.
"""
# Remove 'Add Field' button from scroll_layout, to be re-added later.
self.scroll_layout.takeAt(self.scroll_layout.count() - 1).widget()
@@ -769,7 +741,6 @@ class PreviewPanel(QWidget):
container.set_inner_widget(inner_container)
# inner_container.field = field
inner_container.updated.connect(
lambda: (
self.write_container(index, field),
@@ -777,7 +748,6 @@ class PreviewPanel(QWidget):
)
)
# NOTE: Tag Boxes have no Edit Button (But will when you can convert field types)
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item)))
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.type.name),
@@ -893,8 +863,6 @@ class PreviewPanel(QWidget):
inner_container = TextWidget(title, str(field.value))
container.set_inner_widget(inner_container)
# if type(item) == Entry:
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item)))
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.type.name),
@@ -912,13 +880,10 @@ class PreviewPanel(QWidget):
else:
logger.warning("write_container - unknown field", field=field)
container.set_title(field.type.name)
# container.set_editable(False)
container.set_inline(False)
title = f"{field.type.name} (Unknown Field Type)"
inner_container = TextWidget(title, field.type.name)
container.set_inner_widget(inner_container)
# if type(item) == Entry:
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item)))
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.type.name),
@@ -971,9 +936,7 @@ class PreviewPanel(QWidget):
remove_mb.setText(prompt)
remove_mb.setWindowTitle("Remove Field")
remove_mb.setIcon(QMessageBox.Icon.Warning)
cancel_button = remove_mb.addButton(
"&Cancel", QMessageBox.ButtonRole.DestructiveRole
)
cancel_button = remove_mb.addButton("&Cancel", QMessageBox.ButtonRole.DestructiveRole)
remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole)
# remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel)
remove_mb.setDefaultButton(cancel_button)

View File

@@ -6,7 +6,7 @@
from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QProgressDialog
from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget
class ProgressWidget(QWidget):
@@ -30,9 +30,7 @@ class ProgressWidget(QWidget):
)
self.root.addWidget(self.pb)
self.setFixedSize(432, 112)
self.setWindowFlags(
self.pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint
)
self.setWindowFlags(self.pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
self.setWindowTitle(window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)

View File

@@ -4,14 +4,13 @@
import math
from types import FunctionType
from pathlib import Path
from types import FunctionType
from PIL import Image
from PySide6.QtCore import Signal, Qt, QEvent
from PySide6.QtGui import QEnterEvent, QAction
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton
from PySide6.QtCore import QEvent, Qt, Signal
from PySide6.QtGui import QAction, QEnterEvent
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
from src.core.library import Tag
from src.core.palette import ColorType, get_tag_color
@@ -67,64 +66,31 @@ class TagWidget(QWidget):
self.inner_layout = QHBoxLayout()
self.inner_layout.setObjectName("innerLayout")
self.inner_layout.setContentsMargins(2, 2, 2, 2)
# self.inner_layout.setAlignment(Qt.AlignmentFlag.AlignRight)
# self.inner_container = QWidget()
# self.inner_container.setLayout(self.inner_layout)
# self.base_layout.addWidget(self.inner_container)
self.bg_button.setLayout(self.inner_layout)
self.bg_button.setMinimumSize(math.ceil(22 * 1.5), 22)
# self.bg_button.setStyleSheet(
# f'QPushButton {{'
# f'border: 2px solid #8f8f91;'
# f'border-radius: 6px;'
# f'background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 {ColorType.PRIMARY}, stop: 1 {ColorType.BORDER});'
# f'min-width: 80px;}}')
self.bg_button.setStyleSheet(
# f'background: {get_tag_color(ColorType.PRIMARY, tag.color)};'
f"QPushButton{{"
f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};"
# f'background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_tag_color(ColorType.PRIMARY, tag.color)}, stop:1.0 {get_tag_color(ColorType.BORDER, tag.color)});'
# f"border-color:{get_tag_color(ColorType.PRIMARY, tag.color)};"
f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
f"font-weight: 600;"
f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: {math.ceil(self.devicePixelRatio())}px;"
# f'border-top:2px solid {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};'
# f'border-bottom:2px solid {get_tag_color(ColorType.BORDER, tag.color)};'
# f'border-left:2px solid {get_tag_color(ColorType.BORDER, tag.color)};'
# f'border-right:2px solid {get_tag_color(ColorType.BORDER, tag.color)};'
# f'padding-top: 0.5px;'
f"padding-right: 4px;"
f"padding-bottom: 1px;"
f"padding-left: 4px;"
f"font-size: 13px"
f"}}"
f"QPushButton::hover{{"
# f'background: {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};'
# f'background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_tag_color(ColorType.PRIMARY, tag.color)}, stop:1.0 {get_tag_color(ColorType.BORDER, tag.color)});'
# f"border-color:{get_tag_color(ColorType.PRIMARY, tag.color)};"
# f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
f"}}"
)
# self.renderer = ThumbRenderer()
# self.renderer.updated.connect(lambda ts, i, s, ext: (self.update_thumb(ts, image=i),
# self.update_size(
# ts, size=s),
# self.set_extension(ext)))
# self.bg_button.setLayout(self.base_layout)
self.base_layout.addWidget(self.bg_button)
# self.setMinimumSize(self.bg_button.size())
# logging.info(tag.color)
if has_remove:
self.remove_button = QPushButton(self)
self.remove_button.setFlat(True)
@@ -133,28 +99,16 @@ class TagWidget(QWidget):
self.remove_button.setStyleSheet(
f"color: {get_tag_color(ColorType.PRIMARY, tag.color)};"
f"background: {get_tag_color(ColorType.TEXT, tag.color)};"
# f"color: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# f"border-color: {get_tag_color(ColorType.BORDER, tag.color)};"
f"font-weight: 800;"
# f"border-color:{'black' if color not in [
# 'black', 'gray', 'dark gray',
# 'cool gray', 'warm gray', 'blue',
# 'purple', 'violet'] else 'white'};"
f"border-radius: 4px;"
# f'border-style:solid;'
f"border-width:0;"
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
f"padding-bottom: 4px;"
# f'padding-left: 4px;'
f"font-size: 14px"
)
self.remove_button.setMinimumSize(19, 19)
self.remove_button.setMaximumSize(19, 19)
# self.remove_button.clicked.connect(on_remove_callback)
self.remove_button.clicked.connect(self.on_remove.emit)
# self.inner_layout.addWidget(self.edit_button)
if has_remove:
self.inner_layout.addWidget(self.remove_button)
self.inner_layout.addStretch(1)
@@ -162,21 +116,16 @@ class TagWidget(QWidget):
# NOTE: Do this if you don't want the tag to stretch, like in a search.
# self.bg_button.setMaximumWidth(self.bg_button.sizeHint().width())
# self.set_click(on_click_callback)
self.bg_button.clicked.connect(self.on_click.emit)
# self.setMinimumSize(50,20)
def enterEvent(self, event: QEnterEvent) -> None:
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
if self.has_remove:
self.remove_button.setHidden(False)
# self.edit_button.setHidden(False)
self.update()
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
if self.has_remove:
self.remove_button.setHidden(True)
# self.edit_button.setHidden(True)
self.update()
return super().leaveEvent(event)

View File

@@ -7,19 +7,18 @@ import math
import typing
import structlog
from PySide6.QtCore import Signal, Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QPushButton
from src.core.constants import TAG_FAVORITE, TAG_ARCHIVED
from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from src.core.library import Entry, Tag
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.fields import TagBoxField
from src.qt.flowlayout import FlowLayout
from src.qt.widgets.fields import FieldWidget
from src.qt.widgets.tag import TagWidget
from src.qt.widgets.panel import PanelModal
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.modals.tag_search import TagSearchPanel
from src.qt.widgets.fields import FieldWidget
from src.qt.widgets.panel import PanelModal
from src.qt.widgets.tag import TagWidget
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
@@ -42,10 +41,12 @@ class TagBoxWidget(FieldWidget):
assert isinstance(field, TagBoxField), f"field is {type(field)}"
self.field = field
self.driver = driver # Used for creating tag click callbacks that search entries for that tag.
self.driver = (
driver # Used for creating tag click callbacks that search entries for that tag.
)
self.setObjectName("tagBox")
self.base_layout = FlowLayout()
self.base_layout.setGridEfficiency(False)
self.base_layout.enable_grid_optimizations(value=False)
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.base_layout)
@@ -94,7 +95,7 @@ class TagBoxWidget(FieldWidget):
is_recycled = True
for tag in tags:
tag_widget = TagWidget(tag, True, True)
tag_widget = TagWidget(tag, has_edit=True, has_remove=True)
tag_widget.on_click.connect(
lambda tag_id=tag.id: (
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
@@ -133,7 +134,6 @@ class TagBoxWidget(FieldWidget):
done_callback=self.driver.preview_panel.update_widgets,
has_save=True,
)
# self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t))
# TODO - this was update_tag()
self.edit_modal.saved.connect(
lambda: self.driver.lib.update_tag(
@@ -141,7 +141,6 @@ class TagBoxWidget(FieldWidget):
subtag_ids=build_tag_panel.subtags,
)
)
# panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag))
self.edit_modal.show()
def add_tag_callback(self, tag_id: int):

View File

@@ -21,9 +21,7 @@ class TextWidget(FieldWidget):
# self.text_label.textFormat(Qt.TextFormat.RichText)
self.text_label.setStyleSheet("font-size: 12px")
self.text_label.setWordWrap(True)
self.text_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.base_layout.addWidget(self.text_label)
self.set_text(text)

View File

@@ -3,8 +3,7 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtWidgets import QVBoxLayout, QPlainTextEdit
from PySide6.QtWidgets import QPlainTextEdit, QVBoxLayout
from src.qt.widgets.panel import PanelWidget

View File

@@ -3,8 +3,7 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from typing import Callable
from PySide6.QtWidgets import QVBoxLayout, QLineEdit
from PySide6.QtWidgets import QLineEdit, QVBoxLayout
from src.qt.widgets.panel import PanelWidget

View File

@@ -5,7 +5,7 @@
from PySide6 import QtCore
from PySide6.QtCore import QEvent
from PySide6.QtGui import QEnterEvent, QPainter, QColor, QPen, QPainterPath, QPaintEvent
from PySide6.QtGui import QColor, QEnterEvent, QPainter, QPainterPath, QPaintEvent, QPen
from PySide6.QtWidgets import QWidget
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
@@ -19,7 +19,7 @@ class ThumbButton(QPushButtonWrapper):
# self.clicked.connect(lambda checked: self.set_selected(True))
def paintEvent(self, event: QPaintEvent) -> None:
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802
super().paintEvent(event)
if self.hovered or self.selected:
painter = QPainter()
@@ -47,9 +47,7 @@ class ThumbButton(QPushButtonWrapper):
# painter.drawPath(path)
if self.selected:
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_HardLight
)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_HardLight)
color = QColor("#bb4ff0")
color.setAlphaF(0.5)
pen = QPen(color, width)
@@ -57,29 +55,25 @@ class ThumbButton(QPushButtonWrapper):
painter.fillPath(path, color)
painter.drawPath(path)
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_Source
)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
color = QColor("#bb4ff0") if not self.hovered else QColor("#55bbf6")
pen = QPen(color, width)
painter.setPen(pen)
painter.drawPath(path)
elif self.hovered:
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_Source
)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
color = QColor("#55bbf6")
pen = QPen(color, width)
painter.setPen(pen)
painter.drawPath(path)
painter.end()
def enterEvent(self, event: QEnterEvent) -> None:
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
self.hovered = True
self.repaint()
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
self.hovered = False
self.repaint()
return super().leaveEvent(event)

View File

@@ -8,30 +8,28 @@ from pathlib import Path
import cv2
import rawpy
from pillow_heif import register_heif_opener, register_avif_opener
import structlog
from PIL import (
Image,
UnidentifiedImageError,
ImageQt,
ImageDraw,
ImageFile,
ImageFont,
ImageOps,
ImageFile,
ImageQt,
UnidentifiedImageError,
)
from PIL.Image import DecompressionBombError
from PySide6.QtCore import QObject, Signal, QSize
from pillow_heif import register_avif_opener, register_heif_opener
from PySide6.QtCore import QObject, QSize, Signal
from PySide6.QtGui import QPixmap
from src.qt.helpers.gradient import four_corner_gradient_background
from src.core.constants import (
PLAINTEXT_TYPES,
VIDEO_TYPES,
IMAGE_TYPES,
PLAINTEXT_TYPES,
RAW_IMAGE_TYPES,
VIDEO_TYPES,
)
import structlog
from src.core.utils.encoding import detect_char_encoding
from src.qt.helpers.gradient import four_corner_gradient_background
ImageFile.LOAD_TRUNCATED_IMAGES = True
@@ -91,12 +89,11 @@ class ThumbRenderer(QObject):
filepath: str | Path,
base_size: tuple[int, int],
pixel_ratio: float,
is_loading=False,
gradient=False,
update_on_ratio_change=False,
is_loading: bool = False,
gradient: bool = False,
update_on_ratio_change: bool = False,
):
"""Internal renderer. Render an entry/element thumbnail for the GUI."""
logger.debug("rendering thumbnail", path=filepath)
image: Image.Image = None
@@ -136,9 +133,7 @@ class ThumbRenderer(QObject):
image = ImageOps.exif_transpose(image)
except DecompressionBombError as e:
logger.error(
"Couldn't Render thumbnail", filepath=filepath, error=e
)
logger.error("Couldn't Render thumbnail", filepath=filepath, error=e)
elif _filepath.suffix.lower() in RAW_IMAGE_TYPES:
try:
@@ -151,17 +146,13 @@ class ThumbRenderer(QObject):
decoder_name="raw",
)
except DecompressionBombError as e:
logger.error(
"Couldn't Render thumbnail", filepath=filepath, error=e
)
logger.error("Couldn't Render thumbnail", filepath=filepath, error=e)
except (
rawpy._rawpy.LibRawIOError,
rawpy._rawpy.LibRawFileUnsupportedError,
) as e:
logger.error(
"Couldn't Render thumbnail", filepath=filepath, error=e
)
logger.error("Couldn't Render thumbnail", filepath=filepath, error=e)
# Videos =======================================================
elif _filepath.suffix.lower() in VIDEO_TYPES:
@@ -232,8 +223,7 @@ class ThumbRenderer(QObject):
resampling_method = (
Image.Resampling.NEAREST
if max(image.size[0], image.size[1])
< max(base_size[0], base_size[1])
if max(image.size[0], image.size[1]) < max(base_size[0], base_size[1])
else Image.Resampling.BILINEAR
)
image = image.resize((new_x, new_y), resample=resampling_method)
@@ -272,9 +262,7 @@ class ThumbRenderer(QObject):
UnicodeDecodeError,
) as e:
if e is not UnicodeDecodeError:
logger.error(
"Couldn't Render thumbnail", filepath=filepath, error=e
)
logger.error("Couldn't Render thumbnail", filepath=filepath, error=e)
if update_on_ratio_change:
self.updated_ratio.emit(1)
@@ -299,6 +287,4 @@ class ThumbRenderer(QObject):
)
else:
self.updated.emit(
timestamp, QPixmap(), QSize(*base_size), _filepath.suffix.lower()
)
self.updated.emit(timestamp, QPixmap(), QSize(*base_size), _filepath.suffix.lower())

View File

@@ -2,36 +2,34 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import logging
import typing
from PySide6.QtCore import (
Qt,
QSize,
QTimer,
QVariantAnimation,
QUrl,
QObject,
QEvent,
QRectF,
)
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene
from PySide6.QtGui import (
QPen,
QColor,
QBrush,
QResizeEvent,
QWheelEvent,
QAction,
QRegion,
QBitmap,
)
from PySide6.QtSvgWidgets import QSvgWidget
from src.qt.helpers.file_opener import FileOpenerHelper
from PIL import Image, ImageDraw
from PySide6.QtCore import (
QEvent,
QObject,
QRectF,
QSize,
Qt,
QTimer,
QUrl,
QVariantAnimation,
)
from PySide6.QtGui import (
QAction,
QBitmap,
QBrush,
QColor,
QPen,
QRegion,
QResizeEvent,
)
from PySide6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
from PySide6.QtSvgWidgets import QSvgWidget
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView
from src.core.enums import SettingItems
from src.qt.helpers.file_opener import FileOpenerHelper
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
@@ -49,11 +47,9 @@ class VideoPlayer(QGraphicsView):
self.driver = driver
self.resolution = QSize(1280, 720)
self.animation = QVariantAnimation(self)
self.animation.valueChanged.connect(
lambda value: self.setTintTransparency(value)
)
self.animation.valueChanged.connect(lambda value: self.set_tint_opacity(value))
self.hover_fix_timer = QTimer()
self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered())
self.hover_fix_timer.timeout.connect(lambda: self.check_if_hovered())
self.hover_fix_timer.setSingleShot(True)
self.content_visible = False
self.filepath = None
@@ -63,16 +59,14 @@ class VideoPlayer(QGraphicsView):
self.setScene(QGraphicsScene(self))
self.player = QMediaPlayer(self)
self.player.mediaStatusChanged.connect(
lambda: self.checkMediaStatus(self.player.mediaStatus())
lambda: self.check_media_status(self.player.mediaStatus())
)
self.video_preview = VideoPreview()
self.player.setVideoOutput(self.video_preview)
self.video_preview.setAcceptHoverEvents(True)
self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.RightButton)
self.video_preview.installEventFilter(self)
self.player.setAudioOutput(
QAudioOutput(QMediaDevices().defaultAudioOutput(), self.player)
)
self.player.setAudioOutput(QAudioOutput(QMediaDevices().defaultAudioOutput(), self.player))
self.player.audioOutput().setMuted(True)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
@@ -92,7 +86,7 @@ class VideoPlayer(QGraphicsView):
# Set up the buttons.
self.play_pause = QSvgWidget()
self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, on=True)
self.play_pause.setMouseTracking(True)
self.play_pause.installEventFilter(self)
self.scene().addWidget(self.play_pause)
@@ -104,7 +98,7 @@ class VideoPlayer(QGraphicsView):
self.play_pause.hide()
self.mute_button = QSvgWidget()
self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, on=True)
self.mute_button.setMouseTracking(True)
self.mute_button.installEventFilter(self)
self.scene().addWidget(self.mute_button)
@@ -121,9 +115,9 @@ class VideoPlayer(QGraphicsView):
autoplay_action.setCheckable(True)
self.addAction(autoplay_action)
autoplay_action.setChecked(
bool(self.driver.settings.value(SettingItems.AUTOPLAY, True, type=bool))
bool(self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool))
)
autoplay_action.triggered.connect(lambda: self.toggleAutoplay())
autoplay_action.triggered.connect(lambda: self.toggle_autoplay())
self.autoplay = autoplay_action
open_file_action = QAction("Open file", self)
@@ -137,11 +131,12 @@ class VideoPlayer(QGraphicsView):
self.player.stop()
super().close(*args, **kwargs)
def toggleAutoplay(self) -> None:
def toggle_autoplay(self) -> None:
"""Toggle the autoplay state of the video."""
self.driver.settings.setValue(SettingItems.AUTOPLAY, self.autoplay.isChecked())
self.driver.settings.sync()
def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None:
def check_media_status(self, media_status: QMediaPlayer.MediaStatus) -> None:
if media_status == QMediaPlayer.MediaStatus.EndOfMedia:
# Switches current video to with video at filepath.
# Reason for this is because Pyside6 can't handle setting a new source and freezes.
@@ -155,10 +150,11 @@ class VideoPlayer(QGraphicsView):
else:
self.player.pause()
self.opener.set_filepath(self.filepath)
self.keepControlsInPlace()
self.updateControls()
self.reposition_controls()
self.update_controls()
def updateControls(self) -> None:
def update_controls(self) -> None:
"""Update the icons of the video player controls."""
if self.player.audioOutput().isMuted():
self.mute_button.load(self.driver.rm.volume_mute_icon)
else:
@@ -169,19 +165,16 @@ class VideoPlayer(QGraphicsView):
else:
self.play_pause.load(self.driver.rm.play_icon)
def wheelEvent(self, event: QWheelEvent) -> None:
return
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
# This chunk of code is for the video controls.
def eventFilter(self, obj: QObject, event: QEvent) -> bool: # noqa: N802
"""Manage events for the video player."""
if (
event.type() == QEvent.Type.MouseButtonPress
and event.button() == Qt.MouseButton.LeftButton # type: ignore
):
if obj == self.play_pause and self.player.hasVideo():
self.pauseToggle()
self.toggle_pause()
elif obj == self.mute_button and self.player.hasAudio():
self.muteToggle()
self.toggle_mute()
elif obj == self.video_preview:
if event.type() in (
@@ -192,8 +185,7 @@ class VideoPlayer(QGraphicsView):
self.underMouse()
self.hover_fix_timer.start(10)
elif (
event.type()
in (QEvent.Type.GraphicsSceneHoverLeave, QEvent.Type.HoverLeave)
event.type() in (QEvent.Type.GraphicsSceneHoverLeave, QEvent.Type.HoverLeave)
and not self.video_preview.isUnderMouse()
):
self.hover_fix_timer.stop()
@@ -201,32 +193,37 @@ class VideoPlayer(QGraphicsView):
return super().eventFilter(obj, event)
def checkIfStillHovered(self) -> None:
# I don't know why, but the HoverLeave event is not triggered sometimes
# and does not hide the controls.
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse
# is still in the video preview.
def check_if_hovered(self) -> None:
"""Check if the mouse is still hovering over the video player."""
# Sometimes the HoverLeave event does not trigger and is unable to hide the video controls.
# As a workaround, this is called by a QTimer every 10ms
# to check if the mouse is still in the video preview.
if not self.video_preview.isUnderMouse():
self.releaseMouse()
else:
self.hover_fix_timer.start(10)
def setTintTransparency(self, value) -> None:
self.video_tint.setBrush(QBrush(QColor(0, 0, 0, value)))
def set_tint_opacity(self, opacity: int) -> None:
"""Set the opacity of the video player's tint.
def underMouse(self) -> bool:
Args:
opacity(int): The opacity value, from 0-255.
"""
self.video_tint.setBrush(QBrush(QColor(0, 0, 0, opacity)))
def underMouse(self) -> bool: # noqa: N802
self.animation.setStartValue(self.video_tint.brush().color().alpha())
self.animation.setEndValue(100)
self.animation.setDuration(250)
self.animation.start()
self.play_pause.show()
self.mute_button.show()
self.keepControlsInPlace()
self.updateControls()
self.reposition_controls()
self.update_controls()
return super().underMouse()
def releaseMouse(self) -> None:
def releaseMouse(self) -> None: # noqa: N802
self.animation.setStartValue(self.video_tint.brush().color().alpha())
self.animation.setEndValue(0)
self.animation.setDuration(500)
@@ -236,12 +233,13 @@ class VideoPlayer(QGraphicsView):
return super().releaseMouse()
def resetControlsToDefault(self) -> None:
# Resets the video controls to their default state.
def reset_controls(self) -> None:
"""Reset the video controls to their default state."""
self.play_pause.load(self.driver.rm.pause_icon)
self.mute_button.load(self.driver.rm.volume_mute_icon)
def pauseToggle(self) -> None:
def toggle_pause(self) -> None:
"""Toggle the pause state of the video."""
if self.player.isPlaying():
self.player.pause()
self.play_pause.load(self.driver.rm.play_icon)
@@ -249,7 +247,8 @@ class VideoPlayer(QGraphicsView):
self.player.play()
self.play_pause.load(self.driver.rm.pause_icon)
def muteToggle(self) -> None:
def toggle_mute(self) -> None:
"""Toggle the mute state of the video."""
if self.player.audioOutput().isMuted():
self.player.audioOutput().setMuted(False)
self.mute_button.load(self.driver.rm.volume_icon)
@@ -258,8 +257,10 @@ class VideoPlayer(QGraphicsView):
self.mute_button.load(self.driver.rm.volume_mute_icon)
def play(self, filepath: str, resolution: QSize) -> None:
# Sets the filepath and sends the current player position to the very end,
# so that the new video can be played.
"""Set the filepath and send the current player position to the very end.
This is used so that the new video can be played.
"""
logging.info(f"Playing {filepath}")
self.resolution = resolution
self.filepath = filepath
@@ -267,14 +268,18 @@ class VideoPlayer(QGraphicsView):
self.player.setPosition(self.player.duration())
self.player.play()
else:
self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)
self.check_media_status(QMediaPlayer.MediaStatus.EndOfMedia)
def stop(self) -> None:
self.filepath = None
self.player.stop()
def resizeVideo(self, new_size: QSize) -> None:
# Resizes the video preview to the new size.
def resize_video(self, new_size: QSize) -> None:
"""Resize the video player.
Args:
new_size(QSize): The new size of the video player to set.
"""
self.video_preview.setSize(new_size)
self.video_tint.setRect(
0, 0, self.video_preview.size().width(), self.video_preview.size().height()
@@ -282,11 +287,12 @@ class VideoPlayer(QGraphicsView):
contents = self.contentsRect()
self.centerOn(self.video_preview)
self.roundCorners()
self.apply_rounded_corners()
self.setSceneRect(0, 0, contents.width(), contents.height())
self.keepControlsInPlace()
self.reposition_controls()
def roundCorners(self) -> None:
def apply_rounded_corners(self) -> None:
"""Apply a rounded corner effect to the video player."""
width: int = int(max(self.contentsRect().size().width(), 0))
height: int = int(max(self.contentsRect().size().height(), 0))
mask = Image.new(
@@ -306,8 +312,8 @@ class VideoPlayer(QGraphicsView):
final_mask = mask.getchannel("A").toqpixmap()
self.setMask(QRegion(QBitmap(final_mask)))
def keepControlsInPlace(self) -> None:
# Keeps the video controls in the places they should be.
def reposition_controls(self) -> None:
"""Reposition video controls to their intended locations."""
self.play_pause.move(
int(self.width() / 2 - self.play_pause.size().width() / 2),
int(self.height() / 2 - self.play_pause.size().height() / 2),
@@ -317,10 +323,10 @@ class VideoPlayer(QGraphicsView):
int(self.height() - self.mute_button.size().height() - 10),
)
def resizeEvent(self, event: QResizeEvent) -> None:
# Keeps the video preview in the center of the screen.
def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
"""Keep the video preview in the center of the screen."""
self.centerOn(self.video_preview)
self.resizeVideo(
self.resize_video(
QSize(
int(self.video_preview.size().width()),
int(self.video_preview.size().height()),
@@ -329,7 +335,7 @@ class VideoPlayer(QGraphicsView):
class VideoPreview(QGraphicsVideoItem):
def boundingRect(self):
def boundingRect(self): # noqa: N802
return QRectF(0, 0, self.size().width(), self.size().height())
def paint(self, painter, option, widget=None) -> None:

View File

@@ -5,13 +5,12 @@
"""TagStudio launcher."""
import structlog
import logging
from src.qt.ts_qt import QtDriver
import argparse
import logging
import traceback
import structlog
from src.qt.ts_qt import QtDriver
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
@@ -66,7 +65,7 @@ def main():
driver.start()
except Exception:
traceback.print_exc()
print(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
logging.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
input()

View File

@@ -1,7 +1,7 @@
import sys
import pathlib
import sys
from tempfile import TemporaryDirectory
from unittest.mock import patch, Mock
from unittest.mock import Mock, patch
import pytest
@@ -9,10 +9,10 @@ CWD = pathlib.Path(__file__).parent
# this needs to be above `src` imports
sys.path.insert(0, str(CWD.parent))
from src.core.library import Library, Tag, Entry
from src.core.library import Entry, Library, Tag
from src.core.library import alchemy as backend
from src.core.library.alchemy.enums import TagColor
from src.core.library.alchemy.fields import TagBoxField, _FieldID
from src.core.library import alchemy as backend
from src.qt.ts_qt import QtDriver

View File

@@ -3,7 +3,5 @@ from src.qt.modals.folders_to_tags import folders_to_tags
def test_folders_to_tags(library):
folders_to_tags(library)
entry = [
x for x in library.get_entries(with_joins=True) if "bar.md" in str(x.path)
][0]
entry = [x for x in library.get_entries(with_joins=True) if "bar.md" in str(x.path)][0]
assert {x.name for x in entry.tags} == {"two", "bar"}

View File

@@ -2,7 +2,6 @@ import pathlib
from tempfile import TemporaryDirectory
import pytest
from src.core.library import Library
from src.core.library.alchemy.enums import FilterState
from src.core.utils.missing_files import MissingRegistry

View File

@@ -2,7 +2,6 @@ import pathlib
from tempfile import TemporaryDirectory
import pytest
from src.core.constants import LibraryPrefs
from src.core.utils.refresh_dir import RefreshDirTracker

View File

@@ -3,7 +3,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from src.core.enums import MacroID
from src.core.library.alchemy.fields import _FieldID

View File

@@ -1,7 +1,6 @@
from pathlib import Path
from unittest.mock import Mock
from src.core.library import Entry
from src.core.library.alchemy.enums import FilterState
from src.core.library.json.library import ItemType
@@ -49,22 +48,22 @@ def test_select_item_bridge(qt_driver, entry_min):
assert len(qt_driver.item_thumbs) == 3
# select first item
qt_driver.select_item(0, False, False)
qt_driver.select_item(0, append=False, bridge=False)
assert qt_driver.selected == [0]
# add second item to selection
qt_driver.select_item(1, False, bridge=True)
qt_driver.select_item(1, append=False, bridge=True)
assert qt_driver.selected == [0, 1]
# add third item to selection
qt_driver.select_item(2, False, bridge=True)
qt_driver.select_item(2, append=False, bridge=True)
assert qt_driver.selected == [0, 1, 2]
# select third item only
qt_driver.select_item(2, False, bridge=False)
qt_driver.select_item(2, append=False, bridge=False)
assert qt_driver.selected == [2]
qt_driver.select_item(0, False, bridge=True)
qt_driver.select_item(0, append=False, bridge=True)
assert qt_driver.selected == [0, 1, 2]

View File

@@ -1,6 +1,5 @@
from PySide6.QtCore import QRect
from PySide6.QtWidgets import QWidget, QPushButton
from PySide6.QtWidgets import QPushButton, QWidget
from src.qt.flowlayout import FlowLayout
@@ -10,9 +9,9 @@ def test_flow_layout_happy_path(qtbot):
super().__init__()
self.flow_layout = FlowLayout(self)
self.flow_layout.setGridEfficiency(True)
self.flow_layout.enable_grid_optimizations(value=True)
self.flow_layout.addWidget(QPushButton("Short"))
window = Window()
assert window.flow_layout.count()
assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), False)
assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), test_only=False)

View File

@@ -1,7 +1,6 @@
import pytest
from src.core.library import ItemType
from src.qt.widgets.item_thumb import ItemThumb, BadgeType
from src.qt.widgets.item_thumb import BadgeType, ItemThumb
@pytest.mark.parametrize("new_value", (True, False))

View File

@@ -2,10 +2,9 @@ from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from src.core.library import Entry
from src.core.library.alchemy.enums import FieldTypeEnum
from src.core.library.alchemy.fields import _FieldID, TextField
from src.core.library.alchemy.fields import TextField, _FieldID
from src.qt.widgets.preview_panel import PreviewPanel

View File

@@ -1,10 +1,9 @@
from unittest.mock import patch
from src.core.library.alchemy.fields import _FieldID
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.widgets.tag import TagWidget
from src.qt.widgets.tag_box import TagBoxWidget
from src.qt.modals.build_tag import BuildTagPanel
def test_tag_widget(qtbot, library, qt_driver):
@@ -26,9 +25,7 @@ def test_tag_widget(qtbot, library, qt_driver):
def test_tag_widget_add_existing_raises(library, qt_driver, entry_full):
# Given
tag_field = [
f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name
][0]
tag_field = [f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name][0]
assert len(entry_full.tags) == 1
tag = next(iter(entry_full.tags))
@@ -69,9 +66,7 @@ def test_tag_widget_remove(qtbot, qt_driver, library, entry_full):
assert tag
assert entry_full.tag_box_fields
tag_field = [
f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name
][0]
tag_field = [f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name][0]
tag_widget = TagBoxWidget(tag_field, "title", qt_driver)
tag_widget.driver.selected = [0]
@@ -93,9 +88,7 @@ def test_tag_widget_edit(qtbot, qt_driver, library, entry_full):
assert tag
assert entry_full.tag_box_fields
tag_field = [
f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name
][0]
tag_field = [f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name][0]
tag_box_widget = TagBoxWidget(tag_field, "title", qt_driver)
tag_box_widget.driver.selected = [0]

View File

@@ -1,5 +1,4 @@
import pytest
from src.core.library.alchemy.enums import FilterState

View File

@@ -2,12 +2,10 @@ from pathlib import Path, PureWindowsPath
from tempfile import TemporaryDirectory
import pytest
from src.core.constants import LibraryPrefs
from src.core.library.alchemy import Entry
from src.core.library.alchemy import Library
from src.core.library.alchemy import Entry, Library
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.fields import _FieldID, TextField
from src.core.library.alchemy.fields import TextField, _FieldID
def test_library_bootstrap():
@@ -100,10 +98,7 @@ def test_get_entry(library, entry_min):
def test_entries_count(library):
entries = [
Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[])
for x in range(10)
]
entries = [Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[]) for x in range(10)]
library.add_entries(entries)
results = library.search_library(
FilterState(
@@ -379,12 +374,8 @@ def test_update_field_order(library, entry_full):
title_field = entry_full.text_fields[0]
# When add two more fields
library.add_entry_field_type(
entry_full.id, field_id=title_field.type_key, value="first"
)
library.add_entry_field_type(
entry_full.id, field_id=title_field.type_key, value="second"
)
library.add_entry_field_type(entry_full.id, field_id=title_field.type_key, value="first")
library.add_entry_field_type(entry_full.id, field_id=title_field.type_key, value="second")
# remove the one on first position
assert title_field.position == 0