mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
feat!: expanded tag color system (#709)
* attempt at adding `TagColor` table * store both columns of `TagColor` inside `Tag` table * fix: fix tag color relationships * refactor: replace TagColor enums with TagColorGroup system * ui: derive tag accent colors from primary * refactor: move dynamic tag color logic, apply to "+" button * feat(ui): add neon tag colors * remove tag text color field * ui: use secondary color for border and text * ui: add TagColorPreview widget * feat(ui): add new tag color selector * ui: add color name tooltips * ui: tweak tag colors and selector * feat: add `namespaces` table * translations: add + update translation keys * tests: update fixtures * chore: code cleanup * ui: add spacing between color groups * fix: expand refactor to create and add button * chore: format with ruff
This commit is contained in:
committed by
GitHub
parent
4c337cb1a3
commit
c06f3bb336
@@ -2,6 +2,7 @@
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Library '{library_dir}'",
|
||||
"color.title.no_color": "No Color",
|
||||
"drop_import.description": "The following files have filenames already exist in the library",
|
||||
"drop_import.duplicates_choice.plural": "The following {count} files have filenames that already exist in the library.",
|
||||
"drop_import.duplicates_choice.singular": "The following file has a filename that already exists in the library.",
|
||||
@@ -88,6 +89,7 @@
|
||||
"generic.filename": "Filename",
|
||||
"generic.navigation.back": "Back",
|
||||
"generic.navigation.next": "Next",
|
||||
"generic.none": "None",
|
||||
"generic.overwrite_alt": "&Overwrite",
|
||||
"generic.overwrite": "Overwrite",
|
||||
"generic.paste": "Paste",
|
||||
@@ -131,7 +133,7 @@
|
||||
"json_migration.heading.paths": "Paths:",
|
||||
"json_migration.heading.shorthands": "Shorthands:",
|
||||
"json_migration.heading.tags": "Tags:",
|
||||
"json_migration.info.description": "Library save files created with TagStudio versions <b>9.4 and below</b> will need to be migrated to the new <b>v9.5+</b> format.<br><h2>What you need to know:</h2><ul><li>Your existing library save file will <b><i>NOT</i></b> be deleted</li><li>Your personal files will <b><i>NOT</i></b> be deleted, moved, or modified</li><li>The new v9.5+ save format can not be opened in earlier versions of TagStudio</li></ul>",
|
||||
"json_migration.info.description": "Library save files created with TagStudio versions <b>9.4 and below</b> will need to be migrated to the new <b>v9.5+</b> format.<br><h2>What you need to know:</h2><ul><li>Your existing library save file will <b><i>NOT</i></b> be deleted</li><li>Your personal files will <b><i>NOT</i></b> be deleted, moved, or modified</li><li>The new v9.5+ save format can not be opened in earlier versions of TagStudio</li></ul><h3>What's changed:</h3><ul><li>\"Tag Fields\" have been replaced by \"Tags Categories\". Instead of adding tags to fields first, tags now get added directly to file entries. They're then automatically organized into categories based on parent tags marked with the new \"Is Category\" property in the tag editing menu. Any tag can be marked as a category, and child tags will sort themselves underneath parent tags marked as categories. The \"Favorite\" and \"Archived\" tags now inherit from a new \"Meta Tags\" tag which is marked as a category by default.</li><li>Tag colors have been tweaked and expanded upon. Some colors have been renamed or consolidated, however all tag colors will still convert to exact or close matches in v9.5.</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "Migrating {entries:,d} File Entries...",
|
||||
"json_migration.migration_complete_with_discrepancies": "Migration Complete, Discrepancies Found",
|
||||
"json_migration.migration_complete": "Migration Complete!",
|
||||
@@ -202,6 +204,7 @@
|
||||
"tag.add.plural": "Add Tags",
|
||||
"tag.add": "Add Tag",
|
||||
"tag.aliases": "Aliases",
|
||||
"tag.choose_color": "Choose Tag Color",
|
||||
"tag.color": "Color",
|
||||
"tag.confirm_delete": "Are you sure you want to delete the tag \"{tag_name}\"?",
|
||||
"tag.create": "Create Tag",
|
||||
|
||||
@@ -71,4 +71,4 @@ class LibraryPrefs(DefaultEnum):
|
||||
IS_EXCLUDE_LIST = True
|
||||
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
|
||||
PAGE_SIZE: int = 500
|
||||
DB_VERSION: int = 3
|
||||
DB_VERSION: int = 4
|
||||
|
||||
@@ -49,8 +49,8 @@ def make_tables(engine: Engine) -> None:
|
||||
if not autoincrement_val or autoincrement_val <= RESERVED_TAG_END:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO tags (id, name, color, is_category) VALUES "
|
||||
f"({RESERVED_TAG_END}, 'temp', 1, false)"
|
||||
"INSERT INTO tags (id, name, color_namespace, color_slug, is_category) VALUES "
|
||||
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false)"
|
||||
)
|
||||
)
|
||||
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))
|
||||
|
||||
539
tagstudio/src/core/library/alchemy/default_color_groups.py
Normal file
539
tagstudio/src/core/library/alchemy/default_color_groups.py
Normal file
@@ -0,0 +1,539 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import structlog
|
||||
|
||||
from .models import Namespace, TagColorGroup
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def namespaces() -> list[Namespace]:
|
||||
tagstudio_standard = Namespace("tagstudio-standard", "TagStudio Standard")
|
||||
tagstudio_pastels = Namespace("tagstudio-pastels", "TagStudio Pastels")
|
||||
tagstudio_shades = Namespace("tagstudio-shades", "TagStudio Shades")
|
||||
tagstudio_earth_tones = Namespace("tagstudio-earth-tones", "TagStudio Earth Tones")
|
||||
tagstudio_grayscale = Namespace("tagstudio-grayscale", "TagStudio Grayscale")
|
||||
tagstudio_neon = Namespace("tagstudio-neon", "TagStudio Neon")
|
||||
return [
|
||||
tagstudio_standard,
|
||||
tagstudio_pastels,
|
||||
tagstudio_shades,
|
||||
tagstudio_earth_tones,
|
||||
tagstudio_grayscale,
|
||||
tagstudio_neon,
|
||||
]
|
||||
|
||||
|
||||
def json_to_sql_color(json_color: str) -> tuple[str | None, str | None]:
|
||||
"""Convert a color string from a <=9.4 JSON library to a 9.5+ (namespace, slug) tuple."""
|
||||
json_color_ = json_color.lower()
|
||||
match json_color_:
|
||||
case "black":
|
||||
return ("tagstudio-grayscale", "black")
|
||||
case "dark gray":
|
||||
return ("tagstudio-grayscale", "dark-gray")
|
||||
case "gray":
|
||||
return ("tagstudio-grayscale", "gray")
|
||||
case "light gray":
|
||||
return ("tagstudio-grayscale", "light-gray")
|
||||
case "white":
|
||||
return ("tagstudio-grayscale", "white")
|
||||
case "light pink":
|
||||
return ("tagstudio-pastels", "light-pink")
|
||||
case "pink":
|
||||
return ("tagstudio-standard", "pink")
|
||||
case "magenta":
|
||||
return ("tagstudio-standard", "magenta")
|
||||
case "red":
|
||||
return ("tagstudio-standard", "red")
|
||||
case "red orange":
|
||||
return ("tagstudio-standard", "red-orange")
|
||||
case "salmon":
|
||||
return ("tagstudio-pastels", "salmon")
|
||||
case "orange":
|
||||
return ("tagstudio-standard", "orange")
|
||||
case "yellow orange":
|
||||
return ("tagstudio-standard", "amber")
|
||||
case "yellow":
|
||||
return ("tagstudio-standard", "yellow")
|
||||
case "mint":
|
||||
return ("tagstudio-pastels", "mint")
|
||||
case "lime":
|
||||
return ("tagstudio-standard", "lime")
|
||||
case "light green":
|
||||
return ("tagstudio-pastels", "light-green")
|
||||
case "green":
|
||||
return ("tagstudio-standard", "green")
|
||||
case "teal":
|
||||
return ("tagstudio-standard", "teal")
|
||||
case "cyan":
|
||||
return ("tagstudio-standard", "cyan")
|
||||
case "light blue":
|
||||
return ("tagstudio-pastels", "light-blue")
|
||||
case "blue":
|
||||
return ("tagstudio-standard", "blue")
|
||||
case "blue violet":
|
||||
return ("tagstudio-shades", "navy")
|
||||
case "violet":
|
||||
return ("tagstudio-standard", "indigo")
|
||||
case "purple":
|
||||
return ("tagstudio-standard", "purple")
|
||||
case "peach":
|
||||
return ("tagstudio-earth-tones", "peach")
|
||||
case "brown":
|
||||
return ("tagstudio-earth-tones", "brown")
|
||||
case "lavender":
|
||||
return ("tagstudio-pastels", "lavender")
|
||||
case "blonde":
|
||||
return ("tagstudio-earth-tones", "blonde")
|
||||
case "auburn":
|
||||
return ("tagstudio-shades", "auburn")
|
||||
case "light brown":
|
||||
return ("tagstudio-earth-tones", "light-brown")
|
||||
case "dark brown":
|
||||
return ("tagstudio-earth-tones", "dark-brown")
|
||||
case "cool gray":
|
||||
return ("tagstudio-earth-tones", "cool-gray")
|
||||
case "warm gray":
|
||||
return ("tagstudio-earth-tones", "warm-gray")
|
||||
case "olive":
|
||||
return ("tagstudio-shades", "olive")
|
||||
case "berry":
|
||||
return ("tagstudio-shades", "berry")
|
||||
case _:
|
||||
return (None, None)
|
||||
|
||||
|
||||
def standard() -> list[TagColorGroup]:
|
||||
red = TagColorGroup(
|
||||
slug="red",
|
||||
namespace="tagstudio-standard",
|
||||
name="Red",
|
||||
primary="#E22C3C",
|
||||
)
|
||||
red_orange = TagColorGroup(
|
||||
slug="red-orange",
|
||||
namespace="tagstudio-standard",
|
||||
name="Red Orange",
|
||||
primary="#E83726",
|
||||
)
|
||||
orange = TagColorGroup(
|
||||
slug="orange",
|
||||
namespace="tagstudio-standard",
|
||||
name="Orange",
|
||||
primary="#ED6022",
|
||||
)
|
||||
amber = TagColorGroup(
|
||||
slug="amber",
|
||||
namespace="tagstudio-standard",
|
||||
name="Amber",
|
||||
primary="#FA9A2C",
|
||||
)
|
||||
yellow = TagColorGroup(
|
||||
slug="yellow",
|
||||
namespace="tagstudio-standard",
|
||||
name="Yellow",
|
||||
primary="#FFD63D",
|
||||
)
|
||||
lime = TagColorGroup(
|
||||
slug="lime",
|
||||
namespace="tagstudio-standard",
|
||||
name="Lime",
|
||||
primary="#92E649",
|
||||
)
|
||||
green = TagColorGroup(
|
||||
slug="green",
|
||||
namespace="tagstudio-standard",
|
||||
name="Green",
|
||||
primary="#45D649",
|
||||
)
|
||||
teal = TagColorGroup(
|
||||
slug="teal",
|
||||
namespace="tagstudio-standard",
|
||||
name="Teal",
|
||||
primary="#22D589",
|
||||
)
|
||||
cyan = TagColorGroup(
|
||||
slug="cyan",
|
||||
namespace="tagstudio-standard",
|
||||
name="Cyan",
|
||||
primary="#3DDBDB",
|
||||
)
|
||||
blue = TagColorGroup(
|
||||
slug="blue",
|
||||
namespace="tagstudio-standard",
|
||||
name="Blue",
|
||||
primary="#3B87F0",
|
||||
)
|
||||
indigo = TagColorGroup(
|
||||
slug="indigo",
|
||||
namespace="tagstudio-standard",
|
||||
name="Indigo",
|
||||
primary="#874FF5",
|
||||
)
|
||||
purple = TagColorGroup(
|
||||
slug="purple",
|
||||
namespace="tagstudio-standard",
|
||||
name="Purple",
|
||||
primary="#BB4FF0",
|
||||
)
|
||||
magenta = TagColorGroup(
|
||||
slug="magenta",
|
||||
namespace="tagstudio-standard",
|
||||
name="Magenta",
|
||||
primary="#F64680",
|
||||
)
|
||||
pink = TagColorGroup(
|
||||
slug="pink",
|
||||
namespace="tagstudio-standard",
|
||||
name="Pink",
|
||||
primary="#FF62AF",
|
||||
)
|
||||
return [
|
||||
red,
|
||||
red_orange,
|
||||
orange,
|
||||
amber,
|
||||
yellow,
|
||||
lime,
|
||||
green,
|
||||
teal,
|
||||
cyan,
|
||||
blue,
|
||||
indigo,
|
||||
purple,
|
||||
pink,
|
||||
magenta,
|
||||
]
|
||||
|
||||
|
||||
def pastels() -> list[TagColorGroup]:
|
||||
coral = TagColorGroup(
|
||||
slug="coral",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Coral",
|
||||
primary="#F2525F",
|
||||
)
|
||||
salmon = TagColorGroup(
|
||||
slug="salmon",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Salmon",
|
||||
primary="#F66348",
|
||||
)
|
||||
light_orange = TagColorGroup(
|
||||
slug="light-orange",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Orange",
|
||||
primary="#FF9450",
|
||||
)
|
||||
light_amber = TagColorGroup(
|
||||
slug="light-amber",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Amber",
|
||||
primary="#FFBA57",
|
||||
)
|
||||
light_yellow = TagColorGroup(
|
||||
slug="light-yellow",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Yellow",
|
||||
primary="#FFE173",
|
||||
)
|
||||
light_lime = TagColorGroup(
|
||||
slug="light-lime",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Lime",
|
||||
primary="#C9FF7A",
|
||||
)
|
||||
light_green = TagColorGroup(
|
||||
slug="light-green",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Green",
|
||||
primary="#81FF76",
|
||||
)
|
||||
mint = TagColorGroup(
|
||||
slug="mint",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Mint",
|
||||
primary="#68FFB4",
|
||||
)
|
||||
sky_blue = TagColorGroup(
|
||||
slug="sky-blue",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Sky Blue",
|
||||
primary="#8EFFF4",
|
||||
)
|
||||
light_blue = TagColorGroup(
|
||||
slug="light-blue",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Blue",
|
||||
primary="#64C6FF",
|
||||
)
|
||||
lavender = TagColorGroup(
|
||||
slug="lavender",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Lavender",
|
||||
primary="#908AF6",
|
||||
)
|
||||
lilac = TagColorGroup(
|
||||
slug="lilac",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Lilac",
|
||||
primary="#DF95FF",
|
||||
)
|
||||
light_pink = TagColorGroup(
|
||||
slug="light-pink",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Pink",
|
||||
primary="#FF87BA",
|
||||
)
|
||||
return [
|
||||
coral,
|
||||
salmon,
|
||||
light_orange,
|
||||
light_amber,
|
||||
light_yellow,
|
||||
light_lime,
|
||||
light_green,
|
||||
mint,
|
||||
sky_blue,
|
||||
light_blue,
|
||||
lavender,
|
||||
lilac,
|
||||
light_pink,
|
||||
]
|
||||
|
||||
|
||||
def shades() -> list[TagColorGroup]:
|
||||
auburn = TagColorGroup(
|
||||
slug="auburn",
|
||||
namespace="tagstudio-shades",
|
||||
name="Auburn",
|
||||
primary="#A13220",
|
||||
)
|
||||
olive = TagColorGroup(
|
||||
slug="olive",
|
||||
namespace="tagstudio-shades",
|
||||
name="Olive",
|
||||
primary="#4C652E",
|
||||
)
|
||||
navy = TagColorGroup(
|
||||
slug="navy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Navy",
|
||||
primary="#104B98",
|
||||
)
|
||||
berry = TagColorGroup(
|
||||
slug="berry",
|
||||
namespace="tagstudio-shades",
|
||||
name="Berry",
|
||||
primary="#9F2AA7",
|
||||
)
|
||||
return [auburn, olive, navy, berry]
|
||||
|
||||
|
||||
def earth_tones() -> list[TagColorGroup]:
|
||||
dark_brown = TagColorGroup(
|
||||
slug="dark-brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Dark Brown",
|
||||
primary="#4C2315",
|
||||
)
|
||||
brown = TagColorGroup(
|
||||
slug="brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Brown",
|
||||
primary="#823216",
|
||||
)
|
||||
light_brown = TagColorGroup(
|
||||
slug="light-brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Light Brown",
|
||||
primary="#BE5B2D",
|
||||
)
|
||||
blonde = TagColorGroup(
|
||||
slug="blonde",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Blonde",
|
||||
primary="#EFC664",
|
||||
)
|
||||
peach = TagColorGroup(
|
||||
slug="peach",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Peach",
|
||||
primary="#F1C69C",
|
||||
)
|
||||
warm_gray = TagColorGroup(
|
||||
slug="warm-gray",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Warm Gray",
|
||||
primary="#625550",
|
||||
)
|
||||
cool_gray = TagColorGroup(
|
||||
slug="cool-gray",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Cool Gray",
|
||||
primary="#515768",
|
||||
)
|
||||
return [dark_brown, brown, light_brown, blonde, peach, warm_gray, cool_gray]
|
||||
|
||||
|
||||
def grayscale() -> list[TagColorGroup]:
|
||||
black = TagColorGroup(
|
||||
slug="black",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Black",
|
||||
primary="#111018",
|
||||
)
|
||||
dark_gray = TagColorGroup(
|
||||
slug="dark-gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Dark Gray",
|
||||
primary="#242424",
|
||||
)
|
||||
gray = TagColorGroup(
|
||||
slug="gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Gray",
|
||||
primary="#53525A",
|
||||
)
|
||||
light_gray = TagColorGroup(
|
||||
slug="light-gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Light Gray",
|
||||
primary="#AAAAAA",
|
||||
)
|
||||
white = TagColorGroup(
|
||||
slug="white",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="White",
|
||||
primary="#F2F1F8",
|
||||
)
|
||||
return [black, dark_gray, gray, light_gray, white]
|
||||
|
||||
|
||||
def neon() -> list[TagColorGroup]:
|
||||
neon_red = TagColorGroup(
|
||||
slug="neon-red",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Red",
|
||||
primary="#180607",
|
||||
secondary="#E22C3C",
|
||||
)
|
||||
neon_red_orange = TagColorGroup(
|
||||
slug="neon-red-orange",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Red Orange",
|
||||
primary="#220905",
|
||||
secondary="#E83726",
|
||||
)
|
||||
neon_orange = TagColorGroup(
|
||||
slug="neon-orange",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Orange",
|
||||
primary="#1F0D05",
|
||||
secondary="#ED6022",
|
||||
)
|
||||
neon_amber = TagColorGroup(
|
||||
slug="neon-amber",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Amber",
|
||||
primary="#251507",
|
||||
secondary="#FA9A2C",
|
||||
)
|
||||
neon_yellow = TagColorGroup(
|
||||
slug="neon-yellow",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Yellow",
|
||||
primary="#2B1C0B",
|
||||
secondary="#FFD63D",
|
||||
)
|
||||
neon_lime = TagColorGroup(
|
||||
slug="neon-lime",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Lime",
|
||||
primary="#1B220C",
|
||||
secondary="#92E649",
|
||||
)
|
||||
neon_green = TagColorGroup(
|
||||
slug="neon-green",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Green",
|
||||
primary="#091610",
|
||||
secondary="#45D649",
|
||||
)
|
||||
neon_teal = TagColorGroup(
|
||||
slug="neon-teal",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Teal",
|
||||
primary="#09191D",
|
||||
secondary="#22D589",
|
||||
)
|
||||
neon_cyan = TagColorGroup(
|
||||
slug="neon-cyan",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Cyan",
|
||||
primary="#0B191C",
|
||||
secondary="#3DDBDB",
|
||||
)
|
||||
neon_blue = TagColorGroup(
|
||||
slug="neon-blue",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Blue",
|
||||
primary="#09101C",
|
||||
secondary="#3B87F0",
|
||||
)
|
||||
neon_indigo = TagColorGroup(
|
||||
slug="neon-indigo",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Indigo",
|
||||
primary="#150B24",
|
||||
secondary="#874FF5",
|
||||
)
|
||||
neon_purple = TagColorGroup(
|
||||
slug="neon-purple",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Purple",
|
||||
primary="#1E0B26",
|
||||
secondary="#BB4FF0",
|
||||
)
|
||||
neon_magenta = TagColorGroup(
|
||||
slug="neon-magenta",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Magenta",
|
||||
primary="#220A13",
|
||||
secondary="#F64680",
|
||||
)
|
||||
neon_pink = TagColorGroup(
|
||||
slug="neon-pink",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Pink",
|
||||
primary="#210E15",
|
||||
secondary="#FF62AF",
|
||||
)
|
||||
neon_white = TagColorGroup(
|
||||
slug="neon-white",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon White",
|
||||
primary="#131315",
|
||||
secondary="#F2F1F8",
|
||||
)
|
||||
return [
|
||||
neon_red,
|
||||
neon_red_orange,
|
||||
neon_orange,
|
||||
neon_amber,
|
||||
neon_yellow,
|
||||
neon_lime,
|
||||
neon_green,
|
||||
neon_teal,
|
||||
neon_cyan,
|
||||
neon_blue,
|
||||
neon_indigo,
|
||||
neon_purple,
|
||||
neon_pink,
|
||||
neon_magenta,
|
||||
neon_white,
|
||||
]
|
||||
@@ -8,7 +8,7 @@ from src.core.query_lang import Constraint, ConstraintType, Parser
|
||||
MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140
|
||||
|
||||
|
||||
class TagColor(enum.IntEnum):
|
||||
class TagColorEnum(enum.IntEnum):
|
||||
DEFAULT = 1
|
||||
BLACK = 2
|
||||
DARK_GRAY = 3
|
||||
@@ -48,11 +48,11 @@ class TagColor(enum.IntEnum):
|
||||
OLIVE = 37
|
||||
|
||||
@staticmethod
|
||||
def get_color_from_str(color_name: str) -> "TagColor":
|
||||
for color in TagColor:
|
||||
def get_color_from_str(color_name: str) -> "TagColorEnum":
|
||||
for color in TagColorEnum:
|
||||
if color.name == color_name.upper().replace(" ", "_"):
|
||||
return color
|
||||
return TagColor.DEFAULT
|
||||
return TagColorEnum.DEFAULT
|
||||
|
||||
|
||||
class ItemType(enum.Enum):
|
||||
|
||||
@@ -53,8 +53,9 @@ from ...constants import (
|
||||
TS_FOLDER_NAME,
|
||||
)
|
||||
from ...enums import LibraryPrefs
|
||||
from . import default_color_groups
|
||||
from .db import make_tables
|
||||
from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum, TagColor
|
||||
from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum
|
||||
from .fields import (
|
||||
BaseField,
|
||||
DatetimeField,
|
||||
@@ -62,7 +63,7 @@ from .fields import (
|
||||
_FieldID,
|
||||
)
|
||||
from .joins import TagEntry, TagParent
|
||||
from .models import Entry, Folder, Preferences, Tag, TagAlias, ValueType
|
||||
from .models import Entry, Folder, Namespace, Preferences, Tag, TagAlias, TagColorGroup, ValueType
|
||||
from .visitors import SQLBoolExpressionBuilder
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -93,7 +94,8 @@ def get_default_tags() -> tuple[Tag, ...]:
|
||||
name="Archived",
|
||||
aliases={TagAlias(name="Archive")},
|
||||
parent_tags={meta_tag},
|
||||
color=TagColor.RED,
|
||||
color_slug="red",
|
||||
color_namespace="tagstudio-standard",
|
||||
)
|
||||
favorite_tag = Tag(
|
||||
id=TAG_FAVORITE,
|
||||
@@ -103,7 +105,8 @@ def get_default_tags() -> tuple[Tag, ...]:
|
||||
TagAlias(name="Favorites"),
|
||||
},
|
||||
parent_tags={meta_tag},
|
||||
color=TagColor.YELLOW,
|
||||
color_slug="yellow",
|
||||
color_namespace="tagstudio-standard",
|
||||
)
|
||||
|
||||
return archive_tag, favorite_tag, meta_tag
|
||||
@@ -179,18 +182,23 @@ class Library:
|
||||
|
||||
# Tags
|
||||
for tag in json_lib.tags:
|
||||
color_namespace, color_slug = default_color_groups.json_to_sql_color(tag.color)
|
||||
self.add_tag(
|
||||
Tag(
|
||||
id=tag.id,
|
||||
name=tag.name,
|
||||
shorthand=tag.shorthand,
|
||||
color=TagColor.get_color_from_str(tag.color),
|
||||
color_namespace=color_namespace,
|
||||
color_slug=color_slug,
|
||||
)
|
||||
)
|
||||
# Apply user edits to built-in JSON tags.
|
||||
if tag.id in range(RESERVED_TAG_START, RESERVED_TAG_END + 1):
|
||||
updated_tag = self.get_tag(tag.id)
|
||||
updated_tag.color = TagColor.get_color_from_str(tag.color)
|
||||
if not updated_tag:
|
||||
continue
|
||||
updated_tag.color_namespace = color_namespace
|
||||
updated_tag.color_slug = color_slug
|
||||
self.update_tag(updated_tag) # NOTE: This just calls add_tag?
|
||||
|
||||
# Tag Aliases
|
||||
@@ -292,7 +300,34 @@ class Library:
|
||||
with Session(self.engine) as session:
|
||||
make_tables(self.engine)
|
||||
|
||||
# Add default tags to new libraries only.
|
||||
# TODO: Determine a good way of updating built-in data after updates.
|
||||
|
||||
# Add default tag color namespaces.
|
||||
if is_new:
|
||||
namespaces = default_color_groups.namespaces()
|
||||
try:
|
||||
session.add_all(namespaces)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
logger.error("[Library] Couldn't add default tag color namespaces", error=e)
|
||||
session.rollback()
|
||||
|
||||
# Add default tag colors.
|
||||
if is_new:
|
||||
tag_colors: list[TagColorGroup] = default_color_groups.standard()
|
||||
tag_colors += default_color_groups.pastels()
|
||||
tag_colors += default_color_groups.shades()
|
||||
tag_colors += default_color_groups.grayscale()
|
||||
tag_colors += default_color_groups.earth_tones()
|
||||
tag_colors += default_color_groups.neon()
|
||||
try:
|
||||
session.add_all(tag_colors)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
logger.error("[Library] Couldn't add default tag colors", error=e)
|
||||
session.rollback()
|
||||
|
||||
# Add default tags.
|
||||
if is_new:
|
||||
tags = get_default_tags()
|
||||
try:
|
||||
@@ -981,10 +1016,12 @@ class Library:
|
||||
|
||||
return target_path
|
||||
|
||||
def get_tag(self, tag_id: int) -> Tag:
|
||||
def get_tag(self, tag_id: int) -> Tag | None:
|
||||
with Session(self.engine) as session:
|
||||
tags_query = select(Tag).options(
|
||||
selectinload(Tag.parent_tags), selectinload(Tag.aliases)
|
||||
selectinload(Tag.parent_tags),
|
||||
selectinload(Tag.aliases),
|
||||
joinedload(Tag.color),
|
||||
)
|
||||
tag = session.scalar(tags_query.where(Tag.id == tag_id))
|
||||
|
||||
@@ -1006,12 +1043,19 @@ class Library:
|
||||
)
|
||||
return session.scalar(statement)
|
||||
|
||||
def get_alias(self, tag_id: int, alias_id: int) -> TagAlias:
|
||||
def get_alias(self, tag_id: int, alias_id: int) -> TagAlias | None:
|
||||
with Session(self.engine) as session:
|
||||
alias_query = select(TagAlias).where(TagAlias.id == alias_id, TagAlias.tag_id == tag_id)
|
||||
alias = session.scalar(alias_query.where(TagAlias.id == alias_id))
|
||||
|
||||
return alias
|
||||
return session.scalar(alias_query.where(TagAlias.id == alias_id))
|
||||
|
||||
def get_tag_color(self, slug: str, namespace: str) -> TagColorGroup | None:
|
||||
with Session(self.engine) as session:
|
||||
statement = select(TagColorGroup).where(
|
||||
and_(TagColorGroup.slug == slug, TagColorGroup.namespace == namespace)
|
||||
)
|
||||
|
||||
return session.scalar(statement)
|
||||
|
||||
def add_parent_tag(self, parent_id: int, child_id: int) -> bool:
|
||||
if parent_id == child_id:
|
||||
@@ -1144,3 +1188,24 @@ class Library:
|
||||
field_id=field.type_key,
|
||||
value=field.value,
|
||||
)
|
||||
|
||||
@property
|
||||
def tag_color_groups(self) -> dict[str, list[TagColorGroup]]:
|
||||
"""Return every TagColorGroup in the library."""
|
||||
with Session(self.engine) as session:
|
||||
color_groups: dict[str, list[TagColorGroup]] = {}
|
||||
results = session.scalars(select(TagColorGroup).order_by(asc(TagColorGroup.namespace)))
|
||||
for color in results:
|
||||
if not color_groups.get(color.namespace):
|
||||
color_groups[color.namespace] = []
|
||||
color_groups[color.namespace].append(color)
|
||||
session.expunge(color)
|
||||
return color_groups
|
||||
|
||||
def get_namespace_name(self, namespace: str) -> str:
|
||||
with Session(self.engine) as session:
|
||||
result = session.scalar(select(Namespace).where(Namespace.namespace == namespace))
|
||||
if result:
|
||||
session.expunge(result)
|
||||
|
||||
return "" if not result else result.name
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, Integer, event
|
||||
from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ...constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from .db import Base, PathType
|
||||
from .enums import TagColor
|
||||
from .fields import (
|
||||
BaseField,
|
||||
BooleanField,
|
||||
@@ -20,6 +19,22 @@ from .fields import (
|
||||
from .joins import TagParent
|
||||
|
||||
|
||||
class Namespace(Base):
|
||||
__tablename__ = "namespaces"
|
||||
|
||||
namespace: Mapped[str] = mapped_column(primary_key=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
name: str,
|
||||
):
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
super().__init__()
|
||||
|
||||
|
||||
class TagAlias(Base):
|
||||
__tablename__ = "tag_aliases"
|
||||
|
||||
@@ -37,20 +52,47 @@ class TagAlias(Base):
|
||||
super().__init__()
|
||||
|
||||
|
||||
class TagColorGroup(Base):
|
||||
__tablename__ = "tag_colors"
|
||||
|
||||
slug: Mapped[str] = mapped_column(primary_key=True, nullable=False)
|
||||
namespace: Mapped[str] = mapped_column(
|
||||
ForeignKey("namespaces.namespace"), primary_key=True, nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column()
|
||||
primary: Mapped[str] = mapped_column(nullable=False)
|
||||
secondary: Mapped[str | None]
|
||||
|
||||
# TODO: Determine if slug and namespace can be optional and generated/added here if needed.
|
||||
def __init__(
|
||||
self,
|
||||
slug: str,
|
||||
namespace: str,
|
||||
name: str,
|
||||
primary: str,
|
||||
secondary: str | None = None,
|
||||
):
|
||||
self.slug = slug
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
self.primary = primary
|
||||
if secondary:
|
||||
self.secondary = secondary
|
||||
super().__init__()
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
__tablename__ = "tags"
|
||||
__table_args__ = {"sqlite_autoincrement": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
name: Mapped[str]
|
||||
shorthand: Mapped[str | None]
|
||||
color: Mapped[TagColor]
|
||||
color_namespace: Mapped[str | None] = mapped_column()
|
||||
color_slug: Mapped[str | None] = mapped_column()
|
||||
color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
|
||||
is_category: Mapped[bool]
|
||||
icon: Mapped[str | None]
|
||||
|
||||
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
|
||||
|
||||
parent_tags: Mapped[set["Tag"]] = relationship(
|
||||
secondary=TagParent.__tablename__,
|
||||
primaryjoin="Tag.id == TagParent.parent_id",
|
||||
@@ -58,6 +100,13 @@ class Tag(Base):
|
||||
back_populates="parent_tags",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
[color_namespace, color_slug], [TagColorGroup.namespace, TagColorGroup.slug]
|
||||
),
|
||||
{"sqlite_autoincrement": True},
|
||||
)
|
||||
|
||||
@property
|
||||
def parent_ids(self) -> list[int]:
|
||||
return [tag.id for tag in self.parent_tags]
|
||||
@@ -78,13 +127,15 @@ class Tag(Base):
|
||||
aliases: set[TagAlias] | None = None,
|
||||
parent_tags: set["Tag"] | None = None,
|
||||
icon: str | None = None,
|
||||
color: TagColor = TagColor.DEFAULT,
|
||||
color_namespace: str | None = None,
|
||||
color_slug: str | None = None,
|
||||
is_category: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.aliases = aliases or set()
|
||||
self.parent_tags = parent_tags or set()
|
||||
self.color = color
|
||||
self.color_namespace = color_namespace
|
||||
self.color_slug = color_slug
|
||||
self.icon = icon
|
||||
self.shorthand = shorthand
|
||||
self.is_category = is_category
|
||||
|
||||
@@ -6,7 +6,7 @@ from enum import IntEnum
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from src.core.library.alchemy.enums import TagColor
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -29,266 +29,14 @@ class UiColor(IntEnum):
|
||||
PURPLE = 6
|
||||
|
||||
|
||||
TAG_COLORS: dict[TagColor, dict[ColorType, Any]] = {
|
||||
TagColor.DEFAULT: {
|
||||
ColorType.PRIMARY: "#1e1e1e",
|
||||
TAG_COLORS: dict[TagColorEnum, dict[ColorType, Any]] = {
|
||||
TagColorEnum.DEFAULT: {
|
||||
ColorType.PRIMARY: "#111111",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#333333",
|
||||
ColorType.LIGHT_ACCENT: "#FFFFFF",
|
||||
ColorType.DARK_ACCENT: "#222222",
|
||||
},
|
||||
TagColor.BLACK: {
|
||||
ColorType.PRIMARY: "#111018",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#18171e",
|
||||
ColorType.LIGHT_ACCENT: "#b7b6be",
|
||||
ColorType.DARK_ACCENT: "#03020a",
|
||||
},
|
||||
TagColor.DARK_GRAY: {
|
||||
ColorType.PRIMARY: "#24232a",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#2a2930",
|
||||
ColorType.LIGHT_ACCENT: "#bdbcc4",
|
||||
ColorType.DARK_ACCENT: "#07060e",
|
||||
},
|
||||
TagColor.GRAY: {
|
||||
ColorType.PRIMARY: "#53525a",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#5b5a62",
|
||||
ColorType.LIGHT_ACCENT: "#cbcad2",
|
||||
ColorType.DARK_ACCENT: "#191820",
|
||||
},
|
||||
TagColor.LIGHT_GRAY: {
|
||||
ColorType.PRIMARY: "#aaa9b0",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b6b4bc",
|
||||
ColorType.LIGHT_ACCENT: "#cbcad2",
|
||||
ColorType.DARK_ACCENT: "#191820",
|
||||
},
|
||||
TagColor.WHITE: {
|
||||
ColorType.PRIMARY: "#f2f1f8",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#fefeff",
|
||||
ColorType.LIGHT_ACCENT: "#ffffff",
|
||||
ColorType.DARK_ACCENT: "#302f36",
|
||||
},
|
||||
TagColor.LIGHT_PINK: {
|
||||
ColorType.PRIMARY: "#ff99c4",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ffaad0",
|
||||
ColorType.LIGHT_ACCENT: "#ffcbe7",
|
||||
ColorType.DARK_ACCENT: "#6c2e3b",
|
||||
},
|
||||
TagColor.PINK: {
|
||||
ColorType.PRIMARY: "#F96BB1",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#FA7EBC",
|
||||
ColorType.LIGHT_ACCENT: "#FDB6DC",
|
||||
ColorType.DARK_ACCENT: "#5B2135",
|
||||
},
|
||||
TagColor.MAGENTA: {
|
||||
ColorType.PRIMARY: "#f6466f",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f7587f",
|
||||
ColorType.LIGHT_ACCENT: "#fba4bf",
|
||||
ColorType.DARK_ACCENT: "#61152f",
|
||||
},
|
||||
TagColor.RED: {
|
||||
ColorType.PRIMARY: "#e22c3c",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#e54252",
|
||||
ColorType.LIGHT_ACCENT: "#f39caa",
|
||||
ColorType.DARK_ACCENT: "#440d12",
|
||||
},
|
||||
TagColor.RED_ORANGE: {
|
||||
ColorType.PRIMARY: "#e83726",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ea4b3b",
|
||||
ColorType.LIGHT_ACCENT: "#f5a59d",
|
||||
ColorType.DARK_ACCENT: "#61120b",
|
||||
},
|
||||
TagColor.SALMON: {
|
||||
ColorType.PRIMARY: "#f65848",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f76c5f",
|
||||
ColorType.LIGHT_ACCENT: "#fcadaa",
|
||||
ColorType.DARK_ACCENT: "#6f1b16",
|
||||
},
|
||||
TagColor.ORANGE: {
|
||||
ColorType.PRIMARY: "#ed6022",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ef7038",
|
||||
ColorType.LIGHT_ACCENT: "#f7b79b",
|
||||
ColorType.DARK_ACCENT: "#551e0a",
|
||||
},
|
||||
TagColor.YELLOW_ORANGE: {
|
||||
ColorType.PRIMARY: "#fa9a2c",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#fba94b",
|
||||
ColorType.LIGHT_ACCENT: "#fdd7ab",
|
||||
ColorType.DARK_ACCENT: "#66330d",
|
||||
},
|
||||
TagColor.YELLOW: {
|
||||
ColorType.PRIMARY: "#ffd63d",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ffe071",
|
||||
ColorType.LIGHT_ACCENT: "#fff3c4",
|
||||
ColorType.DARK_ACCENT: "#754312",
|
||||
},
|
||||
TagColor.MINT: {
|
||||
ColorType.PRIMARY: "#4aed90",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#79f2b1",
|
||||
ColorType.LIGHT_ACCENT: "#c8fbe9",
|
||||
ColorType.DARK_ACCENT: "#164f3e",
|
||||
},
|
||||
TagColor.LIME: {
|
||||
ColorType.PRIMARY: "#92e649",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b2ed72",
|
||||
ColorType.LIGHT_ACCENT: "#e9f9b7",
|
||||
ColorType.DARK_ACCENT: "#405516",
|
||||
},
|
||||
TagColor.LIGHT_GREEN: {
|
||||
ColorType.PRIMARY: "#85ec76",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#a3f198",
|
||||
ColorType.LIGHT_ACCENT: "#e7fbe4",
|
||||
ColorType.DARK_ACCENT: "#2b5524",
|
||||
},
|
||||
TagColor.GREEN: {
|
||||
ColorType.PRIMARY: "#28bb48",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#43c568",
|
||||
ColorType.LIGHT_ACCENT: "#93e2c8",
|
||||
ColorType.DARK_ACCENT: "#0d3828",
|
||||
},
|
||||
TagColor.TEAL: {
|
||||
ColorType.PRIMARY: "#1ad9b2",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#4de3c7",
|
||||
ColorType.LIGHT_ACCENT: "#a0f3e8",
|
||||
ColorType.DARK_ACCENT: "#08424b",
|
||||
},
|
||||
TagColor.CYAN: {
|
||||
ColorType.PRIMARY: "#49e4d5",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#76ebdf",
|
||||
ColorType.LIGHT_ACCENT: "#bff5f0",
|
||||
ColorType.DARK_ACCENT: "#0f4246",
|
||||
},
|
||||
TagColor.LIGHT_BLUE: {
|
||||
ColorType.PRIMARY: "#55bbf6",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#70c6f7",
|
||||
ColorType.LIGHT_ACCENT: "#bbe4fb",
|
||||
ColorType.DARK_ACCENT: "#122541",
|
||||
},
|
||||
TagColor.BLUE: {
|
||||
ColorType.PRIMARY: "#3b87f0",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#4e95f2",
|
||||
ColorType.LIGHT_ACCENT: "#aedbfa",
|
||||
ColorType.DARK_ACCENT: "#122948",
|
||||
},
|
||||
TagColor.BLUE_VIOLET: {
|
||||
ColorType.PRIMARY: "#5948f2",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#6258f3",
|
||||
ColorType.LIGHT_ACCENT: "#9cb8fb",
|
||||
ColorType.DARK_ACCENT: "#1b1649",
|
||||
},
|
||||
TagColor.VIOLET: {
|
||||
ColorType.PRIMARY: "#874ff5",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#9360f6",
|
||||
ColorType.LIGHT_ACCENT: "#c9b0fa",
|
||||
ColorType.DARK_ACCENT: "#3a1860",
|
||||
},
|
||||
TagColor.PURPLE: {
|
||||
ColorType.PRIMARY: "#bb4ff0",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#c364f2",
|
||||
ColorType.LIGHT_ACCENT: "#dda7f7",
|
||||
ColorType.DARK_ACCENT: "#531862",
|
||||
},
|
||||
TagColor.PEACH: {
|
||||
ColorType.PRIMARY: "#f1c69c",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f4d4b4",
|
||||
ColorType.LIGHT_ACCENT: "#fbeee1",
|
||||
ColorType.DARK_ACCENT: "#613f2f",
|
||||
},
|
||||
TagColor.BROWN: {
|
||||
ColorType.PRIMARY: "#823216",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#8a3e22",
|
||||
ColorType.LIGHT_ACCENT: "#cd9d83",
|
||||
ColorType.DARK_ACCENT: "#3a1804",
|
||||
},
|
||||
TagColor.LAVENDER: {
|
||||
ColorType.PRIMARY: "#ad8eef",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b99ef2",
|
||||
ColorType.LIGHT_ACCENT: "#d5c7fa",
|
||||
ColorType.DARK_ACCENT: "#492b65",
|
||||
},
|
||||
TagColor.BLONDE: {
|
||||
ColorType.PRIMARY: "#efc664",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f3d387",
|
||||
ColorType.LIGHT_ACCENT: "#faebc6",
|
||||
ColorType.DARK_ACCENT: "#6d461e",
|
||||
},
|
||||
TagColor.AUBURN: {
|
||||
ColorType.PRIMARY: "#a13220",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#aa402f",
|
||||
ColorType.LIGHT_ACCENT: "#d98a7f",
|
||||
ColorType.DARK_ACCENT: "#3d100a",
|
||||
},
|
||||
TagColor.LIGHT_BROWN: {
|
||||
ColorType.PRIMARY: "#be5b2d",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#c4693d",
|
||||
ColorType.LIGHT_ACCENT: "#e5b38c",
|
||||
ColorType.DARK_ACCENT: "#4c290e",
|
||||
},
|
||||
TagColor.DARK_BROWN: {
|
||||
ColorType.PRIMARY: "#4c2315",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#542a1c",
|
||||
ColorType.LIGHT_ACCENT: "#b78171",
|
||||
ColorType.DARK_ACCENT: "#211006",
|
||||
},
|
||||
TagColor.COOL_GRAY: {
|
||||
ColorType.PRIMARY: "#515768",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#5b6174",
|
||||
ColorType.LIGHT_ACCENT: "#9ea1c3",
|
||||
ColorType.DARK_ACCENT: "#181a37",
|
||||
},
|
||||
TagColor.WARM_GRAY: {
|
||||
ColorType.PRIMARY: "#625550",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#6c5e57",
|
||||
ColorType.LIGHT_ACCENT: "#c0a392",
|
||||
ColorType.DARK_ACCENT: "#371d18",
|
||||
},
|
||||
TagColor.OLIVE: {
|
||||
ColorType.PRIMARY: "#4c652e",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#586f36",
|
||||
ColorType.LIGHT_ACCENT: "#b4c17a",
|
||||
ColorType.DARK_ACCENT: "#23300e",
|
||||
},
|
||||
TagColor.BERRY: {
|
||||
ColorType.PRIMARY: "#9f2aa7",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#aa43b4",
|
||||
ColorType.LIGHT_ACCENT: "#cc8fdc",
|
||||
ColorType.DARK_ACCENT: "#41114a",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
UI_COLORS: dict[UiColor, dict[ColorType, Any]] = {
|
||||
@@ -337,7 +85,7 @@ UI_COLORS: dict[UiColor, dict[ColorType, Any]] = {
|
||||
}
|
||||
|
||||
|
||||
def get_tag_color(color_type: ColorType, color_id: TagColor) -> str:
|
||||
def get_tag_color(color_type: ColorType, color_id: TagColorEnum) -> str:
|
||||
"""Return a hex value given a tag color name and ColorType.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -11,7 +11,6 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -23,12 +22,14 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library import Library, Tag
|
||||
from src.core.library.alchemy.enums import TagColor
|
||||
from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.core.palette import ColorType, UiColor, get_ui_color
|
||||
from src.qt.modals.tag_color_selection import TagColorSelection
|
||||
from src.qt.modals.tag_search import TagSearchPanel
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelModal, PanelWidget
|
||||
from src.qt.widgets.tag import TagWidget
|
||||
from src.qt.widgets.tag_color_preview import TagColorPreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -59,6 +60,8 @@ class BuildTagPanel(PanelWidget):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.tag: Tag # NOTE: This gets set at the end of the init.
|
||||
self.tag_color_namespace: str | None
|
||||
self.tag_color_slug: str | None
|
||||
|
||||
self.setMinimumSize(300, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
@@ -111,14 +114,13 @@ class BuildTagPanel(PanelWidget):
|
||||
self.aliases_table.horizontalHeader().setVisible(False)
|
||||
self.aliases_table.verticalHeader().setVisible(False)
|
||||
self.aliases_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.aliases_table.setColumnWidth(0, 35)
|
||||
self.aliases_table.setColumnWidth(0, 32)
|
||||
self.aliases_table.setTabKeyNavigation(False)
|
||||
self.aliases_table.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
|
||||
self.alias_add_button = QPushButton()
|
||||
self.alias_add_button.setText("+")
|
||||
|
||||
self.alias_add_button.clicked.connect(self.add_alias_callback)
|
||||
self.aliases_add_button = QPushButton()
|
||||
self.aliases_add_button.setText("+")
|
||||
self.aliases_add_button.clicked.connect(self.add_alias_callback)
|
||||
|
||||
# Parent Tags ----------------------------------------------------------
|
||||
self.parent_tags_widget = QWidget()
|
||||
@@ -134,18 +136,15 @@ class BuildTagPanel(PanelWidget):
|
||||
|
||||
self.scroll_contents = QWidget()
|
||||
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
|
||||
self.parent_tags_scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.parent_tags_scroll_layout.setContentsMargins(6, 6, 6, 0)
|
||||
self.parent_tags_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
# 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.parent_tags_layout.addWidget(self.scroll_area)
|
||||
|
||||
self.parent_tags_add_button = QPushButton()
|
||||
@@ -168,32 +167,31 @@ class BuildTagPanel(PanelWidget):
|
||||
self.color_widget = QWidget()
|
||||
self.color_layout = QVBoxLayout(self.color_widget)
|
||||
self.color_layout.setStretch(1, 1)
|
||||
self.color_layout.setContentsMargins(0, 0, 0, 24)
|
||||
self.color_layout.setSpacing(0)
|
||||
self.color_layout.setContentsMargins(0, 0, 0, 6)
|
||||
self.color_layout.setSpacing(6)
|
||||
self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.color_title = QLabel()
|
||||
Translations.translate_qobject(self.color_title, "tag.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 TagColor:
|
||||
self.color_field.addItem(color.name.replace("_", " ").title(), userData=color.value)
|
||||
# self.color_field.setProperty("appearance", "flat")
|
||||
self.color_field.currentIndexChanged.connect(
|
||||
lambda c: (
|
||||
self.color_field.setStyleSheet(
|
||||
"combobox-popup:0;"
|
||||
"font-weight:600;"
|
||||
f"color:{get_tag_color(ColorType.TEXT, self.color_field.currentData())};"
|
||||
f"background-color:{get_tag_color(
|
||||
ColorType.PRIMARY,
|
||||
self.color_field.currentData())};"
|
||||
)
|
||||
)
|
||||
self.color_button: TagColorPreview
|
||||
try:
|
||||
self.color_button = TagColorPreview(tag.color)
|
||||
except Exception as e:
|
||||
# TODO: Investigate why this happens during tests
|
||||
logger.error("[BuildTag] Could not access Tag member attributes", error=e)
|
||||
self.color_button = TagColorPreview(None)
|
||||
self.tag_color_selection = TagColorSelection(self.lib)
|
||||
chose_tag_color_title = Translations.translate_formatted("tag.choose_color")
|
||||
self.choose_color_modal = PanelModal(
|
||||
self.tag_color_selection,
|
||||
chose_tag_color_title,
|
||||
chose_tag_color_title,
|
||||
done_callback=lambda: self.choose_color_callback(
|
||||
self.tag_color_selection.selected_color
|
||||
),
|
||||
)
|
||||
self.color_layout.addWidget(self.color_field)
|
||||
self.color_button.button.clicked.connect(self.choose_color_modal.show)
|
||||
self.color_layout.addWidget(self.color_button)
|
||||
|
||||
# Category -------------------------------------------------------------
|
||||
self.cat_widget = QWidget()
|
||||
@@ -229,7 +227,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.root_layout.addWidget(self.shorthand_widget)
|
||||
self.root_layout.addWidget(self.aliases_widget)
|
||||
self.root_layout.addWidget(self.aliases_table)
|
||||
self.root_layout.addWidget(self.alias_add_button)
|
||||
self.root_layout.addWidget(self.aliases_add_button)
|
||||
self.root_layout.addWidget(self.parent_tags_widget)
|
||||
self.root_layout.addWidget(self.color_widget)
|
||||
self.root_layout.addWidget(QLabel("<h3>Properties</h3>"))
|
||||
@@ -302,6 +300,16 @@ class BuildTagPanel(PanelWidget):
|
||||
self.alias_ids.remove(alias_id)
|
||||
self._set_aliases()
|
||||
|
||||
def choose_color_callback(self, tag_color_group: TagColorGroup | None):
|
||||
logger.info("choose_color_callback", tag_color_group=tag_color_group)
|
||||
if tag_color_group:
|
||||
self.tag_color_namespace = tag_color_group.namespace
|
||||
self.tag_color_slug = tag_color_group.slug
|
||||
else:
|
||||
self.tag_color_namespace = None
|
||||
self.tag_color_slug = None
|
||||
self.color_button.set_tag_color_group(tag_color_group)
|
||||
|
||||
def set_parent_tags(self):
|
||||
while self.parent_tags_scroll_layout.itemAt(0):
|
||||
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
|
||||
@@ -326,11 +334,9 @@ class BuildTagPanel(PanelWidget):
|
||||
names: set[str] = set()
|
||||
for i in range(0, self.aliases_table.rowCount()):
|
||||
widget = self.aliases_table.cellWidget(i, 1)
|
||||
|
||||
names.add(cast(CustomTableItem, widget).text())
|
||||
|
||||
remove: set[str] = set(self.alias_names) - names
|
||||
|
||||
self.alias_names = list(set(self.alias_names) - remove)
|
||||
|
||||
for name in names:
|
||||
@@ -341,8 +347,6 @@ class BuildTagPanel(PanelWidget):
|
||||
self.alias_names.remove(name)
|
||||
|
||||
def _update_new_alias_name_dict(self):
|
||||
row = self.aliases_table.rowCount()
|
||||
logger.info(row)
|
||||
for i in range(0, self.aliases_table.rowCount()):
|
||||
widget = self.aliases_table.cellWidget(i, 1)
|
||||
self.new_alias_names[widget.id] = widget.text() # type: ignore
|
||||
@@ -355,7 +359,7 @@ class BuildTagPanel(PanelWidget):
|
||||
|
||||
self.alias_names.clear()
|
||||
|
||||
last: QWidget = self.panel_save_button or self.color_field
|
||||
last: QWidget = self.panel_save_button
|
||||
for alias_id in self.alias_ids:
|
||||
alias = self.lib.get_alias(self.tag.id, alias_id)
|
||||
|
||||
@@ -394,6 +398,7 @@ class BuildTagPanel(PanelWidget):
|
||||
def set_tag(self, tag: Tag):
|
||||
logger.info("[BuildTagPanel] Setting Tag", tag=tag)
|
||||
self.tag = tag
|
||||
|
||||
self.name_field.setText(tag.name)
|
||||
self.shorthand_field.setText(tag.shorthand or "")
|
||||
|
||||
@@ -405,11 +410,15 @@ class BuildTagPanel(PanelWidget):
|
||||
self.parent_ids.add(parent_id)
|
||||
self.set_parent_tags()
|
||||
|
||||
# select item in self.color_field where the userData value matched tag.color
|
||||
for i in range(self.color_field.count()):
|
||||
if self.color_field.itemData(i) == tag.color:
|
||||
self.color_field.setCurrentIndex(i)
|
||||
break
|
||||
try:
|
||||
self.tag_color_namespace = tag.color_namespace
|
||||
self.tag_color_slug = tag.color_slug
|
||||
self.color_button.set_tag_color_group(tag.color)
|
||||
self.tag_color_selection.select_radio_button(tag.color)
|
||||
except Exception as e:
|
||||
# TODO: Investigate why this happens during tests
|
||||
logger.error("[BuildTag] Could not access Tag member attributes", error=e)
|
||||
self.color_button.set_tag_color_group(None)
|
||||
|
||||
self.cat_checkbox.setChecked(tag.is_category)
|
||||
|
||||
@@ -426,24 +435,25 @@ class BuildTagPanel(PanelWidget):
|
||||
self.panel_save_button.setDisabled(is_empty)
|
||||
|
||||
def build_tag(self) -> Tag:
|
||||
color = self.color_field.currentData() or TagColor.DEFAULT
|
||||
tag = self.tag
|
||||
self.add_aliases()
|
||||
|
||||
tag.name = self.name_field.text()
|
||||
tag.shorthand = self.shorthand_field.text()
|
||||
tag.color = color
|
||||
tag.is_category = self.cat_checkbox.isChecked()
|
||||
|
||||
tag.color_namespace = self.tag_color_namespace
|
||||
tag.color_slug = self.tag_color_slug
|
||||
|
||||
logger.info("built tag", tag=tag)
|
||||
return tag
|
||||
|
||||
def parent_post_init(self):
|
||||
self.setTabOrder(self.name_field, self.shorthand_field)
|
||||
self.setTabOrder(self.shorthand_field, self.alias_add_button)
|
||||
self.setTabOrder(self.alias_add_button, self.parent_tags_add_button)
|
||||
self.setTabOrder(self.parent_tags_add_button, self.color_field)
|
||||
self.setTabOrder(self.color_field, self.panel_cancel_button)
|
||||
self.setTabOrder(self.shorthand_field, self.aliases_add_button)
|
||||
self.setTabOrder(self.aliases_add_button, self.parent_tags_add_button)
|
||||
self.setTabOrder(self.parent_tags_add_button, self.color_button)
|
||||
self.setTabOrder(self.color_button, self.panel_cancel_button)
|
||||
self.setTabOrder(self.panel_cancel_button, self.panel_save_button)
|
||||
self.setTabOrder(self.panel_save_button, self.aliases_table.cellWidget(0, 1))
|
||||
self.name_field.selectAll()
|
||||
|
||||
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from src.core.library import Library, Tag
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.translations import Translations
|
||||
@@ -329,10 +330,10 @@ class ModifiedTagWidget(QWidget):
|
||||
|
||||
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"background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
|
||||
f"color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:inset;"
|
||||
f"border-width: {math.ceil(self.devicePixelRatio())}px;"
|
||||
@@ -342,7 +343,7 @@ class ModifiedTagWidget(QWidget):
|
||||
f"font-size: 13px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
|
||||
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
|
||||
167
tagstudio/src/qt/modals/tag_color_selection.py
Normal file
167
tagstudio/src/qt/modals/tag_color_selection.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QLabel,
|
||||
QRadioButton,
|
||||
QSpacerItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library import Library
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
from src.qt.widgets.tag_color_preview import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_primary_color,
|
||||
get_text_color,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagColorSelection(PanelWidget):
|
||||
def __init__(self, library: Library):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.selected_color: TagColorGroup | None = None
|
||||
|
||||
self.setMinimumSize(308, 540)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setSpacing(6)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Add Widgets to Layout ================================================
|
||||
tag_color_groups = self.lib.tag_color_groups
|
||||
self.button_group = QButtonGroup(self)
|
||||
|
||||
self.add_no_color_widget()
|
||||
self.root_layout.addSpacerItem(QSpacerItem(1, 12))
|
||||
for group, colors in tag_color_groups.items():
|
||||
display_name: str = self.lib.get_namespace_name(group)
|
||||
self.root_layout.addWidget(
|
||||
QLabel(f"<h4>{display_name if display_name else group}</h4>")
|
||||
)
|
||||
color_box_widget = QWidget()
|
||||
color_group_layout = FlowLayout()
|
||||
color_group_layout.setSpacing(4)
|
||||
color_group_layout.enable_grid_optimizations(value=False)
|
||||
color_group_layout.setContentsMargins(0, 0, 0, 0)
|
||||
color_box_widget.setLayout(color_group_layout)
|
||||
for color in colors:
|
||||
primary_color = get_primary_color(color)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (color and color.secondary)
|
||||
else (QColor(color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
primary_color if not (color and color.secondary) else QColor(color.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if color and color.secondary:
|
||||
text_color = QColor(color.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
radio_button = QRadioButton()
|
||||
radio_button.setObjectName(f"{color.namespace}.{color.slug}")
|
||||
radio_button.setToolTip(color.name)
|
||||
radio_button.setFixedSize(24, 24)
|
||||
radio_button.setStyleSheet(
|
||||
f"QRadioButton{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 3px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"}}"
|
||||
f"QRadioButton::indicator{{"
|
||||
f"width: 12px;"
|
||||
f"height: 12px;"
|
||||
f"border-radius: 1px;"
|
||||
f"margin: 4px;"
|
||||
f"}}"
|
||||
f"QRadioButton::indicator:checked{{"
|
||||
f"background: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QRadioButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
radio_button.clicked.connect(lambda checked=False, x=color: self.select_color(x))
|
||||
color_group_layout.addWidget(radio_button)
|
||||
self.button_group.addButton(radio_button)
|
||||
self.root_layout.addWidget(color_box_widget)
|
||||
self.root_layout.addSpacerItem(QSpacerItem(1, 12))
|
||||
|
||||
def add_no_color_widget(self):
|
||||
no_color_str: str = Translations.translate_formatted("color.title.no_color")
|
||||
self.root_layout.addWidget(QLabel(f"<h4>{no_color_str}</h4>"))
|
||||
color_box_widget = QWidget()
|
||||
color_group_layout = FlowLayout()
|
||||
color_group_layout.setSpacing(4)
|
||||
color_group_layout.enable_grid_optimizations(value=False)
|
||||
color_group_layout.setContentsMargins(0, 0, 0, 0)
|
||||
color_box_widget.setLayout(color_group_layout)
|
||||
color = None
|
||||
primary_color = get_primary_color(color)
|
||||
border_color = get_border_color(primary_color)
|
||||
highlight_color = get_highlight_color(primary_color)
|
||||
text_color: QColor
|
||||
if color and color.secondary:
|
||||
text_color = QColor(color.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
radio_button = QRadioButton()
|
||||
radio_button.setObjectName("None") # NOTE: Internal use, no translation needed.
|
||||
radio_button.setToolTip(no_color_str)
|
||||
radio_button.setFixedSize(24, 24)
|
||||
radio_button.setStyleSheet(
|
||||
f"QRadioButton{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 3px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"}}"
|
||||
f"QRadioButton::indicator{{"
|
||||
f"width: 12px;"
|
||||
f"height: 12px;"
|
||||
f"border-radius: 1px;"
|
||||
f"margin: 4px;"
|
||||
f"}}"
|
||||
f"QRadioButton::indicator:checked{{"
|
||||
f"background: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QRadioButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
radio_button.clicked.connect(lambda checked=False, x=color: self.select_color(x))
|
||||
color_group_layout.addWidget(radio_button)
|
||||
self.button_group.addButton(radio_button)
|
||||
self.root_layout.addWidget(color_box_widget)
|
||||
|
||||
def select_color(self, color: TagColorGroup):
|
||||
self.selected_color = color
|
||||
|
||||
def select_radio_button(self, color: TagColorGroup | None):
|
||||
object_name: str = "None" if not color else f"{color.namespace}.{color.slug}"
|
||||
for button in self.button_group.buttons():
|
||||
if button.objectName() == object_name:
|
||||
button.setChecked(True)
|
||||
break
|
||||
@@ -3,12 +3,10 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import math
|
||||
|
||||
import src.qt.modals.build_tag as build_tag
|
||||
import structlog
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtGui import QColor, QShowEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -20,11 +18,17 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from src.core.library import Library, Tag
|
||||
from src.core.library.alchemy.enums import TagColor
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelModal, PanelWidget
|
||||
from src.qt.widgets.tag import TagWidget
|
||||
from src.qt.widgets.tag import (
|
||||
TagWidget,
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_primary_color,
|
||||
get_text_color,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -90,28 +94,45 @@ class TagSearchPanel(PanelWidget):
|
||||
tag_widget.on_remove.connect(lambda t=tag: self.remove_tag(t))
|
||||
row.addWidget(tag_widget)
|
||||
|
||||
primary_color = get_primary_color(tag)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (tag.color and tag.color.secondary)
|
||||
else (QColor(tag.color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
primary_color
|
||||
if not (tag.color and tag.color.secondary)
|
||||
else QColor(tag.color.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if tag.color and tag.color.secondary:
|
||||
text_color = QColor(tag.color.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
if self.is_tag_chooser:
|
||||
add_button = QPushButton()
|
||||
add_button.setMinimumSize(23, 23)
|
||||
add_button.setMaximumSize(23, 23)
|
||||
add_button.setMinimumSize(22, 22)
|
||||
add_button.setMaximumSize(22, 22)
|
||||
add_button.setText("+")
|
||||
add_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};"
|
||||
f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: {math.ceil(self.devicePixelRatio())}px;"
|
||||
f"padding-bottom: 5px;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-bottom: 4px;"
|
||||
f"font-size: 20px;"
|
||||
f"}}"
|
||||
f"QPushButton::hover"
|
||||
f"{{"
|
||||
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
|
||||
f"color: {get_tag_color(ColorType.DARK_ACCENT, tag.color)};"
|
||||
f"background: {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"color: rgba{primary_color.toTuple()};"
|
||||
f"background: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
tag_id = tag.id
|
||||
@@ -134,24 +155,24 @@ class TagSearchPanel(PanelWidget):
|
||||
inner_layout.setObjectName("innerLayout")
|
||||
inner_layout.setContentsMargins(2, 2, 2, 2)
|
||||
create_button.setLayout(inner_layout)
|
||||
create_button.setMinimumSize(math.ceil(22 * 1.5), 22)
|
||||
create_button.setMinimumSize(22, 22)
|
||||
|
||||
create_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"background: {get_tag_color(ColorType.PRIMARY, TagColor.DEFAULT)};"
|
||||
f"color: {get_tag_color(ColorType.TEXT, TagColor.DEFAULT)};"
|
||||
f"background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
|
||||
f"color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, TagColor.DEFAULT)};"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: {math.ceil(self.devicePixelRatio())}px;"
|
||||
f"border-width: 2px;"
|
||||
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, TagColor.DEFAULT)};"
|
||||
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
@@ -21,7 +22,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from src.core.constants import LEGACY_TAG_FIELD_IDS, TS_FOLDER_NAME
|
||||
from src.core.enums import LibraryPrefs
|
||||
from src.core.library.alchemy.enums import TagColor
|
||||
from src.core.library.alchemy import default_color_groups
|
||||
from src.core.library.alchemy.joins import TagParent
|
||||
from src.core.library.alchemy.library import TAG_ARCHIVED, TAG_FAVORITE, TAG_META
|
||||
from src.core.library.alchemy.library import Library as SqliteLibrary
|
||||
@@ -413,6 +414,8 @@ class JsonMigrationModal(QObject):
|
||||
self.done = True
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
logger.error("[MigrationModal] Error:", error=e)
|
||||
yield f"Error: {type(e).__name__}"
|
||||
QApplication.beep()
|
||||
QApplication.alert(self.paged_panel)
|
||||
@@ -719,17 +722,13 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
def check_color_parity(self) -> bool:
|
||||
"""Check if all JSON tag colors match the new SQL tag colors."""
|
||||
sql_color: str = None
|
||||
json_color: str = None
|
||||
sql_color: tuple[str | None, str | None] = (None, None)
|
||||
json_color: tuple[str | None, str | None] = (None, None)
|
||||
|
||||
for tag in self.sql_lib.tags:
|
||||
tag_id = tag.id # Tag IDs start at 0
|
||||
sql_color = tag.color.name
|
||||
json_color = (
|
||||
TagColor.get_color_from_str(self.json_lib.get_tag(tag_id).color).name
|
||||
if (self.json_lib.get_tag(tag_id).color) != ""
|
||||
else TagColor.DEFAULT.name
|
||||
)
|
||||
sql_color = (tag.color_namespace, tag.color_slug)
|
||||
json_color = default_color_groups.json_to_sql_color(self.json_lib.get_tag(tag_id).color)
|
||||
|
||||
logger.info(
|
||||
"[Color Parity]",
|
||||
@@ -738,7 +737,7 @@ class JsonMigrationModal(QObject):
|
||||
sql_color=sql_color,
|
||||
)
|
||||
|
||||
if not (sql_color is not None and json_color is not None and (sql_color == json_color)):
|
||||
if sql_color != json_color:
|
||||
self.discrepancies.append(
|
||||
f"[Color Parity][Tag ID: {tag_id}]:"
|
||||
f"\nOLD (JSON):{json_color}\nNEW (SQL):{sql_color}"
|
||||
|
||||
@@ -207,7 +207,7 @@ class PreviewThumb(QWidget):
|
||||
image = Image.open(str(filepath))
|
||||
stats["width"] = image.width
|
||||
stats["height"] = image.height
|
||||
except UnidentifiedImageError as e:
|
||||
except (UnidentifiedImageError, FileNotFoundError) as e:
|
||||
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import math
|
||||
from types import FunctionType
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QAction, QEnterEvent, QFontMetrics
|
||||
from PySide6.QtGui import QAction, QColor, QEnterEvent, QFontMetrics
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLineEdit,
|
||||
@@ -16,10 +16,12 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library import Tag
|
||||
from src.core.library.alchemy.enums import TagColor
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagAliasWidget(QWidget):
|
||||
on_remove = Signal()
|
||||
@@ -57,8 +59,8 @@ class TagAliasWidget(QWidget):
|
||||
self.remove_button.setText("–")
|
||||
self.remove_button.setHidden(False)
|
||||
self.remove_button.setStyleSheet(
|
||||
f"color: {get_tag_color(ColorType.PRIMARY, TagColor.DEFAULT)};"
|
||||
f"background: {get_tag_color(ColorType.TEXT, TagColor.DEFAULT)};"
|
||||
f"color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
|
||||
f"background: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
|
||||
f"font-weight: 800;"
|
||||
f"border-radius: 4px;"
|
||||
f"border-width:0;"
|
||||
@@ -142,26 +144,45 @@ class TagWidget(QWidget):
|
||||
self.inner_layout.setContentsMargins(2, 2, 2, 2)
|
||||
|
||||
self.bg_button.setLayout(self.inner_layout)
|
||||
self.bg_button.setMinimumSize(math.ceil(22 * 2), 22)
|
||||
self.bg_button.setMinimumSize(22, 22)
|
||||
|
||||
primary_color = get_primary_color(tag)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (tag.color and tag.color.secondary)
|
||||
else (QColor(tag.color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
primary_color
|
||||
if not (tag.color and tag.color.secondary)
|
||||
else QColor(tag.color.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if tag.color and tag.color.secondary:
|
||||
text_color = QColor(tag.color.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
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"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: {math.ceil(self.devicePixelRatio())}px;"
|
||||
f"border-width: 2px;"
|
||||
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"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
self.bg_button.setMinimumHeight(22)
|
||||
self.bg_button.setMaximumHeight(22)
|
||||
|
||||
self.base_layout.addWidget(self.bg_button)
|
||||
|
||||
@@ -171,16 +192,16 @@ class TagWidget(QWidget):
|
||||
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: rgba{primary_color.toTuple()};"
|
||||
f"background: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 800;"
|
||||
f"border-radius: 4px;"
|
||||
f"border-radius: 3px;"
|
||||
f"border-width:0;"
|
||||
f"padding-bottom: 4px;"
|
||||
f"font-size: 14px"
|
||||
)
|
||||
self.remove_button.setMinimumSize(19, 19)
|
||||
self.remove_button.setMaximumSize(19, 19)
|
||||
self.remove_button.setMinimumSize(18, 18)
|
||||
self.remove_button.setMaximumSize(18, 18)
|
||||
self.remove_button.clicked.connect(self.on_remove.emit)
|
||||
|
||||
if has_remove:
|
||||
@@ -203,3 +224,41 @@ class TagWidget(QWidget):
|
||||
self.remove_button.setHidden(True)
|
||||
self.update()
|
||||
return super().leaveEvent(event)
|
||||
|
||||
|
||||
def get_primary_color(tag: Tag) -> QColor:
|
||||
primary_color = QColor(
|
||||
get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
|
||||
if not tag.color
|
||||
else tag.color.primary
|
||||
)
|
||||
|
||||
return primary_color
|
||||
|
||||
|
||||
def get_border_color(primary_color: QColor) -> QColor:
|
||||
border_color: QColor = QColor(primary_color)
|
||||
border_color.setRed(min(border_color.red() + 20, 255))
|
||||
border_color.setGreen(min(border_color.green() + 20, 255))
|
||||
border_color.setBlue(min(border_color.blue() + 20, 255))
|
||||
|
||||
return border_color
|
||||
|
||||
|
||||
def get_highlight_color(primary_color: QColor) -> QColor:
|
||||
highlight_color: QColor = QColor(primary_color)
|
||||
highlight_color = highlight_color.toHsl()
|
||||
highlight_color.setHsl(highlight_color.hue(), min(highlight_color.saturation(), 200), 225, 255)
|
||||
highlight_color = highlight_color.toRgb()
|
||||
|
||||
return highlight_color
|
||||
|
||||
|
||||
def get_text_color(primary_color: QColor, highlight_color: QColor) -> QColor:
|
||||
if primary_color.lightness() > 120:
|
||||
text_color = QColor(primary_color)
|
||||
text_color = text_color.toHsl()
|
||||
text_color.setHsl(text_color.hue(), text_color.saturation(), 50, 255)
|
||||
return text_color.toRgb()
|
||||
else:
|
||||
return highlight_color
|
||||
|
||||
128
tagstudio/src/qt/widgets/tag_color_preview.py
Normal file
128
tagstudio/src/qt/widgets/tag_color_preview.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagColorPreview(QWidget):
|
||||
on_click = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tag_color_group: TagColorGroup | None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.tag_color_group = tag_color_group
|
||||
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.base_layout = QVBoxLayout(self)
|
||||
self.base_layout.setObjectName("baseLayout")
|
||||
self.base_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.button = QPushButton(self)
|
||||
self.button.setFlat(True)
|
||||
self.button.setMinimumSize(56, 28)
|
||||
self.button.setMaximumHeight(28)
|
||||
self.button.clicked.connect(self.on_click.emit)
|
||||
|
||||
self.base_layout.addWidget(self.button)
|
||||
|
||||
self.set_tag_color_group(tag_color_group)
|
||||
|
||||
def set_tag_color_group(self, tag_color_group: TagColorGroup | None):
|
||||
self.tag_color_group = tag_color_group
|
||||
|
||||
if tag_color_group:
|
||||
self.button.setText(tag_color_group.name)
|
||||
else:
|
||||
Translations.translate_qobject(self.button, "generic.none")
|
||||
|
||||
primary_color = get_primary_color(tag_color_group)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (tag_color_group and tag_color_group.secondary)
|
||||
else (QColor(tag_color_group.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
primary_color
|
||||
if not (tag_color_group and tag_color_group.secondary)
|
||||
else QColor(tag_color_group.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if tag_color_group and tag_color_group.secondary:
|
||||
text_color = QColor(tag_color_group.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
self.button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 8px;"
|
||||
f"padding-bottom: 1px;"
|
||||
f"padding-left: 8px;"
|
||||
f"font-size: 14px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
self.button.setMaximumWidth(self.button.sizeHint().width())
|
||||
|
||||
|
||||
def get_primary_color(tag_color_group: TagColorGroup | None) -> QColor:
|
||||
primary_color = QColor(
|
||||
get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
|
||||
if not tag_color_group
|
||||
else tag_color_group.primary
|
||||
)
|
||||
|
||||
return primary_color
|
||||
|
||||
|
||||
def get_border_color(primary_color: QColor) -> QColor:
|
||||
border_color: QColor = QColor(primary_color)
|
||||
border_color.setRed(min(border_color.red() + 20, 255))
|
||||
border_color.setGreen(min(border_color.green() + 20, 255))
|
||||
border_color.setBlue(min(border_color.blue() + 20, 255))
|
||||
|
||||
return border_color
|
||||
|
||||
|
||||
def get_highlight_color(primary_color: QColor) -> QColor:
|
||||
highlight_color: QColor = QColor(primary_color)
|
||||
highlight_color = highlight_color.toHsl()
|
||||
highlight_color.setHsl(highlight_color.hue(), min(highlight_color.saturation(), 200), 225, 255)
|
||||
highlight_color = highlight_color.toRgb()
|
||||
|
||||
return highlight_color
|
||||
|
||||
|
||||
def get_text_color(primary_color: QColor, highlight_color: QColor) -> QColor:
|
||||
if primary_color.lightness() > 120:
|
||||
text_color = QColor(primary_color)
|
||||
text_color = text_color.toHsl()
|
||||
text_color.setHsl(text_color.hue(), text_color.saturation(), 50, 255)
|
||||
return text_color.toRgb()
|
||||
else:
|
||||
return highlight_color
|
||||
@@ -11,7 +11,6 @@ sys.path.insert(0, str(CWD.parent))
|
||||
|
||||
from src.core.library import Entry, Library, Tag
|
||||
from src.core.library import alchemy as backend
|
||||
from src.core.library.alchemy.enums import TagColor
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
@@ -67,21 +66,24 @@ def library(request):
|
||||
|
||||
tag = Tag(
|
||||
name="foo",
|
||||
color=TagColor.RED,
|
||||
color_namespace="tagstudio-standard",
|
||||
color_slug="red",
|
||||
)
|
||||
assert lib.add_tag(tag)
|
||||
|
||||
parent_tag = Tag(
|
||||
id=1500,
|
||||
name="subbar",
|
||||
color=TagColor.YELLOW,
|
||||
color_namespace="tagstudio-standard",
|
||||
color_slug="yellow",
|
||||
)
|
||||
assert lib.add_tag(parent_tag)
|
||||
|
||||
tag2 = Tag(
|
||||
id=2000,
|
||||
name="bar",
|
||||
color=TagColor.BLUE,
|
||||
color_namespace="tagstudio-standard",
|
||||
color_slug="blue",
|
||||
parent_tags={parent_tag},
|
||||
)
|
||||
assert lib.add_tag(tag2)
|
||||
@@ -154,7 +156,7 @@ def qt_driver(qtbot, library):
|
||||
@pytest.fixture
|
||||
def generate_tag():
|
||||
def inner(name, **kwargs):
|
||||
params = dict(name=name, color=TagColor.RED) | kwargs
|
||||
params = dict(name=name, color_namespace="tagstudio-standard", color_slug="red") | kwargs
|
||||
return Tag(**params)
|
||||
|
||||
yield inner
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -15,7 +15,7 @@ def test_add_tag_callback(qt_driver):
|
||||
|
||||
# When
|
||||
qt_driver.modal.widget.name_field.setText("xxx")
|
||||
qt_driver.modal.widget.color_field.setCurrentIndex(1)
|
||||
# qt_driver.modal.widget.color_field.setCurrentIndex(1)
|
||||
qt_driver.modal.saved.emit()
|
||||
|
||||
# Then
|
||||
|
||||
Reference in New Issue
Block a user