Reformatted using Ruff

This commit is contained in:
Travis Abendshien 2024-05-03 19:40:36 -07:00
parent 77d7559014
commit 089c8dd50c
46 changed files with 13032 additions and 11320 deletions

View file

@ -1,2 +1,2 @@
[tool.ruff]
exclude = ["main_window.py", "home_ui.py"]
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"]

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
class FieldTemplate:
"""A TagStudio Library Field Template object."""
@ -11,7 +12,7 @@ class FieldTemplate:
self.type = type
def __str__(self) -> str:
return f'\nID: {self.id}\nName: {self.name}\nType: {self.type}\n'
return f"\nID: {self.id}\nName: {self.name}\nType: {self.type}\n"
def __repr__(self) -> str:
return self.__str__()
@ -20,8 +21,8 @@ class FieldTemplate:
"""An alternative to __dict__ that only includes fields containing non-default data."""
obj = {}
# All Field fields (haha) are mandatory, so no value checks are done.
obj['id'] = self.id
obj['name'] = self.name
obj['type'] = self.type
obj["id"] = self.id
obj["name"] = self.name
obj["type"] = self.type
return obj

View file

@ -1,33 +1,38 @@
from typing import TypedDict
class JsonLibary(TypedDict("",{"ts-version":str})):
#"ts-version": str
class JsonLibary(TypedDict("", {"ts-version": str})):
# "ts-version": str
tags: "list[JsonTag]"
collations: "list[JsonCollation]"
fields: list #TODO
fields: list # TODO
macros: "list[JsonMacro]"
entries: "list[JsonEntry]"
class JsonBase(TypedDict):
id: int
class JsonTag(JsonBase,total=False):
class JsonTag(JsonBase, total=False):
name: str
aliases: list[str]
color: str
shorthand: str
subtag_ids: list[int]
class JsonCollation(JsonBase,total=False):
class JsonCollation(JsonBase, total=False):
title: str
e_ids_and_pages: list[list[int]]
sort_order: str
cover_id: int
class JsonEntry(JsonBase,total=False):
class JsonEntry(JsonBase, total=False):
filename: str
path: str
fields: list[dict] #TODO
fields: list[dict] # TODO
class JsonMacro(JsonBase,total=False):
... #TODO
class JsonMacro(JsonBase, total=False): ... # TODO

File diff suppressed because it is too large Load diff

View file

@ -14,230 +14,267 @@ class ColorType(Enum):
_TAG_COLORS = {
'': {ColorType.PRIMARY: '#1E1A33',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#2B2547',
ColorType.LIGHT_ACCENT: '#CDA7F7',
ColorType.DARK_ACCENT: '#1E1A33',
},
'black': {ColorType.PRIMARY: '#111018',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#18171e',
ColorType.LIGHT_ACCENT: '#b7b6be',
ColorType.DARK_ACCENT: '#03020a',
},
'dark gray': {ColorType.PRIMARY: '#24232a',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#2a2930',
ColorType.LIGHT_ACCENT: '#bdbcc4',
ColorType.DARK_ACCENT: '#07060e',
},
'gray': {ColorType.PRIMARY: '#53525a',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#5b5a62',
ColorType.LIGHT_ACCENT: '#cbcad2',
ColorType.DARK_ACCENT: '#191820',
},
'light gray': {ColorType.PRIMARY: '#aaa9b0',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#b6b4bc',
ColorType.LIGHT_ACCENT: '#cbcad2',
ColorType.DARK_ACCENT: '#191820',
},
'white': {ColorType.PRIMARY: '#f2f1f8',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#fefeff',
ColorType.LIGHT_ACCENT: '#ffffff',
ColorType.DARK_ACCENT: '#302f36',
},
'light pink': {ColorType.PRIMARY: '#ff99c4',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#ffaad0',
ColorType.LIGHT_ACCENT: '#ffcbe7',
ColorType.DARK_ACCENT: '#6c2e3b',
},
'pink': {ColorType.PRIMARY: '#ff99c4',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#ffaad0',
ColorType.LIGHT_ACCENT: '#ffcbe7',
ColorType.DARK_ACCENT: '#6c2e3b',
},
'magenta': {ColorType.PRIMARY: '#f6466f',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#f7587f',
ColorType.LIGHT_ACCENT: '#fba4bf',
ColorType.DARK_ACCENT: '#61152f',
},
'red': {ColorType.PRIMARY: '#e22c3c',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#b21f2d',
# ColorType.BORDER: '#e54252',
ColorType.LIGHT_ACCENT: '#f39caa',
ColorType.DARK_ACCENT: '#440d12',
},
'red orange': {ColorType.PRIMARY: '#e83726',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#ea4b3b',
ColorType.LIGHT_ACCENT: '#f5a59d',
ColorType.DARK_ACCENT: '#61120b',
},
'salmon': {ColorType.PRIMARY: '#f65848',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#f76c5f',
ColorType.LIGHT_ACCENT: '#fcadaa',
ColorType.DARK_ACCENT: '#6f1b16',
},
'orange': {ColorType.PRIMARY: '#ed6022',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#ef7038',
ColorType.LIGHT_ACCENT: '#f7b79b',
ColorType.DARK_ACCENT: '#551e0a',
},
'yellow orange': {ColorType.PRIMARY: '#fa9a2c',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#fba94b',
ColorType.LIGHT_ACCENT: '#fdd7ab',
ColorType.DARK_ACCENT: '#66330d',
},
'yellow': {ColorType.PRIMARY: '#ffd63d',
ColorType.TEXT: ColorType.DARK_ACCENT,
# ColorType.BORDER: '#ffe071',
ColorType.BORDER: '#e8af31',
ColorType.LIGHT_ACCENT: '#fff3c4',
ColorType.DARK_ACCENT: '#754312',
},
'mint': {ColorType.PRIMARY: '#4aed90',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#79f2b1',
ColorType.LIGHT_ACCENT: '#c8fbe9',
ColorType.DARK_ACCENT: '#164f3e',
},
'lime': {ColorType.PRIMARY: '#92e649',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#b2ed72',
ColorType.LIGHT_ACCENT: '#e9f9b7',
ColorType.DARK_ACCENT: '#405516',
},
'light green': {ColorType.PRIMARY: '#85ec76',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#a3f198',
ColorType.LIGHT_ACCENT: '#e7fbe4',
ColorType.DARK_ACCENT: '#2b5524',
},
'green': {ColorType.PRIMARY: '#28bb48',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#43c568',
ColorType.LIGHT_ACCENT: '#93e2c8',
ColorType.DARK_ACCENT: '#0d3828',
},
'teal': {ColorType.PRIMARY: '#1ad9b2',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#4de3c7',
ColorType.LIGHT_ACCENT: '#a0f3e8',
ColorType.DARK_ACCENT: '#08424b',
},
'cyan': {ColorType.PRIMARY: '#49e4d5',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#76ebdf',
ColorType.LIGHT_ACCENT: '#bff5f0',
ColorType.DARK_ACCENT: '#0f4246',
},
'light blue': {ColorType.PRIMARY: '#55bbf6',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#70c6f7',
ColorType.LIGHT_ACCENT: '#bbe4fb',
ColorType.DARK_ACCENT: '#122541',
},
'blue': {ColorType.PRIMARY: '#3b87f0',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#4e95f2',
ColorType.LIGHT_ACCENT: '#aedbfa',
ColorType.DARK_ACCENT: '#122948',
},
'blue violet': {ColorType.PRIMARY: '#5948f2',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#6258f3',
ColorType.LIGHT_ACCENT: '#9cb8fb',
ColorType.DARK_ACCENT: '#1b1649',
},
'violet': {ColorType.PRIMARY: '#874ff5',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#9360f6',
ColorType.LIGHT_ACCENT: '#c9b0fa',
ColorType.DARK_ACCENT: '#3a1860',
},
'purple': {ColorType.PRIMARY: '#bb4ff0',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#c364f2',
ColorType.LIGHT_ACCENT: '#dda7f7',
ColorType.DARK_ACCENT: '#531862',
},
'peach': {ColorType.PRIMARY: '#f1c69c',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#f4d4b4',
ColorType.LIGHT_ACCENT: '#fbeee1',
ColorType.DARK_ACCENT: '#613f2f',
},
'brown': {ColorType.PRIMARY: '#823216',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#8a3e22',
ColorType.LIGHT_ACCENT: '#cd9d83',
ColorType.DARK_ACCENT: '#3a1804',
},
'lavender': {ColorType.PRIMARY: '#ad8eef',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#b99ef2',
ColorType.LIGHT_ACCENT: '#d5c7fa',
ColorType.DARK_ACCENT: '#492b65',
},
'blonde': {ColorType.PRIMARY: '#efc664',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#f3d387',
ColorType.LIGHT_ACCENT: '#faebc6',
ColorType.DARK_ACCENT: '#6d461e',
},
'auburn': {ColorType.PRIMARY: '#a13220',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#aa402f',
ColorType.LIGHT_ACCENT: '#d98a7f',
ColorType.DARK_ACCENT: '#3d100a',
},
'light brown': {ColorType.PRIMARY: '#be5b2d',
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: '#c4693d',
ColorType.LIGHT_ACCENT: '#e5b38c',
ColorType.DARK_ACCENT: '#4c290e',
},
'dark brown': {ColorType.PRIMARY: '#4c2315',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#542a1c',
ColorType.LIGHT_ACCENT: '#b78171',
ColorType.DARK_ACCENT: '#211006',
},
'cool gray': {ColorType.PRIMARY: '#515768',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#5b6174',
ColorType.LIGHT_ACCENT: '#9ea1c3',
ColorType.DARK_ACCENT: '#181a37',
},
'warm gray': {ColorType.PRIMARY: '#625550',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#6c5e57',
ColorType.LIGHT_ACCENT: '#c0a392',
ColorType.DARK_ACCENT: '#371d18',
},
'olive': {ColorType.PRIMARY: '#4c652e',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#586f36',
ColorType.LIGHT_ACCENT: '#b4c17a',
ColorType.DARK_ACCENT: '#23300e',
},
'berry': {ColorType.PRIMARY: '#9f2aa7',
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: '#aa43b4',
ColorType.LIGHT_ACCENT: '#cc8fdc',
ColorType.DARK_ACCENT: '#41114a',
},
"": {
ColorType.PRIMARY: "#1E1A33",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#2B2547",
ColorType.LIGHT_ACCENT: "#CDA7F7",
ColorType.DARK_ACCENT: "#1E1A33",
},
"black": {
ColorType.PRIMARY: "#111018",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#18171e",
ColorType.LIGHT_ACCENT: "#b7b6be",
ColorType.DARK_ACCENT: "#03020a",
},
"dark gray": {
ColorType.PRIMARY: "#24232a",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#2a2930",
ColorType.LIGHT_ACCENT: "#bdbcc4",
ColorType.DARK_ACCENT: "#07060e",
},
"gray": {
ColorType.PRIMARY: "#53525a",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#5b5a62",
ColorType.LIGHT_ACCENT: "#cbcad2",
ColorType.DARK_ACCENT: "#191820",
},
"light gray": {
ColorType.PRIMARY: "#aaa9b0",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#b6b4bc",
ColorType.LIGHT_ACCENT: "#cbcad2",
ColorType.DARK_ACCENT: "#191820",
},
"white": {
ColorType.PRIMARY: "#f2f1f8",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#fefeff",
ColorType.LIGHT_ACCENT: "#ffffff",
ColorType.DARK_ACCENT: "#302f36",
},
"light pink": {
ColorType.PRIMARY: "#ff99c4",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#ffaad0",
ColorType.LIGHT_ACCENT: "#ffcbe7",
ColorType.DARK_ACCENT: "#6c2e3b",
},
"pink": {
ColorType.PRIMARY: "#ff99c4",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#ffaad0",
ColorType.LIGHT_ACCENT: "#ffcbe7",
ColorType.DARK_ACCENT: "#6c2e3b",
},
"magenta": {
ColorType.PRIMARY: "#f6466f",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#f7587f",
ColorType.LIGHT_ACCENT: "#fba4bf",
ColorType.DARK_ACCENT: "#61152f",
},
"red": {
ColorType.PRIMARY: "#e22c3c",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#b21f2d",
# ColorType.BORDER: '#e54252',
ColorType.LIGHT_ACCENT: "#f39caa",
ColorType.DARK_ACCENT: "#440d12",
},
"red orange": {
ColorType.PRIMARY: "#e83726",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#ea4b3b",
ColorType.LIGHT_ACCENT: "#f5a59d",
ColorType.DARK_ACCENT: "#61120b",
},
"salmon": {
ColorType.PRIMARY: "#f65848",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#f76c5f",
ColorType.LIGHT_ACCENT: "#fcadaa",
ColorType.DARK_ACCENT: "#6f1b16",
},
"orange": {
ColorType.PRIMARY: "#ed6022",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#ef7038",
ColorType.LIGHT_ACCENT: "#f7b79b",
ColorType.DARK_ACCENT: "#551e0a",
},
"yellow orange": {
ColorType.PRIMARY: "#fa9a2c",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#fba94b",
ColorType.LIGHT_ACCENT: "#fdd7ab",
ColorType.DARK_ACCENT: "#66330d",
},
"yellow": {
ColorType.PRIMARY: "#ffd63d",
ColorType.TEXT: ColorType.DARK_ACCENT,
# ColorType.BORDER: '#ffe071',
ColorType.BORDER: "#e8af31",
ColorType.LIGHT_ACCENT: "#fff3c4",
ColorType.DARK_ACCENT: "#754312",
},
"mint": {
ColorType.PRIMARY: "#4aed90",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#79f2b1",
ColorType.LIGHT_ACCENT: "#c8fbe9",
ColorType.DARK_ACCENT: "#164f3e",
},
"lime": {
ColorType.PRIMARY: "#92e649",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#b2ed72",
ColorType.LIGHT_ACCENT: "#e9f9b7",
ColorType.DARK_ACCENT: "#405516",
},
"light green": {
ColorType.PRIMARY: "#85ec76",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#a3f198",
ColorType.LIGHT_ACCENT: "#e7fbe4",
ColorType.DARK_ACCENT: "#2b5524",
},
"green": {
ColorType.PRIMARY: "#28bb48",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#43c568",
ColorType.LIGHT_ACCENT: "#93e2c8",
ColorType.DARK_ACCENT: "#0d3828",
},
"teal": {
ColorType.PRIMARY: "#1ad9b2",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#4de3c7",
ColorType.LIGHT_ACCENT: "#a0f3e8",
ColorType.DARK_ACCENT: "#08424b",
},
"cyan": {
ColorType.PRIMARY: "#49e4d5",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#76ebdf",
ColorType.LIGHT_ACCENT: "#bff5f0",
ColorType.DARK_ACCENT: "#0f4246",
},
"light blue": {
ColorType.PRIMARY: "#55bbf6",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#70c6f7",
ColorType.LIGHT_ACCENT: "#bbe4fb",
ColorType.DARK_ACCENT: "#122541",
},
"blue": {
ColorType.PRIMARY: "#3b87f0",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#4e95f2",
ColorType.LIGHT_ACCENT: "#aedbfa",
ColorType.DARK_ACCENT: "#122948",
},
"blue violet": {
ColorType.PRIMARY: "#5948f2",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#6258f3",
ColorType.LIGHT_ACCENT: "#9cb8fb",
ColorType.DARK_ACCENT: "#1b1649",
},
"violet": {
ColorType.PRIMARY: "#874ff5",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#9360f6",
ColorType.LIGHT_ACCENT: "#c9b0fa",
ColorType.DARK_ACCENT: "#3a1860",
},
"purple": {
ColorType.PRIMARY: "#bb4ff0",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#c364f2",
ColorType.LIGHT_ACCENT: "#dda7f7",
ColorType.DARK_ACCENT: "#531862",
},
"peach": {
ColorType.PRIMARY: "#f1c69c",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#f4d4b4",
ColorType.LIGHT_ACCENT: "#fbeee1",
ColorType.DARK_ACCENT: "#613f2f",
},
"brown": {
ColorType.PRIMARY: "#823216",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#8a3e22",
ColorType.LIGHT_ACCENT: "#cd9d83",
ColorType.DARK_ACCENT: "#3a1804",
},
"lavender": {
ColorType.PRIMARY: "#ad8eef",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#b99ef2",
ColorType.LIGHT_ACCENT: "#d5c7fa",
ColorType.DARK_ACCENT: "#492b65",
},
"blonde": {
ColorType.PRIMARY: "#efc664",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#f3d387",
ColorType.LIGHT_ACCENT: "#faebc6",
ColorType.DARK_ACCENT: "#6d461e",
},
"auburn": {
ColorType.PRIMARY: "#a13220",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#aa402f",
ColorType.LIGHT_ACCENT: "#d98a7f",
ColorType.DARK_ACCENT: "#3d100a",
},
"light brown": {
ColorType.PRIMARY: "#be5b2d",
ColorType.TEXT: ColorType.DARK_ACCENT,
ColorType.BORDER: "#c4693d",
ColorType.LIGHT_ACCENT: "#e5b38c",
ColorType.DARK_ACCENT: "#4c290e",
},
"dark brown": {
ColorType.PRIMARY: "#4c2315",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#542a1c",
ColorType.LIGHT_ACCENT: "#b78171",
ColorType.DARK_ACCENT: "#211006",
},
"cool gray": {
ColorType.PRIMARY: "#515768",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#5b6174",
ColorType.LIGHT_ACCENT: "#9ea1c3",
ColorType.DARK_ACCENT: "#181a37",
},
"warm gray": {
ColorType.PRIMARY: "#625550",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#6c5e57",
ColorType.LIGHT_ACCENT: "#c0a392",
ColorType.DARK_ACCENT: "#371d18",
},
"olive": {
ColorType.PRIMARY: "#4c652e",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#586f36",
ColorType.LIGHT_ACCENT: "#b4c17a",
ColorType.DARK_ACCENT: "#23300e",
},
"berry": {
ColorType.PRIMARY: "#9f2aa7",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#aa43b4",
ColorType.LIGHT_ACCENT: "#cc8fdc",
ColorType.DARK_ACCENT: "#41114a",
},
}
@ -249,4 +286,4 @@ def get_tag_color(type: ColorType, color: str):
else:
return _TAG_COLORS[color][type]
except KeyError:
return '#FF00FF'
return "#FF00FF"

View file

@ -9,223 +9,336 @@ import os
from src.core.library import Entry, Library
VERSION: str = '9.2.0' # Major.Minor.Patch
VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release
VERSION: str = "9.2.0" # Major.Minor.Patch
VERSION_BRANCH: str = "Alpha" # 'Alpha', 'Beta', or '' for Full Release
# The folder & file names where TagStudio keeps its data relative to a library.
TS_FOLDER_NAME: str = '.TagStudio'
BACKUP_FOLDER_NAME: str = 'backups'
COLLAGE_FOLDER_NAME: str = 'collages'
LIBRARY_FILENAME: str = 'ts_library.json'
TS_FOLDER_NAME: str = ".TagStudio"
BACKUP_FOLDER_NAME: str = "backups"
COLLAGE_FOLDER_NAME: str = "collages"
LIBRARY_FILENAME: str = "ts_library.json"
# TODO: Turn this whitelist into a user-configurable blacklist.
IMAGE_TYPES: list[str] = ['png', 'jpg', 'jpeg', 'jpg_large', 'jpeg_large',
'jfif', 'gif', 'tif', 'tiff', 'heic', 'heif', 'webp',
'bmp', 'svg', 'avif', 'apng', 'jp2', 'j2k', 'jpg2']
VIDEO_TYPES: list[str] = ['mp4', 'webm', 'mov', 'hevc', 'mkv', 'avi', 'wmv',
'flv', 'gifv', 'm4p', 'm4v', '3gp']
AUDIO_TYPES: list[str] = ['mp3', 'mp4', 'mpeg4', 'm4a', 'aac', 'wav', 'flac',
'alac', 'wma', 'ogg', 'aiff']
DOC_TYPES: list[str] = ['txt', 'rtf', 'md',
'doc', 'docx', 'pdf', 'tex', 'odt', 'pages']
PLAINTEXT_TYPES: list[str] = ['txt', 'md', 'css', 'html', 'xml', 'json', 'js',
'ts', 'ini', 'htm', 'csv', 'php', 'sh', 'bat']
SPREADSHEET_TYPES: list[str] = ['csv', 'xls', 'xlsx', 'numbers', 'ods']
PRESENTATION_TYPES: list[str] = ['ppt', 'pptx', 'key', 'odp']
ARCHIVE_TYPES: list[str] = ['zip', 'rar', 'tar', 'tar.gz', 'tgz', '7z']
PROGRAM_TYPES: list[str] = ['exe', 'app']
SHORTCUT_TYPES: list[str] = ['lnk', 'desktop', 'url']
IMAGE_TYPES: list[str] = [
"png",
"jpg",
"jpeg",
"jpg_large",
"jpeg_large",
"jfif",
"gif",
"tif",
"tiff",
"heic",
"heif",
"webp",
"bmp",
"svg",
"avif",
"apng",
"jp2",
"j2k",
"jpg2",
]
VIDEO_TYPES: list[str] = [
"mp4",
"webm",
"mov",
"hevc",
"mkv",
"avi",
"wmv",
"flv",
"gifv",
"m4p",
"m4v",
"3gp",
]
AUDIO_TYPES: list[str] = [
"mp3",
"mp4",
"mpeg4",
"m4a",
"aac",
"wav",
"flac",
"alac",
"wma",
"ogg",
"aiff",
]
DOC_TYPES: list[str] = ["txt", "rtf", "md", "doc", "docx", "pdf", "tex", "odt", "pages"]
PLAINTEXT_TYPES: list[str] = [
"txt",
"md",
"css",
"html",
"xml",
"json",
"js",
"ts",
"ini",
"htm",
"csv",
"php",
"sh",
"bat",
]
SPREADSHEET_TYPES: list[str] = ["csv", "xls", "xlsx", "numbers", "ods"]
PRESENTATION_TYPES: list[str] = ["ppt", "pptx", "key", "odp"]
ARCHIVE_TYPES: list[str] = ["zip", "rar", "tar", "tar.gz", "tgz", "7z"]
PROGRAM_TYPES: list[str] = ["exe", "app"]
SHORTCUT_TYPES: list[str] = ["lnk", "desktop", "url"]
ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES + AUDIO_TYPES + \
DOC_TYPES + SPREADSHEET_TYPES + PRESENTATION_TYPES + \
ARCHIVE_TYPES + PROGRAM_TYPES + SHORTCUT_TYPES
ALL_FILE_TYPES: list[str] = (
IMAGE_TYPES
+ VIDEO_TYPES
+ AUDIO_TYPES
+ DOC_TYPES
+ SPREADSHEET_TYPES
+ PRESENTATION_TYPES
+ ARCHIVE_TYPES
+ PROGRAM_TYPES
+ SHORTCUT_TYPES
)
BOX_FIELDS = ['tag_box', 'text_box']
TEXT_FIELDS = ['text_line', 'text_box']
DATE_FIELDS = ['datetime']
BOX_FIELDS = ["tag_box", "text_box"]
TEXT_FIELDS = ["text_line", "text_box"]
DATE_FIELDS = ["datetime"]
TAG_COLORS = ['', 'black', 'dark gray', 'gray', 'light gray', 'white', 'light pink',
'pink', 'red', 'red orange', 'orange', 'yellow orange', 'yellow',
'lime', 'light green', 'mint', 'green','teal', 'cyan', 'light blue',
'blue', 'blue violet', 'violet', 'purple', 'lavender', 'berry',
'magenta', 'salmon', 'auburn', 'dark brown', 'brown', 'light brown',
'blonde', 'peach', 'warm gray', 'cool gray', 'olive']
TAG_COLORS = [
"",
"black",
"dark gray",
"gray",
"light gray",
"white",
"light pink",
"pink",
"red",
"red orange",
"orange",
"yellow orange",
"yellow",
"lime",
"light green",
"mint",
"green",
"teal",
"cyan",
"light blue",
"blue",
"blue violet",
"violet",
"purple",
"lavender",
"berry",
"magenta",
"salmon",
"auburn",
"dark brown",
"brown",
"light brown",
"blonde",
"peach",
"warm gray",
"cool gray",
"olive",
]
class TagStudioCore:
"""
Instantiate this to establish a TagStudio session.
Holds all TagStudio session data and provides methods to manage it.
"""
"""
Instantiate this to establish a TagStudio session.
Holds all TagStudio session data and provides methods to manage it.
"""
def __init__(self):
self.lib: Library = Library()
def __init__(self):
self.lib: Library = Library()
def get_gdl_sidecar(self, filepath: str, source: str = "") -> dict:
"""
Attempts to open and dump a Gallery-DL Sidecar sidecar file for
the filepath.\n Returns a formatted object with notable values or an
empty object if none is found.
"""
json_dump = {}
info = {}
def get_gdl_sidecar(self, filepath: str, source: str = '') -> dict:
"""
Attempts to open and dump a Gallery-DL Sidecar sidecar file for
the filepath.\n Returns a formatted object with notable values or an
empty object if none is found.
"""
json_dump = {}
info = {}
# NOTE: This fixes an unknown (recent?) bug in Gallery-DL where Instagram sidecar
# files may be downloaded with indices starting at 1 rather than 0, unlike the posts.
# This may only occur with sidecar files that are downloaded separate from posts.
if source == "instagram":
if not os.path.isfile(os.path.normpath(filepath + ".json")):
filepath = filepath[:-16] + "1" + filepath[-15:]
# NOTE: This fixes an unknown (recent?) bug in Gallery-DL where Instagram sidecar
# files may be downloaded with indices starting at 1 rather than 0, unlike the posts.
# This may only occur with sidecar files that are downloaded separate from posts.
if source == 'instagram':
if not os.path.isfile(os.path.normpath(filepath + ".json")):
filepath = filepath[:-16] + '1' + filepath[-15:]
try:
with open(os.path.normpath(filepath + ".json"), "r", encoding="utf8") as f:
json_dump = json.load(f)
try:
with open(os.path.normpath(filepath + ".json"), "r", encoding="utf8") as f:
json_dump = json.load(f)
if json_dump:
if source == "twitter":
info["content"] = json_dump["content"].strip()
info["date_published"] = json_dump["date"]
elif source == "instagram":
info["description"] = json_dump["description"].strip()
info["date_published"] = json_dump["date"]
elif source == "artstation":
info["title"] = json_dump["title"].strip()
info["artist"] = json_dump["user"]["full_name"].strip()
info["description"] = json_dump["description"].strip()
info["tags"] = json_dump["tags"]
# info["tags"] = [x for x in json_dump["mediums"]["name"]]
info["date_published"] = json_dump["date"]
elif source == "newgrounds":
# info["title"] = json_dump["title"]
# info["artist"] = json_dump["artist"]
# info["description"] = json_dump["description"]
info["tags"] = json_dump["tags"]
info["date_published"] = json_dump["date"]
info["artist"] = json_dump["user"].strip()
info["description"] = json_dump["description"].strip()
info["source"] = json_dump["post_url"].strip()
# else:
# print(
# f'[INFO]: TagStudio does not currently support sidecar files for "{source}"')
if json_dump:
if source == "twitter":
info["content"] = json_dump["content"].strip()
info["date_published"] = json_dump["date"]
elif source == "instagram":
info["description"] = json_dump["description"].strip()
info["date_published"] = json_dump["date"]
elif source == "artstation":
info["title"] = json_dump["title"].strip()
info["artist"] = json_dump["user"]["full_name"].strip()
info["description"] = json_dump["description"].strip()
info["tags"] = json_dump["tags"]
# info["tags"] = [x for x in json_dump["mediums"]["name"]]
info["date_published"] = json_dump["date"]
elif source == "newgrounds":
# info["title"] = json_dump["title"]
# info["artist"] = json_dump["artist"]
# info["description"] = json_dump["description"]
info["tags"] = json_dump["tags"]
info["date_published"] = json_dump["date"]
info["artist"] = json_dump["user"].strip()
info["description"] = json_dump["description"].strip()
info["source"] = json_dump["post_url"].strip()
# else:
# print(
# f'[INFO]: TagStudio does not currently support sidecar files for "{source}"')
# except FileNotFoundError:
except:
# print(
# f'[INFO]: No sidecar file found at "{os.path.normpath(file_path + ".json")}"')
pass
# except FileNotFoundError:
except:
# print(
# f'[INFO]: No sidecar file found at "{os.path.normpath(file_path + ".json")}"')
pass
return info
return info
# def scrape(self, entry_id):
# entry = self.lib.get_entry(entry_id)
# if entry.fields:
# urls: list[str] = []
# if self.lib.get_field_index_in_entry(entry, 21):
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
# for x in self.lib.get_field_index_in_entry(entry, 21)])
# if self.lib.get_field_index_in_entry(entry, 3):
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
# for x in self.lib.get_field_index_in_entry(entry, 3)])
# # try:
# if urls:
# for url in urls:
# url = "https://" + url if 'https://' not in url else url
# html_doc = requests.get(url).text
# soup = bs(html_doc, "html.parser")
# print(soup)
# input()
# def scrape(self, entry_id):
# entry = self.lib.get_entry(entry_id)
# if entry.fields:
# urls: list[str] = []
# if self.lib.get_field_index_in_entry(entry, 21):
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
# for x in self.lib.get_field_index_in_entry(entry, 21)])
# if self.lib.get_field_index_in_entry(entry, 3):
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
# for x in self.lib.get_field_index_in_entry(entry, 3)])
# # try:
# if urls:
# for url in urls:
# url = "https://" + url if 'https://' not in url else url
# html_doc = requests.get(url).text
# soup = bs(html_doc, "html.parser")
# print(soup)
# input()
# # except:
# # # print("Could not resolve URL.")
# # pass
# # except:
# # # print("Could not resolve URL.")
# # pass
def match_conditions(self, entry_id: int) -> None:
"""Matches defined conditions against a file to add Entry data."""
def match_conditions(self, entry_id: int) -> None:
"""Matches defined conditions against a file to add Entry data."""
cond_file = os.path.normpath(
f"{self.lib.library_dir}/{TS_FOLDER_NAME}/conditions.json"
)
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
entry: Entry = self.lib.get_entry(entry_id)
try:
if os.path.isfile(cond_file):
with open(cond_file, "r", encoding="utf8") as f:
json_dump = json.load(f)
for c in json_dump["conditions"]:
match: bool = False
for path_c in c["path_conditions"]:
if os.path.normpath(path_c) in entry.path:
match = True
break
if match:
if fields := c.get("fields"):
for field in fields:
field_id = self.lib.get_field_attr(field, "id")
content = field[field_id]
cond_file = os.path.normpath(f'{self.lib.library_dir}/{TS_FOLDER_NAME}/conditions.json')
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
entry: Entry = self.lib.get_entry(entry_id)
try:
if os.path.isfile(cond_file):
with open(cond_file, "r", encoding="utf8") as f:
json_dump = json.load(f)
for c in json_dump['conditions']:
match: bool = False
for path_c in c['path_conditions']:
if os.path.normpath(path_c) in entry.path:
match = True
break
if match:
if fields := c.get('fields'):
for field in fields:
if (
self.lib.get_field_obj(int(field_id))["type"]
== "tag_box"
):
existing_fields: list[int] = (
self.lib.get_field_index_in_entry(
entry, field_id
)
)
if existing_fields:
self.lib.update_entry_field(
entry_id,
existing_fields[0],
content,
"append",
)
else:
self.lib.add_field_to_entry(
entry_id, field_id
)
self.lib.update_entry_field(
entry_id, -1, content, "append"
)
field_id = self.lib.get_field_attr(
field, 'id')
content = field[field_id]
if (
self.lib.get_field_obj(int(field_id))["type"]
in TEXT_FIELDS
):
if not self.lib.does_field_content_exist(
entry_id, field_id, content
):
self.lib.add_field_to_entry(
entry_id, field_id
)
self.lib.update_entry_field(
entry_id, -1, content, "replace"
)
except:
print("Error in match_conditions...")
# input()
pass
if self.lib.get_field_obj(int(field_id))['type'] == 'tag_box':
existing_fields: list[int] = self.lib.get_field_index_in_entry(
entry, field_id)
if existing_fields:
self.lib.update_entry_field(
entry_id, existing_fields[0], content, 'append')
else:
self.lib.add_field_to_entry(
entry_id, field_id)
self.lib.update_entry_field(
entry_id, -1, content, 'append')
def build_url(self, entry_id: int, source: str) -> str:
"""Tries to rebuild a source URL given a specific filename structure."""
if self.lib.get_field_obj(int(field_id))['type'] in TEXT_FIELDS:
if not self.lib.does_field_content_exist(entry_id, field_id, content):
self.lib.add_field_to_entry(
entry_id, field_id)
self.lib.update_entry_field(
entry_id, -1, content, 'replace')
except:
print('Error in match_conditions...')
# input()
pass
source = source.lower().replace("-", " ").replace("_", " ")
if "twitter" in source:
return self._build_twitter_url(entry_id)
elif "instagram" in source:
return self._build_instagram_url(entry_id)
def build_url(self, entry_id: int, source: str) -> str:
"""Tries to rebuild a source URL given a specific filename structure."""
def _build_twitter_url(self, entry_id: int):
"""
Builds an Twitter URL given a specific filename structure.
Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD'
"""
try:
entry = self.lib.get_entry(entry_id)
stubs = entry.filename.rsplit("_", 3)
# print(stubs)
# source, author = os.path.split(entry.path)
url = f"www.twitter.com/{stubs[0]}/status/{stubs[-3]}/photo/{stubs[-2]}"
return url
except:
return ""
source = source.lower().replace('-', ' ').replace('_', ' ')
if 'twitter' in source:
return self._build_twitter_url(entry_id)
elif 'instagram' in source:
return self._build_instagram_url(entry_id)
def _build_twitter_url(self, entry_id: int):
"""
Builds an Twitter URL given a specific filename structure.
Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD'
"""
try:
entry = self.lib.get_entry(entry_id)
stubs = entry.filename.rsplit('_', 3)
# print(stubs)
# source, author = os.path.split(entry.path)
url = f"www.twitter.com/{stubs[0]}/status/{stubs[-3]}/photo/{stubs[-2]}"
return url
except:
return ''
def _build_instagram_url(self, entry_id: int):
"""
Builds an Instagram URL given a specific filename structure.
Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD'
"""
try:
entry = self.lib.get_entry(entry_id)
stubs = entry.filename.rsplit('_', 2)
# stubs[0] = stubs[0].replace(f"{author}_", '', 1)
# print(stubs)
# NOTE: Both Instagram usernames AND their ID can have underscores in them,
# so unless you have the exact username (which can change) on hand to remove,
# your other best bet is to hope that the ID is only 11 characters long, which
# seems to more or less be the case... for now...
url = f"www.instagram.com/p/{stubs[-3][-11:]}"
return url
except:
return ''
def _build_instagram_url(self, entry_id: int):
"""
Builds an Instagram URL given a specific filename structure.
Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD'
"""
try:
entry = self.lib.get_entry(entry_id)
stubs = entry.filename.rsplit("_", 2)
# stubs[0] = stubs[0].replace(f"{author}_", '', 1)
# print(stubs)
# NOTE: Both Instagram usernames AND their ID can have underscores in them,
# so unless you have the exact username (which can change) on hand to remove,
# your other best bet is to hope that the ID is only 11 characters long, which
# seems to more or less be the case... for now...
url = f"www.instagram.com/p/{stubs[-3][-11:]}"
return url
except:
return ""

View file

@ -2,9 +2,10 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
def clean_folder_name(folder_name: str) -> str:
cleaned_name = folder_name
invalid_chars = "<>:\"/\\|?*."
invalid_chars = '<>:"/\\|?*.'
for char in invalid_chars:
cleaned_name = cleaned_name.replace(char, '_')
cleaned_name = cleaned_name.replace(char, "_")
return cleaned_name

View file

@ -2,10 +2,25 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
def strip_punctuation(string: str) -> str:
"""Returns a given string stripped of all punctuation characters."""
return string.replace('(', '').replace(')', '').replace('[', '') \
.replace(']', '').replace('{', '').replace('}', '').replace("'", '') \
.replace('`', '').replace('', '').replace('', '').replace('"', '') \
.replace('', '').replace('', '').replace('_', '').replace('-', '') \
.replace(' ', '').replace(' ', '')
return (
string.replace("(", "")
.replace(")", "")
.replace("[", "")
.replace("]", "")
.replace("{", "")
.replace("}", "")
.replace("'", "")
.replace("`", "")
.replace("", "")
.replace("", "")
.replace('"', "")
.replace("", "")
.replace("", "")
.replace("_", "")
.replace("-", "")
.replace(" ", "")
.replace(" ", "")
)

View file

@ -2,9 +2,10 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
def strip_web_protocol(string: str) -> str:
"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
prefixes = ['https://','http://','www.','www2.']
prefixes = ["https://", "http://", "www.", "www2."]
for prefix in prefixes:
string = string.removeprefix(prefix)
return string

View file

@ -21,143 +21,153 @@ from PySide6.QtWidgets import QLayout, QSizePolicy, QWidget
# self.setWindowTitle("Flow Layout")
class FlowWidget(QWidget):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ignore_size: bool = False
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ignore_size: bool = False
class FlowLayout(QLayout):
def __init__(self, parent=None):
super().__init__(parent)
def __init__(self, parent=None):
super().__init__(parent)
if parent is not None:
self.setContentsMargins(QMargins(0, 0, 0, 0))
if parent is not None:
self.setContentsMargins(QMargins(0, 0, 0, 0))
self._item_list = []
self.grid_efficiency = False
self._item_list = []
self.grid_efficiency = False
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def addItem(self, item):
self._item_list.append(item)
def addItem(self, item):
self._item_list.append(item)
def count(self):
return len(self._item_list)
def count(self):
return len(self._item_list)
def itemAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list[index]
def itemAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
return None
def takeAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
def takeAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
return None
return None
def expandingDirections(self):
return Qt.Orientation(0)
def expandingDirections(self):
return Qt.Orientation(0)
def hasHeightForWidth(self):
return True
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
height = self._do_layout(QRect(0, 0, width, 0), True)
return height
def heightForWidth(self, width):
height = self._do_layout(QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self._do_layout(rect, False)
def setGridEfficiency(self, bool):
"""
Enables or Disables efficiencies when all objects are equally sized.
"""
self.grid_efficiency = bool
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self._do_layout(rect, False)
def sizeHint(self):
return self.minimumSize()
def setGridEfficiency(self, bool):
"""
Enables or Disables efficiencies when all objects are equally sized.
"""
self.grid_efficiency = bool
def minimumSize(self):
if self.grid_efficiency:
if self._item_list:
return self._item_list[0].minimumSize()
else:
return QSize()
else:
size = QSize()
def sizeHint(self):
return self.minimumSize()
for item in self._item_list:
size = size.expandedTo(item.minimumSize())
def minimumSize(self):
if self.grid_efficiency:
if self._item_list:
return self._item_list[0].minimumSize()
else:
return QSize()
else:
size = QSize()
size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
return size
for item in self._item_list:
size = size.expandedTo(item.minimumSize())
def _do_layout(self, rect, test_only):
x = rect.x()
y = rect.y()
line_height = 0
spacing = self.spacing()
item = None
style = None
layout_spacing_x = None
layout_spacing_y = None
size += QSize(
2 * self.contentsMargins().top(), 2 * self.contentsMargins().top()
)
return size
if self.grid_efficiency:
if self._item_list:
item = self._item_list[0]
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
)
layout_spacing_y = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
)
for i, item in enumerate(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):
skip_count += 1
def _do_layout(self, rect, test_only):
x = rect.x()
y = rect.y()
line_height = 0
spacing = self.spacing()
item = None
style = None
layout_spacing_x = None
layout_spacing_y = None
if (issubclass(type(item.widget()), FlowWidget) and not item.widget().ignore_size) or (not issubclass(type(item.widget()), FlowWidget)):
# print(f'Item {i}')
if not self.grid_efficiency:
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
)
layout_spacing_y = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
)
space_x = spacing + layout_spacing_x
space_y = spacing + layout_spacing_y
next_x = x + item.sizeHint().width() + space_x
if next_x - space_x > rect.right() and line_height > 0:
x = rect.x()
y = y + line_height + space_y
next_x = x + item.sizeHint().width() + space_x
line_height = 0
if self.grid_efficiency:
if self._item_list:
item = self._item_list[0]
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
)
layout_spacing_y = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
)
for i, item in enumerate(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
):
skip_count += 1
if not test_only:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
if (
issubclass(type(item.widget()), FlowWidget)
and not item.widget().ignore_size
) or (not issubclass(type(item.widget()), FlowWidget)):
# print(f'Item {i}')
if not self.grid_efficiency:
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
)
layout_spacing_y = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
)
space_x = spacing + layout_spacing_x
space_y = spacing + layout_spacing_y
next_x = x + item.sizeHint().width() + space_x
if next_x - space_x > rect.right() and line_height > 0:
x = rect.x()
y = y + line_height + space_y
next_x = x + item.sizeHint().width() + space_x
line_height = 0
x = next_x
line_height = max(line_height, item.sizeHint().height())
if not test_only:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
# 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))
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())
# sys.exit(app.exec())

View file

@ -16,4 +16,4 @@ class CustomRunnable(QRunnable, QObject):
def run(self):
self.function()
self.done.emit()
self.done.emit()

View file

@ -12,121 +12,139 @@ import traceback
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
def open_file(path: str, file_manager: bool = False):
"""Open a file in the default application or file explorer.
"""Open a file in the default application or file explorer.
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.
"""
logging.info(f'Opening file: {path}')
if not os.path.exists(path):
logging.error(f'File not found: {path}')
return
try:
if sys.platform == "win32":
normpath = os.path.normpath(path)
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
subprocess.Popen(command_name + command_args, shell=True, close_fds=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_BREAKAWAY_FROM_JOB)
else:
command_name = "start"
# first parameter is for title, NOT filepath
command_args = ["", normpath]
subprocess.Popen([command_name] + command_args, shell=True, close_fds=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_BREAKAWAY_FROM_JOB)
else:
if sys.platform == "darwin":
command_name = "open"
command_args = [path]
if file_manager:
# will reveal in Finder
command_args.append("-R")
else:
if file_manager:
command_name = "dbus-send"
# might not be guaranteed to launch default?
command_args = ["--session", "--dest=org.freedesktop.FileManager1", "--type=method_call",
"/org/freedesktop/FileManager1", "org.freedesktop.FileManager1.ShowItems",
f"array:string:file://{path}", "string:"]
else:
command_name = "xdg-open"
command_args = [path]
command = shutil.which(command_name)
if command is not None:
subprocess.Popen([command] + command_args, close_fds=True)
else:
logging.info(f"Could not find {command_name} on system PATH")
except:
traceback.print_exc()
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.
"""
logging.info(f"Opening file: {path}")
if not os.path.exists(path):
logging.error(f"File not found: {path}")
return
try:
if sys.platform == "win32":
normpath = os.path.normpath(path)
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
subprocess.Popen(
command_name + command_args,
shell=True,
close_fds=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
)
else:
command_name = "start"
# first parameter is for title, NOT filepath
command_args = ["", normpath]
subprocess.Popen(
[command_name] + command_args,
shell=True,
close_fds=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
)
else:
if sys.platform == "darwin":
command_name = "open"
command_args = [path]
if file_manager:
# will reveal in Finder
command_args.append("-R")
else:
if file_manager:
command_name = "dbus-send"
# might not be guaranteed to launch default?
command_args = [
"--session",
"--dest=org.freedesktop.FileManager1",
"--type=method_call",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
f"array:string:file://{path}",
"string:",
]
else:
command_name = "xdg-open"
command_args = [path]
command = shutil.which(command_name)
if command is not None:
subprocess.Popen([command] + command_args, close_fds=True)
else:
logging.info(f"Could not find {command_name} on system PATH")
except:
traceback.print_exc()
class FileOpenerHelper:
def __init__(self, filepath: str):
"""Initialize the FileOpenerHelper.
def __init__(self, filepath: str):
"""Initialize the FileOpenerHelper.
Args:
filepath (str): The path to the file to open.
"""
self.filepath = filepath
Args:
filepath (str): The path to the file to open.
"""
self.filepath = filepath
def set_filepath(self, filepath: str):
"""Set the filepath to open.
def set_filepath(self, filepath: str):
"""Set the filepath to open.
Args:
filepath (str): The path to the file to open.
"""
self.filepath = filepath
Args:
filepath (str): The path to the file to open.
"""
self.filepath = filepath
def open_file(self):
"""Open the file in the default application."""
open_file(self.filepath)
def open_file(self):
"""Open the file in the default application."""
open_file(self.filepath)
def open_explorer(self):
"""Open the file in the default file explorer."""
open_file(self.filepath, file_manager=True)
def open_explorer(self):
"""Open the file in the default file explorer."""
open_file(self.filepath, file_manager=True)
class FileOpenerLabel(QLabel):
def __init__(self, text, parent=None):
"""Initialize the FileOpenerLabel.
def __init__(self, text, parent=None):
"""Initialize the FileOpenerLabel.
Args:
text (str): The text to display.
parent (QWidget, optional): The parent widget. Defaults to None.
"""
super().__init__(text, parent)
Args:
text (str): The text to display.
parent (QWidget, optional): The parent widget. Defaults to None.
"""
super().__init__(text, parent)
def setFilePath(self, filepath):
"""Set the filepath to open.
def setFilePath(self, filepath):
"""Set the filepath to open.
Args:
filepath (str): The path to the file to open.
"""
self.filepath = filepath
Args:
filepath (str): The path to the file to open.
"""
self.filepath = filepath
def mousePressEvent(self, event):
"""Handle mouse press events.
def mousePressEvent(self, event):
"""Handle mouse press events.
On a left click, open the file in the default file explorer. On a right click, show a context menu.
Args:
event (QMouseEvent): The mouse press event.
"""
super().mousePressEvent(event)
On a left click, open the file in the default file explorer. On a right click, show a context menu.
if event.button() == Qt.LeftButton:
opener = FileOpenerHelper(self.filepath)
opener.open_explorer()
elif event.button() == Qt.RightButton:
# Show context menu
pass
Args:
event (QMouseEvent): The mouse press event.
"""
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
opener = FileOpenerHelper(self.filepath)
opener.open_explorer()
elif event.button() == Qt.RightButton:
# Show context menu
pass

View file

@ -10,6 +10,7 @@ from PySide6.QtCore import Signal, QObject
class FunctionIterator(QObject):
"""Iterates over a yielding function and emits progress as the 'value' signal.\n\nThread-Safe Guarantee™"""
value = Signal(object)
def __init__(self, function: FunctionType):

View file

@ -8,4 +8,4 @@ from .relink_unlinked import RelinkUnlinkedEntries
from .fix_unlinked import FixUnlinkedEntriesModal
from .mirror_entities import MirrorEntriesModal
from .fix_dupes import FixDupeFilesModal
from .folders_to_tags import FoldersToTagsModal
from .folders_to_tags import FoldersToTagsModal

View file

@ -4,75 +4,87 @@
from PySide6.QtCore import Signal, Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QComboBox,
)
from src.core.library import Library
class AddFieldModal(QWidget):
done = Signal(int)
def __init__(self, library:'Library'):
# [Done]
# - OR -
# [Cancel] [Save]
super().__init__()
self.lib = library
self.setWindowTitle(f'Add Field')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
done = Signal(int)
self.title_widget = QLabel()
self.title_widget.setObjectName('fieldTitle')
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
'font-weight:bold;'
'font-size:14px;'
'padding-top: 6px'
'')
self.title_widget.setText('Add Field')
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.combo_box = QComboBox()
self.combo_box.setEditable(False)
# self.combo_box.setMaxVisibleItems(5)
self.combo_box.setStyleSheet('combobox-popup:0;')
self.combo_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
for df in self.lib.default_fields:
self.combo_box.addItem(f'{df["name"]} ({df["type"].replace("_", " ").title()})')
def __init__(self, library: "Library"):
# [Done]
# - OR -
# [Cancel] [Save]
super().__init__()
self.lib = library
self.setWindowTitle(f"Add Field")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6,6,6,6)
self.button_layout.addStretch(1)
self.title_widget = QLabel()
self.title_widget.setObjectName("fieldTitle")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
"font-weight:bold;" "font-size:14px;" "padding-top: 6px" ""
)
self.title_widget.setText("Add Field")
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.cancel_button = QPushButton()
# self.cancel_button.setText('Cancel')
self.combo_box = QComboBox()
self.combo_box.setEditable(False)
# self.combo_box.setMaxVisibleItems(5)
self.combo_box.setStyleSheet("combobox-popup:0;")
self.combo_box.view().setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
for df in self.lib.default_fields:
self.combo_box.addItem(
f'{df["name"]} ({df["type"].replace("_", " ").title()})'
)
self.cancel_button = QPushButton()
self.cancel_button.setText('Cancel')
self.cancel_button.clicked.connect(self.hide)
# self.cancel_button.clicked.connect(widget.reset)
self.button_layout.addWidget(self.cancel_button)
self.save_button = QPushButton()
self.save_button.setText('Add')
# self.save_button.setAutoDefault(True)
self.save_button.setDefault(True)
self.save_button.clicked.connect(self.hide)
self.save_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.save_button)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.done.connect(lambda x: callback(x))
self.root_layout.addWidget(self.title_widget)
self.root_layout.addWidget(self.combo_box)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)
# self.cancel_button = QPushButton()
# self.cancel_button.setText('Cancel')
self.cancel_button = QPushButton()
self.cancel_button.setText("Cancel")
self.cancel_button.clicked.connect(self.hide)
# self.cancel_button.clicked.connect(widget.reset)
self.button_layout.addWidget(self.cancel_button)
self.save_button = QPushButton()
self.save_button.setText("Add")
# self.save_button.setAutoDefault(True)
self.save_button.setDefault(True)
self.save_button.clicked.connect(self.hide)
self.save_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.save_button)
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.done.connect(lambda x: callback(x))
self.root_layout.addWidget(self.title_widget)
self.root_layout.addWidget(self.combo_box)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)

View file

@ -6,7 +6,17 @@
import logging
from PySide6.QtCore import Signal, Qt
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QScrollArea, QFrame, QTextEdit, QComboBox)
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QLabel,
QPushButton,
QLineEdit,
QScrollArea,
QFrame,
QTextEdit,
QComboBox,
)
from src.core.library import Library, Tag
from src.core.palette import ColorType, get_tag_color
@ -15,216 +25,220 @@ from src.qt.widgets import PanelWidget, PanelModal, TagWidget
from src.qt.modals import TagSearchPanel
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class BuildTagPanel(PanelWidget):
on_edit = Signal(Tag)
def __init__(self, library, tag_id:int=-1):
super().__init__()
self.lib: Library = library
# self.callback = callback
# self.tag_id = tag_id
self.tag = None
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,0,6,0)
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
on_edit = Signal(Tag)
# Name -----------------------------------------------------------------
self.name_widget = QWidget()
self.name_layout = QVBoxLayout(self.name_widget)
self.name_layout.setStretch(1, 1)
self.name_layout.setContentsMargins(0,0,0,0)
self.name_layout.setSpacing(0)
self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.name_title = QLabel()
self.name_title.setText('Name')
self.name_layout.addWidget(self.name_title)
self.name_field = QLineEdit()
self.name_layout.addWidget(self.name_field)
def __init__(self, library, tag_id: int = -1):
super().__init__()
self.lib: Library = library
# self.callback = callback
# self.tag_id = tag_id
self.tag = None
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Shorthand ------------------------------------------------------------
self.shorthand_widget = QWidget()
self.shorthand_layout = QVBoxLayout(self.shorthand_widget)
self.shorthand_layout.setStretch(1, 1)
self.shorthand_layout.setContentsMargins(0,0,0,0)
self.shorthand_layout.setSpacing(0)
self.shorthand_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.shorthand_title = QLabel()
self.shorthand_title.setText('Shorthand')
self.shorthand_layout.addWidget(self.shorthand_title)
self.shorthand_field = QLineEdit()
self.shorthand_layout.addWidget(self.shorthand_field)
# Name -----------------------------------------------------------------
self.name_widget = QWidget()
self.name_layout = QVBoxLayout(self.name_widget)
self.name_layout.setStretch(1, 1)
self.name_layout.setContentsMargins(0, 0, 0, 0)
self.name_layout.setSpacing(0)
self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.name_title = QLabel()
self.name_title.setText("Name")
self.name_layout.addWidget(self.name_title)
self.name_field = QLineEdit()
self.name_layout.addWidget(self.name_field)
# Aliases --------------------------------------------------------------
self.aliases_widget = QWidget()
self.aliases_layout = QVBoxLayout(self.aliases_widget)
self.aliases_layout.setStretch(1, 1)
self.aliases_layout.setContentsMargins(0,0,0,0)
self.aliases_layout.setSpacing(0)
self.aliases_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.aliases_title = QLabel()
self.aliases_title.setText('Aliases')
self.aliases_layout.addWidget(self.aliases_title)
self.aliases_field = QTextEdit()
self.aliases_field.setAcceptRichText(False)
self.aliases_field.setMinimumHeight(40)
self.aliases_layout.addWidget(self.aliases_field)
# Shorthand ------------------------------------------------------------
self.shorthand_widget = QWidget()
self.shorthand_layout = QVBoxLayout(self.shorthand_widget)
self.shorthand_layout.setStretch(1, 1)
self.shorthand_layout.setContentsMargins(0, 0, 0, 0)
self.shorthand_layout.setSpacing(0)
self.shorthand_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.shorthand_title = QLabel()
self.shorthand_title.setText("Shorthand")
self.shorthand_layout.addWidget(self.shorthand_title)
self.shorthand_field = QLineEdit()
self.shorthand_layout.addWidget(self.shorthand_field)
# Subtags ------------------------------------------------------------
self.subtags_widget = QWidget()
self.subtags_layout = QVBoxLayout(self.subtags_widget)
self.subtags_layout.setStretch(1, 1)
self.subtags_layout.setContentsMargins(0,0,0,0)
self.subtags_layout.setSpacing(0)
self.subtags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.subtags_title = QLabel()
self.subtags_title.setText('Subtags')
self.subtags_layout.addWidget(self.subtags_title)
# Aliases --------------------------------------------------------------
self.aliases_widget = QWidget()
self.aliases_layout = QVBoxLayout(self.aliases_widget)
self.aliases_layout.setStretch(1, 1)
self.aliases_layout.setContentsMargins(0, 0, 0, 0)
self.aliases_layout.setSpacing(0)
self.aliases_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.aliases_title = QLabel()
self.aliases_title.setText("Aliases")
self.aliases_layout.addWidget(self.aliases_title)
self.aliases_field = QTextEdit()
self.aliases_field.setAcceptRichText(False)
self.aliases_field.setMinimumHeight(40)
self.aliases_layout.addWidget(self.aliases_field)
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)
# Subtags ------------------------------------------------------------
self.subtags_widget = QWidget()
self.subtags_layout = QVBoxLayout(self.subtags_widget)
self.subtags_layout.setStretch(1, 1)
self.subtags_layout.setContentsMargins(0, 0, 0, 0)
self.subtags_layout.setSpacing(0)
self.subtags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.subtags_title = QLabel()
self.subtags_title.setText("Subtags")
self.subtags_layout.addWidget(self.subtags_title)
self.scroll_area = QScrollArea()
# 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)
self.scroll_area.setWidget(self.scroll_contents)
# self.scroll_area.setMinimumHeight(60)
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.subtags_layout.addWidget(self.scroll_area)
self.scroll_area = QScrollArea()
# 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)
self.scroll_area.setWidget(self.scroll_contents)
# self.scroll_area.setMinimumHeight(60)
self.subtags_add_button = QPushButton()
self.subtags_add_button.setText('+')
tsp = TagSearchPanel(self.lib)
tsp.tag_chosen.connect(lambda x: self.add_subtag_callback(x))
self.add_tag_modal = PanelModal(tsp, 'Add Subtags', 'Add Subtags')
self.subtags_add_button.clicked.connect(self.add_tag_modal.show)
self.subtags_layout.addWidget(self.subtags_add_button)
self.subtags_layout.addWidget(self.scroll_area)
# self.subtags_field = TagBoxWidget()
# self.subtags_field.setMinimumHeight(60)
# self.subtags_layout.addWidget(self.subtags_field)
self.subtags_add_button = QPushButton()
self.subtags_add_button.setText("+")
tsp = TagSearchPanel(self.lib)
tsp.tag_chosen.connect(lambda x: self.add_subtag_callback(x))
self.add_tag_modal = PanelModal(tsp, "Add Subtags", "Add Subtags")
self.subtags_add_button.clicked.connect(self.add_tag_modal.show)
self.subtags_layout.addWidget(self.subtags_add_button)
# Shorthand ------------------------------------------------------------
self.color_widget = QWidget()
self.color_layout = QVBoxLayout(self.color_widget)
self.color_layout.setStretch(1, 1)
self.color_layout.setContentsMargins(0,0,0,0)
self.color_layout.setSpacing(0)
self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.color_title = QLabel()
self.color_title.setText('Color')
self.color_layout.addWidget(self.color_title)
self.color_field = QComboBox()
self.color_field.setEditable(False)
self.color_field.setMaxVisibleItems(10)
self.color_field.setStyleSheet('combobox-popup:0;')
for color in TAG_COLORS:
self.color_field.addItem(color.title())
# self.color_field.setProperty("appearance", "flat")
self.color_field.currentTextChanged.connect(lambda c: self.color_field.setStyleSheet(f'''combobox-popup:0;
# self.subtags_field = TagBoxWidget()
# self.subtags_field.setMinimumHeight(60)
# self.subtags_layout.addWidget(self.subtags_field)
# Shorthand ------------------------------------------------------------
self.color_widget = QWidget()
self.color_layout = QVBoxLayout(self.color_widget)
self.color_layout.setStretch(1, 1)
self.color_layout.setContentsMargins(0, 0, 0, 0)
self.color_layout.setSpacing(0)
self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.color_title = QLabel()
self.color_title.setText("Color")
self.color_layout.addWidget(self.color_title)
self.color_field = QComboBox()
self.color_field.setEditable(False)
self.color_field.setMaxVisibleItems(10)
self.color_field.setStyleSheet("combobox-popup:0;")
for color in TAG_COLORS:
self.color_field.addItem(color.title())
# self.color_field.setProperty("appearance", "flat")
self.color_field.currentTextChanged.connect(
lambda c: self.color_field.setStyleSheet(f"""combobox-popup:0;
font-weight:600;
color:{get_tag_color(ColorType.TEXT, c.lower())};
background-color:{get_tag_color(ColorType.PRIMARY, c.lower())};
'''))
self.color_layout.addWidget(self.color_field)
""")
)
self.color_layout.addWidget(self.color_field)
# Add Widgets to Layout ================================================
self.root_layout.addWidget(self.name_widget)
self.root_layout.addWidget(self.shorthand_widget)
self.root_layout.addWidget(self.aliases_widget)
self.root_layout.addWidget(self.subtags_widget)
self.root_layout.addWidget(self.color_widget)
# self.parent().done.connect(self.update_tag)
# Add Widgets to Layout ================================================
self.root_layout.addWidget(self.name_widget)
self.root_layout.addWidget(self.shorthand_widget)
self.root_layout.addWidget(self.aliases_widget)
self.root_layout.addWidget(self.subtags_widget)
self.root_layout.addWidget(self.color_widget)
# self.parent().done.connect(self.update_tag)
if tag_id >= 0:
self.tag = self.lib.get_tag(tag_id)
else:
self.tag = Tag(-1, "New Tag", "", [], [], "")
self.set_tag(self.tag)
if tag_id >= 0:
self.tag = self.lib.get_tag(tag_id)
else:
self.tag = Tag(-1, 'New Tag', '', [], [], '')
self.set_tag(self.tag)
def add_subtag_callback(self, tag_id:int):
logging.info(f'adding {tag_id}')
# tag = self.lib.get_tag(self.tag_id)
# TODO: Create a single way to update tags and refresh library data
# new = self.build_tag()
self.tag.add_subtag(tag_id)
# self.tag = new
# self.lib.update_tag(new)
self.set_subtags()
# self.on_edit.emit(self.build_tag())
def remove_subtag_callback(self, tag_id:int):
logging.info(f'removing {tag_id}')
# tag = self.lib.get_tag(self.tag_id)
# TODO: Create a single way to update tags and refresh library data
# new = self.build_tag()
self.tag.remove_subtag(tag_id)
# self.tag = new
# self.lib.update_tag(new)
self.set_subtags()
# self.on_edit.emit(self.build_tag())
def set_subtags(self):
while self.scroll_layout.itemAt(0):
self.scroll_layout.takeAt(0).widget().deleteLater()
logging.info(f'Setting {self.tag.subtag_ids}')
c = QWidget()
l = QVBoxLayout(c)
l.setContentsMargins(0,0,0,0)
l.setSpacing(3)
for tag_id in self.tag.subtag_ids:
tw = TagWidget(self.lib, self.lib.get_tag(tag_id), False, True)
tw.on_remove.connect(lambda checked=False, t=tag_id: self.remove_subtag_callback(t))
l.addWidget(tw)
self.scroll_layout.addWidget(c)
def add_subtag_callback(self, tag_id: int):
logging.info(f"adding {tag_id}")
# tag = self.lib.get_tag(self.tag_id)
# TODO: Create a single way to update tags and refresh library data
# new = self.build_tag()
self.tag.add_subtag(tag_id)
# self.tag = new
# self.lib.update_tag(new)
self.set_subtags()
# self.on_edit.emit(self.build_tag())
def set_tag(self, tag:Tag):
# tag = self.lib.get_tag(tag_id)
self.name_field.setText(tag.name)
self.shorthand_field.setText(tag.shorthand)
self.aliases_field.setText('\n'.join(tag.aliases))
self.set_subtags()
self.color_field.setCurrentIndex(TAG_COLORS.index(tag.color.lower()))
# self.tag_id = tag.id
def build_tag(self) -> Tag:
# tag: Tag = self.tag
# if self.tag_id >= 0:
# tag = self.lib.get_tag(self.tag_id)
# else:
# tag = Tag(-1, '', '', [], [], '')
new_tag: Tag = Tag(
id=self.tag.id,
name=self.name_field.text(),
shorthand=self.shorthand_field.text(),
aliases=self.aliases_field.toPlainText().split('\n'),
subtags_ids=self.tag.subtag_ids,
color=self.color_field.currentText().lower())
logging.info(f'built {new_tag}')
return new_tag
# NOTE: The callback and signal do the same thing, I'm currently
# transitioning from using callbacks to the Qt method of using signals.
# self.tag_updated.emit(new_tag)
# self.callback(new_tag)
# def on_return(self, callback, text:str):
# if text and self.first_tag_id >= 0:
# callback(self.first_tag_id)
# self.search_field.setText('')
# self.update_tags('')
# else:
# self.search_field.setFocus()
# self.parentWidget().hide()
def remove_subtag_callback(self, tag_id: int):
logging.info(f"removing {tag_id}")
# tag = self.lib.get_tag(self.tag_id)
# TODO: Create a single way to update tags and refresh library data
# new = self.build_tag()
self.tag.remove_subtag(tag_id)
# self.tag = new
# self.lib.update_tag(new)
self.set_subtags()
# self.on_edit.emit(self.build_tag())
def set_subtags(self):
while self.scroll_layout.itemAt(0):
self.scroll_layout.takeAt(0).widget().deleteLater()
logging.info(f"Setting {self.tag.subtag_ids}")
c = QWidget()
l = QVBoxLayout(c)
l.setContentsMargins(0, 0, 0, 0)
l.setSpacing(3)
for tag_id in self.tag.subtag_ids:
tw = TagWidget(self.lib, self.lib.get_tag(tag_id), False, True)
tw.on_remove.connect(
lambda checked=False, t=tag_id: self.remove_subtag_callback(t)
)
l.addWidget(tw)
self.scroll_layout.addWidget(c)
def set_tag(self, tag: Tag):
# tag = self.lib.get_tag(tag_id)
self.name_field.setText(tag.name)
self.shorthand_field.setText(tag.shorthand)
self.aliases_field.setText("\n".join(tag.aliases))
self.set_subtags()
self.color_field.setCurrentIndex(TAG_COLORS.index(tag.color.lower()))
# self.tag_id = tag.id
def build_tag(self) -> Tag:
# tag: Tag = self.tag
# if self.tag_id >= 0:
# tag = self.lib.get_tag(self.tag_id)
# else:
# tag = Tag(-1, '', '', [], [], '')
new_tag: Tag = Tag(
id=self.tag.id,
name=self.name_field.text(),
shorthand=self.shorthand_field.text(),
aliases=self.aliases_field.toPlainText().split("\n"),
subtags_ids=self.tag.subtag_ids,
color=self.color_field.currentText().lower(),
)
logging.info(f"built {new_tag}")
return new_tag
# NOTE: The callback and signal do the same thing, I'm currently
# transitioning from using callbacks to the Qt method of using signals.
# self.tag_updated.emit(new_tag)
# self.callback(new_tag)
# def on_return(self, callback, text:str):
# if text and self.first_tag_id >= 0:
# callback(self.first_tag_id)
# self.search_field.setText('')
# self.update_tags('')
# else:
# self.search_field.setFocus()
# self.parentWidget().hide()

View file

@ -6,7 +6,14 @@ import typing
from PySide6.QtCore import Signal, Qt, QThreadPool
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListView
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QListView,
)
from src.core.library import ItemType, Library
from src.qt.helpers import CustomRunnable, FunctionIterator
@ -14,115 +21,120 @@ from src.qt.widgets import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
class DeleteUnlinkedEntriesModal(QWidget):
done = Signal()
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver = driver
self.setWindowTitle(f'Delete Unlinked Entries')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
done = Signal()
self.desc_widget = QLabel()
self.desc_widget.setObjectName('descriptionLabel')
self.desc_widget.setWordWrap(True)
self.desc_widget.setText(f'''
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
self.setWindowTitle(f"Delete Unlinked Entries")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.desc_widget = QLabel()
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setText(f"""
Are you sure you want to delete the following {len(self.lib.missing_files)} entries?
''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
""")
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6,6,6,6)
self.button_layout.addStretch(1)
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
self.cancel_button = QPushButton()
self.cancel_button.setText('&Cancel')
self.cancel_button.setDefault(True)
self.cancel_button.clicked.connect(self.hide)
self.button_layout.addWidget(self.cancel_button)
self.delete_button = QPushButton()
self.delete_button.setText('&Delete')
self.delete_button.clicked.connect(self.hide)
self.delete_button.clicked.connect(lambda: self.delete_entries())
self.button_layout.addWidget(self.delete_button)
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
def refresh_list(self):
self.desc_widget.setText(f'''
self.cancel_button = QPushButton()
self.cancel_button.setText("&Cancel")
self.cancel_button.setDefault(True)
self.cancel_button.clicked.connect(self.hide)
self.button_layout.addWidget(self.cancel_button)
self.delete_button = QPushButton()
self.delete_button.setText("&Delete")
self.delete_button.clicked.connect(self.hide)
self.delete_button.clicked.connect(lambda: self.delete_entries())
self.button_layout.addWidget(self.delete_button)
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
def refresh_list(self):
self.desc_widget.setText(f"""
Are you sure you want to delete the following {len(self.lib.missing_files)} entries?
''')
""")
self.model.clear()
for i in self.lib.missing_files:
self.model.appendRow(QStandardItem(i))
self.model.clear()
for i in self.lib.missing_files:
self.model.appendRow(QStandardItem(i))
def delete_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Deleting Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
def delete_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Deleting Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.lib.ref(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
# r = CustomRunnable(lambda: self.lib.ref(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
iterator = FunctionIterator(self.lib.remove_missing_files)
pw = ProgressWidget(
window_title="Deleting Entries",
label_text="",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.missing_files),
)
pw.show()
iterator = FunctionIterator(self.lib.remove_missing_files)
iterator.value.connect(lambda x: pw.update_progress(x[0] + 1))
iterator.value.connect(
lambda x: pw.update_label(
f"Deleting {x[0]+1}/{len(self.lib.missing_files)} Unlinked Entries"
)
)
iterator.value.connect(
lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1])
)
pw = ProgressWidget(
window_title='Deleting Entries',
label_text='',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.missing_files)
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0]+1))
iterator.value.connect(lambda x: pw.update_label(f'Deleting {x[0]+1}/{len(self.lib.missing_files)} Unlinked Entries'))
iterator.value.connect(lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1]))
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
r = CustomRunnable(lambda:iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
# def delete_entries_runnable(self):
# deleted = []
# for i, missing in enumerate(self.lib.missing_files):
# # pb.setValue(i)
# # pb.setLabelText(f'Deleting {i}/{len(self.lib.missing_files)} Unlinked Entries')
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# yield i
# for d in deleted:
# self.lib.missing_files.remove(d)
# # self.driver.filter_items('')
# # self.done.emit()
# def delete_entries_runnable(self):
# deleted = []
# for i, missing in enumerate(self.lib.missing_files):
# # pb.setValue(i)
# # pb.setLabelText(f'Deleting {i}/{len(self.lib.missing_files)} Unlinked Entries')
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# yield i
# for d in deleted:
# self.lib.missing_files.remove(d)
# # self.driver.filter_items('')
# # self.done.emit()

View file

@ -11,41 +11,44 @@ from src.qt.widgets import PanelWidget
class FileExtensionModal(PanelWidget):
done = Signal()
def __init__(self, library:'Library'):
super().__init__()
self.lib = library
self.setWindowTitle(f'File Extensions')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(200, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
done = Signal()
self.table = QTableWidget(len(self.lib.ignored_extensions), 1)
self.table.horizontalHeader().setVisible(False)
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setStretchLastSection(True)
def __init__(self, library: "Library"):
super().__init__()
self.lib = library
self.setWindowTitle(f"File Extensions")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(200, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.add_button = QPushButton()
self.add_button.setText('&Add Extension')
self.add_button.clicked.connect(self.add_item)
self.add_button.setDefault(True)
self.add_button.setMinimumWidth(100)
self.table = QTableWidget(len(self.lib.ignored_extensions), 1)
self.table.horizontalHeader().setVisible(False)
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setStretchLastSection(True)
self.root_layout.addWidget(self.table)
self.root_layout.addWidget(self.add_button, alignment=Qt.AlignmentFlag.AlignCenter)
self.refresh_list()
def refresh_list(self):
for i, ext in enumerate(self.lib.ignored_extensions):
self.table.setItem(i, 0, QTableWidgetItem(ext))
def add_item(self):
self.table.insertRow(self.table.rowCount())
def save(self):
self.lib.ignored_extensions.clear()
for i in range(self.table.rowCount()):
ext = self.table.item(i, 0)
if ext and ext.text():
self.lib.ignored_extensions.append(ext.text())
self.add_button = QPushButton()
self.add_button.setText("&Add Extension")
self.add_button.clicked.connect(self.add_item)
self.add_button.setDefault(True)
self.add_button.setMinimumWidth(100)
self.root_layout.addWidget(self.table)
self.root_layout.addWidget(
self.add_button, alignment=Qt.AlignmentFlag.AlignCenter
)
self.refresh_list()
def refresh_list(self):
for i, ext in enumerate(self.lib.ignored_extensions):
self.table.setItem(i, 0, QTableWidgetItem(ext))
def add_item(self):
self.table.insertRow(self.table.rowCount())
def save(self):
self.lib.ignored_extensions.clear()
for i in range(self.table.rowCount()):
ext = self.table.item(i, 0)
if ext and ext.text():
self.lib.ignored_extensions.append(ext.text())

View file

@ -7,153 +7,167 @@ import os
import typing
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QFileDialog,
)
from src.core.library import Library
from src.qt.modals import MirrorEntriesModal
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
class FixDupeFilesModal(QWidget):
# done = Signal(int)
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver = driver
self.count = -1
self.filename = ''
self.setWindowTitle(f'Fix Duplicate Files')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
# done = Signal(int)
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
self.count = -1
self.filename = ""
self.setWindowTitle(f"Fix Duplicate Files")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
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.setText('''TagStudio supports importing DupeGuru results to manage duplicate files.''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
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.setText(
"""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.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.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.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()
self.open_button.setText('&Load DupeGuru File')
self.open_button.clicked.connect(lambda: self.select_file())
self.open_button = QPushButton()
self.open_button.setText("&Load DupeGuru File")
self.open_button.clicked.connect(lambda: self.select_file())
self.mirror_button = QPushButton()
self.mirror_modal = MirrorEntriesModal(self.lib, self.driver)
self.mirror_modal.done.connect(lambda: self.refresh_dupes())
self.mirror_button.setText('&Mirror Entries')
self.mirror_button.clicked.connect(lambda: self.mirror_modal.show())
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.""")
self.mirror_button = QPushButton()
self.mirror_modal = MirrorEntriesModal(self.lib, self.driver)
self.mirror_modal.done.connect(lambda: self.refresh_dupes())
self.mirror_button.setText("&Mirror Entries")
self.mirror_button.clicked.connect(lambda: self.mirror_modal.show())
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."""
)
# self.mirror_delete_button = QPushButton()
# self.mirror_delete_button.setText('Mirror && Delete')
# self.mirror_delete_button = QPushButton()
# self.mirror_delete_button.setText('Mirror && Delete')
self.advice_label = QLabel()
self.advice_label.setWordWrap(True)
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.""")
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6,6,6,6)
self.button_layout.addStretch(1)
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.advice_label = QLabel()
self.advice_label.setWordWrap(True)
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."""
)
# 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.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
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)
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.set_dupe_count(self.count)
def select_file(self):
qfd = QFileDialog(self,
'Open DupeGuru Results File',
os.path.normpath(self.lib.library_dir))
qfd.setFileMode(QFileDialog.FileMode.ExistingFile)
qfd.setNameFilter("DupeGuru Files (*.dupeguru)")
if qfd.exec_():
filename = qfd.selectedFiles()
if filename:
self.set_filename(filename[0])
def set_filename(self, filename:str):
if filename:
self.file_label.setText(filename)
else:
self.file_label.setText('No DupeGuru File Selected')
self.filename = filename
self.refresh_dupes()
self.mirror_modal.refresh_list()
def refresh_dupes(self):
self.lib.refresh_dupe_files(self.filename)
self.set_dupe_count(len(self.lib.dupe_files))
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
def set_dupe_count(self, count:int):
self.count = count
if self.count < 0:
self.mirror_button.setDisabled(True)
self.dupe_count.setText(f'Duplicate File Matches: N/A')
elif self.count == 0:
self.mirror_button.setDisabled(True)
self.dupe_count.setText(f'Duplicate File Matches: {count}')
else:
self.mirror_button.setDisabled(False)
self.dupe_count.setText(f'Duplicate File Matches: {count}')
# 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)
self.set_dupe_count(self.count)
def select_file(self):
qfd = QFileDialog(
self, "Open DupeGuru Results File", os.path.normpath(self.lib.library_dir)
)
qfd.setFileMode(QFileDialog.FileMode.ExistingFile)
qfd.setNameFilter("DupeGuru Files (*.dupeguru)")
if qfd.exec_():
filename = qfd.selectedFiles()
if filename:
self.set_filename(filename[0])
def set_filename(self, filename: str):
if filename:
self.file_label.setText(filename)
else:
self.file_label.setText("No DupeGuru File Selected")
self.filename = filename
self.refresh_dupes()
self.mirror_modal.refresh_list()
def refresh_dupes(self):
self.lib.refresh_dupe_files(self.filename)
self.set_dupe_count(len(self.lib.dupe_files))
def set_dupe_count(self, count: int):
self.count = count
if self.count < 0:
self.mirror_button.setDisabled(True)
self.dupe_count.setText(f"Duplicate File Matches: N/A")
elif self.count == 0:
self.mirror_button.setDisabled(True)
self.dupe_count.setText(f"Duplicate File Matches: {count}")
else:
self.mirror_button.setDisabled(False)
self.dupe_count.setText(f"Duplicate File Matches: {count}")

View file

@ -12,168 +12,175 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushBu
from src.core.library import Library
from src.qt.helpers import FunctionIterator, CustomRunnable
from src.qt.modals import DeleteUnlinkedEntriesModal, RelinkUnlinkedEntries
from src.qt.widgets import ProgressWidget
from src.qt.widgets import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class FixUnlinkedEntriesModal(QWidget):
# done = Signal(int)
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver = driver
self.count = -1
self.setWindowTitle(f'Fix Unlinked Entries')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
# done = Signal(int)
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
self.count = -1
self.setWindowTitle(f"Fix Unlinked Entries")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
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.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.''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
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.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.""")
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.missing_count = QLabel()
self.missing_count.setObjectName('missingCountLabel')
self.missing_count.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
'font-weight:bold;'
'font-size:14px;'
# 'padding-top: 6px'
'')
self.missing_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.missing_count.setText('Missing Files: N/A')
self.missing_count = QLabel()
self.missing_count.setObjectName("missingCountLabel")
self.missing_count.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
"font-weight:bold;"
"font-size:14px;"
# 'padding-top: 6px'
""
)
self.missing_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.missing_count.setText('Missing Files: N/A')
self.refresh_button = QPushButton()
self.refresh_button.setText('&Refresh')
self.refresh_button.clicked.connect(lambda: self.refresh_missing_files())
self.refresh_button = QPushButton()
self.refresh_button.setText("&Refresh")
self.refresh_button.clicked.connect(lambda: self.refresh_missing_files())
self.search_button = QPushButton()
self.search_button.setText('&Search && Relink')
self.relink_class = RelinkUnlinkedEntries(self.lib, self.driver)
self.relink_class.done.connect(lambda: self.refresh_missing_files())
self.relink_class.done.connect(lambda: self.driver.update_thumbs())
self.search_button.clicked.connect(lambda: self.relink_class.repair_entries())
self.search_button = QPushButton()
self.search_button.setText("&Search && Relink")
self.relink_class = RelinkUnlinkedEntries(self.lib, self.driver)
self.relink_class.done.connect(lambda: self.refresh_missing_files())
self.relink_class.done.connect(lambda: self.driver.update_thumbs())
self.search_button.clicked.connect(lambda: self.relink_class.repair_entries())
self.manual_button = QPushButton()
self.manual_button.setText('&Manual Relink')
self.manual_button = QPushButton()
self.manual_button.setText("&Manual Relink")
self.delete_button = QPushButton()
self.delete_modal = DeleteUnlinkedEntriesModal(self.lib, self.driver)
self.delete_modal.done.connect(lambda: self.set_missing_count(len(self.lib.missing_files)))
self.delete_modal.done.connect(lambda: self.driver.update_thumbs())
self.delete_button.setText('De&lete Unlinked Entries')
self.delete_button.clicked.connect(lambda: self.delete_modal.show())
# self.combo_box = QComboBox()
# self.combo_box.setEditable(False)
# # self.combo_box.setMaxVisibleItems(5)
# self.combo_box.setStyleSheet('combobox-popup:0;')
# self.combo_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
# for df in self.lib.default_fields:
# self.combo_box.addItem(f'{df["name"]} ({df["type"].replace("_", " ").title()})')
self.delete_button = QPushButton()
self.delete_modal = DeleteUnlinkedEntriesModal(self.lib, self.driver)
self.delete_modal.done.connect(
lambda: self.set_missing_count(len(self.lib.missing_files))
)
self.delete_modal.done.connect(lambda: self.driver.update_thumbs())
self.delete_button.setText("De&lete Unlinked Entries")
self.delete_button.clicked.connect(lambda: self.delete_modal.show())
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6,6,6,6)
self.button_layout.addStretch(1)
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.combo_box = QComboBox()
# self.combo_box.setEditable(False)
# # self.combo_box.setMaxVisibleItems(5)
# self.combo_box.setStyleSheet('combobox-popup:0;')
# self.combo_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
# for df in self.lib.default_fields:
# self.combo_box.addItem(f'{df["name"]} ({df["type"].replace("_", " ").title()})')
# 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.missing_count)
self.root_layout.addWidget(self.refresh_button)
self.root_layout.addWidget(self.search_button)
self.manual_button.setHidden(True)
self.root_layout.addWidget(self.manual_button)
self.root_layout.addWidget(self.delete_button)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
self.set_missing_count(self.count)
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)
def refresh_missing_files(self):
logging.info(f'Start RMF: {QThread.currentThread()}')
# pb = QProgressDialog(f'Scanning Library for Unlinked Entries...', None, 0,len(self.lib.entries))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Scanning Library')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
iterator = FunctionIterator(self.lib.refresh_missing_files)
pw = ProgressWidget(
window_title='Scanning Library',
label_text=f'Scanning Library for Unlinked Entries...',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.entries)
)
pw.show()
iterator.value.connect(lambda v: pw.update_progress(v+1))
# rmf.value.connect(lambda v: pw.update_label(f'Progress: {v}'))
r = CustomRunnable(lambda:iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(),
self.set_missing_count(len(self.lib.missing_files)),
self.delete_modal.refresh_list()))
# self.done.connect(lambda x: callback(x))
# r = CustomRunnable(lambda: self.lib.refresh_missing_files(lambda v: self.update_scan_value(pb, v)))
# r.done.connect(lambda: (pb.hide(), pb.deleteLater(), self.set_missing_count(len(self.lib.missing_files)), self.delete_modal.refresh_list()))
# QThreadPool.globalInstance().start(r)
# # r.run()
# pass
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.missing_count)
self.root_layout.addWidget(self.refresh_button)
self.root_layout.addWidget(self.search_button)
self.manual_button.setHidden(True)
self.root_layout.addWidget(self.manual_button)
self.root_layout.addWidget(self.delete_button)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)
# def update_scan_value(self, pb:QProgressDialog, value=int):
# # pb.setLabelText(f'Scanning Library for Unlinked Entries ({value}/{len(self.lib.entries)})...')
# pb.setValue(value)
self.set_missing_count(self.count)
def set_missing_count(self, count:int):
self.count = count
if self.count < 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f'Unlinked Entries: N/A')
elif self.count == 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f'Unlinked Entries: {count}')
else:
self.search_button.setDisabled(False)
self.delete_button.setDisabled(False)
self.missing_count.setText(f'Unlinked Entries: {count}')
def refresh_missing_files(self):
logging.info(f"Start RMF: {QThread.currentThread()}")
# pb = QProgressDialog(f'Scanning Library for Unlinked Entries...', None, 0,len(self.lib.entries))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Scanning Library')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
iterator = FunctionIterator(self.lib.refresh_missing_files)
pw = ProgressWidget(
window_title="Scanning Library",
label_text=f"Scanning Library for Unlinked Entries...",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.entries),
)
pw.show()
iterator.value.connect(lambda v: pw.update_progress(v + 1))
# rmf.value.connect(lambda v: pw.update_label(f'Progress: {v}'))
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.set_missing_count(len(self.lib.missing_files)),
self.delete_modal.refresh_list(),
)
)
# r = CustomRunnable(lambda: self.lib.refresh_missing_files(lambda v: self.update_scan_value(pb, v)))
# r.done.connect(lambda: (pb.hide(), pb.deleteLater(), self.set_missing_count(len(self.lib.missing_files)), self.delete_modal.refresh_list()))
# QThreadPool.globalInstance().start(r)
# # r.run()
# pass
# def update_scan_value(self, pb:QProgressDialog, value=int):
# # pb.setLabelText(f'Scanning Library for Unlinked Entries ({value}/{len(self.lib.entries)})...')
# pb.setValue(value)
def set_missing_count(self, count: int):
self.count = count
if self.count < 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f"Unlinked Entries: N/A")
elif self.count == 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f"Unlinked Entries: {count}")
else:
self.search_button.setDisabled(False)
self.delete_button.setDisabled(False)
self.missing_count.setText(f"Unlinked Entries: {count}")

View file

@ -8,7 +8,15 @@ import math
import typing
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea, QFrame
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QFrame,
)
from src.core.library import Library, Tag
from src.core.palette import ColorType, get_tag_color
@ -16,306 +24,329 @@ from src.qt.flowlayout import FlowLayout
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
def folders_to_tags(library:Library):
logging.info("Converting folders to Tags")
tree = dict(dirs={})
def add_tag_to_tree(list:list[Tag]):
branch = tree
for tag in list:
if tag.name not in branch["dirs"]:
branch["dirs"][tag.name] = dict(dirs={},tag=tag)
branch = branch["dirs"][tag.name]
def folders_to_tags(library: Library):
logging.info("Converting folders to Tags")
tree = dict(dirs={})
def add_folders_to_tree(list:list[str])->Tag:
branch = tree
for folder in list:
if folder not in branch["dirs"]:
new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"")
library.add_tag_to_library(new_tag)
branch["dirs"][folder] = dict(dirs={},tag=new_tag)
branch = branch["dirs"][folder]
return branch["tag"]
def add_tag_to_tree(list: list[Tag]):
branch = tree
for tag in list:
if tag.name not in branch["dirs"]:
branch["dirs"][tag.name] = dict(dirs={}, tag=tag)
branch = branch["dirs"][tag.name]
def add_folders_to_tree(list: list[str]) -> Tag:
branch = tree
for folder in list:
if folder not in branch["dirs"]:
new_tag = Tag(
-1,
folder,
"",
[],
([branch["tag"].id] if "tag" in branch else []),
"",
)
library.add_tag_to_library(new_tag)
branch["dirs"][folder] = dict(dirs={}, tag=new_tag)
branch = branch["dirs"][folder]
return branch["tag"]
for tag in library.tags:
reversed_tag = reverse_tag(library, tag, None)
add_tag_to_tree(reversed_tag)
for entry in library.entries:
folders = entry.path.split("\\")
if len(folders) == 1 and folders[0] == "":
continue
tag = add_folders_to_tree(folders)
if tag:
if not entry.has_tag(library, tag.id):
entry.add_tag(library, tag.id, 6)
logging.info("Done")
for tag in library.tags:
reversed_tag = reverse_tag(library,tag,None)
add_tag_to_tree(reversed_tag)
def reverse_tag(library: Library, tag: Tag, list: list[Tag]) -> list[Tag]:
if list != None:
list.append(tag)
else:
list = [tag]
for entry in library.entries:
folders = entry.path.split("\\")
if len(folders)== 1 and folders[0]=="": continue
tag = add_folders_to_tree(folders)
if tag:
if not entry.has_tag(library,tag.id):
entry.add_tag(library,tag.id,6)
if len(tag.subtag_ids) == 0:
list.reverse()
return list
else:
for subtag_id in tag.subtag_ids:
subtag = library.get_tag(subtag_id)
return reverse_tag(library, subtag, list)
logging.info("Done")
def reverse_tag(library:Library,tag:Tag,list:list[Tag]) -> list[Tag]:
if list != None:
list.append(tag)
else:
list = [tag]
# =========== UI ===========
if len(tag.subtag_ids) == 0:
list.reverse()
return list
else:
for subtag_id in tag.subtag_ids:
subtag = library.get_tag(subtag_id)
return reverse_tag(library,subtag,list)
#=========== UI ===========
def generate_preview_data(library: Library):
tree = dict(dirs={}, files=[])
def generate_preview_data(library:Library):
tree = dict(dirs={},files=[])
def add_tag_to_tree(list: list[Tag]):
branch = tree
for tag in list:
if tag.name not in branch["dirs"]:
branch["dirs"][tag.name] = dict(dirs={}, tag=tag, files=[])
branch = branch["dirs"][tag.name]
def add_tag_to_tree(list:list[Tag]):
branch = tree
for tag in list:
if tag.name not in branch["dirs"]:
branch["dirs"][tag.name] = dict(dirs={},tag=tag,files=[])
branch = branch["dirs"][tag.name]
def add_folders_to_tree(list: list[str]) -> Tag:
branch = tree
for folder in list:
if folder not in branch["dirs"]:
new_tag = Tag(-1, folder, "", [], [], "green")
branch["dirs"][folder] = dict(dirs={}, tag=new_tag, files=[])
branch = branch["dirs"][folder]
return branch
def add_folders_to_tree(list:list[str])->Tag:
branch = tree
for folder in list:
if folder not in branch["dirs"]:
new_tag = Tag(-1, folder,"",[],[],"green")
branch["dirs"][folder] = dict(dirs={},tag=new_tag,files=[])
branch = branch["dirs"][folder]
return branch
for tag in library.tags:
reversed_tag = reverse_tag(library,tag,None)
add_tag_to_tree(reversed_tag)
for entry in library.entries:
folders = entry.path.split("\\")
if len(folders) == 1 and folders[0] == "": continue
branch = add_folders_to_tree(folders)
if branch:
field_indexes = library.get_field_index_in_entry(entry,6)
has_tag=False
for index in field_indexes:
content = library.get_field_attr(entry.fields[index],"content")
for tag_id in content:
tag = library.get_tag(tag_id)
if tag.name == branch["tag"].name:
has_tag=True
break
if not has_tag:
branch["files"].append(entry.filename)
for tag in library.tags:
reversed_tag = reverse_tag(library, tag, None)
add_tag_to_tree(reversed_tag)
def cut_branches_adding_nothing(branch:dict):
folders = set(branch["dirs"].keys())
for folder in folders:
cut = cut_branches_adding_nothing(branch["dirs"][folder])
if cut:
branch['dirs'].pop(folder)
for entry in library.entries:
folders = entry.path.split("\\")
if len(folders) == 1 and folders[0] == "":
continue
branch = add_folders_to_tree(folders)
if branch:
field_indexes = library.get_field_index_in_entry(entry, 6)
has_tag = False
for index in field_indexes:
content = library.get_field_attr(entry.fields[index], "content")
for tag_id in content:
tag = library.get_tag(tag_id)
if tag.name == branch["tag"].name:
has_tag = True
break
if not has_tag:
branch["files"].append(entry.filename)
if not "tag" in branch: return
if branch["tag"].id == -1 or len(branch["files"])>0:#Needs to be first
return False
if len(branch["dirs"].keys()) == 0:
return True
def cut_branches_adding_nothing(branch: dict):
folders = set(branch["dirs"].keys())
for folder in folders:
cut = cut_branches_adding_nothing(branch["dirs"][folder])
if cut:
branch["dirs"].pop(folder)
cut_branches_adding_nothing(tree)
if not "tag" in branch:
return
if branch["tag"].id == -1 or len(branch["files"]) > 0: # Needs to be first
return False
if len(branch["dirs"].keys()) == 0:
return True
return tree
cut_branches_adding_nothing(tree)
return tree
class FoldersToTagsModal(QWidget):
# done = Signal(int)
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.library = library
self.driver = driver
self.count = -1
self.filename = ''
self.setWindowTitle(f'Folders To Tags')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 800)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
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:18px;'
# 'padding-top: 6px'
'')
self.desc_widget.setText('''Creates tags based on the folder structure and applies them to entries.\n The Structure below shows all the tags that would be added and to which files they would be added. It being empty means that there are no Tag to be created or assigned''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.open_close_button_w = QWidget()
self.open_close_button_layout = QHBoxLayout(self.open_close_button_w)
self.open_all_button = QPushButton()
self.open_all_button.setText("Open All")
self.open_all_button.clicked.connect(lambda:self.set_all_branches(False))
self.close_all_button = QPushButton()
self.close_all_button.setText("Close All")
self.close_all_button.clicked.connect(lambda:self.set_all_branches(True))
self.open_close_button_layout.addWidget(self.open_all_button)
self.open_close_button_layout.addWidget(self.close_all_button)
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)
# done = Signal(int)
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.library = library
self.driver = driver
self.count = -1
self.filename = ""
self.scroll_area = QScrollArea()
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)
self.scroll_area.setWidget(self.scroll_contents)
self.setWindowTitle(f"Folders To Tags")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 800)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
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:18px;"
# 'padding-top: 6px'
""
)
self.desc_widget.setText(
"""Creates tags based on the folder structure and applies them to entries.\n The Structure below shows all the tags that would be added and to which files they would be added. It being empty means that there are no Tag to be created or assigned"""
)
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.open_close_button_w = QWidget()
self.open_close_button_layout = QHBoxLayout(self.open_close_button_w)
self.open_all_button = QPushButton()
self.open_all_button.setText("Open All")
self.open_all_button.clicked.connect(lambda: self.set_all_branches(False))
self.close_all_button = QPushButton()
self.close_all_button.setText("Close All")
self.close_all_button.clicked.connect(lambda: self.set_all_branches(True))
self.open_close_button_layout.addWidget(self.open_all_button)
self.open_close_button_layout.addWidget(self.close_all_button)
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.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOn
)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setWidget(self.scroll_contents)
self.apply_button = QPushButton()
self.apply_button.setText("&Apply")
self.apply_button.clicked.connect(self.on_apply)
self.showEvent = self.on_open
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)
def on_apply(self, event):
folders_to_tags(self.library)
self.close()
self.driver.preview_panel.update_widgets()
def on_open(self, event):
for i in reversed(range(self.scroll_layout.count())):
self.scroll_layout.itemAt(i).widget().setParent(None)
data = generate_preview_data(self.library)
for folder in data["dirs"].values():
test = TreeItem(folder, None)
self.scroll_layout.addWidget(test)
def set_all_branches(self, hidden: bool):
for i in reversed(range(self.scroll_layout.count())):
child = self.scroll_layout.itemAt(i).widget()
if type(child) == TreeItem:
child.set_all_branches(hidden)
self.apply_button = QPushButton()
self.apply_button.setText('&Apply')
self.apply_button.clicked.connect(self.on_apply)
self.showEvent = self.on_open
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)
def on_apply(self,event):
folders_to_tags(self.library)
self.close()
self.driver.preview_panel.update_widgets()
def on_open(self,event):
for i in reversed(range(self.scroll_layout.count())):
self.scroll_layout.itemAt(i).widget().setParent(None)
data = generate_preview_data(self.library)
for folder in data["dirs"].values():
test = TreeItem(folder,None)
self.scroll_layout.addWidget(test)
def set_all_branches(self,hidden:bool):
for i in reversed(range(self.scroll_layout.count())):
child = self.scroll_layout.itemAt(i).widget()
if type(child) == TreeItem:
child.set_all_branches(hidden)
class TreeItem(QWidget):
def __init__(self,data:dict,parentTag:Tag):
super().__init__()
self.setStyleSheet("QLabel{font-size: 13px}")
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(20,0,0,0)
self.root_layout.setSpacing(1)
def __init__(self, data: dict, parentTag: Tag):
super().__init__()
self.test = QWidget()
self.root_layout.addWidget(self.test)
self.tag_layout = FlowLayout(self.test)
self.label = QLabel()
self.tag_layout.addWidget(self.label)
self.tag_widget = ModifiedTagWidget(data["tag"],parentTag)
self.tag_widget.bg_button.clicked.connect(lambda:self.hide_show())
self.tag_layout.addWidget(self.tag_widget)
self.setStyleSheet("QLabel{font-size: 13px}")
self.children_widget = QWidget()
self.children_layout = QVBoxLayout(self.children_widget)
self.root_layout.addWidget(self.children_widget)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(20, 0, 0, 0)
self.root_layout.setSpacing(1)
self.populate(data)
self.test = QWidget()
self.root_layout.addWidget(self.test)
def hide_show(self):
self.children_widget.setHidden(not self.children_widget.isHidden())
self.label.setText(">" if self.children_widget.isHidden() else "v")
self.tag_layout = FlowLayout(self.test)
def populate(self,data:dict):
for folder in data["dirs"].values():
item = TreeItem(folder,data["tag"])
self.children_layout.addWidget(item)
for file in data["files"]:
label = QLabel()
label.setText(" -> "+file)
self.children_layout.addWidget(label)
if len(data["files"]) == 0 and len(data["dirs"].values()) == 0:
self.hide_show()
else:
self.label.setText("v")
def set_all_branches(self,hidden:bool):
for i in reversed(range(self.children_layout.count())):
child = self.children_layout.itemAt(i).widget()
if type(child) == TreeItem:
child.set_all_branches(hidden)
self.children_widget.setHidden(hidden)
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:
super().__init__()
self.tag = tag
self.label = QLabel()
self.tag_layout.addWidget(self.label)
self.tag_widget = ModifiedTagWidget(data["tag"], parentTag)
self.tag_widget.bg_button.clicked.connect(lambda: self.hide_show())
self.tag_layout.addWidget(self.tag_widget)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName('baseLayout')
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.children_widget = QWidget()
self.children_layout = QVBoxLayout(self.children_widget)
self.root_layout.addWidget(self.children_widget)
self.bg_button = QPushButton(self)
self.bg_button.setFlat(True)
if parentTag != None:
text = f"{tag.name} ({parentTag.name})".replace('&', '&&')
else:
text = tag.name.replace('&', '&&')
self.bg_button.setText(text)
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.populate(data)
self.inner_layout = QHBoxLayout()
self.inner_layout.setObjectName('innerLayout')
self.inner_layout.setContentsMargins(2, 2, 2, 2)
self.bg_button.setLayout(self.inner_layout)
self.bg_button.setMinimumSize(math.ceil(22*1.5), 22)
def hide_show(self):
self.children_widget.setHidden(not self.children_widget.isHidden())
self.label.setText(">" if self.children_widget.isHidden() else "v")
self.bg_button.setStyleSheet(
f'QPushButton{{'
f'background: {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:inset;'
f'border-width: {math.ceil(1*self.devicePixelRatio())}px;'
f'padding-right: 4px;'
f'padding-bottom: 1px;'
f'padding-left: 4px;'
f'font-size: 13px'
f'}}'
f'QPushButton::hover{{'
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
f'}}')
def populate(self, data: dict):
for folder in data["dirs"].values():
item = TreeItem(folder, data["tag"])
self.children_layout.addWidget(item)
for file in data["files"]:
label = QLabel()
label.setText(" -> " + file)
self.children_layout.addWidget(label)
self.base_layout.addWidget(self.bg_button)
self.setMinimumSize(50,20)
if len(data["files"]) == 0 and len(data["dirs"].values()) == 0:
self.hide_show()
else:
self.label.setText("v")
def set_all_branches(self, hidden: bool):
for i in reversed(range(self.children_layout.count())):
child = self.children_layout.itemAt(i).widget()
if type(child) == TreeItem:
child.set_all_branches(hidden)
self.children_widget.setHidden(hidden)
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:
super().__init__()
self.tag = tag
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName("baseLayout")
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.bg_button = QPushButton(self)
self.bg_button.setFlat(True)
if parentTag != None:
text = f"{tag.name} ({parentTag.name})".replace("&", "&&")
else:
text = tag.name.replace("&", "&&")
self.bg_button.setText(text)
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.inner_layout = QHBoxLayout()
self.inner_layout.setObjectName("innerLayout")
self.inner_layout.setContentsMargins(2, 2, 2, 2)
self.bg_button.setLayout(self.inner_layout)
self.bg_button.setMinimumSize(math.ceil(22 * 1.5), 22)
self.bg_button.setStyleSheet(
f"QPushButton{{"
f"background: {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:inset;"
f"border-width: {math.ceil(1*self.devicePixelRatio())}px;"
f"padding-right: 4px;"
f"padding-bottom: 1px;"
f"padding-left: 4px;"
f"font-size: 13px"
f"}}"
f"QPushButton::hover{{"
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
f"}}"
)
self.base_layout.addWidget(self.bg_button)
self.setMinimumSize(50, 20)

View file

@ -8,120 +8,132 @@ import typing
from PySide6.QtCore import Signal, Qt, QThreadPool
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListView
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QListView,
)
from src.core.library import Library
from src.qt.helpers import FunctionIterator, CustomRunnable
from src.qt.widgets import ProgressWidget
from src.qt.widgets import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
class MirrorEntriesModal(QWidget):
done = Signal()
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver = driver
self.setWindowTitle(f'Mirror Entries')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,6,6,6)
done = Signal()
self.desc_widget = QLabel()
self.desc_widget.setObjectName('descriptionLabel')
self.desc_widget.setWordWrap(True)
self.desc_widget.setText(f'''
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
self.setWindowTitle(f"Mirror Entries")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.desc_widget = QLabel()
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setText(f"""
Are you sure you want to mirror the following {len(self.lib.dupe_files)} Entries?
''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
""")
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6,6,6,6)
self.button_layout.addStretch(1)
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
self.cancel_button = QPushButton()
self.cancel_button.setText('&Cancel')
self.cancel_button.setDefault(True)
self.cancel_button.clicked.connect(self.hide)
self.button_layout.addWidget(self.cancel_button)
self.mirror_button = QPushButton()
self.mirror_button.setText('&Mirror')
self.mirror_button.clicked.connect(self.hide)
self.mirror_button.clicked.connect(lambda: self.mirror_entries())
self.button_layout.addWidget(self.mirror_button)
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
def refresh_list(self):
self.desc_widget.setText(f'''
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
self.cancel_button = QPushButton()
self.cancel_button.setText("&Cancel")
self.cancel_button.setDefault(True)
self.cancel_button.clicked.connect(self.hide)
self.button_layout.addWidget(self.cancel_button)
self.mirror_button = QPushButton()
self.mirror_button.setText("&Mirror")
self.mirror_button.clicked.connect(self.hide)
self.mirror_button.clicked.connect(lambda: self.mirror_entries())
self.button_layout.addWidget(self.mirror_button)
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
def refresh_list(self):
self.desc_widget.setText(f"""
Are you sure you want to mirror the following {len(self.lib.dupe_files)} Entries?
''')
""")
self.model.clear()
for i in self.lib.dupe_files:
self.model.appendRow(QStandardItem(str(i)))
self.model.clear()
for i in self.lib.dupe_files:
self.model.appendRow(QStandardItem(str(i)))
def mirror_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.dupe_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Mirroring Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
def mirror_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.dupe_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Mirroring Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.mirror_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# r.done.connect(lambda: self.driver.preview_panel.refresh())
# # r.done.connect(lambda: self.model.clear())
# # QThreadPool.globalInstance().start(r)
# r.run()
# r = CustomRunnable(lambda: self.mirror_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# r.done.connect(lambda: self.driver.preview_panel.refresh())
# # r.done.connect(lambda: self.model.clear())
# # QThreadPool.globalInstance().start(r)
# r.run()
iterator = FunctionIterator(self.mirror_entries_runnable)
pw = ProgressWidget(
window_title='Mirroring Entries',
label_text=f'Mirroring 1/{len(self.lib.dupe_files)} Entries...',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.dupe_files)
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x+1))
iterator.value.connect(lambda x: pw.update_label(f'Mirroring {x+1}/{len(self.lib.dupe_files)} Entries...'))
r = CustomRunnable(lambda:iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (
pw.hide(),
pw.deleteLater(),
self.driver.preview_panel.update_widgets(),
self.done.emit()
))
def mirror_entries_runnable(self):
mirrored = []
for i, dupe in enumerate(self.lib.dupe_files):
# pb.setValue(i)
# pb.setLabelText(f'Mirroring {i}/{len(self.lib.dupe_files)} Entries')
entry_id_1 = self.lib.get_entry_id_from_filepath(
dupe[0])
entry_id_2 = self.lib.get_entry_id_from_filepath(
dupe[1])
self.lib.mirror_entry_fields([entry_id_1, entry_id_2])
sleep(0.005)
yield i
for d in mirrored:
self.lib.dupe_files.remove(d)
# self.driver.filter_items('')
# self.done.emit()
iterator = FunctionIterator(self.mirror_entries_runnable)
pw = ProgressWidget(
window_title="Mirroring Entries",
label_text=f"Mirroring 1/{len(self.lib.dupe_files)} Entries...",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.dupe_files),
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x + 1))
iterator.value.connect(
lambda x: pw.update_label(
f"Mirroring {x+1}/{len(self.lib.dupe_files)} Entries..."
)
)
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.driver.preview_panel.update_widgets(),
self.done.emit(),
)
)
def mirror_entries_runnable(self):
mirrored = []
for i, dupe in enumerate(self.lib.dupe_files):
# pb.setValue(i)
# pb.setLabelText(f'Mirroring {i}/{len(self.lib.dupe_files)} Entries')
entry_id_1 = self.lib.get_entry_id_from_filepath(dupe[0])
entry_id_2 = self.lib.get_entry_id_from_filepath(dupe[1])
self.lib.mirror_entry_fields([entry_id_1, entry_id_2])
sleep(0.005)
yield i
for d in mirrored:
self.lib.dupe_files.remove(d)
# self.driver.filter_items('')
# self.done.emit()

View file

@ -8,83 +8,91 @@ from PySide6.QtCore import QObject, Signal, QThreadPool
from src.core.library import Library
from src.qt.helpers import FunctionIterator, CustomRunnable
from src.qt.widgets import ProgressWidget
from src.qt.widgets import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
class RelinkUnlinkedEntries(QObject):
done = Signal()
done = Signal()
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver = driver
self.fixed = 0
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
self.fixed = 0
def repair_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Relinking Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
def repair_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Relinking Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.repair_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
# r = CustomRunnable(lambda: self.repair_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
iterator = FunctionIterator(self.lib.fix_missing_files)
iterator = FunctionIterator(self.lib.fix_missing_files)
pw = ProgressWidget(
window_title="Relinking Entries",
label_text="",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.missing_files),
)
pw.show()
pw = ProgressWidget(
window_title='Relinking Entries',
label_text='',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.missing_files)
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0] + 1))
iterator.value.connect(
lambda x: (
self.increment_fixed() if x[1] else (),
pw.update_label(
f"Attempting to Relink {x[0]+1}/{len(self.lib.missing_files)} Entries, {self.fixed} Successfully Relinked"
),
)
)
# iterator.value.connect(lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1]))
iterator.value.connect(lambda x: pw.update_progress(x[0]+1))
iterator.value.connect(lambda x: (self.increment_fixed() if x[1] else (), pw.update_label(f'Attempting to Relink {x[0]+1}/{len(self.lib.missing_files)} Entries, {self.fixed} Successfully Relinked')))
# iterator.value.connect(lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1]))
r = CustomRunnable(lambda: iterator.run())
r.done.connect(
lambda: (pw.hide(), pw.deleteLater(), self.done.emit(), self.reset_fixed())
)
QThreadPool.globalInstance().start(r)
r = CustomRunnable(lambda:iterator.run())
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit(), self.reset_fixed()))
QThreadPool.globalInstance().start(r)
def increment_fixed(self):
self.fixed += 1
def reset_fixed(self):
self.fixed = 0
def increment_fixed(self):
self.fixed += 1
# def repair_entries_runnable(self, pb: QProgressDialog):
# fixed = 0
# for i in self.lib.fix_missing_files():
# if i[1]:
# fixed += 1
# pb.setValue(i[0])
# pb.setLabelText(f'Attempting to Relink {i[0]+1}/{len(self.lib.missing_files)} Entries, {fixed} Successfully Relinked')
def reset_fixed(self):
self.fixed = 0
# for i, missing in enumerate(self.lib.missing_files):
# pb.setValue(i)
# pb.setLabelText(f'Relinking {i}/{len(self.lib.missing_files)} Unlinked Entries')
# self.lib.fix_missing_files()
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# for d in deleted:
# self.lib.missing_files.remove(d)
# def repair_entries_runnable(self, pb: QProgressDialog):
# fixed = 0
# for i in self.lib.fix_missing_files():
# if i[1]:
# fixed += 1
# pb.setValue(i[0])
# pb.setLabelText(f'Attempting to Relink {i[0]+1}/{len(self.lib.missing_files)} Entries, {fixed} Successfully Relinked')
# for i, missing in enumerate(self.lib.missing_files):
# pb.setValue(i)
# pb.setLabelText(f'Relinking {i}/{len(self.lib.missing_files)} Unlinked Entries')
# self.lib.fix_missing_files()
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# for d in deleted:
# self.lib.missing_files.remove(d)

View file

@ -3,7 +3,14 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Signal, Qt, QSize
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QScrollArea, QFrame
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLineEdit,
QScrollArea,
QFrame,
)
from src.core.library import Library
from src.qt.widgets import PanelWidget, PanelModal, TagWidget
@ -11,116 +18,128 @@ from src.qt.modals import BuildTagPanel
class TagDatabasePanel(PanelWidget):
tag_chosen = Signal(int)
tag_chosen = Signal(int)
def __init__(self, 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)
self.root_layout.setContentsMargins(6,0,6,0)
self.search_field = QLineEdit()
self.search_field.setObjectName('searchField')
self.search_field.setMinimumSize(QSize(0, 32))
self.search_field.setPlaceholderText('Search Tags')
self.search_field.textEdited.connect(lambda x=self.search_field.text(): self.update_tags(x))
self.search_field.returnPressed.connect(lambda checked=False: self.on_return(self.search_field.text()))
def __init__(self, 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.content_container = QWidget()
# self.content_layout = QHBoxLayout(self.content_container)
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
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.search_field = QLineEdit()
self.search_field.setObjectName("searchField")
self.search_field.setMinimumSize(QSize(0, 32))
self.search_field.setPlaceholderText("Search Tags")
self.search_field.textEdited.connect(
lambda x=self.search_field.text(): self.update_tags(x)
)
self.search_field.returnPressed.connect(
lambda checked=False: self.on_return(self.search_field.text())
)
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.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.content_container = QWidget()
# self.content_layout = QHBoxLayout(self.content_container)
# 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.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.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
self.update_tags('')
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.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)
# 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)
self.search_field.setText('')
self.update_tags('')
else:
self.search_field.setFocus()
self.parentWidget().hide()
# 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)
def update_tags(self, query: str):
# TODO: Look at recycling rather than deleting and reinitializing
while self.scroll_layout.itemAt(0):
self.scroll_layout.takeAt(0).widget().deleteLater()
self.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
self.update_tags("")
# If there is a query, get a list of tag_ids that match, otherwise return all
if query:
tags = self.lib.search_tags(query, include_cluster=True)[:self.tag_limit-1]
else:
# Get tag ids to keep this behaviorally identical
tags = [t.id for t in self.lib.tags]
# def reset(self):
# self.search_field.setText('')
# self.update_tags('')
# self.search_field.setFocus()
first_id_set = False
for tag_id in tags:
if not first_id_set:
self.first_tag_id = tag_id
first_id_set = True
container = QWidget()
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(3)
tw = TagWidget(self.lib, self.lib.get_tag(tag_id), True, False)
tw.on_edit.connect(lambda checked=False, t=self.lib.get_tag(tag_id): (self.edit_tag(t.id)))
row.addWidget(tw)
self.scroll_layout.addWidget(container)
def on_return(self, text: str):
if text and self.first_tag_id >= 0:
# callback(self.first_tag_id)
self.search_field.setText("")
self.update_tags("")
else:
self.search_field.setFocus()
self.parentWidget().hide()
self.search_field.setFocus()
def edit_tag(self, tag_id:int):
btp = BuildTagPanel(self.lib, tag_id)
# btp.on_edit.connect(lambda x: self.edit_tag_callback(x))
self.edit_modal = PanelModal(btp,
self.lib.get_tag(tag_id).display_name(self.lib),
'Edit Tag',
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(btp))
self.edit_modal.show()
def edit_tag_callback(self, btp:BuildTagPanel):
self.lib.update_tag(btp.build_tag())
self.update_tags(self.search_field.text())
def update_tags(self, query: str):
# TODO: Look at recycling rather than deleting and reinitializing
while self.scroll_layout.itemAt(0):
self.scroll_layout.takeAt(0).widget().deleteLater()
# def enterEvent(self, event: QEnterEvent) -> None:
# self.search_field.setFocus()
# return super().enterEvent(event)
# self.focusOutEvent
# If there is a query, get a list of tag_ids that match, otherwise return all
if query:
tags = self.lib.search_tags(query, include_cluster=True)[
: self.tag_limit - 1
]
else:
# Get tag ids to keep this behaviorally identical
tags = [t.id for t in self.lib.tags]
first_id_set = False
for tag_id in tags:
if not first_id_set:
self.first_tag_id = tag_id
first_id_set = True
container = QWidget()
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(3)
tw = TagWidget(self.lib, self.lib.get_tag(tag_id), True, False)
tw.on_edit.connect(
lambda checked=False, t=self.lib.get_tag(tag_id): (self.edit_tag(t.id))
)
row.addWidget(tw)
self.scroll_layout.addWidget(container)
self.search_field.setFocus()
def edit_tag(self, tag_id: int):
btp = BuildTagPanel(self.lib, tag_id)
# btp.on_edit.connect(lambda x: self.edit_tag_callback(x))
self.edit_modal = PanelModal(
btp,
self.lib.get_tag(tag_id).display_name(self.lib),
"Edit Tag",
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(btp))
self.edit_modal.show()
def edit_tag_callback(self, btp: BuildTagPanel):
self.lib.update_tag(btp.build_tag())
self.update_tags(self.search_field.text())
# def enterEvent(self, event: QEnterEvent) -> None:
# self.search_field.setFocus()
# return super().enterEvent(event)
# self.focusOutEvent

View file

@ -7,136 +7,154 @@ import logging
import math
from PySide6.QtCore import Signal, Qt, QSize
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QScrollArea, QFrame
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QLineEdit,
QScrollArea,
QFrame,
)
from src.core.library import Library
from src.core.palette import ColorType, get_tag_color
from src.qt.widgets import PanelWidget, TagWidget
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class TagSearchPanel(PanelWidget):
tag_chosen = Signal(int)
def __init__(self, library):
super().__init__()
self.lib: Library = library
# self.callback = callback
self.first_tag_id = None
self.tag_limit = 30
# self.selected_tag: int = 0
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,0,6,0)
tag_chosen = Signal(int)
self.search_field = QLineEdit()
self.search_field.setObjectName('searchField')
self.search_field.setMinimumSize(QSize(0, 32))
self.search_field.setPlaceholderText('Search Tags')
self.search_field.textEdited.connect(lambda x=self.search_field.text(): self.update_tags(x))
self.search_field.returnPressed.connect(lambda checked=False: self.on_return(self.search_field.text()))
def __init__(self, library):
super().__init__()
self.lib: Library = library
# self.callback = callback
self.first_tag_id = None
self.tag_limit = 30
# self.selected_tag: int = 0
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
# self.content_container = QWidget()
# self.content_layout = QHBoxLayout(self.content_container)
self.search_field = QLineEdit()
self.search_field.setObjectName("searchField")
self.search_field.setMinimumSize(QSize(0, 32))
self.search_field.setPlaceholderText("Search Tags")
self.search_field.textEdited.connect(
lambda x=self.search_field.text(): self.update_tags(x)
)
self.search_field.returnPressed.connect(
lambda checked=False: self.on_return(self.search_field.text())
)
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.content_container = QWidget()
# self.content_layout = QHBoxLayout(self.content_container)
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.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.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.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.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.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.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
# 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)
# 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)
self.tag_chosen.emit(self.first_tag_id)
self.search_field.setText('')
self.update_tags()
else:
self.search_field.setFocus()
self.parentWidget().hide()
self.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
def update_tags(self, query:str =''):
# for c in self.scroll_layout.children():
# c.widget().deleteLater()
while self.scroll_layout.count():
# logging.info(f"I'm deleting { self.scroll_layout.itemAt(0).widget()}")
self.scroll_layout.takeAt(0).widget().deleteLater()
# def reset(self):
# self.search_field.setText('')
# self.update_tags('')
# self.search_field.setFocus()
found_tags = self.lib.search_tags(query, include_cluster=True)[:self.tag_limit - 1]
self.first_tag_id = found_tags[0] if found_tags else None
def on_return(self, text: str):
if text and self.first_tag_id is not None:
# callback(self.first_tag_id)
self.tag_chosen.emit(self.first_tag_id)
self.search_field.setText("")
self.update_tags()
else:
self.search_field.setFocus()
self.parentWidget().hide()
for tag_id in found_tags:
c = QWidget()
l = QHBoxLayout(c)
l.setContentsMargins(0, 0, 0, 0)
l.setSpacing(3)
tw = TagWidget(self.lib, self.lib.get_tag(tag_id), False, False)
ab = QPushButton()
ab.setMinimumSize(23, 23)
ab.setMaximumSize(23, 23)
ab.setText('+')
ab.setStyleSheet(
f'QPushButton{{'
f'background: {get_tag_color(ColorType.PRIMARY, self.lib.get_tag(tag_id).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, self.lib.get_tag(tag_id).color)};"
f'font-weight: 600;'
f"border-color:{get_tag_color(ColorType.BORDER, self.lib.get_tag(tag_id).color)};"
f'border-radius: 6px;'
f'border-style:solid;'
f'border-width: {math.ceil(1*self.devicePixelRatio())}px;'
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
f'padding-bottom: 5px;'
# f'padding-left: 4px;'
f'font-size: 20px;'
f'}}'
f'QPushButton::hover'
f'{{'
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};"
f"color: {get_tag_color(ColorType.DARK_ACCENT, self.lib.get_tag(tag_id).color)};"
f'background: {get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};'
f'}}')
def update_tags(self, query: str = ""):
# for c in self.scroll_layout.children():
# c.widget().deleteLater()
while self.scroll_layout.count():
# logging.info(f"I'm deleting { self.scroll_layout.itemAt(0).widget()}")
self.scroll_layout.takeAt(0).widget().deleteLater()
ab.clicked.connect(lambda checked=False, x=tag_id: self.tag_chosen.emit(x))
found_tags = self.lib.search_tags(query, include_cluster=True)[
: self.tag_limit - 1
]
self.first_tag_id = found_tags[0] if found_tags else None
l.addWidget(tw)
l.addWidget(ab)
self.scroll_layout.addWidget(c)
for tag_id in found_tags:
c = QWidget()
l = QHBoxLayout(c)
l.setContentsMargins(0, 0, 0, 0)
l.setSpacing(3)
tw = TagWidget(self.lib, self.lib.get_tag(tag_id), False, False)
ab = QPushButton()
ab.setMinimumSize(23, 23)
ab.setMaximumSize(23, 23)
ab.setText("+")
ab.setStyleSheet(
f"QPushButton{{"
f"background: {get_tag_color(ColorType.PRIMARY, self.lib.get_tag(tag_id).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, self.lib.get_tag(tag_id).color)};"
f"font-weight: 600;"
f"border-color:{get_tag_color(ColorType.BORDER, self.lib.get_tag(tag_id).color)};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: {math.ceil(1*self.devicePixelRatio())}px;"
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
f"padding-bottom: 5px;"
# f'padding-left: 4px;'
f"font-size: 20px;"
f"}}"
f"QPushButton::hover"
f"{{"
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};"
f"color: {get_tag_color(ColorType.DARK_ACCENT, self.lib.get_tag(tag_id).color)};"
f"background: {get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};"
f"}}"
)
self.search_field.setFocus()
# def enterEvent(self, event: QEnterEvent) -> None:
# self.search_field.setFocus()
# return super().enterEvent(event)
# self.focusOutEvent
ab.clicked.connect(lambda checked=False, x=tag_id: self.tag_chosen.emit(x))
l.addWidget(tw)
l.addWidget(ab)
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

File diff suppressed because it is too large Load diff

View file

@ -15419,10 +15419,17 @@ qt_resource_struct = b"\
\x00\x00\x01\x8a\xd2\x83?\x9d\
"
def qInitResources():
QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
qInitResources()

View file

@ -16901,10 +16901,17 @@ qt_resource_struct = b"\
\x00\x00\x01\x8a\xd2\x83?\x9d\
"
def qInitResources():
QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
qInitResources()

File diff suppressed because it is too large Load diff

View file

@ -10,4 +10,4 @@ from .tag import TagWidget
from .tag_box import TagBoxWidget
from .text import TextWidget
from .item_thumb import ItemThumb
from .preview_panel import PreviewPanel
from .preview_panel import PreviewPanel

View file

@ -9,133 +9,171 @@ from pathlib import Path
import cv2
from PIL import Image, ImageChops, UnidentifiedImageError
from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer, QSettings
from PySide6.QtCore import (
QObject,
QThread,
Signal,
QRunnable,
Qt,
QThreadPool,
QSize,
QEvent,
QTimer,
QSettings,
)
from src.core.library import Library
from src.core.ts_core import DOC_TYPES, VIDEO_TYPES, IMAGE_TYPES
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class CollageIconRenderer(QObject):
rendered = Signal(Image.Image)
done = Signal()
rendered = Signal(Image.Image)
done = Signal()
def __init__(self, library:Library):
QObject.__init__(self)
self.lib = library
def render(self, entry_id, size:tuple[int,int], data_tint_mode, data_only_mode, keep_aspect):
entry = self.lib.get_entry(entry_id)
filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}')
file_type = os.path.splitext(filepath)[1].lower()[1:]
color: str = ''
def __init__(self, library: Library):
QObject.__init__(self)
self.lib = library
try:
if data_tint_mode or data_only_mode:
color = '#000000' # Black (Default)
if entry.fields:
has_any_tags:bool = False
has_content_tags:bool = False
has_meta_tags:bool = False
for field in entry.fields:
if self.lib.get_field_attr(field, 'type') == 'tag_box':
if self.lib.get_field_attr(field, 'content'):
has_any_tags = True
if self.lib.get_field_attr(field, 'id') == 7:
has_content_tags = True
elif self.lib.get_field_attr(field, 'id') == 8:
has_meta_tags = True
if has_content_tags and has_meta_tags:
color = '#28bb48' # Green
elif has_any_tags:
color = '#ffd63d' # Yellow
# color = '#95e345' # Yellow-Green
else:
# color = '#fa9a2c' # Yellow-Orange
color = '#ed8022' # Orange
else:
color = '#e22c3c' # Red
def render(
self,
entry_id,
size: tuple[int, int],
data_tint_mode,
data_only_mode,
keep_aspect,
):
entry = self.lib.get_entry(entry_id)
filepath = os.path.normpath(
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
)
file_type = os.path.splitext(filepath)[1].lower()[1:]
color: str = ""
if data_only_mode:
pic: Image = Image.new('RGB', size, color)
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
if not data_only_mode:
logging.info(f'\r{INFO} Combining [ID:{entry_id}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}\033[0m')
# sys.stdout.write(f'\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}')
# sys.stdout.flush()
if file_type in IMAGE_TYPES:
with Image.open(os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}')) as pic:
if keep_aspect:
pic.thumbnail(size)
else:
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))
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
elif file_type in VIDEO_TYPES:
video = cv2.VideoCapture(filepath)
video.set(cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2))
success, frame = video.read()
if not success:
# Depending on the video format, compression, and frame
# count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
with Image.fromarray(frame, mode='RGB') as pic:
if keep_aspect:
pic.thumbnail(size)
else:
pic = pic.resize(size)
if data_tint_mode and 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):
logging.info(f'\n{ERROR} Couldn\'t read {entry.path}{os.sep}{entry.filename}')
with Image.open(os.path.normpath(f'{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_broken_512.png')) as pic:
pic.thumbnail(size)
if data_tint_mode and color:
pic = pic.convert(mode='RGB')
pic = ImageChops.hard_light(pic, Image.new('RGB', size, color))
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
except KeyboardInterrupt:
# self.quit(save=False, backup=True)
run = False
# clear()
logging.info('\n')
logging.info(f'{INFO} Collage operation cancelled.')
clear_scr=False
except:
logging.info(f'{ERROR} {entry.path}{os.sep}{entry.filename}')
traceback.print_exc()
logging.info('Continuing...')
self.done.emit()
# logging.info('Done!')
def get_file_color(self, ext: str):
if ext.lower().replace('.','',1) == 'gif':
return '\033[93m'
if ext.lower().replace('.','',1) in IMAGE_TYPES:
return '\033[37m'
elif ext.lower().replace('.','',1) in VIDEO_TYPES:
return '\033[96m'
elif ext.lower().replace('.','',1) in DOC_TYPES:
return '\033[92m'
else:
return '\033[97m'
try:
if data_tint_mode or data_only_mode:
color = "#000000" # Black (Default)
if entry.fields:
has_any_tags: bool = False
has_content_tags: bool = False
has_meta_tags: bool = False
for field in entry.fields:
if self.lib.get_field_attr(field, "type") == "tag_box":
if self.lib.get_field_attr(field, "content"):
has_any_tags = True
if self.lib.get_field_attr(field, "id") == 7:
has_content_tags = True
elif self.lib.get_field_attr(field, "id") == 8:
has_meta_tags = True
if has_content_tags and has_meta_tags:
color = "#28bb48" # Green
elif has_any_tags:
color = "#ffd63d" # Yellow
# color = '#95e345' # Yellow-Green
else:
# color = '#fa9a2c' # Yellow-Orange
color = "#ed8022" # Orange
else:
color = "#e22c3c" # Red
if data_only_mode:
pic: Image = Image.new("RGB", size, color)
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
if not data_only_mode:
logging.info(
f"\r{INFO} Combining [ID:{entry_id}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}\033[0m"
)
# sys.stdout.write(f'\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}')
# sys.stdout.flush()
if file_type in IMAGE_TYPES:
with Image.open(
os.path.normpath(
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
)
) as pic:
if keep_aspect:
pic.thumbnail(size)
else:
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)
)
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
elif file_type in VIDEO_TYPES:
video = cv2.VideoCapture(filepath)
video.set(
cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
)
success, frame = video.read()
if not success:
# Depending on the video format, compression, and frame
# count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
with Image.fromarray(frame, mode="RGB") as pic:
if keep_aspect:
pic.thumbnail(size)
else:
pic = pic.resize(size)
if data_tint_mode and 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):
logging.info(
f"\n{ERROR} Couldn't read {entry.path}{os.sep}{entry.filename}"
)
with Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_broken_512.png"
)
) as pic:
pic.thumbnail(size)
if data_tint_mode and color:
pic = pic.convert(mode="RGB")
pic = ImageChops.hard_light(pic, Image.new("RGB", size, color))
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
except KeyboardInterrupt:
# self.quit(save=False, backup=True)
run = False
# clear()
logging.info("\n")
logging.info(f"{INFO} Collage operation cancelled.")
clear_scr = False
except:
logging.info(f"{ERROR} {entry.path}{os.sep}{entry.filename}")
traceback.print_exc()
logging.info("Continuing...")
self.done.emit()
# logging.info('Done!')
def get_file_color(self, ext: str):
if ext.lower().replace(".", "", 1) == "gif":
return "\033[93m"
if ext.lower().replace(".", "", 1) in IMAGE_TYPES:
return "\033[37m"
elif ext.lower().replace(".", "", 1) in VIDEO_TYPES:
return "\033[96m"
elif ext.lower().replace(".", "", 1) in DOC_TYPES:
return "\033[92m"
else:
return "\033[97m"

View file

@ -10,190 +10,203 @@ from pathlib import Path
from typing import Optional
from PIL import Image, ImageQt
from PySide6.QtCore import Qt,QEvent
from PySide6.QtCore import Qt, QEvent
from PySide6.QtGui import QPixmap, QEnterEvent
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
class FieldContainer(QWidget):
# TODO: reference a resources folder rather than path.parent.parent.parent.parent?
clipboard_icon_128: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/clipboard_icon_128.png')).resize((math.floor(24*1.25),math.floor(24*1.25)))
clipboard_icon_128.load()
# TODO: reference a resources folder rather than path.parent.parent.parent.parent?
clipboard_icon_128: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/clipboard_icon_128.png"
)
).resize((math.floor(24 * 1.25), math.floor(24 * 1.25)))
clipboard_icon_128.load()
edit_icon_128: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/edit_icon_128.png')).resize((math.floor(24*1.25),math.floor(24*1.25)))
edit_icon_128.load()
edit_icon_128: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/edit_icon_128.png"
)
).resize((math.floor(24 * 1.25), math.floor(24 * 1.25)))
edit_icon_128.load()
trash_icon_128: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/trash_icon_128.png')).resize((math.floor(24*1.25),math.floor(24*1.25)))
trash_icon_128.load()
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:FunctionType = None
self.edit_callback:FunctionType = None
self.remove_callback:FunctionType = None
button_size = 24
# self.setStyleSheet('border-style:solid;border-color:#1e1a33;border-radius:8px;border-width:2px;')
trash_icon_128: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/trash_icon_128.png"
)
).resize((math.floor(24 * 1.25), math.floor(24 * 1.25)))
trash_icon_128.load()
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: FunctionType = None
self.edit_callback: FunctionType = None
self.remove_callback: FunctionType = 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.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')
self.inner_layout.setContentsMargins(0,0,0,0)
self.inner_layout.setSpacing(0)
self.inner_container = QWidget()
self.inner_container.setObjectName('innerContainer')
self.inner_container.setLayout(self.inner_layout)
self.root_layout.addWidget(self.inner_container)
self.inner_layout = QVBoxLayout()
self.inner_layout.setObjectName("innerLayout")
self.inner_layout.setContentsMargins(0, 0, 0, 0)
self.inner_layout.setSpacing(0)
self.inner_container = QWidget()
self.inner_container.setObjectName("innerContainer")
self.inner_container.setLayout(self.inner_layout)
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')
self.title_layout.setContentsMargins(0,0,0,0)
self.title_layout.setSpacing(0)
self.inner_layout.addWidget(self.title_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")
self.title_layout.setContentsMargins(0, 0, 0, 0)
self.title_layout.setSpacing(0)
self.inner_layout.addWidget(self.title_container)
self.title_widget = QLabel()
self.title_widget.setMinimumHeight(button_size)
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_widget = QLabel()
self.title_widget.setMinimumHeight(button_size)
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.title_layout.addStretch(2)
self.copy_button = QPushButton()
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.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.copy_button)
self.copy_button.setHidden(True)
self.copy_button = QPushButton()
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.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.copy_button)
self.copy_button.setHidden(True)
self.edit_button = QPushButton()
self.edit_button.setMinimumSize(button_size, button_size)
self.edit_button.setMaximumSize(button_size, button_size)
self.edit_button.setFlat(True)
self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon_128)))
self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.edit_button)
self.edit_button.setHidden(True)
self.edit_button = QPushButton()
self.edit_button.setMinimumSize(button_size,button_size)
self.edit_button.setMaximumSize(button_size,button_size)
self.edit_button.setFlat(True)
self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon_128)))
self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.edit_button)
self.edit_button.setHidden(True)
self.remove_button = QPushButton()
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.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.remove_button)
self.remove_button.setHidden(True)
self.remove_button = QPushButton()
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.setCursor(Qt.CursorShape.PointingHandCursor)
self.title_layout.addWidget(self.remove_button)
self.remove_button.setHidden(True)
self.field_container = QWidget()
self.field_container.setObjectName("fieldContainer")
self.field_layout = QHBoxLayout()
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.field_container = QWidget()
self.field_container.setObjectName('fieldContainer')
self.field_layout = QHBoxLayout()
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)
# self.set_inner_widget(mode)
def set_copy_callback(self, callback: Optional[FunctionType]):
try:
self.copy_button.clicked.disconnect()
except RuntimeError:
pass
def set_copy_callback(self, callback:Optional[FunctionType]):
try:
self.copy_button.clicked.disconnect()
except RuntimeError:
pass
self.copy_callback = callback
self.copy_button.clicked.connect(callback)
self.copy_callback = callback
self.copy_button.clicked.connect(callback)
def set_edit_callback(self, callback: Optional[FunctionType]):
try:
self.edit_button.clicked.disconnect()
except RuntimeError:
pass
def set_edit_callback(self, callback:Optional[FunctionType]):
try:
self.edit_button.clicked.disconnect()
except RuntimeError:
pass
self.edit_callback = callback
self.edit_button.clicked.connect(callback)
self.edit_callback = callback
self.edit_button.clicked.connect(callback)
def set_remove_callback(self, callback: Optional[FunctionType]):
try:
self.remove_button.clicked.disconnect()
except RuntimeError:
pass
def set_remove_callback(self, callback:Optional[FunctionType]):
try:
self.remove_button.clicked.disconnect()
except RuntimeError:
pass
self.remove_callback = callback
self.remove_button.clicked.connect(callback)
self.remove_callback = callback
self.remove_button.clicked.connect(callback)
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)
def get_inner_widget(self) -> Optional['FieldWidget']:
if self.field_layout.itemAt(0):
return self.field_layout.itemAt(0).widget()
return None
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)
def set_title(self, title:str):
self.title = title
self.title_widget.setText(title)
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).
# NOTE: You could pass the hover event to the FieldWidget if needed.
if self.copy_callback:
self.copy_button.setHidden(False)
if self.edit_callback:
self.edit_button.setHidden(False)
if self.remove_callback:
self.remove_button.setHidden(False)
return super().enterEvent(event)
def get_inner_widget(self) -> Optional["FieldWidget"]:
if self.field_layout.itemAt(0):
return self.field_layout.itemAt(0).widget()
return None
def leaveEvent(self, event: QEvent) -> None:
if self.copy_callback:
self.copy_button.setHidden(True)
if self.edit_callback:
self.edit_button.setHidden(True)
if self.remove_callback:
self.remove_button.setHidden(True)
return super().leaveEvent(event)
def set_title(self, title: str):
self.title = title
self.title_widget.setText(title)
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).
# NOTE: You could pass the hover event to the FieldWidget if needed.
if self.copy_callback:
self.copy_button.setHidden(False)
if self.edit_callback:
self.edit_button.setHidden(False)
if self.remove_callback:
self.remove_button.setHidden(False)
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
if self.copy_callback:
self.copy_button.setHidden(True)
if self.edit_callback:
self.edit_button.setHidden(True)
if self.remove_callback:
self.remove_button.setHidden(True)
return super().leaveEvent(event)
class FieldWidget(QWidget):
field = dict
def __init__(self, title) -> None:
super().__init__()
# self.item = item
self.title = title
field = dict
def __init__(self, title) -> None:
super().__init__()
# self.item = item
self.title = title

View file

@ -14,7 +14,14 @@ from typing import Optional
from PIL import Image, ImageQt
from PySide6.QtCore import Qt, QSize, QEvent
from PySide6.QtGui import QPixmap, QEnterEvent, QAction
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QBoxLayout, QCheckBox
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QBoxLayout,
QCheckBox,
)
from src.core.library import ItemType, Library
from src.core.ts_core import AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES
@ -23,461 +30,514 @@ from src.qt.helpers import FileOpenerHelper
from src.qt.widgets import ThumbRenderer, ThumbButton
if typing.TYPE_CHECKING:
from src.qt.widgets import PreviewPanel
from src.qt.widgets import PreviewPanel
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class ItemThumb(FlowWidget):
"""
The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.).
"""
update_cutoff: float = time.time()
"""
The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.).
"""
collation_icon_128: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/collation_icon_128.png'))
collation_icon_128.load()
update_cutoff: float = time.time()
tag_group_icon_128: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/tag_group_icon_128.png'))
tag_group_icon_128.load()
collation_icon_128: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/collation_icon_128.png"
)
)
collation_icon_128.load()
small_text_style = (
f'background-color:rgba(0, 0, 0, 128);'
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;'
)
tag_group_icon_128: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/tag_group_icon_128.png"
)
)
tag_group_icon_128.load()
med_text_style = (
f'background-color:rgba(17, 15, 27, 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;'
)
small_text_style = (
f"background-color:rgba(0, 0, 0, 128);"
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;"
)
def __init__(self, mode: Optional[ItemType], library: Library, panel: 'PreviewPanel', thumb_size: tuple[int, int]):
"""Modes: entry, collation, tag_group"""
super().__init__()
self.lib = library
self.panel = panel
self.mode = mode
self.item_id: int = -1
self.isFavorite: bool = False
self.isArchived: bool = False
self.thumb_size:tuple[int,int]= thumb_size
self.setMinimumSize(*thumb_size)
self.setMaximumSize(*thumb_size)
check_size = 24
# self.setStyleSheet('background-color:red;')
med_text_style = (
f"background-color:rgba(17, 15, 27, 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;"
)
# +----------+
# | ARC FAV| Top Right: Favorite & Archived Badges
# | |
# | |
# |EXT #| Lower Left: File Type, Tag Group Icon, or Collation Icon
# +----------+ Lower Right: Collation Count, Video Length, or Word Count
def __init__(
self,
mode: Optional[ItemType],
library: Library,
panel: "PreviewPanel",
thumb_size: tuple[int, int],
):
"""Modes: entry, collation, tag_group"""
super().__init__()
self.lib = library
self.panel = panel
self.mode = mode
self.item_id: int = -1
self.isFavorite: bool = False
self.isArchived: bool = False
self.thumb_size: tuple[int, int] = thumb_size
self.setMinimumSize(*thumb_size)
self.setMaximumSize(*thumb_size)
check_size = 24
# self.setStyleSheet('background-color:red;')
# Thumbnail ============================================================
# +----------+
# | ARC FAV| Top Right: Favorite & Archived Badges
# | |
# | |
# |EXT #| Lower Left: File Type, Tag Group Icon, or Collation Icon
# +----------+ Lower Right: Collation Count, Video Length, or Word Count
# +----------+
# |*--------*|
# || ||
# || ||
# |*--------*|
# +----------+
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName('baseLayout')
# self.base_layout.setRowStretch(1, 2)
self.base_layout.setContentsMargins(0, 0, 0, 0)
# Thumbnail ============================================================
# +----------+
# |[~~~~~~~~]|
# | |
# | |
# | |
# +----------+
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)
self.base_layout.addWidget(self.top_container)
# +----------+
# |*--------*|
# || ||
# || ||
# |*--------*|
# +----------+
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName("baseLayout")
# self.base_layout.setRowStretch(1, 2)
self.base_layout.setContentsMargins(0, 0, 0, 0)
# +----------+
# |[~~~~~~~~]|
# | ^ |
# | | |
# | v |
# +----------+
self.base_layout.addStretch(2)
# +----------+
# |[~~~~~~~~]|
# | |
# | |
# | |
# +----------+
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)
self.base_layout.addWidget(self.top_container)
# +----------+
# |[~~~~~~~~]|
# | ^ |
# | v |
# |[~~~~~~~~]|
# +----------+
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)
# +----------+
# |[~~~~~~~~]|
# | ^ |
# | | |
# | v |
# +----------+
self.base_layout.addStretch(2)
# 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)
# +----------+
# |[~~~~~~~~]|
# | ^ |
# | v |
# |[~~~~~~~~]|
# +----------+
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.thumb_button = ThumbButton(self, thumb_size)
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.thumb_button.setFlat(True)
# 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.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 = ThumbButton(self, thumb_size)
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.thumb_button.setFlat(True)
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)
open_explorer_action = QAction('Open file in explorer', self)
open_explorer_action.triggered.connect(self.opener.open_explorer)
self.thumb_button.addAction(open_file_action)
self.thumb_button.addAction(open_explorer_action)
# 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)
# Static Badges ========================================================
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)
open_explorer_action = QAction("Open file in explorer", self)
open_explorer_action.triggered.connect(self.opener.open_explorer)
self.thumb_button.addAction(open_file_action)
self.thumb_button.addAction(open_explorer_action)
# Item Type Badge ------------------------------------------------------
# Used for showing the Tag Group / Collation icons.
# Mutually exclusive with the File Extension Badge.
self.item_type_badge = QLabel()
self.item_type_badge.setObjectName('itemBadge')
self.item_type_badge.setPixmap(QPixmap.fromImage(ImageQt.ImageQt(
ItemThumb.collation_icon_128.resize((check_size, check_size), Image.Resampling.BILINEAR))))
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)
# Static Badges ========================================================
# 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)
# Item Type Badge ------------------------------------------------------
# Used for showing the Tag Group / Collation icons.
# Mutually exclusive with the File Extension Badge.
self.item_type_badge = QLabel()
self.item_type_badge.setObjectName("itemBadge")
self.item_type_badge.setPixmap(
QPixmap.fromImage(
ImageQt.ImageQt(
ItemThumb.collation_icon_128.resize(
(check_size, check_size), Image.Resampling.BILINEAR
)
)
)
)
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)
self.bottom_layout.addStretch(2)
# 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)
# 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.addStretch(2)
self.top_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
)
# 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.top_layout.addStretch(2)
# Favorite Badge -------------------------------------------------------
self.favorite_badge = QCheckBox()
self.favorite_badge.setObjectName('favBadge')
self.favorite_badge.setToolTip('Favorite')
self.favorite_badge.setStyleSheet(f'QCheckBox::indicator{{width: {check_size}px;height: {check_size}px;}}'
f'QCheckBox::indicator::unchecked{{image: url(:/images/star_icon_empty_128.png)}}'
f'QCheckBox::indicator::checked{{image: url(:/images/star_icon_filled_128.png)}}'
# f'QCheckBox{{background-color:yellow;}}'
)
self.favorite_badge.setMinimumSize(check_size, check_size)
self.favorite_badge.setMaximumSize(check_size, check_size)
self.favorite_badge.stateChanged.connect(
lambda x=self.favorite_badge.isChecked(): self.on_favorite_check(bool(x)))
# 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.fav_badge.setContentsMargins(0,0,0,0)
# tr_layout.addWidget(self.fav_badge)
# root_layout.addWidget(self.fav_badge, 0, 2)
self.cb_layout.addWidget(self.favorite_badge)
self.favorite_badge.setHidden(True)
# Favorite Badge -------------------------------------------------------
self.favorite_badge = QCheckBox()
self.favorite_badge.setObjectName("favBadge")
self.favorite_badge.setToolTip("Favorite")
self.favorite_badge.setStyleSheet(
f"QCheckBox::indicator{{width: {check_size}px;height: {check_size}px;}}"
f"QCheckBox::indicator::unchecked{{image: url(:/images/star_icon_empty_128.png)}}"
f"QCheckBox::indicator::checked{{image: url(:/images/star_icon_filled_128.png)}}"
# f'QCheckBox{{background-color:yellow;}}'
)
self.favorite_badge.setMinimumSize(check_size, check_size)
self.favorite_badge.setMaximumSize(check_size, check_size)
self.favorite_badge.stateChanged.connect(
lambda x=self.favorite_badge.isChecked(): self.on_favorite_check(bool(x))
)
# Archive Badge --------------------------------------------------------
self.archived_badge = QCheckBox()
self.archived_badge.setObjectName('archiveBadge')
self.archived_badge.setToolTip('Archive')
self.archived_badge.setStyleSheet(f'QCheckBox::indicator{{width: {check_size}px;height: {check_size}px;}}'
f'QCheckBox::indicator::unchecked{{image: url(:/images/box_icon_empty_128.png)}}'
f'QCheckBox::indicator::checked{{image: url(:/images/box_icon_filled_128.png)}}'
# f'QCheckBox{{background-color:red;}}'
)
self.archived_badge.setMinimumSize(check_size, check_size)
self.archived_badge.setMaximumSize(check_size, check_size)
# self.archived_badge.clicked.connect(lambda x: self.assign_archived(x))
self.archived_badge.stateChanged.connect(
lambda x=self.archived_badge.isChecked(): self.on_archived_check(bool(x)))
# self.fav_badge.setContentsMargins(0,0,0,0)
# tr_layout.addWidget(self.fav_badge)
# root_layout.addWidget(self.fav_badge, 0, 2)
self.cb_layout.addWidget(self.favorite_badge)
self.favorite_badge.setHidden(True)
# tr_layout.addWidget(self.archive_badge)
self.cb_layout.addWidget(self.archived_badge)
self.archived_badge.setHidden(True)
# root_layout.addWidget(self.archive_badge, 0, 2)
# self.dumpObjectTree()
# Archive Badge --------------------------------------------------------
self.archived_badge = QCheckBox()
self.archived_badge.setObjectName("archiveBadge")
self.archived_badge.setToolTip("Archive")
self.archived_badge.setStyleSheet(
f"QCheckBox::indicator{{width: {check_size}px;height: {check_size}px;}}"
f"QCheckBox::indicator::unchecked{{image: url(:/images/box_icon_empty_128.png)}}"
f"QCheckBox::indicator::checked{{image: url(:/images/box_icon_filled_128.png)}}"
# f'QCheckBox{{background-color:red;}}'
)
self.archived_badge.setMinimumSize(check_size, check_size)
self.archived_badge.setMaximumSize(check_size, check_size)
# self.archived_badge.clicked.connect(lambda x: self.assign_archived(x))
self.archived_badge.stateChanged.connect(
lambda x=self.archived_badge.isChecked(): self.on_archived_check(bool(x))
)
self.set_mode(mode)
# tr_layout.addWidget(self.archive_badge)
self.cb_layout.addWidget(self.archived_badge)
self.archived_badge.setHidden(True)
# root_layout.addWidget(self.archive_badge, 0, 2)
# self.dumpObjectTree()
def set_mode(self, mode: Optional[ItemType]) -> None:
if mode is None:
self.unsetCursor()
self.thumb_button.setHidden(True)
# self.check_badges.setHidden(True)
# self.ext_badge.setHidden(True)
# self.item_type_badge.setHidden(True)
pass
elif mode == ItemType.ENTRY and self.mode != ItemType.ENTRY:
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(False)
# Count Badge depends on file extension (video length, word count)
self.item_type_badge.setHidden(True)
self.count_badge.setStyleSheet(ItemThumb.small_text_style)
self.count_badge.setHidden(True)
self.ext_badge.setHidden(True)
elif mode == ItemType.COLLATION and self.mode != ItemType.COLLATION:
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(True)
self.ext_badge.setHidden(True)
self.count_badge.setStyleSheet(ItemThumb.med_text_style)
self.count_badge.setHidden(False)
self.item_type_badge.setHidden(False)
elif mode == ItemType.TAG_GROUP and self.mode != ItemType.TAG_GROUP:
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}')
self.set_mode(mode)
# def update_(self, thumb: QPixmap, size:QSize, ext:str, badges:list[QPixmap]) -> None:
# """Updates the ItemThumb's visuals."""
# if thumb:
# pass
def set_mode(self, mode: Optional[ItemType]) -> None:
if mode is None:
self.unsetCursor()
self.thumb_button.setHidden(True)
# self.check_badges.setHidden(True)
# self.ext_badge.setHidden(True)
# self.item_type_badge.setHidden(True)
pass
elif mode == ItemType.ENTRY and self.mode != ItemType.ENTRY:
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(False)
# Count Badge depends on file extension (video length, word count)
self.item_type_badge.setHidden(True)
self.count_badge.setStyleSheet(ItemThumb.small_text_style)
self.count_badge.setHidden(True)
self.ext_badge.setHidden(True)
elif mode == ItemType.COLLATION and self.mode != ItemType.COLLATION:
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(True)
self.ext_badge.setHidden(True)
self.count_badge.setStyleSheet(ItemThumb.med_text_style)
self.count_badge.setHidden(False)
self.item_type_badge.setHidden(False)
elif mode == ItemType.TAG_GROUP and self.mode != ItemType.TAG_GROUP:
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 not in IMAGE_TYPES or ext in ['gif', 'apng']:
self.ext_badge.setHidden(False)
self.ext_badge.setText(ext.upper())
if ext in VIDEO_TYPES + AUDIO_TYPES:
self.count_badge.setHidden(False)
else:
if self.mode == ItemType.ENTRY:
self.ext_badge.setHidden(True)
self.count_badge.setHidden(True)
# def update_(self, thumb: QPixmap, size:QSize, ext:str, badges:list[QPixmap]) -> None:
# """Updates the ItemThumb's visuals."""
# if thumb:
# pass
def set_count(self, count: str) -> None:
if count:
self.count_badge.setHidden(False)
self.count_badge.setText(count)
else:
if self.mode == ItemType.ENTRY:
self.ext_badge.setHidden(True)
self.count_badge.setHidden(True)
def set_extension(self, ext: str) -> None:
if ext and ext not in IMAGE_TYPES or ext in ["gif", "apng"]:
self.ext_badge.setHidden(False)
self.ext_badge.setText(ext.upper())
if ext in VIDEO_TYPES + AUDIO_TYPES:
self.count_badge.setHidden(False)
else:
if self.mode == ItemType.ENTRY:
self.ext_badge.setHidden(True)
self.count_badge.setHidden(True)
def update_thumb(self, timestamp: float, image: QPixmap = None):
"""Updates 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 set_count(self, count: str) -> None:
if count:
self.count_badge.setHidden(False)
self.count_badge.setText(count)
else:
if self.mode == ItemType.ENTRY:
self.ext_badge.setHidden(True)
self.count_badge.setHidden(True)
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:
if self.thumb_button.iconSize != size:
self.thumb_button.setIconSize(size)
self.thumb_button.setMinimumSize(size)
self.thumb_button.setMaximumSize(size)
def update_thumb(self, timestamp: float, image: QPixmap = None):
"""Updates 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_clickable(self, clickable: FunctionType = None):
"""Updates attributes of a thumbnail element."""
# logging.info(f'[GUI] Updating Click Event for element {id(element)}: {id(clickable) if clickable else None}')
try:
self.thumb_button.clicked.disconnect()
except RuntimeError:
pass
if clickable:
self.thumb_button.clicked.connect(clickable)
def update_badges(self):
if self.mode == ItemType.ENTRY:
# logging.info(f'[UPDATE BADGES] ENTRY: {self.lib.get_entry(self.item_id)}')
# logging.info(f'[UPDATE BADGES] ARCH: {self.lib.get_entry(self.item_id).has_tag(self.lib, 0)}, FAV: {self.lib.get_entry(self.item_id).has_tag(self.lib, 1)}')
self.assign_archived(self.lib.get_entry(self.item_id).has_tag(self.lib, 0))
self.assign_favorite(self.lib.get_entry(self.item_id).has_tag(self.lib, 1))
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:
if self.thumb_button.iconSize != size:
self.thumb_button.setIconSize(size)
self.thumb_button.setMinimumSize(size)
self.thumb_button.setMaximumSize(size)
def update_clickable(self, clickable: FunctionType = None):
"""Updates attributes of a thumbnail element."""
# logging.info(f'[GUI] Updating Click Event for element {id(element)}: {id(clickable) if clickable else None}')
try:
self.thumb_button.clicked.disconnect()
except RuntimeError:
pass
if clickable:
self.thumb_button.clicked.connect(clickable)
def set_item_id(self, id: int):
'''
also sets the filepath for the file opener
'''
self.item_id = id
if(id == -1):
return
entry = self.lib.get_entry(self.item_id)
filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}')
self.opener.set_filepath(filepath)
def update_badges(self):
if self.mode == ItemType.ENTRY:
# logging.info(f'[UPDATE BADGES] ENTRY: {self.lib.get_entry(self.item_id)}')
# logging.info(f'[UPDATE BADGES] ARCH: {self.lib.get_entry(self.item_id).has_tag(self.lib, 0)}, FAV: {self.lib.get_entry(self.item_id).has_tag(self.lib, 1)}')
self.assign_archived(self.lib.get_entry(self.item_id).has_tag(self.lib, 0))
self.assign_favorite(self.lib.get_entry(self.item_id).has_tag(self.lib, 1))
def assign_favorite(self, value: bool):
# Switching mode to None to bypass mode-specific operations when the
# checkbox's state changes.
mode = self.mode
self.mode = None
self.isFavorite = value
self.favorite_badge.setChecked(value)
if not self.thumb_button.underMouse():
self.favorite_badge.setHidden(not self.isFavorite)
self.mode = mode
def set_item_id(self, id: int):
"""
also sets the filepath for the file opener
"""
self.item_id = id
if id == -1:
return
entry = self.lib.get_entry(self.item_id)
filepath = os.path.normpath(
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
)
self.opener.set_filepath(filepath)
def assign_archived(self, value: bool):
# Switching mode to None to bypass mode-specific operations when the
# checkbox's state changes.
mode = self.mode
self.mode = None
self.isArchived = value
self.archived_badge.setChecked(value)
if not self.thumb_button.underMouse():
self.archived_badge.setHidden(not self.isArchived)
self.mode = mode
def assign_favorite(self, value: bool):
# Switching mode to None to bypass mode-specific operations when the
# checkbox's state changes.
mode = self.mode
self.mode = None
self.isFavorite = value
self.favorite_badge.setChecked(value)
if not self.thumb_button.underMouse():
self.favorite_badge.setHidden(not self.isFavorite)
self.mode = mode
def show_check_badges(self, show: bool):
if self.mode != ItemType.TAG_GROUP:
self.favorite_badge.setHidden(
True if (not show and not self.isFavorite) else False)
self.archived_badge.setHidden(
True if (not show and not self.isArchived) else False)
def assign_archived(self, value: bool):
# Switching mode to None to bypass mode-specific operations when the
# checkbox's state changes.
mode = self.mode
self.mode = None
self.isArchived = value
self.archived_badge.setChecked(value)
if not self.thumb_button.underMouse():
self.archived_badge.setHidden(not self.isArchived)
self.mode = mode
def enterEvent(self, event: QEnterEvent) -> None:
self.show_check_badges(True)
return super().enterEvent(event)
def show_check_badges(self, show: bool):
if self.mode != ItemType.TAG_GROUP:
self.favorite_badge.setHidden(
True if (not show and not self.isFavorite) else False
)
self.archived_badge.setHidden(
True if (not show and not self.isArchived) else False
)
def leaveEvent(self, event: QEvent) -> None:
self.show_check_badges(False)
return super().leaveEvent(event)
def enterEvent(self, event: QEnterEvent) -> None:
self.show_check_badges(True)
return super().enterEvent(event)
def on_archived_check(self, value: bool):
# logging.info(f'Archived Check: {value}, Mode: {self.mode}')
if self.mode == ItemType.ENTRY:
self.isArchived = value
DEFAULT_META_TAG_FIELD = 8
temp = (ItemType.ENTRY,self.item_id)
if list(self.panel.driver.selected).count(temp) > 0: # Is the archived badge apart of the selection?
# Yes, then add archived tag to all selected.
for x in self.panel.driver.selected:
e = self.lib.get_entry(x[1])
if value:
self.archived_badge.setHidden(False)
e.add_tag(self.panel.driver.lib, 0, field_id=DEFAULT_META_TAG_FIELD, field_index=-1)
else:
e.remove_tag(self.panel.driver.lib, 0)
else:
# No, then add archived tag to the entry this badge is on.
e = self.lib.get_entry(self.item_id)
if value:
self.favorite_badge.setHidden(False)
e.add_tag(self.panel.driver.lib, 0, field_id=DEFAULT_META_TAG_FIELD, field_index=-1)
else:
e.remove_tag(self.panel.driver.lib, 0)
if self.panel.isOpen:
self.panel.update_widgets()
self.panel.driver.update_badges()
def leaveEvent(self, event: QEvent) -> None:
self.show_check_badges(False)
return super().leaveEvent(event)
def on_archived_check(self, value: bool):
# logging.info(f'Archived Check: {value}, Mode: {self.mode}')
if self.mode == ItemType.ENTRY:
self.isArchived = value
DEFAULT_META_TAG_FIELD = 8
temp = (ItemType.ENTRY, self.item_id)
if (
list(self.panel.driver.selected).count(temp) > 0
): # Is the archived badge apart of the selection?
# Yes, then add archived tag to all selected.
for x in self.panel.driver.selected:
e = self.lib.get_entry(x[1])
if value:
self.archived_badge.setHidden(False)
e.add_tag(
self.panel.driver.lib,
0,
field_id=DEFAULT_META_TAG_FIELD,
field_index=-1,
)
else:
e.remove_tag(self.panel.driver.lib, 0)
else:
# No, then add archived tag to the entry this badge is on.
e = self.lib.get_entry(self.item_id)
if value:
self.favorite_badge.setHidden(False)
e.add_tag(
self.panel.driver.lib,
0,
field_id=DEFAULT_META_TAG_FIELD,
field_index=-1,
)
else:
e.remove_tag(self.panel.driver.lib, 0)
if self.panel.isOpen:
self.panel.update_widgets()
self.panel.driver.update_badges()
# def on_archived_uncheck(self):
# if self.mode == SearchItemType.ENTRY:
# self.isArchived = False
# e = self.lib.get_entry(self.item_id)
# def on_archived_uncheck(self):
# if self.mode == SearchItemType.ENTRY:
# self.isArchived = False
# e = self.lib.get_entry(self.item_id)
def on_favorite_check(self, value: bool):
# logging.info(f'Favorite Check: {value}, Mode: {self.mode}')
if self.mode == ItemType.ENTRY:
self.isFavorite = value
DEFAULT_META_TAG_FIELD = 8
temp = (ItemType.ENTRY,self.item_id)
if list(self.panel.driver.selected).count(temp) > 0: # Is the favorite badge apart of the selection?
# Yes, then add favorite tag to all selected.
for x in self.panel.driver.selected:
e = self.lib.get_entry(x[1])
if value:
self.favorite_badge.setHidden(False)
e.add_tag(self.panel.driver.lib, 1, field_id=DEFAULT_META_TAG_FIELD, field_index=-1)
else:
e.remove_tag(self.panel.driver.lib, 1)
else:
# No, then add favorite tag to the entry this badge is on.
e = self.lib.get_entry(self.item_id)
if value:
self.favorite_badge.setHidden(False)
e.add_tag(self.panel.driver.lib, 1, field_id=DEFAULT_META_TAG_FIELD, field_index=-1)
else:
e.remove_tag(self.panel.driver.lib, 1)
if self.panel.isOpen:
self.panel.update_widgets()
self.panel.driver.update_badges()
def on_favorite_check(self, value: bool):
# logging.info(f'Favorite Check: {value}, Mode: {self.mode}')
if self.mode == ItemType.ENTRY:
self.isFavorite = value
DEFAULT_META_TAG_FIELD = 8
temp = (ItemType.ENTRY, self.item_id)
if (
list(self.panel.driver.selected).count(temp) > 0
): # Is the favorite badge apart of the selection?
# Yes, then add favorite tag to all selected.
for x in self.panel.driver.selected:
e = self.lib.get_entry(x[1])
if value:
self.favorite_badge.setHidden(False)
e.add_tag(
self.panel.driver.lib,
1,
field_id=DEFAULT_META_TAG_FIELD,
field_index=-1,
)
else:
e.remove_tag(self.panel.driver.lib, 1)
else:
# No, then add favorite tag to the entry this badge is on.
e = self.lib.get_entry(self.item_id)
if value:
self.favorite_badge.setHidden(False)
e.add_tag(
self.panel.driver.lib,
1,
field_id=DEFAULT_META_TAG_FIELD,
field_index=-1,
)
else:
e.remove_tag(self.panel.driver.lib, 1)
if self.panel.isOpen:
self.panel.update_widgets()
self.panel.driver.update_badges()
# def on_favorite_uncheck(self):
# if self.mode == SearchItemType.ENTRY:
# self.isFavorite = False
# e = self.lib.get_entry(self.item_id)
# e.remove_tag(1)
# def on_favorite_uncheck(self):
# if self.mode == SearchItemType.ENTRY:
# self.isFavorite = False
# e = self.lib.get_entry(self.item_id)
# e.remove_tag(1)

View file

@ -10,90 +10,100 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushBu
class PanelModal(QWidget):
saved = Signal()
# TODO: Separate callbacks from the buttons you want, and just generally
# figure out what you want from this.
def __init__(self, widget:'PanelWidget', title:str, window_title:str,
done_callback:FunctionType=None,
# cancel_callback:FunctionType=None,
save_callback:FunctionType=None,has_save:bool=False):
# [Done]
# - OR -
# [Cancel] [Save]
super().__init__()
self.widget = widget
self.setWindowTitle(window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,0,6,6)
saved = Signal()
self.title_widget = QLabel()
self.title_widget.setObjectName('fieldTitle')
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
'font-weight:bold;'
'font-size:14px;'
'padding-top: 6px'
'')
self.title_widget.setText(title)
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
# TODO: Separate callbacks from the buttons you want, and just generally
# figure out what you want from this.
def __init__(
self,
widget: "PanelWidget",
title: str,
window_title: str,
done_callback: FunctionType = None,
# cancel_callback:FunctionType=None,
save_callback: FunctionType = None,
has_save: bool = False,
):
# [Done]
# - OR -
# [Cancel] [Save]
super().__init__()
self.widget = widget
self.setWindowTitle(window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 6)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6,6,6,6)
self.button_layout.addStretch(1)
self.title_widget = QLabel()
self.title_widget.setObjectName("fieldTitle")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
"font-weight:bold;" "font-size:14px;" "padding-top: 6px" ""
)
self.title_widget.setText(title)
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.cancel_button = QPushButton()
# self.cancel_button.setText('Cancel')
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
if not (save_callback or has_save):
self.done_button = QPushButton()
self.done_button.setText('Done')
self.done_button.setAutoDefault(True)
self.done_button.clicked.connect(self.hide)
if done_callback:
self.done_button.clicked.connect(done_callback)
self.button_layout.addWidget(self.done_button)
# self.cancel_button = QPushButton()
# self.cancel_button.setText('Cancel')
if (save_callback or has_save):
self.cancel_button = QPushButton()
self.cancel_button.setText('Cancel')
self.cancel_button.clicked.connect(self.hide)
self.cancel_button.clicked.connect(widget.reset)
# self.cancel_button.clicked.connect(cancel_callback)
self.button_layout.addWidget(self.cancel_button)
if (save_callback or has_save):
self.save_button = QPushButton()
self.save_button.setText('Save')
self.save_button.setAutoDefault(True)
self.save_button.clicked.connect(self.hide)
self.save_button.clicked.connect(self.saved.emit)
if done_callback:
self.save_button.clicked.connect(done_callback)
if save_callback:
self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.save_button)
widget.done.connect(lambda: save_callback(widget.get_content()))
self.root_layout.addWidget(self.title_widget)
self.root_layout.addWidget(widget)
self.root_layout.setStretch(1,2)
self.root_layout.addWidget(self.button_container)
if not (save_callback or has_save):
self.done_button = QPushButton()
self.done_button.setText("Done")
self.done_button.setAutoDefault(True)
self.done_button.clicked.connect(self.hide)
if done_callback:
self.done_button.clicked.connect(done_callback)
self.button_layout.addWidget(self.done_button)
if save_callback or has_save:
self.cancel_button = QPushButton()
self.cancel_button.setText("Cancel")
self.cancel_button.clicked.connect(self.hide)
self.cancel_button.clicked.connect(widget.reset)
# self.cancel_button.clicked.connect(cancel_callback)
self.button_layout.addWidget(self.cancel_button)
if save_callback or has_save:
self.save_button = QPushButton()
self.save_button.setText("Save")
self.save_button.setAutoDefault(True)
self.save_button.clicked.connect(self.hide)
self.save_button.clicked.connect(self.saved.emit)
if done_callback:
self.save_button.clicked.connect(done_callback)
if save_callback:
self.save_button.clicked.connect(
lambda: save_callback(widget.get_content())
)
self.button_layout.addWidget(self.save_button)
widget.done.connect(lambda: save_callback(widget.get_content()))
self.root_layout.addWidget(self.title_widget)
self.root_layout.addWidget(widget)
self.root_layout.setStretch(1, 2)
self.root_layout.addWidget(self.button_container)
class PanelWidget(QWidget):
"""
Used for widgets that go in a modal panel, ex. for editing or searching.
"""
done = Signal()
def __init__(self):
super().__init__()
def get_content(self)-> str:
pass
def reset(self):
pass
"""
Used for widgets that go in a modal panel, ex. for editing or searching.
"""
done = Signal()
def __init__(self):
super().__init__()
def get_content(self) -> str:
pass
def reset(self):
pass

File diff suppressed because it is too large Load diff

View file

@ -10,24 +10,34 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QProgressDialog
class ProgressWidget(QWidget):
"""Prebuilt thread-safe progress bar widget."""
def __init__(self, window_title:str, label_text:str, cancel_button_text:Optional[str], minimum:int, maximum:int):
super().__init__()
self.root = QVBoxLayout(self)
self.pb = QProgressDialog(
labelText=label_text,
minimum=minimum,
cancelButtonText=cancel_button_text,
maximum=maximum
)
self.root.addWidget(self.pb)
self.setFixedSize(432, 112)
self.setWindowFlags(self.pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
self.setWindowTitle(window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
def update_label(self, text:str):
self.pb.setLabelText(text)
"""Prebuilt thread-safe progress bar widget."""
def update_progress(self, value:int):
self.pb.setValue(value)
def __init__(
self,
window_title: str,
label_text: str,
cancel_button_text: Optional[str],
minimum: int,
maximum: int,
):
super().__init__()
self.root = QVBoxLayout(self)
self.pb = QProgressDialog(
labelText=label_text,
minimum=minimum,
cancelButtonText=cancel_button_text,
maximum=maximum,
)
self.root.addWidget(self.pb)
self.setFixedSize(432, 112)
self.setWindowFlags(
self.pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint
)
self.setWindowTitle(window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
def update_label(self, text: str):
self.pb.setLabelText(text)
def update_progress(self, value: int):
self.pb.setValue(value)

View file

@ -17,223 +17,236 @@ from src.core.library import Library, Tag
from src.core.palette import ColorType, get_tag_color
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
class TagWidget(QWidget):
edit_icon_128: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/edit_icon_128.png')).resize((math.floor(14*1.25),math.floor(14*1.25)))
edit_icon_128.load()
on_remove = Signal()
on_click = Signal()
on_edit = Signal()
edit_icon_128: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/edit_icon_128.png"
)
).resize((math.floor(14 * 1.25), math.floor(14 * 1.25)))
edit_icon_128.load()
on_remove = Signal()
on_click = Signal()
on_edit = Signal()
def __init__(self, library:Library, tag:Tag, has_edit:bool, has_remove:bool, on_remove_callback:FunctionType=None, on_click_callback:FunctionType=None, on_edit_callback:FunctionType=None) -> None:
super().__init__()
self.lib = library
self.tag = tag
self.has_edit:bool = has_edit
self.has_remove:bool = has_remove
# self.bg_label = QLabel()
# self.setStyleSheet('background-color:blue;')
# if on_click_callback:
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName('baseLayout')
self.base_layout.setContentsMargins(0, 0, 0, 0)
def __init__(
self,
library: Library,
tag: Tag,
has_edit: bool,
has_remove: bool,
on_remove_callback: FunctionType = None,
on_click_callback: FunctionType = None,
on_edit_callback: FunctionType = None,
) -> None:
super().__init__()
self.lib = library
self.tag = tag
self.has_edit: bool = has_edit
self.has_remove: bool = has_remove
# self.bg_label = QLabel()
# self.setStyleSheet('background-color:blue;')
self.bg_button = QPushButton(self)
self.bg_button.setFlat(True)
self.bg_button.setText(tag.display_name(self.lib).replace('&', '&&'))
if has_edit:
edit_action = QAction('Edit', self)
edit_action.triggered.connect(on_edit_callback)
edit_action.triggered.connect(self.on_edit.emit)
self.bg_button.addAction(edit_action)
# if on_click_callback:
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
# if has_remove:
# remove_action = QAction('Remove', self)
# # remove_action.triggered.connect(on_remove_callback)
# remove_action.triggered.connect(self.on_remove.emit())
# self.bg_button.addAction(remove_action)
search_for_tag_action = QAction('Search for Tag', self)
# search_for_tag_action.triggered.connect(on_click_callback)
search_for_tag_action.triggered.connect(self.on_click.emit)
self.bg_button.addAction(search_for_tag_action)
add_to_search_action = QAction('Add to Search', self)
self.bg_button.addAction(add_to_search_action)
# if on_click_callback:
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName("baseLayout")
self.base_layout.setContentsMargins(0, 0, 0, 0)
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 = QPushButton(self)
self.bg_button.setFlat(True)
self.bg_button.setText(tag.display_name(self.lib).replace("&", "&&"))
if has_edit:
edit_action = QAction("Edit", self)
edit_action.triggered.connect(on_edit_callback)
edit_action.triggered.connect(self.on_edit.emit)
self.bg_button.addAction(edit_action)
# if on_click_callback:
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
# if has_remove:
# remove_action = QAction('Remove', self)
# # remove_action.triggered.connect(on_remove_callback)
# remove_action.triggered.connect(self.on_remove.emit())
# self.bg_button.addAction(remove_action)
search_for_tag_action = QAction("Search for Tag", self)
# search_for_tag_action.triggered.connect(on_click_callback)
search_for_tag_action.triggered.connect(self.on_click.emit)
self.bg_button.addAction(search_for_tag_action)
add_to_search_action = QAction("Add to Search", self)
self.bg_button.addAction(add_to_search_action)
# 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.inner_layout = QHBoxLayout()
self.inner_layout.setObjectName("innerLayout")
self.inner_layout.setContentsMargins(2, 2, 2, 2)
# self.inner_layout.setAlignment(Qt.AlignmentFlag.AlignRight)
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(1*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.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.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())
# 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;}}')
# logging.info(tag.color)
if has_remove:
self.remove_button = QPushButton(self)
self.remove_button.setFlat(True)
self.remove_button.setText('')
self.remove_button.setHidden(True)
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.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(1*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"}}"
)
# NOTE: No more edit button! Just make it a right-click option.
# self.edit_button = QPushButton(self)
# self.edit_button.setFlat(True)
# self.edit_button.setText('Edit')
# self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon_128)))
# self.edit_button.setIconSize(QSize(14,14))
# self.edit_button.setHidden(True)
# self.edit_button.setStyleSheet(f'color: {color};'
# f"background: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# # f"color: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# f"border-color: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# f'font-weight: 600;'
# # f"border-color:{'black' if color not in [
# # 'black', 'gray', 'dark gray',
# # 'cool gray', 'warm gray', 'blue',
# # 'purple', 'violet'] else 'white'};"
# # f'QPushButton{{border-image: url(:/images/edit_icon_128.png);}}'
# # f'QPushButton{{border-image: url(:/images/edit_icon_128.png);}}'
# f'border-radius: 4px;'
# # f'border-style:solid;'
# # f'border-width:1px;'
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
# f'padding-bottom: 3px;'
# f'padding-left: 4px;'
# f'font-size: 14px')
# self.edit_button.setMinimumSize(18,18)
# # self.edit_button.setMaximumSize(18,18)
# 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.inner_layout.addWidget(self.edit_button)
if has_remove:
self.inner_layout.addWidget(self.remove_button)
self.inner_layout.addStretch(1)
self.base_layout.addWidget(self.bg_button)
# self.setMinimumSize(self.bg_button.size())
# 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)
# logging.info(tag.color)
if has_remove:
self.remove_button = QPushButton(self)
self.remove_button.setFlat(True)
self.remove_button.setText("")
self.remove_button.setHidden(True)
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.setMinimumSize(50,20)
# NOTE: No more edit button! Just make it a right-click option.
# self.edit_button = QPushButton(self)
# self.edit_button.setFlat(True)
# self.edit_button.setText('Edit')
# self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon_128)))
# self.edit_button.setIconSize(QSize(14,14))
# self.edit_button.setHidden(True)
# self.edit_button.setStyleSheet(f'color: {color};'
# f"background: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# # f"color: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# f"border-color: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};"
# f'font-weight: 600;'
# # f"border-color:{'black' if color not in [
# # 'black', 'gray', 'dark gray',
# # 'cool gray', 'warm gray', 'blue',
# # 'purple', 'violet'] else 'white'};"
# # f'QPushButton{{border-image: url(:/images/edit_icon_128.png);}}'
# # f'QPushButton{{border-image: url(:/images/edit_icon_128.png);}}'
# f'border-radius: 4px;'
# # f'border-style:solid;'
# # f'border-width:1px;'
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
# f'padding-bottom: 3px;'
# f'padding-left: 4px;'
# f'font-size: 14px')
# self.edit_button.setMinimumSize(18,18)
# # self.edit_button.setMaximumSize(18,18)
# def set_name(self, name:str):
# self.bg_label.setText(str)
# self.inner_layout.addWidget(self.edit_button)
if has_remove:
self.inner_layout.addWidget(self.remove_button)
self.inner_layout.addStretch(1)
# def on_remove(self):
# if self.item and self.item[0] == ItemType.ENTRY:
# if self.field_index >= 0:
# self.lib.get_entry(self.item[1]).remove_tag(self.tag.id, self.field_index)
# else:
# self.lib.get_entry(self.item[1]).remove_tag(self.tag.id)
# 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())
# def set_click(self, callback):
# try:
# self.bg_button.clicked.disconnect()
# except RuntimeError:
# pass
# if callback:
# self.bg_button.clicked.connect(callback)
# self.set_click(on_click_callback)
self.bg_button.clicked.connect(self.on_click.emit)
# def set_click(self, function):
# try:
# self.bg.clicked.disconnect()
# except RuntimeError:
# pass
# # self.bg.clicked.connect(lambda checked=False, filepath=filepath: open_file(filepath))
# # self.bg.clicked.connect(function)
def enterEvent(self, event: QEnterEvent) -> None:
if self.has_remove:
self.remove_button.setHidden(False)
# self.edit_button.setHidden(False)
self.update()
return super().enterEvent(event)
# self.setMinimumSize(50,20)
def leaveEvent(self, event: QEvent) -> None:
if self.has_remove:
self.remove_button.setHidden(True)
# self.edit_button.setHidden(True)
self.update()
return super().leaveEvent(event)
# def set_name(self, name:str):
# self.bg_label.setText(str)
# def on_remove(self):
# if self.item and self.item[0] == ItemType.ENTRY:
# if self.field_index >= 0:
# self.lib.get_entry(self.item[1]).remove_tag(self.tag.id, self.field_index)
# else:
# self.lib.get_entry(self.item[1]).remove_tag(self.tag.id)
# def set_click(self, callback):
# try:
# self.bg_button.clicked.disconnect()
# except RuntimeError:
# pass
# if callback:
# self.bg_button.clicked.connect(callback)
# def set_click(self, function):
# try:
# self.bg.clicked.disconnect()
# except RuntimeError:
# pass
# # self.bg.clicked.connect(lambda checked=False, filepath=filepath: open_file(filepath))
# # self.bg.clicked.connect(function)
def enterEvent(self, event: QEnterEvent) -> None:
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:
if self.has_remove:
self.remove_button.setHidden(True)
# self.edit_button.setHidden(True)
self.update()
return super().leaveEvent(event)

View file

@ -17,145 +17,168 @@ from src.qt.modals import BuildTagPanel, TagSearchPanel
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from src.qt.ts_qt import QtDriver
class TagBoxWidget(FieldWidget):
updated = Signal()
def __init__(self, item, title, field_index, library:Library, tags:list[int], driver:'QtDriver') -> None:
super().__init__(title)
# QObject.__init__(self)
self.item = item
self.lib = library
self.driver = driver # Used for creating tag click callbacks that search entries for that tag.
self.field_index = field_index
self.tags:list[int] = tags
self.setObjectName('tagBox')
self.base_layout = FlowLayout()
self.base_layout.setGridEfficiency(False)
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.base_layout)
updated = Signal()
self.add_button = QPushButton()
self.add_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.add_button.setMinimumSize(23, 23)
self.add_button.setMaximumSize(23, 23)
self.add_button.setText('+')
self.add_button.setStyleSheet(
f'QPushButton{{'
# f'background: #1E1A33;'
# f'color: #CDA7F7;'
f'font-weight: bold;'
# f"border-color: #2B2547;"
f'border-radius: 6px;'
f'border-style:solid;'
f'border-width:{math.ceil(1*self.devicePixelRatio())}px;'
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
f'padding-bottom: 5px;'
# f'padding-left: 4px;'
f'font-size: 20px;'
f'}}'
f'QPushButton::hover'
f'{{'
# f'background: #2B2547;'
f'}}')
tsp = TagSearchPanel(self.lib)
tsp.tag_chosen.connect(lambda x: self.add_tag_callback(x))
self.add_modal = PanelModal(tsp, title, 'Add Tags')
self.add_button.clicked.connect(lambda: (tsp.update_tags() ,self.add_modal.show()))
def __init__(
self,
item,
title,
field_index,
library: Library,
tags: list[int],
driver: "QtDriver",
) -> None:
super().__init__(title)
# QObject.__init__(self)
self.item = item
self.lib = library
self.driver = driver # Used for creating tag click callbacks that search entries for that tag.
self.field_index = field_index
self.tags: list[int] = tags
self.setObjectName("tagBox")
self.base_layout = FlowLayout()
self.base_layout.setGridEfficiency(False)
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.base_layout)
self.set_tags(tags)
# self.add_button.setHidden(True)
self.add_button = QPushButton()
self.add_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.add_button.setMinimumSize(23, 23)
self.add_button.setMaximumSize(23, 23)
self.add_button.setText("+")
self.add_button.setStyleSheet(
f"QPushButton{{"
# f'background: #1E1A33;'
# f'color: #CDA7F7;'
f"font-weight: bold;"
# f"border-color: #2B2547;"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width:{math.ceil(1*self.devicePixelRatio())}px;"
# f'padding-top: 1.5px;'
# f'padding-right: 4px;'
f"padding-bottom: 5px;"
# f'padding-left: 4px;'
f"font-size: 20px;"
f"}}"
f"QPushButton::hover"
f"{{"
# f'background: #2B2547;'
f"}}"
)
tsp = TagSearchPanel(self.lib)
tsp.tag_chosen.connect(lambda x: self.add_tag_callback(x))
self.add_modal = PanelModal(tsp, title, "Add Tags")
self.add_button.clicked.connect(
lambda: (tsp.update_tags(), self.add_modal.show())
)
def set_item(self, item):
self.item = item
self.set_tags(tags)
# self.add_button.setHidden(True)
def set_tags(self, tags:list[int]):
logging.info(f'[TAG BOX WIDGET] SET TAGS: T:{tags} for E:{self.item.id}')
is_recycled = False
if self.base_layout.itemAt(0):
# logging.info(type(self.base_layout.itemAt(0).widget()))
while self.base_layout.itemAt(0) and self.base_layout.itemAt(1):
# logging.info(f"I'm deleting { self.base_layout.itemAt(0).widget()}")
self.base_layout.takeAt(0).widget().deleteLater()
is_recycled = True
for tag in tags:
# TODO: Remove space from the special search here (tag_id:x) once that system is finalized.
# tw = TagWidget(self.lib, self.lib.get_tag(tag), True, True,
# on_remove_callback=lambda checked=False, t=tag: (self.lib.get_entry(self.item.id).remove_tag(self.lib, t, self.field_index), self.updated.emit()),
# on_click_callback=lambda checked=False, q=f'tag_id: {tag}': (self.driver.main_window.searchField.setText(q), self.driver.filter_items(q)),
# on_edit_callback=lambda checked=False, t=tag: (self.edit_tag(t))
# )
tw = TagWidget(self.lib, self.lib.get_tag(tag), True, True)
tw.on_click.connect(lambda checked=False, q=f'tag_id: {tag}': (self.driver.main_window.searchField.setText(q), self.driver.filter_items(q)))
tw.on_remove.connect(lambda checked=False, t=tag: (self.remove_tag(t)))
tw.on_edit.connect(lambda checked=False, t=tag: (self.edit_tag(t)))
self.base_layout.addWidget(tw)
self.tags = tags
def set_item(self, item):
self.item = item
# Move or add the '+' button.
if is_recycled:
self.base_layout.addWidget(self.base_layout.takeAt(0).widget())
else:
self.base_layout.addWidget(self.add_button)
# Handles an edge case where there are no more tags and the '+' button
# doesn't move all the way to the left.
if self.base_layout.itemAt(0) and not self.base_layout.itemAt(1):
self.base_layout.update()
def set_tags(self, tags: list[int]):
logging.info(f"[TAG BOX WIDGET] SET TAGS: T:{tags} for E:{self.item.id}")
is_recycled = False
if self.base_layout.itemAt(0):
# logging.info(type(self.base_layout.itemAt(0).widget()))
while self.base_layout.itemAt(0) and self.base_layout.itemAt(1):
# logging.info(f"I'm deleting { self.base_layout.itemAt(0).widget()}")
self.base_layout.takeAt(0).widget().deleteLater()
is_recycled = True
for tag in tags:
# TODO: Remove space from the special search here (tag_id:x) once that system is finalized.
# tw = TagWidget(self.lib, self.lib.get_tag(tag), True, True,
# on_remove_callback=lambda checked=False, t=tag: (self.lib.get_entry(self.item.id).remove_tag(self.lib, t, self.field_index), self.updated.emit()),
# on_click_callback=lambda checked=False, q=f'tag_id: {tag}': (self.driver.main_window.searchField.setText(q), self.driver.filter_items(q)),
# on_edit_callback=lambda checked=False, t=tag: (self.edit_tag(t))
# )
tw = TagWidget(self.lib, self.lib.get_tag(tag), True, True)
tw.on_click.connect(
lambda checked=False, q=f"tag_id: {tag}": (
self.driver.main_window.searchField.setText(q),
self.driver.filter_items(q),
)
)
tw.on_remove.connect(lambda checked=False, t=tag: (self.remove_tag(t)))
tw.on_edit.connect(lambda checked=False, t=tag: (self.edit_tag(t)))
self.base_layout.addWidget(tw)
self.tags = tags
# Move or add the '+' button.
if is_recycled:
self.base_layout.addWidget(self.base_layout.takeAt(0).widget())
else:
self.base_layout.addWidget(self.add_button)
def edit_tag(self, tag_id:int):
btp = BuildTagPanel(self.lib, tag_id)
# btp.on_edit.connect(lambda x: self.edit_tag_callback(x))
self.edit_modal = PanelModal(btp,
self.lib.get_tag(tag_id).display_name(self.lib),
'Edit Tag',
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))
panel: BuildTagPanel = self.edit_modal.widget
self.edit_modal.saved.connect(lambda: self.lib.update_tag(btp.build_tag()))
# panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag))
self.edit_modal.show()
# Handles an edge case where there are no more tags and the '+' button
# doesn't move all the way to the left.
if self.base_layout.itemAt(0) and not self.base_layout.itemAt(1):
self.base_layout.update()
def edit_tag(self, tag_id: int):
btp = BuildTagPanel(self.lib, tag_id)
# btp.on_edit.connect(lambda x: self.edit_tag_callback(x))
self.edit_modal = PanelModal(
btp,
self.lib.get_tag(tag_id).display_name(self.lib),
"Edit Tag",
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))
panel: BuildTagPanel = self.edit_modal.widget
self.edit_modal.saved.connect(lambda: self.lib.update_tag(btp.build_tag()))
# panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag))
self.edit_modal.show()
def add_tag_callback(self, tag_id):
# self.base_layout.addWidget(TagWidget(self.lib, self.lib.get_tag(tag), True))
# self.tags.append(tag)
logging.info(f'[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}')
logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}')
id = list(self.field.keys())[0]
for x in self.driver.selected:
self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=id, field_index=-1)
self.updated.emit()
if tag_id == 0 or tag_id == 1:
self.driver.update_badges()
def add_tag_callback(self, tag_id):
# self.base_layout.addWidget(TagWidget(self.lib, self.lib.get_tag(tag), True))
# self.tags.append(tag)
logging.info(
f"[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}"
)
logging.info(f"[TAG BOX WIDGET] SELECTED T:{self.driver.selected}")
id = list(self.field.keys())[0]
for x in self.driver.selected:
self.driver.lib.get_entry(x[1]).add_tag(
self.driver.lib, tag_id, field_id=id, field_index=-1
)
self.updated.emit()
if tag_id == 0 or tag_id == 1:
self.driver.update_badges()
# if type((x[0]) == ThumbButton):
# # TODO: Remove space from the special search here (tag_id:x) once that system is finalized.
# logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}')
# self.updated.emit()
# if tag_id not in self.tags:
# self.tags.append(tag_id)
# self.set_tags(self.tags)
# elif type((x[0]) == ThumbButton):
# if type((x[0]) == ThumbButton):
# # TODO: Remove space from the special search here (tag_id:x) once that system is finalized.
# logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}')
# self.updated.emit()
# if tag_id not in self.tags:
# self.tags.append(tag_id)
# self.set_tags(self.tags)
# elif type((x[0]) == ThumbButton):
def edit_tag_callback(self, tag:Tag):
self.lib.update_tag(tag)
def remove_tag(self, tag_id):
logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}')
id = list(self.field.keys())[0]
for x in self.driver.selected:
index = self.driver.lib.get_field_index_in_entry(self.driver.lib.get_entry(x[1]),id)
self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id,field_index=index[0])
self.updated.emit()
if tag_id == 0 or tag_id == 1:
self.driver.update_badges()
def edit_tag_callback(self, tag: Tag):
self.lib.update_tag(tag)
# def show_add_button(self, value:bool):
# self.add_button.setHidden(not value)
def remove_tag(self, tag_id):
logging.info(f"[TAG BOX WIDGET] SELECTED T:{self.driver.selected}")
id = list(self.field.keys())[0]
for x in self.driver.selected:
index = self.driver.lib.get_field_index_in_entry(
self.driver.lib.get_entry(x[1]), id
)
self.driver.lib.get_entry(x[1]).remove_tag(
self.driver.lib, tag_id, field_index=index[0]
)
self.updated.emit()
if tag_id == 0 or tag_id == 1:
self.driver.update_badges()
# def show_add_button(self, value:bool):
# self.add_button.setHidden(not value)

View file

@ -9,23 +9,23 @@ from src.qt.widgets import FieldWidget
class TextWidget(FieldWidget):
def __init__(self, title, text: str) -> None:
super().__init__(title)
# self.item = item
self.setObjectName("textBox")
# self.setStyleSheet('background-color:purple;')
self.base_layout = QHBoxLayout()
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.base_layout)
self.text_label = QLabel()
# 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.base_layout.addWidget(self.text_label)
self.set_text(text)
def __init__(self, title, text:str) -> None:
super().__init__(title)
# self.item = item
self.setObjectName('textBox')
# self.setStyleSheet('background-color:purple;')
self.base_layout = QHBoxLayout()
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.base_layout)
self.text_label = QLabel()
# 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.base_layout.addWidget(self.text_label)
self.set_text(text)
def set_text(self, text:str):
self.text_label.setText(text)
def set_text(self, text: str):
self.text_label.setText(text)

View file

@ -9,19 +9,19 @@ from src.qt.widgets import PanelWidget
class EditTextBox(PanelWidget):
def __init__(self, text):
super().__init__()
# self.setLayout()
self.setMinimumSize(480, 480)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,0,6,0)
self.text = text
self.text_edit = QPlainTextEdit()
self.text_edit.setPlainText(text)
self.root_layout.addWidget(self.text_edit)
def get_content(self)-> str:
return self.text_edit.toPlainText()
def reset(self):
self.text_edit.setPlainText(self.text)
def __init__(self, text):
super().__init__()
# self.setLayout()
self.setMinimumSize(480, 480)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.text = text
self.text_edit = QPlainTextEdit()
self.text_edit.setPlainText(text)
self.root_layout.addWidget(self.text_edit)
def get_content(self) -> str:
return self.text_edit.toPlainText()
def reset(self):
self.text_edit.setPlainText(self.text)

View file

@ -9,20 +9,20 @@ from src.qt.widgets import PanelWidget
class EditTextLine(PanelWidget):
def __init__(self, text):
super().__init__()
# self.setLayout()
self.setMinimumWidth(480)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6,0,6,0)
self.text = text
self.text_edit = QLineEdit()
self.text_edit.setText(text)
self.text_edit.returnPressed.connect(self.done.emit)
self.root_layout.addWidget(self.text_edit)
def get_content(self)-> str:
return self.text_edit.text()
def reset(self):
self.text_edit.setText(self.text)
def __init__(self, text):
super().__init__()
# self.setLayout()
self.setMinimumWidth(480)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.text = text
self.text_edit = QLineEdit()
self.text_edit.setText(text)
self.text_edit.returnPressed.connect(self.done.emit)
self.root_layout.addWidget(self.text_edit)
def get_content(self) -> str:
return self.text_edit.text()
def reset(self):
self.text_edit.setText(self.text)

View file

@ -10,64 +10,79 @@ from PySide6.QtWidgets import QWidget, QPushButton
class ThumbButton(QPushButton):
def __init__(self, parent:QWidget, thumb_size:tuple[int,int]) -> None:
super().__init__(parent)
self.thumb_size:tuple[int,int] = thumb_size
self.hovered = False
self.selected = False
def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None:
super().__init__(parent)
self.thumb_size: tuple[int, int] = thumb_size
self.hovered = False
self.selected = False
# self.clicked.connect(lambda checked: self.set_selected(True))
def paintEvent(self, event:QEvent) -> None:
super().paintEvent(event)
if self.hovered or self.selected:
painter = QPainter()
painter.begin(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
path = QPainterPath()
width = 3
radius = 6
path.addRoundedRect(QtCore.QRectF(width/2,width/2,self.thumb_size[0]-width, self.thumb_size[1]-width), radius, radius)
# self.clicked.connect(lambda checked: self.set_selected(True))
# color = QColor('#bb4ff0') if self.selected else QColor('#55bbf6')
# pen = QPen(color, width)
# painter.setPen(pen)
# # brush.setColor(fill)
# painter.drawPath(path)
def paintEvent(self, event: QEvent) -> None:
super().paintEvent(event)
if self.hovered or self.selected:
painter = QPainter()
painter.begin(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
path = QPainterPath()
width = 3
radius = 6
path.addRoundedRect(
QtCore.QRectF(
width / 2,
width / 2,
self.thumb_size[0] - width,
self.thumb_size[1] - width,
),
radius,
radius,
)
if self.selected:
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_HardLight)
color = QColor('#bb4ff0')
color.setAlphaF(0.5)
pen = QPen(color, width)
painter.setPen(pen)
painter.fillPath(path, color)
painter.drawPath(path)
# color = QColor('#bb4ff0') if self.selected else QColor('#55bbf6')
# pen = QPen(color, width)
# painter.setPen(pen)
# # brush.setColor(fill)
# painter.drawPath(path)
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)
color = QColor('#55bbf6')
pen = QPen(color, width)
painter.setPen(pen)
painter.drawPath(path)
painter.end()
def enterEvent(self, event: QEnterEvent) -> None:
self.hovered = True
self.repaint()
return super().enterEvent(event)
if self.selected:
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_HardLight
)
color = QColor("#bb4ff0")
color.setAlphaF(0.5)
pen = QPen(color, width)
painter.setPen(pen)
painter.fillPath(path, color)
painter.drawPath(path)
def leaveEvent(self, event: QEvent) -> None:
self.hovered = False
self.repaint()
return super().leaveEvent(event)
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
)
color = QColor("#55bbf6")
pen = QPen(color, width)
painter.setPen(pen)
painter.drawPath(path)
painter.end()
def set_selected(self, value:bool) -> None:
self.selected = value
self.repaint()
def enterEvent(self, event: QEnterEvent) -> None:
self.hovered = True
self.repaint()
return super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
self.hovered = False
self.repaint()
return super().leaveEvent(event)
def set_selected(self, value: bool) -> None:
self.selected = value
self.repaint()

View file

@ -10,412 +10,493 @@ import os
from pathlib import Path
import cv2
from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance, ImageOps
from PIL import (
Image,
ImageChops,
UnidentifiedImageError,
ImageQt,
ImageDraw,
ImageFont,
ImageEnhance,
ImageOps,
)
from PySide6.QtCore import QObject, Signal, QSize
from PySide6.QtGui import QPixmap
from src.core.ts_core import PLAINTEXT_TYPES, VIDEO_TYPES, IMAGE_TYPES
ERROR = f'[ERROR]'
WARNING = f'[WARNING]'
INFO = f'[INFO]'
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class ThumbRenderer(QObject):
# finished = Signal()
updated = Signal(float, QPixmap, QSize, str)
updated_ratio = Signal(float)
# updatedImage = Signal(QPixmap)
# updatedSize = Signal(QSize)
# finished = Signal()
updated = Signal(float, QPixmap, QSize, str)
updated_ratio = Signal(float)
# updatedImage = Signal(QPixmap)
# updatedSize = Signal(QSize)
thumb_mask_512: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_mask_512.png'))
thumb_mask_512.load()
thumb_mask_512: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_mask_512.png"
)
)
thumb_mask_512.load()
thumb_mask_hl_512: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_mask_hl_512.png'))
thumb_mask_hl_512.load()
thumb_mask_hl_512: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_mask_hl_512.png"
)
)
thumb_mask_hl_512.load()
thumb_loading_512: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_loading_512.png'))
thumb_loading_512.load()
thumb_loading_512: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_loading_512.png"
)
)
thumb_loading_512.load()
thumb_broken_512: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_broken_512.png'))
thumb_broken_512.load()
thumb_broken_512: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_broken_512.png"
)
)
thumb_broken_512.load()
thumb_file_default_512: Image.Image = Image.open(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_file_default_512.png'))
thumb_file_default_512.load()
thumb_file_default_512: Image.Image = Image.open(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/images/thumb_file_default_512.png"
)
)
thumb_file_default_512.load()
# thumb_debug: Image.Image = Image.open(os.path.normpath(
# f'{Path(__file__).parent.parent.parent}/resources/qt/images/temp.jpg'))
# thumb_debug.load()
# thumb_debug: Image.Image = Image.open(os.path.normpath(
# f'{Path(__file__).parent.parent.parent}/resources/qt/images/temp.jpg'))
# thumb_debug.load()
# TODO: Make dynamic font sized given different pixel ratios
font_pixel_ratio: float = 1
ext_font = ImageFont.truetype(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*font_pixel_ratio))
# TODO: Make dynamic font sized given different pixel ratios
font_pixel_ratio: float = 1
ext_font = ImageFont.truetype(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf"
),
math.floor(12 * font_pixel_ratio),
)
def render(self, timestamp: float, filepath, base_size: tuple[int, int], pixelRatio: float, isLoading=False):
"""Renders an entry/element thumbnail for the GUI."""
adj_size: int = 1
image = None
pixmap = None
final = None
extension: str = None
broken_thumb = False
# adj_font_size = math.floor(12 * pixelRatio)
if ThumbRenderer.font_pixel_ratio != pixelRatio:
ThumbRenderer.font_pixel_ratio = pixelRatio
ThumbRenderer.ext_font = ImageFont.truetype(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*ThumbRenderer.font_pixel_ratio))
def render(
self,
timestamp: float,
filepath,
base_size: tuple[int, int],
pixelRatio: float,
isLoading=False,
):
"""Renders an entry/element thumbnail for the GUI."""
adj_size: int = 1
image = None
pixmap = None
final = None
extension: str = None
broken_thumb = False
# adj_font_size = math.floor(12 * pixelRatio)
if ThumbRenderer.font_pixel_ratio != pixelRatio:
ThumbRenderer.font_pixel_ratio = pixelRatio
ThumbRenderer.ext_font = ImageFont.truetype(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf"
),
math.floor(12 * ThumbRenderer.font_pixel_ratio),
)
if isLoading or filepath:
adj_size = math.ceil(base_size[0] * pixelRatio)
if isLoading or filepath:
adj_size = math.ceil(base_size[0] * pixelRatio)
if isLoading:
li: Image.Image = ThumbRenderer.thumb_loading_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
qim = ImageQt.ImageQt(li)
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
elif filepath:
mask: Image.Image = ThumbRenderer.thumb_mask_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR).getchannel(3)
hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
if isLoading:
li: Image.Image = ThumbRenderer.thumb_loading_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
qim = ImageQt.ImageQt(li)
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
elif filepath:
mask: Image.Image = ThumbRenderer.thumb_mask_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
).getchannel(3)
hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
extension = os.path.splitext(filepath)[1][1:].lower()
extension = os.path.splitext(filepath)[1][1:].lower()
try:
# Images =======================================================
if extension in IMAGE_TYPES:
image = Image.open(filepath)
# image = self.thumb_debug
if image.mode == 'RGBA':
# logging.info(image.getchannel(3).tobytes())
new_bg = Image.new('RGB', image.size, color='#222222')
new_bg.paste(image, mask=image.getchannel(3))
image = new_bg
if image.mode != 'RGB':
image = image.convert(mode='RGB')
try:
# Images =======================================================
if extension in IMAGE_TYPES:
image = Image.open(filepath)
# image = self.thumb_debug
if image.mode == "RGBA":
# logging.info(image.getchannel(3).tobytes())
new_bg = Image.new("RGB", image.size, color="#222222")
new_bg.paste(image, mask=image.getchannel(3))
image = new_bg
if image.mode != "RGB":
image = image.convert(mode="RGB")
image = ImageOps.exif_transpose(image)
image = ImageOps.exif_transpose(image)
# Videos =======================================================
elif extension in VIDEO_TYPES:
video = cv2.VideoCapture(filepath)
video.set(cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2))
success, frame = video.read()
if not success:
# Depending on the video format, compression, and frame
# count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(frame)
# Videos =======================================================
elif extension in VIDEO_TYPES:
video = cv2.VideoCapture(filepath)
video.set(
cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
)
success, frame = video.read()
if not success:
# Depending on the video format, compression, and frame
# count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(frame)
# Plain Text ===================================================
elif extension in PLAINTEXT_TYPES:
try:
text: str = extension
with open(filepath, 'r', encoding='utf-8') as text_file:
text = text_file.read(256)
bg = Image.new('RGB',(256,256), color='#222222')
draw = ImageDraw.Draw(bg)
draw.text((16,16), text, file=(255,255,255))
image = bg
except:
logging.info(f'[ThumbRenderer][ERROR]: Coulnd\'t render thumbnail for {filepath}')
# No Rendered Thumbnail ========================================
else:
image = ThumbRenderer.thumb_file_default_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
# Plain Text ===================================================
elif extension in PLAINTEXT_TYPES:
try:
text: str = extension
with open(filepath, "r", encoding="utf-8") as text_file:
text = text_file.read(256)
bg = Image.new("RGB", (256, 256), color="#222222")
draw = ImageDraw.Draw(bg)
draw.text((16, 16), text, file=(255, 255, 255))
image = bg
except:
logging.info(
f"[ThumbRenderer][ERROR]: Coulnd't render thumbnail for {filepath}"
)
# No Rendered Thumbnail ========================================
else:
image = ThumbRenderer.thumb_file_default_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
if not image:
raise UnidentifiedImageError
if not image:
raise UnidentifiedImageError
orig_x, orig_y = image.size
new_x, new_y = (adj_size, adj_size)
if orig_x > orig_y:
new_x = adj_size
new_y = math.ceil(adj_size * (orig_y / orig_x))
elif orig_y > orig_x:
new_y = adj_size
new_x = math.ceil(adj_size * (orig_x / orig_y))
orig_x, orig_y = image.size
new_x, new_y = (adj_size, adj_size)
# img_ratio = new_x / new_y
image = image.resize(
(new_x, new_y), resample=Image.Resampling.BILINEAR)
if orig_x > orig_y:
new_x = adj_size
new_y = math.ceil(adj_size * (orig_y / orig_x))
elif orig_y > orig_x:
new_y = adj_size
new_x = math.ceil(adj_size * (orig_x / orig_y))
if image.size != (adj_size, adj_size):
# Old 1 color method.
# bg_col = image.copy().resize((1, 1)).getpixel((0,0))
# bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
# bg.thumbnail((1, 1))
# bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
# img_ratio = new_x / new_y
image = image.resize((new_x, new_y), resample=Image.Resampling.BILINEAR)
# 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)
if image.size != (adj_size, adj_size):
# Old 1 color method.
# bg_col = image.copy().resize((1, 1)).getpixel((0,0))
# bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
# bg.thumbnail((1, 1))
# bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
# Four-Corner Gradient Background.
# Not exactly a one-liner, but it's (subjectively) really cool.
tl = image.getpixel((0, 0))
tr = image.getpixel(((image.size[0]-1), 0))
bl = image.getpixel((0, (image.size[1]-1)))
br = image.getpixel(((image.size[0]-1), (image.size[1]-1)))
bg = Image.new(mode='RGB', size=(2, 2))
bg.paste(tl, (0, 0, 2, 2))
bg.paste(tr, (1, 0, 2, 2))
bg.paste(bl, (0, 1, 2, 2))
bg.paste(br, (1, 1, 2, 2))
bg = bg.resize((adj_size, adj_size),
resample=Image.Resampling.BICUBIC)
# 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.paste(image, box=(
(adj_size-image.size[0])//2, (adj_size-image.size[1])//2))
# Four-Corner Gradient Background.
# Not exactly a one-liner, but it's (subjectively) really cool.
tl = image.getpixel((0, 0))
tr = image.getpixel(((image.size[0] - 1), 0))
bl = image.getpixel((0, (image.size[1] - 1)))
br = image.getpixel(((image.size[0] - 1), (image.size[1] - 1)))
bg = Image.new(mode="RGB", size=(2, 2))
bg.paste(tl, (0, 0, 2, 2))
bg.paste(tr, (1, 0, 2, 2))
bg.paste(bl, (0, 1, 2, 2))
bg.paste(br, (1, 1, 2, 2))
bg = bg.resize(
(adj_size, adj_size), resample=Image.Resampling.BICUBIC
)
bg.putalpha(mask)
final = bg
bg.paste(
image,
box=(
(adj_size - image.size[0]) // 2,
(adj_size - image.size[1]) // 2,
),
)
else:
image.putalpha(mask)
final = image
bg.putalpha(mask)
final = bg
hl_soft = hl.copy()
hl_soft.putalpha(ImageEnhance.Brightness(
hl.getchannel(3)).enhance(.5))
final.paste(ImageChops.soft_light(final, hl_soft),
mask=hl_soft.getchannel(3))
else:
image.putalpha(mask)
final = image
# hl_add = hl.copy()
# hl_add.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(.25))
# final.paste(hl_add, mask=hl_add.getchannel(3))
hl_soft = hl.copy()
hl_soft.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(0.5))
final.paste(
ImageChops.soft_light(final, hl_soft), mask=hl_soft.getchannel(3)
)
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
broken_thumb = True
final = ThumbRenderer.thumb_broken_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
# hl_add = hl.copy()
# hl_add.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(.25))
# final.paste(hl_add, mask=hl_add.getchannel(3))
qim = ImageQt.ImageQt(final)
if image:
image.close()
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
broken_thumb = True
final = ThumbRenderer.thumb_broken_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
if pixmap:
self.updated.emit(timestamp, pixmap, QSize(*base_size), extension)
qim = ImageQt.ImageQt(final)
if image:
image.close()
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
else:
self.updated.emit(timestamp, QPixmap(),
QSize(*base_size), extension)
if pixmap:
self.updated.emit(timestamp, pixmap, QSize(*base_size), extension)
def render_big(self, timestamp: float, filepath, base_size: tuple[int, int], pixelRatio: float, isLoading=False):
"""Renders a large, non-square entry/element thumbnail for the GUI."""
adj_size: int = 1
image: Image.Image = None
pixmap: QPixmap = None
final: Image.Image = None
extension: str = None
broken_thumb = False
img_ratio = 1
# adj_font_size = math.floor(12 * pixelRatio)
if ThumbRenderer.font_pixel_ratio != pixelRatio:
ThumbRenderer.font_pixel_ratio = pixelRatio
ThumbRenderer.ext_font = ImageFont.truetype(os.path.normpath(
f'{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*ThumbRenderer.font_pixel_ratio))
else:
self.updated.emit(timestamp, QPixmap(), QSize(*base_size), extension)
if isLoading or filepath:
adj_size = math.ceil(max(base_size[0], base_size[1]) * pixelRatio)
def render_big(
self,
timestamp: float,
filepath,
base_size: tuple[int, int],
pixelRatio: float,
isLoading=False,
):
"""Renders a large, non-square entry/element thumbnail for the GUI."""
adj_size: int = 1
image: Image.Image = None
pixmap: QPixmap = None
final: Image.Image = None
extension: str = None
broken_thumb = False
img_ratio = 1
# adj_font_size = math.floor(12 * pixelRatio)
if ThumbRenderer.font_pixel_ratio != pixelRatio:
ThumbRenderer.font_pixel_ratio = pixelRatio
ThumbRenderer.ext_font = ImageFont.truetype(
os.path.normpath(
f"{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf"
),
math.floor(12 * ThumbRenderer.font_pixel_ratio),
)
if isLoading:
adj_size = math.ceil((512 * pixelRatio))
final: Image.Image = ThumbRenderer.thumb_loading_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
qim = ImageQt.ImageQt(final)
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
self.updated_ratio.emit(1)
elif filepath:
# mask: Image.Image = ThumbRenderer.thumb_mask_512.resize(
# (adj_size, adj_size), resample=Image.Resampling.BILINEAR).getchannel(3)
# hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize(
# (adj_size, adj_size), resample=Image.Resampling.BILINEAR)
if isLoading or filepath:
adj_size = math.ceil(max(base_size[0], base_size[1]) * pixelRatio)
extension = os.path.splitext(filepath)[1][1:].lower()
if isLoading:
adj_size = math.ceil((512 * pixelRatio))
final: Image.Image = ThumbRenderer.thumb_loading_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
qim = ImageQt.ImageQt(final)
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
self.updated_ratio.emit(1)
try:
# Images =======================================================
if extension in IMAGE_TYPES:
image = Image.open(filepath)
# image = self.thumb_debug
if image.mode == 'RGBA':
# logging.info(image.getchannel(3).tobytes())
new_bg = Image.new('RGB', image.size, color='#222222')
new_bg.paste(image, mask=image.getchannel(3))
image = new_bg
if image.mode != 'RGB':
image = image.convert(mode='RGB')
image = ImageOps.exif_transpose(image)
elif filepath:
# mask: Image.Image = ThumbRenderer.thumb_mask_512.resize(
# (adj_size, adj_size), resample=Image.Resampling.BILINEAR).getchannel(3)
# hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize(
# (adj_size, adj_size), resample=Image.Resampling.BILINEAR)
# Videos =======================================================
elif extension in VIDEO_TYPES:
video = cv2.VideoCapture(filepath)
video.set(cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2))
success, frame = video.read()
if not success:
# Depending on the video format, compression, and frame
# count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(frame)
# Plain Text ===================================================
elif extension in PLAINTEXT_TYPES:
try:
text: str = extension
with open(filepath, 'r', encoding='utf-8') as text_file:
text = text_file.read(256)
bg = Image.new('RGB',(256,256), color='#222222')
draw = ImageDraw.Draw(bg)
draw.text((16,16), text, file=(255,255,255))
image = bg
except:
logging.info(f'[ThumbRenderer][ERROR]: Coulnd\'t render thumbnail for {filepath}')
# No Rendered Thumbnail ========================================
else:
image = ThumbRenderer.thumb_file_default_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
extension = os.path.splitext(filepath)[1][1:].lower()
if not image:
raise UnidentifiedImageError
try:
# Images =======================================================
if extension in IMAGE_TYPES:
image = Image.open(filepath)
# image = self.thumb_debug
if image.mode == "RGBA":
# logging.info(image.getchannel(3).tobytes())
new_bg = Image.new("RGB", image.size, color="#222222")
new_bg.paste(image, mask=image.getchannel(3))
image = new_bg
if image.mode != "RGB":
image = image.convert(mode="RGB")
orig_x, orig_y = image.size
if orig_x < adj_size and orig_y < adj_size:
new_x, new_y = (adj_size, adj_size)
if orig_x > orig_y:
new_x = adj_size
new_y = math.ceil(adj_size * (orig_y / orig_x))
elif orig_y > orig_x:
new_y = adj_size
new_x = math.ceil(adj_size * (orig_x / orig_y))
else:
new_x, new_y = (adj_size, adj_size)
if orig_x > orig_y:
new_x = adj_size
new_y = math.ceil(adj_size * (orig_y / orig_x))
elif orig_y > orig_x:
new_y = adj_size
new_x = math.ceil(adj_size * (orig_x / orig_y))
image = ImageOps.exif_transpose(image)
self.updated_ratio.emit(new_x / new_y)
image = image.resize(
(new_x, new_y), resample=Image.Resampling.BILINEAR)
# Videos =======================================================
elif extension in VIDEO_TYPES:
video = cv2.VideoCapture(filepath)
video.set(
cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
)
success, frame = video.read()
if not success:
# Depending on the video format, compression, and frame
# count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(frame)
# Plain Text ===================================================
elif extension in PLAINTEXT_TYPES:
try:
text: str = extension
with open(filepath, "r", encoding="utf-8") as text_file:
text = text_file.read(256)
bg = Image.new("RGB", (256, 256), color="#222222")
draw = ImageDraw.Draw(bg)
draw.text((16, 16), text, file=(255, 255, 255))
image = bg
except:
logging.info(
f"[ThumbRenderer][ERROR]: Coulnd't render thumbnail for {filepath}"
)
# No Rendered Thumbnail ========================================
else:
image = ThumbRenderer.thumb_file_default_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
# image = image.resize(
# (new_x, new_y), resample=Image.Resampling.BILINEAR)
if not image:
raise UnidentifiedImageError
# if image.size != (adj_size, adj_size):
# # Old 1 color method.
# # bg_col = image.copy().resize((1, 1)).getpixel((0,0))
# # bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
# # bg.thumbnail((1, 1))
# # bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
orig_x, orig_y = image.size
if orig_x < adj_size and orig_y < adj_size:
new_x, new_y = (adj_size, adj_size)
if orig_x > orig_y:
new_x = adj_size
new_y = math.ceil(adj_size * (orig_y / orig_x))
elif orig_y > orig_x:
new_y = adj_size
new_x = math.ceil(adj_size * (orig_x / orig_y))
else:
new_x, new_y = (adj_size, adj_size)
if orig_x > orig_y:
new_x = adj_size
new_y = math.ceil(adj_size * (orig_y / orig_x))
elif orig_y > orig_x:
new_y = adj_size
new_x = math.ceil(adj_size * (orig_x / orig_y))
# # 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)
self.updated_ratio.emit(new_x / new_y)
image = image.resize((new_x, new_y), resample=Image.Resampling.BILINEAR)
# # Four-Corner Gradient Background.
# # Not exactly a one-liner, but it's (subjectively) really cool.
# tl = image.getpixel((0, 0))
# tr = image.getpixel(((image.size[0]-1), 0))
# bl = image.getpixel((0, (image.size[1]-1)))
# br = image.getpixel(((image.size[0]-1), (image.size[1]-1)))
# bg = Image.new(mode='RGB', size=(2, 2))
# bg.paste(tl, (0, 0, 2, 2))
# bg.paste(tr, (1, 0, 2, 2))
# bg.paste(bl, (0, 1, 2, 2))
# bg.paste(br, (1, 1, 2, 2))
# bg = bg.resize((adj_size, adj_size),
# resample=Image.Resampling.BICUBIC)
# image = image.resize(
# (new_x, new_y), resample=Image.Resampling.BILINEAR)
# bg.paste(image, box=(
# (adj_size-image.size[0])//2, (adj_size-image.size[1])//2))
# if image.size != (adj_size, adj_size):
# # Old 1 color method.
# # bg_col = image.copy().resize((1, 1)).getpixel((0,0))
# # bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
# # bg.thumbnail((1, 1))
# # bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
# bg.putalpha(mask)
# final = bg
# # 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)
# else:
# image.putalpha(mask)
# final = image
# # Four-Corner Gradient Background.
# # Not exactly a one-liner, but it's (subjectively) really cool.
# tl = image.getpixel((0, 0))
# tr = image.getpixel(((image.size[0]-1), 0))
# bl = image.getpixel((0, (image.size[1]-1)))
# br = image.getpixel(((image.size[0]-1), (image.size[1]-1)))
# bg = Image.new(mode='RGB', size=(2, 2))
# bg.paste(tl, (0, 0, 2, 2))
# bg.paste(tr, (1, 0, 2, 2))
# bg.paste(bl, (0, 1, 2, 2))
# bg.paste(br, (1, 1, 2, 2))
# bg = bg.resize((adj_size, adj_size),
# resample=Image.Resampling.BICUBIC)
# hl_soft = hl.copy()
# hl_soft.putalpha(ImageEnhance.Brightness(
# hl.getchannel(3)).enhance(.5))
# final.paste(ImageChops.soft_light(final, hl_soft),
# mask=hl_soft.getchannel(3))
# bg.paste(image, box=(
# (adj_size-image.size[0])//2, (adj_size-image.size[1])//2))
# hl_add = hl.copy()
# hl_add.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(.25))
# final.paste(hl_add, mask=hl_add.getchannel(3))
scalar = 4
rec: Image.Image = Image.new('RGB', tuple(
[d * scalar for d in image.size]), 'black')
draw = ImageDraw.Draw(rec)
draw.rounded_rectangle(
(0, 0)+rec.size, (base_size[0]//32) * scalar * pixelRatio, fill='red')
rec = rec.resize(
tuple([d // scalar for d in rec.size]), resample=Image.Resampling.BILINEAR)
# final = image
final = Image.new('RGBA', image.size, (0, 0, 0, 0))
# logging.info(rec.size)
# logging.info(image.size)
final.paste(image, mask=rec.getchannel(0))
# bg.putalpha(mask)
# final = bg
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
broken_thumb = True
self.updated_ratio.emit(1)
final = ThumbRenderer.thumb_broken_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
# else:
# image.putalpha(mask)
# final = image
# if extension in VIDEO_TYPES + ['gif', 'apng'] or broken_thumb:
# idk = ImageDraw.Draw(final)
# # idk.textlength(file_type)
# ext_offset_x = idk.textlength(
# text=extension.upper(), font=ThumbRenderer.ext_font) / 2
# ext_offset_x = math.floor(ext_offset_x * (1/pixelRatio))
# x_margin = math.floor(
# (adj_size-((base_size[0]//6)+ext_offset_x) * pixelRatio))
# y_margin = math.floor(
# (adj_size-((base_size[0]//8)) * pixelRatio))
# stroke_width = round(2 * pixelRatio)
# fill = 'white' if not broken_thumb else '#E32B41'
# idk.text((x_margin, y_margin), extension.upper(
# ), fill=fill, font=ThumbRenderer.ext_font, stroke_width=stroke_width, stroke_fill=(0, 0, 0))
# hl_soft = hl.copy()
# hl_soft.putalpha(ImageEnhance.Brightness(
# hl.getchannel(3)).enhance(.5))
# final.paste(ImageChops.soft_light(final, hl_soft),
# mask=hl_soft.getchannel(3))
qim = ImageQt.ImageQt(final)
if image:
image.close()
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
# hl_add = hl.copy()
# hl_add.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(.25))
# final.paste(hl_add, mask=hl_add.getchannel(3))
scalar = 4
rec: Image.Image = Image.new(
"RGB", tuple([d * scalar for d in image.size]), "black"
)
draw = ImageDraw.Draw(rec)
draw.rounded_rectangle(
(0, 0) + rec.size,
(base_size[0] // 32) * scalar * pixelRatio,
fill="red",
)
rec = rec.resize(
tuple([d // scalar for d in rec.size]),
resample=Image.Resampling.BILINEAR,
)
# final = image
final = Image.new("RGBA", image.size, (0, 0, 0, 0))
# logging.info(rec.size)
# logging.info(image.size)
final.paste(image, mask=rec.getchannel(0))
if pixmap:
# logging.info(final.size)
# self.updated.emit(pixmap, QSize(*final.size))
self.updated.emit(timestamp, pixmap, QSize(math.ceil(
adj_size * 1/pixelRatio), math.ceil(final.size[1] * 1/pixelRatio)), extension)
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
broken_thumb = True
self.updated_ratio.emit(1)
final = ThumbRenderer.thumb_broken_512.resize(
(adj_size, adj_size), resample=Image.Resampling.BILINEAR
)
else:
self.updated.emit(timestamp, QPixmap(),
QSize(*base_size), extension)
# if extension in VIDEO_TYPES + ['gif', 'apng'] or broken_thumb:
# idk = ImageDraw.Draw(final)
# # idk.textlength(file_type)
# ext_offset_x = idk.textlength(
# text=extension.upper(), font=ThumbRenderer.ext_font) / 2
# ext_offset_x = math.floor(ext_offset_x * (1/pixelRatio))
# x_margin = math.floor(
# (adj_size-((base_size[0]//6)+ext_offset_x) * pixelRatio))
# y_margin = math.floor(
# (adj_size-((base_size[0]//8)) * pixelRatio))
# stroke_width = round(2 * pixelRatio)
# fill = 'white' if not broken_thumb else '#E32B41'
# idk.text((x_margin, y_margin), extension.upper(
# ), fill=fill, font=ThumbRenderer.ext_font, stroke_width=stroke_width, stroke_fill=(0, 0, 0))
qim = ImageQt.ImageQt(final)
if image:
image.close()
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixelRatio)
if pixmap:
# logging.info(final.size)
# self.updated.emit(pixmap, QSize(*final.size))
self.updated.emit(
timestamp,
pixmap,
QSize(
math.ceil(adj_size * 1 / pixelRatio),
math.ceil(final.size[1] * 1 / pixelRatio),
),
extension,
)
else:
self.updated.emit(timestamp, QPixmap(), QSize(*base_size), extension)

View file

@ -17,43 +17,59 @@ def main():
# Parse arguments.
parser = argparse.ArgumentParser()
parser.add_argument('--open', dest='open', type=str,
help='Path to a TagStudio Library folder to open on start.')
parser.add_argument('-o', dest='open', type=str,
help='Path to a TagStudio Library folder to open on start.')
parser.add_argument(
"--open",
dest="open",
type=str,
help="Path to a TagStudio Library folder to open on start.",
)
parser.add_argument(
"-o",
dest="open",
type=str,
help="Path to a TagStudio Library folder to open on start.",
)
# parser.add_argument('--browse', dest='browse', action='store_true',
# help='Jumps to entry browsing on startup.')
# parser.add_argument('--external_preview', dest='external_preview', action='store_true',
# help='Outputs current preview thumbnail to a live-updating file.')
parser.add_argument('--debug', dest='debug', action='store_true',
help='Reveals additional internal data useful for debugging.')
parser.add_argument('--ui', dest='ui', type=str,
help='User interface option for TagStudio. Options: qt, cli (Default: qt)')
parser.add_argument(
"--debug",
dest="debug",
action="store_true",
help="Reveals additional internal data useful for debugging.",
)
parser.add_argument(
"--ui",
dest="ui",
type=str,
help="User interface option for TagStudio. Options: qt, cli (Default: qt)",
)
args = parser.parse_args()
core = TagStudioCore() # The TagStudio Core instance. UI agnostic.
driver = None # The UI driver instance.
ui_name: str = 'unknown' # Display name for the UI, used in logs.
driver = None # The UI driver instance.
ui_name: str = "unknown" # Display name for the UI, used in logs.
# Driver selection based on parameters.
if args.ui and args.ui == 'qt':
if args.ui and args.ui == "qt":
driver = QtDriver(core, args)
ui_name='Qt'
elif args.ui and args.ui == 'cli':
ui_name = "Qt"
elif args.ui and args.ui == "cli":
driver = CliDriver(core, args)
ui_name='CLI'
ui_name = "CLI"
else:
driver = QtDriver(core, args)
ui_name='Qt'
ui_name = "Qt"
# Run the chosen frontend driver.
try:
driver.start()
except Exception:
traceback.print_exc()
print(f'\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...')
print(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
input()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -3,10 +3,16 @@ from src.core.library import Tag
class TestTags:
def test_construction(self):
tag = Tag(id=1, name='Tag Name', shorthand='TN', aliases=[
'First A', 'Second A'], subtags_ids=[2, 3, 4], color='')
assert (tag)
tag = Tag(
id=1,
name="Tag Name",
shorthand="TN",
aliases=["First A", "Second A"],
subtags_ids=[2, 3, 4],
color="",
)
assert tag
def test_empty_construction(self):
tag = Tag(id=1, name='', shorthand='', aliases=[], subtags_ids=[], color='')
assert (tag)
tag = Tag(id=1, name="", shorthand="", aliases=[], subtags_ids=[], color="")
assert tag