mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-01 15:49:09 +00:00
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:
committed by
GitHub
parent
c15963868e
commit
b6e2167605
27
.git-blame-ignore-revs
Normal file
27
.git-blame-ignore-revs
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
|
||||
from src.core.library.alchemy.enums import FilterState
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user