feat: add hidden tags (#1139)

* Add `is_hidden` field to the `tags` table

* Add hidden checkbox to the edit tag panel

* Fix formatting

* Exclude hidden tags from search results

* Fix formatting (I should probably actually check before committing? lmao?)

* Bit of cleanup

* Add toggle for excluding hidden entries below search bar

* That might be important maybe

* Update Save Format Changes page in docs (and include updated test database)

* Simplify query and invert name+logic

* chore: remove unused code, tweak strings

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
This commit is contained in:
Trigam
2025-11-26 01:48:36 -05:00
committed by GitHub
parent c38cc9daaa
commit 88d0b47a86
12 changed files with 187 additions and 8 deletions

View File

@@ -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 | `<Library Folder>`/.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 | `<Library Folder>`/.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.

View File

@@ -11,7 +11,7 @@ JSON_FILENAME: str = "ts_library.json"
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 (

View File

@@ -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}"))

View File

@@ -82,6 +82,8 @@ class BrowsingState:
ascending: bool = True
random_seed: float = 0
show_hidden_entries: bool = False
query: str | None = None
# Abstract Syntax Tree Of the current Search Query
@@ -147,6 +149,9 @@ class BrowsingState:
def with_search_query(self, search_query: str) -> "BrowsingState":
return replace(self, query=search_query)
def with_show_hidden_entries(self, show_hidden_entries: bool) -> "BrowsingState":
return replace(self, show_hidden_entries=show_hidden_entries)
class FieldTypeEnum(enum.Enum):
TEXT_LINE = "Text Line"

View File

@@ -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 @@ class Library:
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 @@ class Library:
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)
@@ -698,6 +703,36 @@ class Library:
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
if Path(library_dir / TS_FOLDER_NAME / IGNORE_NAME).exists():
@@ -1003,13 +1038,19 @@ class Library:
else:
statement = select(Entry.id)
if search.ast:
ast = search.ast
if not search.show_hidden_entries:
statement = statement.where(~Entry.tags.any(Tag.is_hidden))
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

View File

@@ -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 @@ class Tag(Base):
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 @@ class Tag(Base):
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__()

View File

@@ -171,6 +171,8 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
continue
case ConstraintType.FileType:
pass
case ConstraintType.MediaType:
pass
case ConstraintType.Path:
pass
case ConstraintType.Special:

View File

@@ -246,6 +246,46 @@ class BuildTagPanel(PanelWidget):
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)
self.root_layout.addWidget(self.shorthand_widget)
@@ -256,6 +296,7 @@ class BuildTagPanel(PanelWidget):
self.root_layout.addWidget(self.color_widget)
self.root_layout.addWidget(QLabel("<h3>Properties</h3>"))
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 @@ class BuildTagPanel(PanelWidget):
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 @@ class BuildTagPanel(PanelWidget):
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

View File

@@ -627,6 +627,7 @@ class QtDriver(DriverMixin, QObject):
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_show_hidden_entries(self.main_window.show_hidden_entries)
)
except ParsingError as e:
self.main_window.status_bar.showMessage(
@@ -659,6 +660,12 @@ class QtDriver(DriverMixin, QObject):
lambda: self.thumb_size_callback(self.main_window.thumb_size_combobox.currentIndex())
)
# Exclude hidden entries checkbox
self.main_window.show_hidden_entries_checkbox.setChecked(False) # Default: No
self.main_window.show_hidden_entries_checkbox.stateChanged.connect(
self.show_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))
@@ -1174,6 +1181,14 @@ class QtDriver(DriverMixin, QObject):
min(self.main_window.thumb_size // spacing_divisor, min_spacing)
)
def show_hidden_entries_callback(self):
logger.info("Show Hidden Entries Changed", exclude=self.main_window.show_hidden_entries)
self.update_browsing_state(
self.browsing_history.current.with_show_hidden_entries(
self.main_window.show_hidden_entries
)
)
def mouse_navigation(self, event: QMouseEvent):
# print(event.button())
if event.button() == Qt.MouseButton.ForwardButton:

View File

@@ -11,13 +11,15 @@ import structlog
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 PySide6.QtWidgets import (
)
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 @@ class MainWindow(QMainWindow):
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)
## Show hidden entries checkbox
self.show_hidden_entries_widget = QWidget()
self.show_hidden_entries_layout = QHBoxLayout(self.show_hidden_entries_widget)
self.show_hidden_entries_layout.setStretch(1, 1)
self.show_hidden_entries_layout.setContentsMargins(0, 0, 0, 0)
self.show_hidden_entries_layout.setSpacing(6)
self.show_hidden_entries_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.show_hidden_entries_title = QLabel(Translations["home.show_hidden_entries"])
self.show_hidden_entries_checkbox = QCheckBox()
self.show_hidden_entries_checkbox.setFixedSize(22, 22)
self.show_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.show_hidden_entries_checkbox.setChecked(False) # Default: No
self.show_hidden_entries_layout.addWidget(self.show_hidden_entries_checkbox)
self.show_hidden_entries_layout.addWidget(self.show_hidden_entries_title)
self.extra_input_layout.addWidget(self.show_hidden_entries_widget)
## Spacer
self.extra_input_layout.addItem(
QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
)
@@ -712,3 +766,8 @@ class MainWindow(QMainWindow):
@property
def thumb_size(self) -> int:
return self.thumb_size_combobox.currentData()
@property
def show_hidden_entries(self) -> bool:
"""Whether to show entries tagged with hidden tags."""
return self.show_hidden_entries_checkbox.isChecked()

View File

@@ -146,6 +146,7 @@
"home.thumbnail_size.mini": "Mini Thumbnails",
"home.thumbnail_size.small": "Small Thumbnails",
"home.thumbnail_size": "Thumbnail Size",
"home.show_hidden_entries": "Show 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...",
@@ -326,6 +327,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)",