Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/updates/schema_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion src/tagstudio/core/library/alchemy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions src/tagstudio/core/library/alchemy/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"))
Expand Down
5 changes: 5 additions & 0 deletions src/tagstudio/core/library/alchemy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
68 changes: 66 additions & 2 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ANDList, Constraint, ConstraintType, Not, ORList
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.translations import Translations

Expand Down Expand Up @@ -151,6 +152,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",
)
Expand Down Expand Up @@ -540,6 +542,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)

Expand All @@ -551,6 +555,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)
Expand Down Expand Up @@ -698,6 +704,36 @@ def __apply_db102_repairs(self, session: Session):
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():
Expand Down Expand Up @@ -1003,13 +1039,28 @@ def search_library(
else:
statement = select(Entry.id)

if search.ast:
ast = search.ast

if search.exclude_hidden_entries:
hidden_tag_ids = self.get_hidden_tag_ids()
hidden_tag_constraints: list[Constraint] = list(
map(
lambda tag_id: Constraint(ConstraintType.TagID, str(tag_id), []),
hidden_tag_ids,
)
)
hidden_tag_ast = Not(ORList(hidden_tag_constraints))

ast = hidden_tag_ast if not ast else 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
Expand Down Expand Up @@ -1800,6 +1851,19 @@ def update_parent_tags(self, tag: Tag, parent_ids: list[int] | set[int], session
)
session.add(parent_tag)

def get_hidden_tag_ids(self) -> set[int]:
"""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
).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.

Expand Down
3 changes: 3 additions & 0 deletions src/tagstudio/core/library/alchemy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -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__()

Expand Down
2 changes: 2 additions & 0 deletions src/tagstudio/core/library/alchemy/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ def __separate_tags(
continue
case ConstraintType.FileType:
pass
case ConstraintType.MediaType:
pass
case ConstraintType.Path:
pass
case ConstraintType.Special:
Expand Down
43 changes: 43 additions & 0 deletions src/tagstudio/qt/mixed/build_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,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)
self.root_layout.addWidget(self.shorthand_widget)
Expand All @@ -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("<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] = []
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/tagstudio/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand Down
Loading