From 35457ab76fe1fa9d6dd90fcd4c27e4b0b5e3fe3a Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 21 Sep 2025 18:36:01 -0400 Subject: [PATCH 1/9] Add `is_hidden` field to the `tags` table --- .../core/library/alchemy/constants.py | 2 +- src/tagstudio/core/library/alchemy/db.py | 4 +- src/tagstudio/core/library/alchemy/library.py | 37 +++++++++++++++++++ src/tagstudio/core/library/alchemy/models.py | 3 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index 83aab71b0..1e2080249 100644 --- a/src/tagstudio/core/library/alchemy/constants.py +++ b/src/tagstudio/core/library/alchemy/constants.py @@ -11,7 +11,7 @@ DB_VERSION_LEGACY_KEY: str = "DB_VERSION" DB_VERSION_CURRENT_KEY: str = "CURRENT" DB_VERSION_INITIAL_KEY: str = "INITIAL" -DB_VERSION: int = 102 +DB_VERSION: int = 103 TAG_CHILDREN_QUERY = text(""" WITH RECURSIVE ChildTags AS ( diff --git a/src/tagstudio/core/library/alchemy/db.py b/src/tagstudio/core/library/alchemy/db.py index 026678ddf..8e3e6a618 100644 --- a/src/tagstudio/core/library/alchemy/db.py +++ b/src/tagstudio/core/library/alchemy/db.py @@ -57,8 +57,8 @@ def make_tables(engine: Engine) -> None: conn.execute( text( "INSERT INTO tags " - "(id, name, color_namespace, color_slug, is_category) VALUES " - f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false)" + "(id, name, color_namespace, color_slug, is_category, is_hidden) VALUES " + f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false, false)" ) ) conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}")) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index a25231e95..5e689c173 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -151,6 +151,7 @@ def get_default_tags() -> tuple[Tag, ...]: name="Archived", aliases={TagAlias(name="Archive")}, parent_tags={meta_tag}, + is_hidden=True, color_slug="red", color_namespace="tagstudio-standard", ) @@ -540,6 +541,8 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus: self.__apply_db8_schema_changes(session) if loaded_db_version < 9: self.__apply_db9_schema_changes(session) + if loaded_db_version < 103: + self.__apply_db103_schema_changes(session) if loaded_db_version == 6: self.__apply_repairs_for_db6(session) @@ -551,6 +554,8 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus: self.__apply_db100_parent_repairs(session) if loaded_db_version < 102: self.__apply_db102_repairs(session) + if loaded_db_version < 103: + self.__apply_db103_default_data(session) # Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist self.migrate_sql_to_ts_ignore(library_dir) @@ -697,6 +702,38 @@ def __apply_db102_repairs(self, session: Session): session.execute(stmt) session.commit() logger.info("[Library][Migration] Verified TagParent table data") + + def __apply_db103_schema_changes(self, session: Session): + """Apply database schema changes introduced in DB_VERSION 103.""" + add_is_hidden_column = text( + "ALTER TABLE tags ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT 0" + ) + try: + session.execute(add_is_hidden_column) + session.commit() + logger.info("[Library][Migration] Added is_hidden column to tags table") + except Exception as e: + logger.error( + "[Library][Migration] Could not create is_hidden column in tags table!", + error=e, + ) + session.rollback() + + def __apply_db103_default_data(self, session: Session): + """Apply default data changes introduced in DB_VERSION 103.""" + try: + session.query(Tag).\ + filter(Tag.id == TAG_ARCHIVED).\ + update({ 'is_hidden': True }) + session.commit() + logger.info("[Library][Migration] Updated archived tag to be hidden") + session.commit() + except Exception as e: + logger.error( + "[Library][Migration] Could not update archived tag to be hidden!", + error=e, + ) + session.rollback() def migrate_sql_to_ts_ignore(self, library_dir: Path): # Do not continue if existing '.ts_ignore' file is found diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 223dc0216..fb8c5801c 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -97,6 +97,7 @@ class Tag(Base): color_slug: Mapped[str | None] = mapped_column() color: Mapped[TagColorGroup | None] = relationship(lazy="joined") is_category: Mapped[bool] + is_hidden: Mapped[bool] icon: Mapped[str | None] aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag") parent_tags: Mapped[set["Tag"]] = relationship( @@ -138,6 +139,7 @@ def __init__( color_slug: str | None = None, disambiguation_id: int | None = None, is_category: bool = False, + is_hidden: bool = False, ): self.name = name self.aliases = aliases or set() @@ -148,6 +150,7 @@ def __init__( self.shorthand = shorthand self.disambiguation_id = disambiguation_id self.is_category = is_category + self.is_hidden = is_hidden self.id = id # pyright: ignore[reportAttributeAccessIssue] super().__init__() From cda8594f11365dfd1ba22f7e712594aa586b33f2 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 21 Sep 2025 18:47:19 -0400 Subject: [PATCH 2/9] Add hidden checkbox to the edit tag panel --- src/tagstudio/qt/mixed/build_tag.py | 43 ++++++++++++++++++++ src/tagstudio/resources/translations/en.json | 1 + 2 files changed, 44 insertions(+) diff --git a/src/tagstudio/qt/mixed/build_tag.py b/src/tagstudio/qt/mixed/build_tag.py index aabdc47a9..42d53ba36 100644 --- a/src/tagstudio/qt/mixed/build_tag.py +++ b/src/tagstudio/qt/mixed/build_tag.py @@ -245,6 +245,46 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None: ) self.cat_layout.addWidget(self.cat_checkbox) self.cat_layout.addWidget(self.cat_title) + + # Hidden --------------------------------------------------------------- + self.hidden_widget = QWidget() + self.hidden_layout = QHBoxLayout(self.hidden_widget) + self.hidden_layout.setStretch(1, 1) + self.hidden_layout.setContentsMargins(0, 0, 0, 0) + self.hidden_layout.setSpacing(6) + self.hidden_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.hidden_title = QLabel(Translations["tag.is_hidden"]) + self.hidden_checkbox = QCheckBox() + self.hidden_checkbox.setFixedSize(22, 22) + + self.hidden_checkbox.setStyleSheet( + f"QCheckBox{{" + f"background: rgba{primary_color.toTuple()};" + f"color: rgba{text_color.toTuple()};" + f"border-color: rgba{border_color.toTuple()};" + f"border-radius: 6px;" + f"border-style:solid;" + f"border-width: 2px;" + f"}}" + f"QCheckBox::indicator{{" + f"width: 10px;" + f"height: 10px;" + f"border-radius: 2px;" + f"margin: 4px;" + f"}}" + f"QCheckBox::indicator:checked{{" + f"background: rgba{text_color.toTuple()};" + f"}}" + f"QCheckBox::hover{{" + f"border-color: rgba{highlight_color.toTuple()};" + f"}}" + f"QCheckBox::focus{{" + f"border-color: rgba{highlight_color.toTuple()};" + f"outline:none;" + f"}}" + ) + self.hidden_layout.addWidget(self.hidden_checkbox) + self.hidden_layout.addWidget(self.hidden_title) # Add Widgets to Layout ================================================ self.root_layout.addWidget(self.name_widget) @@ -256,6 +296,7 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None: self.root_layout.addWidget(self.color_widget) self.root_layout.addWidget(QLabel("

Properties

")) self.root_layout.addWidget(self.cat_widget) + self.root_layout.addWidget(self.hidden_widget) self.parent_ids: set[int] = set() self.alias_ids: list[int] = [] @@ -544,6 +585,7 @@ def set_tag(self, tag: Tag): self.color_button.set_tag_color_group(None) self.cat_checkbox.setChecked(tag.is_category) + self.hidden_checkbox.setChecked(tag.is_hidden) def on_name_changed(self): is_empty = not self.name_field.text().strip() @@ -567,6 +609,7 @@ def build_tag(self) -> Tag: tag.color_namespace = self.tag_color_namespace tag.color_slug = self.tag_color_slug tag.is_category = self.cat_checkbox.isChecked() + tag.is_hidden = self.hidden_checkbox.isChecked() logger.info("built tag", tag=tag) return tag diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index edda02311..adfd6bb91 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -326,6 +326,7 @@ "tag.disambiguation.tooltip": "Use this tag for disambiguation", "tag.edit": "Edit Tag", "tag.is_category": "Is Category", + "tag.is_hidden": "Is Hidden", "tag.name": "Name", "tag.new": "New Tag", "tag.parent_tags.add": "Add Parent Tag(s)", From e7e01302674e5536a05fb44a7d4d13e0e0ada328 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 21 Sep 2025 18:54:02 -0400 Subject: [PATCH 3/9] Fix formatting --- src/tagstudio/core/library/alchemy/library.py | 8 +++----- src/tagstudio/qt/mixed/build_tag.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 5e689c173..db2af59a0 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -702,7 +702,7 @@ def __apply_db102_repairs(self, session: Session): session.execute(stmt) session.commit() logger.info("[Library][Migration] Verified TagParent table data") - + def __apply_db103_schema_changes(self, session: Session): """Apply database schema changes introduced in DB_VERSION 103.""" add_is_hidden_column = text( @@ -718,13 +718,11 @@ def __apply_db103_schema_changes(self, session: Session): error=e, ) session.rollback() - + def __apply_db103_default_data(self, session: Session): """Apply default data changes introduced in DB_VERSION 103.""" try: - session.query(Tag).\ - filter(Tag.id == TAG_ARCHIVED).\ - update({ 'is_hidden': True }) + session.query(Tag).filter(Tag.id == TAG_ARCHIVED).update({"is_hidden": True}) session.commit() logger.info("[Library][Migration] Updated archived tag to be hidden") session.commit() diff --git a/src/tagstudio/qt/mixed/build_tag.py b/src/tagstudio/qt/mixed/build_tag.py index 42d53ba36..cfffb6595 100644 --- a/src/tagstudio/qt/mixed/build_tag.py +++ b/src/tagstudio/qt/mixed/build_tag.py @@ -245,8 +245,8 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None: ) self.cat_layout.addWidget(self.cat_checkbox) self.cat_layout.addWidget(self.cat_title) - - # Hidden --------------------------------------------------------------- + + # Hidden --------------------------------------------------------------- self.hidden_widget = QWidget() self.hidden_layout = QHBoxLayout(self.hidden_widget) self.hidden_layout.setStretch(1, 1) From 172e083f9f0b8e591b63643209f47704da89d4e9 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Mon, 22 Sep 2025 18:42:46 -0400 Subject: [PATCH 4/9] Exclude hidden tags from search results --- src/tagstudio/core/library/alchemy/library.py | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index db2af59a0..686628a97 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -104,6 +104,7 @@ ) from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder from tagstudio.core.library.json.library import Library as JsonLibrary +from tagstudio.core.query_lang.ast import Constraint, ConstraintType, ANDList, ORList, Not from tagstudio.core.utils.types import unwrap from tagstudio.qt.translations import Translations @@ -1038,13 +1039,27 @@ def search_library( else: statement = select(Entry.id) - if search.ast: + ast = search.ast + + exclude_hidden_tags = True # TODO: Replace with a setting in the BrowsingState + if exclude_hidden_tags: + hidden_tag_ids = self.get_hidden_tag_ids() + hidden_tag_constraints: list[Constraint] = list(map(self.tag_id_to_constraint, hidden_tag_ids)) + hidden_tag_ast = Not( ORList( hidden_tag_constraints ) ) + + if not ast: + ast = hidden_tag_ast + else: + ast = ANDList( [ search.ast, hidden_tag_ast ] ) + + if ast: start_time = time.time() - statement = statement.where(SQLBoolExpressionBuilder(self).visit(search.ast)) + statement = statement.where(SQLBoolExpressionBuilder(self).visit(ast)) end_time = time.time() logger.info( f"SQL Expression Builder finished ({format_timespan(end_time - start_time)})" ) + statement = statement.distinct(Entry.id) sort_on: ColumnExpressionArgument = Entry.id @@ -1835,6 +1850,26 @@ def update_parent_tags(self, tag: Tag, parent_ids: list[int] | set[int], session ) session.add(parent_tag) + def tag_id_to_constraint(self, tag_id: int): + return Constraint( + ConstraintType.TagID, + str(tag_id), + [] + ) + + def get_hidden_tag_ids(self) -> set[int]: + """Get a set containing the IDs of all of the hidden tags and their children.""" + hidden_tag_ids: set[int] = set() + + with Session(self.engine) as session: + root_hidden_tag_ids = session.scalars( + select(Tag.id).where(Tag.is_hidden == True) + ).all() + for root_hidden_tag_id in root_hidden_tag_ids: + hidden_tag_ids.add(root_hidden_tag_id) + + return hidden_tag_ids + def get_version(self, key: str) -> int: """Get a version value from the DB. From 440205c560153e3d984e8175cf74415f74852e44 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Mon, 22 Sep 2025 18:50:05 -0400 Subject: [PATCH 5/9] Fix formatting (I should probably actually check before committing? lmao?) --- src/tagstudio/core/library/alchemy/library.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 686628a97..34a993ddd 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -104,7 +104,7 @@ ) from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder from tagstudio.core.library.json.library import Library as JsonLibrary -from tagstudio.core.query_lang.ast import Constraint, ConstraintType, ANDList, ORList, Not +from tagstudio.core.query_lang.ast import ANDList, Constraint, ConstraintType, Not, ORList from tagstudio.core.utils.types import unwrap from tagstudio.qt.translations import Translations @@ -1044,13 +1044,12 @@ def search_library( exclude_hidden_tags = True # TODO: Replace with a setting in the BrowsingState if exclude_hidden_tags: hidden_tag_ids = self.get_hidden_tag_ids() - hidden_tag_constraints: list[Constraint] = list(map(self.tag_id_to_constraint, hidden_tag_ids)) + hidden_tag_constraints: list[Constraint] = list( + map(self.tag_id_to_constraint, hidden_tag_ids) + ) hidden_tag_ast = Not( ORList( hidden_tag_constraints ) ) - if not ast: - ast = hidden_tag_ast - else: - ast = ANDList( [ search.ast, hidden_tag_ast ] ) + ast = hidden_tag_ast if not ast else ANDList( [ search.ast, hidden_tag_ast ] ) if ast: start_time = time.time() @@ -1863,7 +1862,7 @@ def get_hidden_tag_ids(self) -> set[int]: with Session(self.engine) as session: root_hidden_tag_ids = session.scalars( - select(Tag.id).where(Tag.is_hidden == True) + select(Tag.id).where(Tag.is_hidden == True) # noqa: E712 ).all() for root_hidden_tag_id in root_hidden_tag_ids: hidden_tag_ids.add(root_hidden_tag_id) From 7909e9b83b9fa16cc4be18f651f00b5b927855b2 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Mon, 22 Sep 2025 18:57:00 -0400 Subject: [PATCH 6/9] Bit of cleanup --- src/tagstudio/core/library/alchemy/library.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 34a993ddd..a1d4c45d3 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1041,15 +1041,18 @@ def search_library( ast = search.ast - exclude_hidden_tags = True # TODO: Replace with a setting in the BrowsingState + exclude_hidden_tags = True # TODO: Replace with a setting in the BrowsingState if exclude_hidden_tags: hidden_tag_ids = self.get_hidden_tag_ids() hidden_tag_constraints: list[Constraint] = list( - map(self.tag_id_to_constraint, hidden_tag_ids) + map( + lambda tag_id: Constraint(ConstraintType.TagID, str(tag_id), []), + hidden_tag_ids, + ) ) - hidden_tag_ast = Not( ORList( hidden_tag_constraints ) ) + hidden_tag_ast = Not(ORList(hidden_tag_constraints)) - ast = hidden_tag_ast if not ast else ANDList( [ search.ast, hidden_tag_ast ] ) + ast = hidden_tag_ast if not ast else ANDList([search.ast, hidden_tag_ast]) if ast: start_time = time.time() @@ -1849,20 +1852,13 @@ def update_parent_tags(self, tag: Tag, parent_ids: list[int] | set[int], session ) session.add(parent_tag) - def tag_id_to_constraint(self, tag_id: int): - return Constraint( - ConstraintType.TagID, - str(tag_id), - [] - ) - def get_hidden_tag_ids(self) -> set[int]: - """Get a set containing the IDs of all of the hidden tags and their children.""" + """Get a set containing the IDs of all of the hidden tags.""" hidden_tag_ids: set[int] = set() with Session(self.engine) as session: root_hidden_tag_ids = session.scalars( - select(Tag.id).where(Tag.is_hidden == True) # noqa: E712 + select(Tag.id).where(Tag.is_hidden == True) # noqa: E712 ).all() for root_hidden_tag_id in root_hidden_tag_ids: hidden_tag_ids.add(root_hidden_tag_id) From ef9825eac361400aae0c8ebef0b76b0bc46223c2 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Mon, 22 Sep 2025 20:24:03 -0400 Subject: [PATCH 7/9] Add toggle for excluding hidden entries below search bar --- src/tagstudio/core/library/alchemy/enums.py | 5 ++ src/tagstudio/core/library/alchemy/library.py | 3 +- src/tagstudio/qt/ts_qt.py | 17 +++++ src/tagstudio/qt/views/main_window.py | 65 ++++++++++++++++++- src/tagstudio/resources/translations/en.json | 1 + 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index 76f7fa124..a2d38c48c 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -82,6 +82,8 @@ class BrowsingState: ascending: bool = True random_seed: float = 0 + exclude_hidden_entries: bool = True + query: str | None = None # Abstract Syntax Tree Of the current Search Query @@ -147,6 +149,9 @@ def with_sorting_direction(self, ascending: bool) -> "BrowsingState": def with_search_query(self, search_query: str) -> "BrowsingState": return replace(self, query=search_query) + def with_exclude_hidden_entries(self, exclude_hidden_entries: bool) -> "BrowsingState": + return replace(self, exclude_hidden_entries=exclude_hidden_entries) + class FieldTypeEnum(enum.Enum): TEXT_LINE = "Text Line" diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index a1d4c45d3..717546c88 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1041,8 +1041,7 @@ def search_library( ast = search.ast - exclude_hidden_tags = True # TODO: Replace with a setting in the BrowsingState - if exclude_hidden_tags: + if search.exclude_hidden_entries: hidden_tag_ids = self.get_hidden_tag_ids() hidden_tag_constraints: list[Constraint] = list( map( diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index b2e20f491..393cdbdd6 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -623,6 +623,7 @@ def _update_browsing_state(): BrowsingState.from_search_query(self.main_window.search_field.text()) .with_sorting_mode(self.main_window.sorting_mode) .with_sorting_direction(self.main_window.sorting_direction) + .with_exclude_hidden_entries(self.main_window.exclude_hidden_entries) ) except ParsingError as e: self.main_window.status_bar.showMessage( @@ -655,6 +656,12 @@ def _update_browsing_state(): lambda: self.thumb_size_callback(self.main_window.thumb_size_combobox.currentIndex()) ) + # Exclude hidden entries checkbox + self.main_window.hidden_entries_checkbox.setChecked(True) # Default: Yes + self.main_window.hidden_entries_checkbox.stateChanged.connect( + self.exclude_hidden_entries_callback + ) + self.main_window.back_button.clicked.connect(lambda: self.navigation_callback(-1)) self.main_window.forward_button.clicked.connect(lambda: self.navigation_callback(1)) @@ -1156,6 +1163,16 @@ def thumb_size_callback(self, size: int): min(self.main_window.thumb_size // spacing_divisor, min_spacing) ) + def exclude_hidden_entries_callback(self): + logger.info( + "Exclude Hidden Entries Changed", exclude=self.main_window.exclude_hidden_entries + ) + self.update_browsing_state( + self.browsing_history.current.with_exclude_hidden_entries( + self.main_window.exclude_hidden_entries + ) + ) + def mouse_navigation(self, event: QMouseEvent): # print(event.button()) if event.button() == Qt.MouseButton.ForwardButton: diff --git a/src/tagstudio/qt/views/main_window.py b/src/tagstudio/qt/views/main_window.py index df675fbe6..ee6d1bd6e 100644 --- a/src/tagstudio/qt/views/main_window.py +++ b/src/tagstudio/qt/views/main_window.py @@ -11,13 +11,15 @@ from PIL import Image, ImageQt from PySide6 import QtCore from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt -from PySide6.QtGui import QAction, QPixmap +from PySide6.QtGui import QAction, QColor, QPixmap from PySide6.QtWidgets import ( + QCheckBox, QComboBox, QCompleter, QFrame, QGridLayout, QHBoxLayout, + QLabel, QLayout, QLineEdit, QMainWindow, @@ -34,12 +36,14 @@ ) from tagstudio.core.enums import ShowFilepathOption -from tagstudio.core.library.alchemy.enums import SortingModeEnum +from tagstudio.core.library.alchemy.enums import SortingModeEnum, TagColorEnum from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel from tagstudio.qt.helpers.color_overlay import theme_fg_overlay from tagstudio.qt.mixed.landing import LandingWidget from tagstudio.qt.mixed.pagination import Pagination +from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color from tagstudio.qt.mnemonics import assign_mnemonics +from tagstudio.qt.models.palette import ColorType, get_tag_color from tagstudio.qt.platform_strings import trash_term from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.thumb_grid_layout import ThumbGridLayout @@ -578,7 +582,57 @@ def setup_extra_input_bar(self): self.extra_input_layout = QHBoxLayout() self.extra_input_layout.setObjectName("extra_input_layout") - ## left side spacer + primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)) + border_color = get_border_color(primary_color) + highlight_color = get_highlight_color(primary_color) + text_color: QColor = get_text_color(primary_color, highlight_color) + + ## Exclude hidden tags checkbox + self.hidden_entries_widget = QWidget() + self.hidden_entries_layout = QHBoxLayout(self.hidden_entries_widget) + self.hidden_entries_layout.setStretch(1, 1) + self.hidden_entries_layout.setContentsMargins(0, 0, 0, 0) + self.hidden_entries_layout.setSpacing(6) + self.hidden_entries_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.hidden_entries_title = QLabel(Translations["home.exclude_hidden_entries"]) + self.hidden_entries_checkbox = QCheckBox() + self.hidden_entries_checkbox.setFixedSize(22, 22) + + self.hidden_entries_checkbox.setStyleSheet( + f"QCheckBox{{" + f"background: rgba{primary_color.toTuple()};" + f"color: rgba{text_color.toTuple()};" + f"border-color: rgba{border_color.toTuple()};" + f"border-radius: 6px;" + f"border-style:solid;" + f"border-width: 2px;" + f"}}" + f"QCheckBox::indicator{{" + f"width: 10px;" + f"height: 10px;" + f"border-radius: 2px;" + f"margin: 4px;" + f"}}" + f"QCheckBox::indicator:checked{{" + f"background: rgba{text_color.toTuple()};" + f"}}" + f"QCheckBox::hover{{" + f"border-color: rgba{highlight_color.toTuple()};" + f"}}" + f"QCheckBox::focus{{" + f"border-color: rgba{highlight_color.toTuple()};" + f"outline:none;" + f"}}" + ) + + self.hidden_entries_checkbox.setChecked(True) # Default: Yes + + self.hidden_entries_layout.addWidget(self.hidden_entries_checkbox) + self.hidden_entries_layout.addWidget(self.hidden_entries_title) + + self.extra_input_layout.addWidget(self.hidden_entries_widget) + + ## Spacer self.extra_input_layout.addItem( QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) ) @@ -712,3 +766,8 @@ def sorting_direction(self) -> bool: @property def thumb_size(self) -> int: return self.thumb_size_combobox.currentData() + + @property + def exclude_hidden_entries(self) -> bool: + """Whether to exclude entries tagged with hidden tags.""" + return self.hidden_entries_checkbox.isChecked() diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index adfd6bb91..58e411b0e 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -146,6 +146,7 @@ "home.thumbnail_size.mini": "Mini Thumbnails", "home.thumbnail_size.small": "Small Thumbnails", "home.thumbnail_size": "Thumbnail Size", + "home.exclude_hidden_entries": "Exclude Hidden Entries?", "ignore.open_file": "Show \"{ts_ignore}\" File on Disk", "json_migration.checking_for_parity": "Checking for Parity...", "json_migration.creating_database_tables": "Creating SQL Database Tables...", From 608dce147e5aa9aeec92070852dfd23ec6809e53 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Mon, 22 Sep 2025 20:46:33 -0400 Subject: [PATCH 8/9] That might be important maybe --- src/tagstudio/core/library/alchemy/visitors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tagstudio/core/library/alchemy/visitors.py b/src/tagstudio/core/library/alchemy/visitors.py index c24c8ed7a..50c490ac8 100644 --- a/src/tagstudio/core/library/alchemy/visitors.py +++ b/src/tagstudio/core/library/alchemy/visitors.py @@ -171,6 +171,8 @@ def __separate_tags( continue case ConstraintType.FileType: pass + case ConstraintType.MediaType: + pass case ConstraintType.Path: pass case ConstraintType.Special: From d4d2371240f13beed7479f6209e3cf387322f544 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Mon, 22 Sep 2025 21:14:24 -0400 Subject: [PATCH 9/9] Update Save Format Changes page in docs (and include updated test database) --- docs/updates/schema_changes.md | 9 +++++++++ .../.TagStudio/ts_library.sqlite | Bin 114688 -> 114688 bytes 2 files changed, 9 insertions(+) diff --git a/docs/updates/schema_changes.md b/docs/updates/schema_changes.md index 2d721d8be..7a4aefc63 100644 --- a/docs/updates/schema_changes.md +++ b/docs/updates/schema_changes.md @@ -123,3 +123,12 @@ Migration from the legacy JSON format is provided via a walkthrough when opening | [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | ``/.TagStudio/ts_library.sqlite | - Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted. + +#### Version 103 + +| Used From | Format | Location | +| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- | +| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | SQLite | ``/.TagStudio/ts_library.sqlite | + +- Adds the `is_hidden` column to the `tags` table (default `0`). Used for excluding entries tagged with hidden tags from library searches. +- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default. \ No newline at end of file diff --git a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite index 60b2056248d621b2e55c0fe94d912939b7e739eb..65b05998138eefe50fd0c694ec4046c8402377df 100644 GIT binary patch delta 221 zcmZo@U~gz(pCB!GgMooT28dyRf1-{t>kS6IHqDJG?){AEo7eP*HgNLaV9@7iW8i4p z&LzND!=b6pz{McTYbeaYz`*HPl$?=SmYPzMm|k2`nv$8XTU?Tumy%eNQk0sqeZ2&u zCOZqzbdBwIf@u};_BuY>Jy@1Fjg8 delta 125 zcmZo@U~gz(pCB!Gg@J)V28dyRZ=#Md>lFsQvhIy3?){8uo7eP*HgE!E^*Pov@ZZ?Z zCBRt2G1)*sbNhM;Moo4WH3m+G?RVrE#R?eHwtwzt