Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: tag categories #655

Merged
merged 69 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
260a4cf
refactor: remove TagBoxField and TagField (NOT WORKING)
python357-1 Dec 2, 2024
ab59fc4
refactor: remove tag field types
CyanVoxel Dec 8, 2024
f431cf7
ci: fix mypy and ruff tests
CyanVoxel Dec 8, 2024
d17e279
refactor: split up preview_panel
CyanVoxel Dec 22, 2024
3dd740f
fix: search now uses `TagEntry` (#656)
Computerdores Dec 22, 2024
d16dd57
fix: move theme check inside class
CyanVoxel Dec 30, 2024
3d7e0cb
refactor: reimplement file previews
CyanVoxel Dec 31, 2024
eba5583
refactor: modularize `file_attributes.py`
CyanVoxel Dec 31, 2024
3123dff
ui: show fields in preview panel
CyanVoxel Dec 31, 2024
f58332e
search: remove TagEntry join
CyanVoxel Dec 31, 2024
82e06ec
fix: remove extra `self.filter` assignment
CyanVoxel Jan 1, 2025
a306d68
add success return flag to `add_tags_to_entry()`
CyanVoxel Jan 1, 2025
b79c59d
refactor: use entry IDs instead of objects and indices
CyanVoxel Jan 1, 2025
c320af2
feat: add tag categories to preview panel
CyanVoxel Jan 3, 2025
2d7e89d
ui: add "is category" checkbox in tag panel
CyanVoxel Jan 3, 2025
b03211e
fix: tags can be compared for name sorting
CyanVoxel Jan 3, 2025
56a80ff
fix: don't add tags to previous selections
CyanVoxel Jan 3, 2025
feda150
fix: badges now properly update
CyanVoxel Jan 3, 2025
0e7f75e
ui: hide sizeGrip
CyanVoxel Jan 3, 2025
2c91cf6
ui: add blue ui color
CyanVoxel Jan 3, 2025
ff04802
ui: display empty selection; better multi-selection
CyanVoxel Jan 3, 2025
73bfda7
cleanup comments; rename tsp to tag_search_panel
CyanVoxel Jan 3, 2025
b867bc2
fix(ui): properly unset container callbacks
CyanVoxel Jan 3, 2025
d016eef
fix: optimize queries
CyanVoxel Jan 3, 2025
45d502f
fix: catch int cast exception
CyanVoxel Jan 3, 2025
741c282
fix: remove unnecessary update calls
CyanVoxel Jan 3, 2025
7bb4cb1
fix: restore try/except block in preview_panel
CyanVoxel Jan 4, 2025
1755cb9
fix: correct type hints for get_tag_categories
CyanVoxel Jan 4, 2025
50815aa
fix: tags no longer lazy load subtags and aliases
CyanVoxel Jan 4, 2025
5ef1eeb
fix: recursively include parent tag categories
CyanVoxel Jan 4, 2025
b0324cd
chore: update copyright info
CyanVoxel Jan 4, 2025
d37e461
chore: remove unused code
CyanVoxel Jan 4, 2025
8084e88
fix: load fields for selected entry
CyanVoxel Jan 4, 2025
7d2dadb
refactor: remove `is_connected` from AddFieldModal
CyanVoxel Jan 4, 2025
f72ef71
fix: include category tags under their own categories
CyanVoxel Jan 4, 2025
11fcea1
fix: badges now update when last tag is removed
CyanVoxel Jan 5, 2025
ab93705
fix: resolve differences with main
CyanVoxel Jan 5, 2025
369d2dc
fix: return empty set in place of `None`
CyanVoxel Jan 5, 2025
af511d8
ui: add field highlighting, tweak theming
CyanVoxel Jan 5, 2025
6461eeb
refactor!: eradicate use of the term "subtag"
CyanVoxel Jan 6, 2025
c1cea16
fix: catch and show library load errors
CyanVoxel Jan 6, 2025
70c7f4f
tests: fix and/or remove tests
CyanVoxel Jan 6, 2025
be3d237
suppress db preference warnings
CyanVoxel Jan 7, 2025
91ae1d9
tests: add field container tests
CyanVoxel Jan 7, 2025
ec80f24
tests: add tag category tests
CyanVoxel Jan 7, 2025
0312291
refactor(ui): move recent libraries list to file menu
CyanVoxel Jan 7, 2025
9706483
docs: update roadmap and docs for tag categories
CyanVoxel Jan 7, 2025
1fc584d
fix: restore json migration functionality
CyanVoxel Jan 7, 2025
c2ab38c
logs: remove/update debug logs
CyanVoxel Jan 7, 2025
7e1978d
chore: remove unused code
CyanVoxel Jan 7, 2025
5207f28
tests: remove tests related to `TagBoxWidget`
CyanVoxel Jan 7, 2025
c8bbfe5
ui: optimize selection and badge updates
CyanVoxel Jan 8, 2025
389d0fb
docs: update usage
CyanVoxel Jan 9, 2025
6d2c1ec
fix: change typo of `tag.id` to `tag_id`
CyanVoxel Jan 10, 2025
17e0522
fix: use term "child tags" instead of "subtags" in docstring
CyanVoxel Jan 10, 2025
77d957f
fix: reference `child_id` instead of `parent_id` when deleting tags
CyanVoxel Jan 10, 2025
5e1e4c5
add TODO comment for `update_thumbs()` optimization
CyanVoxel Jan 10, 2025
114bc04
fix: combine and check (most) built-in tag data from JSON
CyanVoxel Jan 11, 2025
3049afa
refactor: rename `select_item()` to `toggle_item_selection()`
CyanVoxel Jan 11, 2025
5a4ba68
add TODO to optimize `add_tags_to_entry()`
CyanVoxel Jan 11, 2025
4c019ca
fix: remove unnecessary joins in search
CyanVoxel Jan 11, 2025
2c966ad
Revert "fix: remove unnecessary joins in search"
CyanVoxel Jan 11, 2025
cf30ee6
fix: remove unnecessary joins in search
CyanVoxel Jan 11, 2025
a0fe867
reremove unused method `get_all_child_tag_ids()`
CyanVoxel Jan 11, 2025
cacfc26
Merge branch 'main' into tag-categories-feat
CyanVoxel Jan 11, 2025
db53d61
fix: migrate user-edited tag colors for built-in tags
CyanVoxel Jan 11, 2025
959a76d
style: update header for contributor-created files
CyanVoxel Jan 11, 2025
4030e80
fix: use absolute path in "open file" context menu
CyanVoxel Jan 11, 2025
59122dc
chore: change paramater type hint
CyanVoxel Jan 11, 2025
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
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ Translation hosting generously provided by [Weblate](https://weblate.org/en/). C
- Create libraries/vaults centered around a system directory. Libraries contain a series of entries: the representations of your files combined with metadata fields. Each entry represents a file in your library’s directory, and is linked to its location.
- Address moved, deleted, or otherwise "unlinked" files by using the "Fix Unlinked Entries" option in the Tools menu.

### Metadata + Tagging
### Tagging + Custom Metadata

- Add custom powerful tags to your library entries
- Add metadata to your library entries, including:
- Name, Author, Artist (Single-Line Text Fields)
- Description, Notes (Multiline Text Fields)
- Tags, Meta Tags, Content Tags (Tag Boxes)
- Create rich tags composed of a name, a list of aliases, and a list of “parent tags” - being tags in which these tags inherit values from.
- Copy and paste tags and fields across file entries
- Generate tags from your existing folder structure with the "Folders to Tags" macro (NOTE: these tags do NOT sync with folders after they are created)
Expand Down Expand Up @@ -136,20 +136,22 @@ In order to scan for new files or file changes, you’ll need to manually go to
> [!NOTE]
> In the future, library refreshing will also be automatically done in the background, or additionally on app startup.

### Adding Metadata to Entries
### Adding Tags to File Entries

To add a metadata field to a file entry, start by clicking the “Add Field” button under the file preview in the right-hand preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.
Click the "Add Tag" button at the bottom of the preview panel with one or more tags selected. Search for existing inside the new dialog popup or create a new one from here. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the first item in the list. Press Enter/Return once more to close the dialog box.

To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.

### Adding Metadata to File Entries

To add a metadata field to a file entry, start by clicking the “Add Field” button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry

### Editing Metadata Fields

#### Text Line / Text Box

Hover over the field and click the pencil icon. From there, add or edit text in the dialog box popup.

#### Tag Box

Click the “+” button at the end of the Tags list, and search for tags to add inside the new dialog popup. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the add the first item in the list. Press Enter/Return once more to close the dialog box

> [!WARNING]
> Keyboard control and navigation is currently _very_ buggy, but will be improved in future versions.

Expand Down
13 changes: 10 additions & 3 deletions docs/library/tag_categories.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
---
tags:
- Upcoming Feature
---

# Tag Categories
# Tag Categories (v9.5)

Replaces [Tag Fields](field.md#tag_box). Tags are able to be marked as a “category” which then displays as tag fields currently do, with any tags inheriting from that category being displayed underneath.
The "Is Category" property of tags determines if a tag should be treated as a category itself when being organized inside the preview panel. Tags marked as categories will show themselves and all tags inheriting from it (including recursively) underneath a field-like section with the tag's name. This means that duplicates of tags can appear on entries if the tag inherits from multiple parent categories, however this is by design and reflects the nature multiple inheritance. Any tags not inheriting from a category tag will simply show under a default "Tag" section.

### Built-In Tags and Categories

The built-in tags "Favorite" and "Archived" inherit from the built-in "Meta Tags" category which is marked as a category by default. This behavior of default tags can be fully customized by disabling the category option and/or by adding/removing the tags' Parent Tags.

### Migrating from v9.4 Libraries

Due to the nature of how tags and Tag Felids operated prior to v9.5, the organization style of Tag Categories vs Tag Fields is not 1:1. Instead of tags being organized into fields on a per-entry basis, tags themselves determine their organizational layout via the "Is Property" flag. Any tags _(not currently inheriting from either the "Favorite" or "Archived" tags)_ will be shown under the default "Tags" header upon migrating to the v9.5+ library format. Similar organization to Tag Fields can be achieved by using the built-in "Meta Tags" tag or any other marked with "Is Category" and then setting those tags as parents for other tags to inherit from.
8 changes: 4 additions & 4 deletions docs/updates/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ Features are broken up into the following priority levels, with nested prioritie
- [ ] Existing colors are now a set of base colors [HIGH]
- [ ] Editable [MEDIUM]
- [ ] Non-removable [HIGH]
- [ ] [Tag Categories](../library/tag_categories.md) [HIGH]
- [ ] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
- [ ] Title is tag name [HIGH]
- [ ] Title has tag color [MEDIUM]
- [ ] Tag marked as category does not display as a tag itself [HIGH]
Expand Down Expand Up @@ -169,8 +169,8 @@ These version milestones are rough estimations for when the previous core featur
- [ ] Existing colors are now a set of base colors [HIGH]
- [ ] Editable [MEDIUM]
- [ ] Non-removable [HIGH]
- [ ] [Tag Categories](../library/tag_categories.md) [HIGH]
- [ ] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
- [ ] Search engine [HIGH]
- [x] Boolean operators [HIGH]
- [ ] Tag objects + autocomplete [HIGH]
Expand Down
17 changes: 11 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ In order to scan for new files or file changes, you’ll need to manually go to
!!! note
In the future, library refreshing will also be automatically done in the background, or additionally on app startup.

## Adding Metadata to Entries
## Adding Tags to File Entries
Click the "Add Tag" button at the bottom of the preview panel with one or more tags selected. Search for existing inside the new dialog popup or create a new one from. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the first item in the list. Press Enter/Return once more to close the dialog box.

To add a metadata field to a file entry, start by clicking the “Add Field” button under the file preview in the right-hand preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.
To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.

## Adding Metadata Fields to File Entries

To add a metadata field to a file entry, start by clicking the “Add Field” button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.

## Editing Metadata Fields

### Text Line / Text Box

Hover over the field and click the pencil icon. From there, add or edit text in the dialog box popup.

### Tag Box

Click the “+” button at the end of the Tags list, and search for tags to add inside the new dialog popup. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the add the first item in the list. Press Enter/Return once more to close the dialog box

!!! warning
Keyboard control and navigation is currently _very_ buggy, but will be improved in future versions.

Expand All @@ -51,6 +52,10 @@ Inevitably, some of the files inside your library will be renamed, moved, or del
!!! warning
If multiple matches for a moved file are found (matches are currently defined as files with a matching filename as the original), TagStudio will currently ignore the match groups. Adding a GUI for manual selection, as well as smarter automated relinking, are top priorities for future versions.

## Deleting Tags

To delete a tag from your library, go to File -> Tag Manager, hover over the tag you wish to delete, and click the "-" icon that appears. You will be prompted to make sure you wish to delete this tag from your library and across all file entries.

## Saving the Library

Libraries are saved upon exiting the program. To manually save, select File -> Save Library from the menu bar. To save a backup of your library, select File -> Save Library Backup from the menu bar.
Expand Down
2 changes: 2 additions & 0 deletions tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@
"menu.file.close_library": "&Close Library",
"menu.file.new_library": "New Library",
"menu.file.open_create_library": "&Open/Create Library",
"menu.file.open_recent_library": "Open Recent",
"menu.file.clear_recent_libraries": "Clear Recent",
"menu.file.open_library": "Open Library",
"menu.file.refresh_directories": "&Refresh Directories",
"menu.file.save_backup": "&Save Library Backup",
Expand Down
11 changes: 10 additions & 1 deletion tagstudio/src/core/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

VERSION: str = "9.5.0" # Major.Minor.Patch
VERSION_BRANCH: str = "EXPERIMENTAL" # Usually "" or "Pre-Release"

Expand All @@ -11,7 +15,12 @@
)
FONT_SAMPLE_SIZES: list[int] = [10, 15, 20]

TAG_FAVORITE = 1
# NOTE: These were the field IDs used for the "Tags", "Content Tags", and "Meta Tags" fields inside
# the legacy JSON database. These are used to help migrate libraries from JSON to SQLite.
LEGACY_TAG_FIELD_IDS: set[int] = {6, 7, 8}

TAG_ARCHIVED = 0
TAG_FAVORITE = 1
TAG_META = 2
RESERVED_TAG_START = 0
RESERVED_TAG_END = 999
15 changes: 10 additions & 5 deletions tagstudio/src/core/enums.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

import enum
from typing import Any
from uuid import uuid4
Expand All @@ -20,10 +24,11 @@ class Theme(str, enum.Enum):
COLOR_DARK_LABEL = "#DD000000"
COLOR_BG = "#65000000"

COLOR_HOVER = "#65AAAAAA"
COLOR_PRESSED = "#65EEEEEE"
COLOR_DISABLED = "#65F39CAA"
COLOR_DISABLED_BG = "#65440D12"
COLOR_HOVER = "#65444444"
COLOR_PRESSED = "#65777777"
COLOR_DISABLED_BG = "#30000000"
COLOR_FORBIDDEN = "#65F39CAA"
COLOR_FORBIDDEN_BG = "#65440D12"


class OpenStatus(enum.IntEnum):
Expand Down Expand Up @@ -65,4 +70,4 @@ class LibraryPrefs(DefaultEnum):
IS_EXCLUDE_LIST = True
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
PAGE_SIZE: int = 500
DB_VERSION: int = 2
DB_VERSION: int = 3
9 changes: 8 additions & 1 deletion tagstudio/src/core/library/alchemy/db.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

from pathlib import Path

import structlog
Expand Down Expand Up @@ -44,7 +48,10 @@ def make_tables(engine: Engine) -> None:
autoincrement_val = result.scalar()
if not autoincrement_val or autoincrement_val <= RESERVED_TAG_END:
conn.execute(
text(f"INSERT INTO tags (id, name, color) VALUES ({RESERVED_TAG_END}, 'temp', 1)")
text(
"INSERT INTO tags (id, name, color, is_category) VALUES "
f"({RESERVED_TAG_END}, 'temp', 1, false)"
)
)
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))
conn.commit()
Expand Down
33 changes: 6 additions & 27 deletions tagstudio/src/core/library/alchemy/fields.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

from __future__ import annotations

from dataclasses import dataclass, field
Expand All @@ -11,7 +15,7 @@
from .enums import FieldTypeEnum

if TYPE_CHECKING:
from .models import Entry, Tag, ValueType
from .models import Entry, ValueType


class BaseField(Base):
Expand Down Expand Up @@ -75,33 +79,11 @@ def __key(self) -> tuple:
def __eq__(self, value) -> bool:
if isinstance(value, TextField):
return self.__key() == value.__key()
elif isinstance(value, (TagBoxField, DatetimeField)):
elif isinstance(value, DatetimeField):
return False
raise NotImplementedError


class TagBoxField(BaseField):
__tablename__ = "tag_box_fields"

tags: Mapped[set[Tag]] = relationship(secondary="tag_fields")

def __key(self):
return (
self.entry_id,
self.type_key,
)

@property
def value(self) -> None:
"""For interface compatibility with other field types."""
return None

def __eq__(self, value) -> bool:
if isinstance(value, TagBoxField):
return self.__key() == value.__key()
raise NotImplementedError


class DatetimeField(BaseField):
__tablename__ = "datetime_fields"

Expand Down Expand Up @@ -133,9 +115,6 @@ class _FieldID(Enum):
URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE)
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_LINE)
NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX)
TAGS = DefaultField(id=6, name="Tags", type=FieldTypeEnum.TAGS)
TAGS_CONTENT = DefaultField(id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True)
TAGS_META = DefaultField(id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True)
COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE)
DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME)
DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME)
Expand Down
14 changes: 9 additions & 5 deletions tagstudio/src/core/library/alchemy/joins.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column

from .db import Base


class TagSubtag(Base):
__tablename__ = "tag_subtags"
class TagParent(Base):
__tablename__ = "tag_parents"

parent_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
child_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)


class TagField(Base):
__tablename__ = "tag_fields"
class TagEntry(Base):
__tablename__ = "tag_entries"

field_id: Mapped[int] = mapped_column(ForeignKey("tag_box_fields.id"), primary_key=True)
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
entry_id: Mapped[int] = mapped_column(ForeignKey("entries.id"), primary_key=True)
Loading
Loading