mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
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:
@@ -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.
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}"))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user