From e3c1d02705ef7d8ea38bbb54fc394e2456e9876a Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 12:47:30 -0400 Subject: [PATCH 01/12] Add URL field type --- .../core/library/alchemy/constants.py | 2 +- src/tagstudio/core/library/alchemy/enums.py | 1 + src/tagstudio/core/library/alchemy/fields.py | 20 ++++- src/tagstudio/core/library/alchemy/library.py | 86 ++++++++++++++++++- src/tagstudio/core/library/alchemy/models.py | 10 +++ src/tagstudio/qt/mixed/field_containers.py | 49 ++++++++++- src/tagstudio/qt/mixed/text_field.py | 22 +---- src/tagstudio/qt/mixed/url_widget.py | 29 +++++++ 8 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 src/tagstudio/qt/mixed/url_widget.py diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index 83aab71b0..7280acaf9 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 = 104 TAG_CHILDREN_QUERY = text(""" WITH RECURSIVE ChildTags AS ( diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index 76f7fa124..4d00fb80d 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -151,6 +151,7 @@ def with_search_query(self, search_query: str) -> "BrowsingState": class FieldTypeEnum(enum.Enum): TEXT_LINE = "Text Line" TEXT_BOX = "Text Box" + URL = "URL" TAGS = "Tags" DATETIME = "Datetime" BOOLEAN = "Checkbox" diff --git a/src/tagstudio/core/library/alchemy/fields.py b/src/tagstudio/core/library/alchemy/fields.py index faffae079..c7236a7f0 100644 --- a/src/tagstudio/core/library/alchemy/fields.py +++ b/src/tagstudio/core/library/alchemy/fields.py @@ -88,6 +88,22 @@ def __eq__(self, value: object) -> bool: raise NotImplementedError +class UrlField(BaseField): + __tablename__ = "url_fields" + + title: Mapped[str | None] + value: Mapped[str | None] + + def __key(self) -> tuple[ValueType, str | None, str | None]: + return self.type, self.title, self.value + + @override + def __eq__(self, value: object) -> bool: + if isinstance(value, UrlField): + return self.__key() == value.__key() + raise NotImplementedError + + class DatetimeField(BaseField): __tablename__ = "datetime_fields" @@ -117,7 +133,7 @@ class FieldID(Enum): TITLE = DefaultField(id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True) AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE) ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE) - URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE) + URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.URL) DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_BOX) NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX) COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE) @@ -132,7 +148,7 @@ class FieldID(Enum): COMIC = DefaultField(id=18, name="Comic", type=FieldTypeEnum.TEXT_LINE) SERIES = DefaultField(id=19, name="Series", type=FieldTypeEnum.TEXT_LINE) MANGA = DefaultField(id=20, name="Manga", type=FieldTypeEnum.TEXT_LINE) - SOURCE = DefaultField(id=21, name="Source", type=FieldTypeEnum.TEXT_LINE) + SOURCE = DefaultField(id=21, name="Source", type=FieldTypeEnum.URL) DATE_UPLOADED = DefaultField(id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME) DATE_RELEASED = DefaultField(id=23, name="Date Released", type=FieldTypeEnum.DATETIME) VOLUME = DefaultField(id=24, name="Volume", type=FieldTypeEnum.TEXT_LINE) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index a25231e95..3100d1dde 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -89,6 +89,7 @@ DatetimeField, FieldID, TextField, + UrlField, ) from tagstudio.core.library.alchemy.joins import TagEntry, TagParent from tagstudio.core.library.alchemy.models import ( @@ -551,6 +552,9 @@ 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 < 104: + self.__apply_db104_value_type_migration(session) + self.__apply_db104_url_migration(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 +702,67 @@ def __apply_db102_repairs(self, session: Session): session.commit() logger.info("[Library][Migration] Verified TagParent table data") + def __apply_db104_value_type_migration(self, session: Session): + """Changes the type of the URL field types to URL.""" + try: + with session: + stmt = ( + update(ValueType) + .filter(ValueType.key.in_([ FieldID.URL.name, FieldID.SOURCE.name ])) + .values( + type=FieldTypeEnum.URL.name, + ) + ) + + session.execute(stmt) + session.commit() + logger.info("[Library][Migration] Changed the type of the URL field types to URL!") + except Exception as e: + logger.error( + "[Library][Migration] Could not change the type of the URL field types to URL!", + error=e, + ) + session.rollback() + + + def __apply_db104_url_migration(self, session: Session): + """Moves all URL text fields to the new URL field table.""" + try: + with session: + # Get all URL fields from the text fields table + source_records = ( + session.query(TextField) + .join(ValueType) + .filter(ValueType.type == FieldTypeEnum.URL.name) + .with_for_update() + .all() + ) + + destination_records = [] + for source_record in source_records: + destination_record = UrlField( + title=None, + value=source_record.value, + type_key=source_record.type_key, + entry_id=source_record.entry_id, + position=source_record.position + ) + destination_records.append(destination_record) + + for record in source_records: + session.delete(record) + + session.add_all(destination_records) + session.commit() + + logger.info("[Library][Migration] Migrated URL fields to the url_fields table") + except Exception as e: + logger.error( + "[Library][Migration] Could not migrate URL fields to the url_fields table!", + 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(): @@ -762,9 +827,11 @@ def get_entry_full( if with_fields: entry_stmt = ( entry_stmt.outerjoin(Entry.text_fields) + .outerjoin(Entry.url_fields) .outerjoin(Entry.datetime_fields) .options( selectinload(Entry.text_fields), + selectinload(Entry.url_fields), selectinload(Entry.datetime_fields), ) ) @@ -811,11 +878,13 @@ def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]: statement = select(Entry).where(Entry.id.in_(set(entry_ids))) statement = ( statement.outerjoin(Entry.text_fields) + .outerjoin(Entry.url_fields) .outerjoin(Entry.datetime_fields) .outerjoin(Entry.tags) ) statement = statement.options( selectinload(Entry.text_fields), + selectinload(Entry.url_fields), selectinload(Entry.datetime_fields), selectinload(Entry.tags).options( selectinload(Tag.aliases), @@ -839,8 +908,13 @@ def get_entry_full_by_path(self, path: Path) -> Entry | None: stmt = select(Entry).where(Entry.path == path) stmt = ( stmt.outerjoin(Entry.text_fields) + .outerjoin(Entry.url_fields) .outerjoin(Entry.datetime_fields) - .options(selectinload(Entry.text_fields), selectinload(Entry.datetime_fields)) + .options( + selectinload(Entry.text_fields), + selectinload(Entry.url_fields), + selectinload(Entry.datetime_fields) + ) ) stmt = ( stmt.outerjoin(Entry.tags) @@ -885,11 +959,13 @@ def all_entries(self, with_joins: bool = False) -> Iterator[Entry]: # load Entry with all joins and all tags stmt = ( stmt.outerjoin(Entry.text_fields) + .outerjoin(Entry.url_fields) .outerjoin(Entry.datetime_fields) .outerjoin(Entry.tags) ) stmt = stmt.options( contains_eager(Entry.text_fields), + contains_eager(Entry.url_fields), contains_eager(Entry.datetime_fields), contains_eager(Entry.tags), ) @@ -1285,13 +1361,19 @@ def add_field_to_entry( field_id = field_id.name field = self.get_value_type(unwrap(field_id)) - field_model: TextField | DatetimeField + field_model: TextField | UrlField | DatetimeField if field.type in (FieldTypeEnum.TEXT_LINE, FieldTypeEnum.TEXT_BOX): field_model = TextField( type_key=field.key, value=value or "", ) + elif field.type == FieldTypeEnum.URL: + field_model = UrlField( + type_key=field.key, + value=value or "", + ) + elif field.type == FieldTypeEnum.DATETIME: field_model = DatetimeField( type_key=field.key, diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 223dc0216..7cecaea22 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -18,6 +18,7 @@ BooleanField, DatetimeField, TextField, + UrlField, ) from tagstudio.core.library.alchemy.joins import TagParent @@ -206,6 +207,10 @@ class Entry(Base): back_populates="entry", cascade="all, delete", ) + url_fields: Mapped[list[UrlField]] = relationship( + back_populates="entry", + cascade="all, delete", + ) datetime_fields: Mapped[list[DatetimeField]] = relationship( back_populates="entry", cascade="all, delete", @@ -215,6 +220,7 @@ class Entry(Base): def fields(self) -> list[BaseField]: fields: list[BaseField] = [] fields.extend(self.text_fields) + fields.extend(self.url_fields) fields.extend(self.datetime_fields) fields = sorted(fields, key=lambda field: field.type.position) return fields @@ -255,6 +261,8 @@ def __init__( for field in fields: if isinstance(field, TextField): self.text_fields.append(field) + elif isinstance(field, UrlField): + self.url_fields.append(field) elif isinstance(field, DatetimeField): self.datetime_fields.append(field) else: @@ -290,6 +298,7 @@ class ValueType(Base): # add relations to other tables text_fields: Mapped[list[TextField]] = relationship("TextField", back_populates="type") + url_fields: Mapped[list[UrlField]] = relationship("UrlField", back_populates="type") datetime_fields: Mapped[list[DatetimeField]] = relationship( "DatetimeField", back_populates="type" ) @@ -300,6 +309,7 @@ def as_field(self) -> BaseField: FieldClass = { # noqa: N806 FieldTypeEnum.TEXT_LINE: TextField, FieldTypeEnum.TEXT_BOX: TextField, + FieldTypeEnum.URL: UrlField, FieldTypeEnum.DATETIME: DatetimeField, FieldTypeEnum.BOOLEAN: BooleanField, } diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py index 1128e494c..530322801 100644 --- a/src/tagstudio/qt/mixed/field_containers.py +++ b/src/tagstudio/qt/mixed/field_containers.py @@ -29,6 +29,7 @@ BaseField, DatetimeField, TextField, + UrlField, ) from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry, Tag @@ -37,6 +38,7 @@ from tagstudio.qt.mixed.datetime_picker import DatetimePicker from tagstudio.qt.mixed.field_widget import FieldContainer from tagstudio.qt.mixed.text_field import TextWidget +from tagstudio.qt.mixed.url_widget import UrlWidget from tagstudio.qt.translations import Translations from tagstudio.qt.views.edit_text_box_modal import EditTextBox from tagstudio.qt.views.edit_text_line_modal import EditTextLine @@ -336,6 +338,51 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): ) ) + elif field.type.type == FieldTypeEnum.URL: + logger.info("[FieldContainers][write_container] URL Line Field", field=field) + + container.set_title(field.type.name) + container.set_inline(False) + + # Normalize line endings in any text content. + if not is_mixed: + assert isinstance(field, UrlField) + url_title: str | None = "" + url_value: str = field.value or "" + else: + url_title = "" + url_value = "Mixed Data" + + title = f"{field.type.name} ({field.type.type.value})" + inner_widget = UrlWidget(title, url_title, url_value) + container.set_inner_widget(inner_widget) + if not is_mixed: + modal = PanelModal( + EditTextLine(field.value), + title=title, + window_title=f"Edit {field.type.type.value}", + save_callback=( + lambda content: ( + self.update_field(field, content), # type: ignore + self.update_from_entry(self.cached_entries[0].id), + ) + ), + ) + if "pytest" in sys.modules: + # for better testability + container.modal = modal # pyright: ignore[reportAttributeAccessIssue] + + container.set_edit_callback(modal.show) + container.set_remove_callback( + lambda: self.remove_message_box( + prompt=self.remove_field_prompt(field.type.type.value), + callback=lambda: ( + self.remove_field(field), + self.update_from_entry(self.cached_entries[0].id), + ), + ) + ) + elif field.type.type == FieldTypeEnum.DATETIME: logger.info("[FieldContainers][write_container] Datetime Field", field=field) if not is_mixed: @@ -466,7 +513,7 @@ def update_field(self, field: BaseField, content: str) -> None: """Update a field in all selected Entries, given a field object.""" assert isinstance( field, - TextField | DatetimeField, + TextField | DatetimeField | UrlField, ), f"instance: {type(field)}" entry_ids = [e.id for e in self.cached_entries] diff --git a/src/tagstudio/qt/mixed/text_field.py b/src/tagstudio/qt/mixed/text_field.py index d8052ce96..02489ad81 100644 --- a/src/tagstudio/qt/mixed/text_field.py +++ b/src/tagstudio/qt/mixed/text_field.py @@ -3,9 +3,6 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -import re - -from PySide6.QtCore import Qt from PySide6.QtWidgets import QHBoxLayout, QLabel from tagstudio.qt.mixed.field_widget import FieldWidget @@ -21,25 +18,8 @@ def __init__(self, title, text: str) -> None: self.text_label = QLabel() self.text_label.setStyleSheet("font-size: 12px") self.text_label.setWordWrap(True) - self.text_label.setTextFormat(Qt.TextFormat.MarkdownText) - self.text_label.setOpenExternalLinks(True) - self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) self.base_layout.addWidget(self.text_label) self.set_text(text) def set_text(self, text: str): - text = linkify(text) - self.text_label.setText(text) - - -# Regex from https://stackoverflow.com/a/6041965 -def linkify(text: str): - url_pattern = ( - r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-*]*[\w@?^=%&\/~+#-*])" - ) - return re.sub( - url_pattern, - lambda url: f'{url.group(0)}', - text, - flags=re.IGNORECASE, - ) + self.text_label.setText(text) \ No newline at end of file diff --git a/src/tagstudio/qt/mixed/url_widget.py b/src/tagstudio/qt/mixed/url_widget.py new file mode 100644 index 000000000..dd40263d8 --- /dev/null +++ b/src/tagstudio/qt/mixed/url_widget.py @@ -0,0 +1,29 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import structlog +from PySide6.QtCore import Qt + +from tagstudio.qt.mixed.text_field import TextWidget + +logger = structlog.get_logger(__name__) + + +def to_anchor(url_title: str | None, url_value: str) -> str: + if url_title is None or url_title == "": + url_title = url_value + + return f'{url_title}' + + +class UrlWidget(TextWidget): + def __init__(self, title, url_title: str, url_value: str) -> None: + super().__init__(title, to_anchor(url_title, url_value)) + self.setObjectName("urlLine") + self.text_label.setTextFormat(Qt.TextFormat.MarkdownText) + self.text_label.setOpenExternalLinks(True) + self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + self.set_url(url_title, url_value) + + def set_url(self, url_title: str, url_value: str) -> None: + self.set_text(to_anchor(url_title, url_value)) \ No newline at end of file From 96c66e213bd025c3b4cfd34392c75861a2fec772 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 12:50:31 -0400 Subject: [PATCH 02/12] Oops, forgot to format --- src/tagstudio/core/library/alchemy/library.py | 7 +++---- src/tagstudio/qt/mixed/text_field.py | 2 +- src/tagstudio/qt/mixed/url_widget.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 3100d1dde..0a2e1db0b 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -708,7 +708,7 @@ def __apply_db104_value_type_migration(self, session: Session): with session: stmt = ( update(ValueType) - .filter(ValueType.key.in_([ FieldID.URL.name, FieldID.SOURCE.name ])) + .filter(ValueType.key.in_([FieldID.URL.name, FieldID.SOURCE.name])) .values( type=FieldTypeEnum.URL.name, ) @@ -724,7 +724,6 @@ def __apply_db104_value_type_migration(self, session: Session): ) session.rollback() - def __apply_db104_url_migration(self, session: Session): """Moves all URL text fields to the new URL field table.""" try: @@ -745,7 +744,7 @@ def __apply_db104_url_migration(self, session: Session): value=source_record.value, type_key=source_record.type_key, entry_id=source_record.entry_id, - position=source_record.position + position=source_record.position, ) destination_records.append(destination_record) @@ -913,7 +912,7 @@ def get_entry_full_by_path(self, path: Path) -> Entry | None: .options( selectinload(Entry.text_fields), selectinload(Entry.url_fields), - selectinload(Entry.datetime_fields) + selectinload(Entry.datetime_fields), ) ) stmt = ( diff --git a/src/tagstudio/qt/mixed/text_field.py b/src/tagstudio/qt/mixed/text_field.py index 02489ad81..2949247fc 100644 --- a/src/tagstudio/qt/mixed/text_field.py +++ b/src/tagstudio/qt/mixed/text_field.py @@ -22,4 +22,4 @@ def __init__(self, title, text: str) -> None: self.set_text(text) def set_text(self, text: str): - self.text_label.setText(text) \ No newline at end of file + self.text_label.setText(text) diff --git a/src/tagstudio/qt/mixed/url_widget.py b/src/tagstudio/qt/mixed/url_widget.py index dd40263d8..0d065c4e9 100644 --- a/src/tagstudio/qt/mixed/url_widget.py +++ b/src/tagstudio/qt/mixed/url_widget.py @@ -26,4 +26,4 @@ def __init__(self, title, url_title: str, url_value: str) -> None: self.set_url(url_title, url_value) def set_url(self, url_title: str, url_value: str) -> None: - self.set_text(to_anchor(url_title, url_value)) \ No newline at end of file + self.set_text(to_anchor(url_title, url_value)) From 4d0f92f34fc84a5c08ec8d2fa83f1a6111e6a65a Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 16:02:46 -0400 Subject: [PATCH 03/12] Add GUI for editing the URL title --- src/tagstudio/core/library/alchemy/library.py | 9 +-- src/tagstudio/qt/mixed/field_containers.py | 25 ++++---- src/tagstudio/qt/mixed/url_widget.py | 4 +- src/tagstudio/qt/views/edit_text_box_modal.py | 2 +- .../qt/views/edit_text_line_modal.py | 2 +- src/tagstudio/qt/views/edit_url_modal.py | 63 +++++++++++++++++++ src/tagstudio/qt/views/panel_modal.py | 8 +-- 7 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 src/tagstudio/qt/views/edit_url_modal.py diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 0a2e1db0b..d95360691 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1299,12 +1299,7 @@ def remove_entry_field( # recalculate the remaining positions # self.update_field_position(type(field), field.type, entry_ids) - def update_entry_field( - self, - entry_ids: list[int] | int, - field: BaseField, - content: str | datetime, - ): + def update_entry_field(self, entry_ids: list[int] | int, field: BaseField, **kwargs): if isinstance(entry_ids, int): entry_ids = [entry_ids] @@ -1320,7 +1315,7 @@ def update_entry_field( FieldClass.entry_id.in_(entry_ids), ) ) - .values(value=content) + .values(**kwargs) ) session.execute(update_stmt) diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py index 49edaee09..ad953fced 100644 --- a/src/tagstudio/qt/mixed/field_containers.py +++ b/src/tagstudio/qt/mixed/field_containers.py @@ -42,6 +42,7 @@ from tagstudio.qt.translations import Translations from tagstudio.qt.views.edit_text_box_modal import EditTextBox from tagstudio.qt.views.edit_text_line_modal import EditTextLine +from tagstudio.qt.views.edit_url_modal import EditUrl from tagstudio.qt.views.panel_modal import PanelModal if typing.TYPE_CHECKING: @@ -284,8 +285,8 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): title=title, window_title=f"Edit {field.type.type.value}", save_callback=( - lambda content: ( - self.update_field(field, content), # type: ignore + lambda data: ( + self.update_field(field, value=data), self.update_from_entry(self.cached_entries[0].id), ) ), @@ -323,8 +324,8 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): title=title, window_title=f"Edit {field.type.name}", save_callback=( - lambda content: ( - self.update_field(field, content), # type: ignore + lambda data: ( + self.update_field(field, value=data), self.update_from_entry(self.cached_entries[0].id), ) ), @@ -349,7 +350,7 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): # Normalize line endings in any text content. if not is_mixed: assert isinstance(field, UrlField) - url_title: str | None = "" + url_title: str | None = field.title url_value: str = field.value or "" else: url_title = "" @@ -360,12 +361,12 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): container.set_inner_widget(inner_widget) if not is_mixed: modal = PanelModal( - EditTextLine(field.value), + EditUrl(url_title, url_value), title=title, window_title=f"Edit {field.type.type.value}", save_callback=( - lambda content: ( - self.update_field(field, content), # type: ignore + lambda data: ( + self.update_field(field, title=data[0], value=data[1]), self.update_from_entry(self.cached_entries[0].id), ) ), @@ -408,8 +409,8 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): DatetimePicker(self.driver, field.value or dt.now()), title=f"Edit {field.type.name}", save_callback=( - lambda content: ( - self.update_field(field, content), # type: ignore + lambda data: ( + self.update_field(field, value=data), self.update_from_entry(self.cached_entries[0].id), ) ), @@ -511,7 +512,7 @@ def remove_field(self, field: BaseField): entry_ids = [e.id for e in self.cached_entries] self.lib.remove_entry_field(field, entry_ids) - def update_field(self, field: BaseField, content: str) -> None: + def update_field(self, field: BaseField, **kwargs) -> None: """Update a field in all selected Entries, given a field object.""" assert isinstance( field, @@ -524,7 +525,7 @@ def update_field(self, field: BaseField, content: str) -> None: self.lib.update_entry_field( entry_ids, field, - content, + **kwargs, ) def remove_message_box(self, prompt: str, callback: Callable) -> None: diff --git a/src/tagstudio/qt/mixed/url_widget.py b/src/tagstudio/qt/mixed/url_widget.py index 0d065c4e9..413c6d383 100644 --- a/src/tagstudio/qt/mixed/url_widget.py +++ b/src/tagstudio/qt/mixed/url_widget.py @@ -17,7 +17,7 @@ def to_anchor(url_title: str | None, url_value: str) -> str: class UrlWidget(TextWidget): - def __init__(self, title, url_title: str, url_value: str) -> None: + def __init__(self, title, url_title: str | None, url_value: str) -> None: super().__init__(title, to_anchor(url_title, url_value)) self.setObjectName("urlLine") self.text_label.setTextFormat(Qt.TextFormat.MarkdownText) @@ -25,5 +25,5 @@ def __init__(self, title, url_title: str, url_value: str) -> None: self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) self.set_url(url_title, url_value) - def set_url(self, url_title: str, url_value: str) -> None: + def set_url(self, url_title: str | None, url_value: str) -> None: self.set_text(to_anchor(url_title, url_value)) diff --git a/src/tagstudio/qt/views/edit_text_box_modal.py b/src/tagstudio/qt/views/edit_text_box_modal.py index 3c3bce32b..d6e4c9fc9 100644 --- a/src/tagstudio/qt/views/edit_text_box_modal.py +++ b/src/tagstudio/qt/views/edit_text_box_modal.py @@ -19,7 +19,7 @@ def __init__(self, text): self.text_edit.setPlainText(text) self.root_layout.addWidget(self.text_edit) - def get_content(self) -> str: + def get_content(self): return self.text_edit.toPlainText() def reset(self): diff --git a/src/tagstudio/qt/views/edit_text_line_modal.py b/src/tagstudio/qt/views/edit_text_line_modal.py index 5ee933ccd..732235997 100644 --- a/src/tagstudio/qt/views/edit_text_line_modal.py +++ b/src/tagstudio/qt/views/edit_text_line_modal.py @@ -19,7 +19,7 @@ def __init__(self, text): self.text_edit.setText(text) self.root_layout.addWidget(self.text_edit) - def get_content(self) -> str: + def get_content(self): return self.text_edit.text() def reset(self): diff --git a/src/tagstudio/qt/views/edit_url_modal.py b/src/tagstudio/qt/views/edit_url_modal.py new file mode 100644 index 000000000..e154719a5 --- /dev/null +++ b/src/tagstudio/qt/views/edit_url_modal.py @@ -0,0 +1,63 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio +from collections.abc import Callable + +from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget + +from tagstudio.qt.views.panel_modal import PanelWidget + + +class EditUrl(PanelWidget): + def __init__(self, url_title: str | None, url_value: str): + super().__init__() + self.url_title = url_title + self.url_value = url_value + + self.setMinimumWidth(480) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6, 0, 6, 0) + + # Edit title + self.edit_title_widget = QWidget() + self.edit_title_layout = QHBoxLayout(self.edit_title_widget) + + self.edit_title_label = QLabel("Edit title") + self.edit_title_input = QLineEdit() + self.edit_title_input.setText(self.url_title or "") + + self.edit_title_layout.addWidget(self.edit_title_label) + self.edit_title_layout.addWidget(self.edit_title_input) + + # Edit URL + self.edit_url_widget = QWidget() + self.edit_url_layout = QHBoxLayout(self.edit_url_widget) + + self.edit_url_label = QLabel("Edit URL") + self.edit_url_input = QLineEdit() + self.edit_url_input.setText(self.url_value) + + self.edit_url_layout.addWidget(self.edit_url_label) + self.edit_url_layout.addWidget(self.edit_url_input) + + self.root_layout.addWidget(self.edit_title_widget) + self.root_layout.addWidget(self.edit_url_widget) + + def get_content(self): + # Ensure that blank values preserve being None + url_title: str | None = self.edit_title_input.text() + if url_title == "": + url_title = None + + return url_title, self.edit_url_input.text() + + def reset(self): + self.edit_title_input.setText(self.url_title or "") + self.edit_url_input.setText(self.url_value) + + def add_callback(self, callback: Callable, event: str = "returnPressed"): + if event == "returnPressed": + self.edit_title_input.returnPressed.connect(callback) + self.edit_url_input.returnPressed.connect(callback) + else: + raise ValueError(f"unknown event type: {event}") diff --git a/src/tagstudio/qt/views/panel_modal.py b/src/tagstudio/qt/views/panel_modal.py index 241e57f73..bdf3c3180 100755 --- a/src/tagstudio/qt/views/panel_modal.py +++ b/src/tagstudio/qt/views/panel_modal.py @@ -4,7 +4,7 @@ from collections.abc import Callable -from typing import override +from typing import Any, override import structlog from PySide6 import QtCore, QtGui @@ -27,7 +27,7 @@ def __init__( title: str = "", window_title: str | None = None, done_callback: Callable[[], None] | None = None, - save_callback: Callable[[str], None] | None = None, + save_callback: Callable[..., None | tuple[None, ...]] | None = None, has_save: bool = False, ): # [Done] @@ -120,8 +120,8 @@ class PanelWidget(QWidget): def __init__(self): super().__init__() - def get_content(self) -> str: - return "" + def get_content(self) -> tuple[Any, ...] | Any | None: + pass def reset(self) -> None: pass From 5741aa60073b3175f20f9507bc79a8cc7dc431c4 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 16:40:40 -0400 Subject: [PATCH 04/12] Fix test_library.py --- tests/test_library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_library.py b/tests/test_library.py index 447344512..1e7cc2de0 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -235,7 +235,7 @@ def test_update_entry_field(library: Library, entry_full: Entry): library.update_entry_field( entry_full.id, title_field, - "new value", + value="new value", ) entry = next(library.all_entries(with_joins=True)) @@ -254,7 +254,7 @@ def test_update_entry_with_multiple_identical_fields(library: Library, entry_ful library.update_entry_field( entry_full.id, title_field, - "new value", + value="new value", ) # Then only one should be updated From 42d717c97a5d54a49b521ad96e4b48fb70e84e0f Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 17:44:39 -0400 Subject: [PATCH 05/12] Remove None from allowed get_content() return types --- src/tagstudio/qt/views/panel_modal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/qt/views/panel_modal.py b/src/tagstudio/qt/views/panel_modal.py index bdf3c3180..2fe271f3f 100755 --- a/src/tagstudio/qt/views/panel_modal.py +++ b/src/tagstudio/qt/views/panel_modal.py @@ -120,7 +120,7 @@ class PanelWidget(QWidget): def __init__(self): super().__init__() - def get_content(self) -> tuple[Any, ...] | Any | None: + def get_content(self) -> tuple[Any, ...] | Any: pass def reset(self) -> None: From 0e16f16e5927c0203b34a7b21f4ad0a60a9e2c13 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 17:44:54 -0400 Subject: [PATCH 06/12] Use translations for edit URL modal --- src/tagstudio/qt/views/edit_url_modal.py | 5 +++-- src/tagstudio/resources/translations/en.json | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tagstudio/qt/views/edit_url_modal.py b/src/tagstudio/qt/views/edit_url_modal.py index e154719a5..ede414e13 100644 --- a/src/tagstudio/qt/views/edit_url_modal.py +++ b/src/tagstudio/qt/views/edit_url_modal.py @@ -5,6 +5,7 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget +from tagstudio.qt.translations import Translations from tagstudio.qt.views.panel_modal import PanelWidget @@ -22,7 +23,7 @@ def __init__(self, url_title: str | None, url_value: str): self.edit_title_widget = QWidget() self.edit_title_layout = QHBoxLayout(self.edit_title_widget) - self.edit_title_label = QLabel("Edit title") + self.edit_title_label = QLabel(Translations["field.url.edit_title"]) self.edit_title_input = QLineEdit() self.edit_title_input.setText(self.url_title or "") @@ -33,7 +34,7 @@ def __init__(self, url_title: str | None, url_value: str): self.edit_url_widget = QWidget() self.edit_url_layout = QHBoxLayout(self.edit_url_widget) - self.edit_url_label = QLabel("Edit URL") + self.edit_url_label = QLabel(Translations["field.url.edit_url"]) self.edit_url_input = QLineEdit() self.edit_url_input.setText(self.url_value) diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index edda02311..2153afc91 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -74,6 +74,8 @@ "field.copy": "Copy Field", "field.edit": "Edit Field", "field.paste": "Paste Field", + "field.url.edit_title": "Edit Title", + "field.url.edit_url": "Edit URL", "file.date_added": "Date Added", "file.date_created": "Date Created", "file.date_modified": "Date Modified", From 12d3f02f18d52675c88cc54bf4494eed6f792cba Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sat, 18 Oct 2025 17:51:23 -0400 Subject: [PATCH 07/12] Document library changes (with the assumption this PR will be part of v9.5.7) --- docs/library-changes.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/library-changes.md b/docs/library-changes.md index d37bbedb0..1f27f7c00 100644 --- a/docs/library-changes.md +++ b/docs/library-changes.md @@ -123,3 +123,13 @@ 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 104 + +| Used From | Format | Location | +| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- | +| [v9.5.7](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.7) | SQLite | ``/.TagStudio/ts_library.sqlite | + +- Adds a new `url_fields` table. +- Changes the type key of the `URL` and `SOURCE` fields to be `URL`. +- Migrates any records in the `text_fields` whose type key has the type `URL` (so, if their type is either `URL` or `SOURCE`) to the new `url_fields` table. From 943e14f3cbbb75feb5f94ef176d1986be22e0dbc Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 19 Oct 2025 11:50:47 -0400 Subject: [PATCH 08/12] Allow for copy+pasting fields with multiple properties --- src/tagstudio/core/library/alchemy/library.py | 10 +++++----- src/tagstudio/qt/ts_qt.py | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index d95360691..4bafa7f63 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1338,14 +1338,14 @@ def add_field_to_entry( *, field: ValueType | None = None, field_id: FieldID | str | None = None, - value: str | datetime | None = None, + **kwargs ) -> bool: logger.info( "[Library][add_field_to_entry]", entry_id=entry_id, field_type=field, field_id=field_id, - value=value, + **kwargs, ) # supply only instance or ID, not both assert bool(field) != (field_id is not None) @@ -1359,19 +1359,19 @@ def add_field_to_entry( if field.type in (FieldTypeEnum.TEXT_LINE, FieldTypeEnum.TEXT_BOX): field_model = TextField( type_key=field.key, - value=value or "", + **kwargs ) elif field.type == FieldTypeEnum.URL: field_model = UrlField( type_key=field.key, - value=value or "", + **kwargs ) elif field.type == FieldTypeEnum.DATETIME: field_model = DatetimeField( type_key=field.key, - value=value, + **kwargs ) else: raise NotImplementedError(f"field type not implemented: {field.type}") diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 8d7edde30..85dd3679d 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1209,7 +1209,12 @@ def paste_fields_action_callback(self): if field.type_key == e.type_key and field.value == e.value: exists = True if not exists: - self.lib.add_field_to_entry(id, field_id=field.type_key, value=field.value) + match field.type.type: + case FieldTypeEnum.URL: + self.lib.add_field_to_entry(id, field_id=field.type_key, title=field.title, value=field.value) + case _: + self.lib.add_field_to_entry(id, field_id=field.type_key, value=field.value) + self.lib.add_tags_to_entries(id, self.copy_buffer["tags"]) if len(self.selected) > 1: if TAG_ARCHIVED in self.copy_buffer["tags"]: From ac549a8d913fd82b0a596e869dc87ecb4aa35a17 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 19 Oct 2025 11:52:38 -0400 Subject: [PATCH 09/12] Forgot to ruff check/format --- src/tagstudio/core/library/alchemy/library.py | 17 ++++------------- src/tagstudio/qt/ts_qt.py | 8 ++++++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 4bafa7f63..ba4049333 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1338,7 +1338,7 @@ def add_field_to_entry( *, field: ValueType | None = None, field_id: FieldID | str | None = None, - **kwargs + **kwargs, ) -> bool: logger.info( "[Library][add_field_to_entry]", @@ -1357,22 +1357,13 @@ def add_field_to_entry( field_model: TextField | UrlField | DatetimeField if field.type in (FieldTypeEnum.TEXT_LINE, FieldTypeEnum.TEXT_BOX): - field_model = TextField( - type_key=field.key, - **kwargs - ) + field_model = TextField(type_key=field.key, **kwargs) elif field.type == FieldTypeEnum.URL: - field_model = UrlField( - type_key=field.key, - **kwargs - ) + field_model = UrlField(type_key=field.key, **kwargs) elif field.type == FieldTypeEnum.DATETIME: - field_model = DatetimeField( - type_key=field.key, - **kwargs - ) + field_model = DatetimeField(type_key=field.key, **kwargs) else: raise NotImplementedError(f"field type not implemented: {field.type}") diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 85dd3679d..b488da013 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1211,9 +1211,13 @@ def paste_fields_action_callback(self): if not exists: match field.type.type: case FieldTypeEnum.URL: - self.lib.add_field_to_entry(id, field_id=field.type_key, title=field.title, value=field.value) + self.lib.add_field_to_entry( + id, field_id=field.type_key, title=field.title, value=field.value + ) case _: - self.lib.add_field_to_entry(id, field_id=field.type_key, value=field.value) + self.lib.add_field_to_entry( + id, field_id=field.type_key, value=field.value + ) self.lib.add_tags_to_entries(id, self.copy_buffer["tags"]) if len(self.selected) > 1: From 9632a21f8d6f7b21c97a1ca92924c4f843a22af0 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 19 Oct 2025 14:56:04 -0400 Subject: [PATCH 10/12] Fix test_update_entry_with_multiple_identical_fields --- tests/test_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_library.py b/tests/test_library.py index 1e7cc2de0..c4d44873f 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -259,7 +259,7 @@ def test_update_entry_with_multiple_identical_fields(library: Library, entry_ful # Then only one should be updated entry = next(library.all_entries(with_joins=True)) - assert entry.text_fields[0].value == "" + assert entry.text_fields[0].value is None assert entry.text_fields[1].value == "new value" From 2dbbd43a81828829544c755d2cd35049f15280b3 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 19 Oct 2025 15:23:20 -0400 Subject: [PATCH 11/12] More explicitly handle empty field values using None --- src/tagstudio/core/library/alchemy/fields.py | 2 +- src/tagstudio/core/library/alchemy/library.py | 19 +++++++++---------- src/tagstudio/qt/mixed/url_widget.py | 2 +- .../qt/views/edit_text_line_modal.py | 13 ++++++++----- src/tagstudio/qt/views/edit_url_modal.py | 12 ++++++++---- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/fields.py b/src/tagstudio/core/library/alchemy/fields.py index c7236a7f0..8993b8d0f 100644 --- a/src/tagstudio/core/library/alchemy/fields.py +++ b/src/tagstudio/core/library/alchemy/fields.py @@ -110,7 +110,7 @@ class DatetimeField(BaseField): value: Mapped[str | None] def __key(self): - return (self.type, self.value) + return self.type, self.value @override def __eq__(self, value: object) -> bool: diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index ba4049333..ccc6a53f3 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1356,16 +1356,15 @@ def add_field_to_entry( field = self.get_value_type(unwrap(field_id)) field_model: TextField | UrlField | DatetimeField - if field.type in (FieldTypeEnum.TEXT_LINE, FieldTypeEnum.TEXT_BOX): - field_model = TextField(type_key=field.key, **kwargs) - - elif field.type == FieldTypeEnum.URL: - field_model = UrlField(type_key=field.key, **kwargs) - - elif field.type == FieldTypeEnum.DATETIME: - field_model = DatetimeField(type_key=field.key, **kwargs) - else: - raise NotImplementedError(f"field type not implemented: {field.type}") + match field.type: + case FieldTypeEnum.TEXT_LINE | FieldTypeEnum.TEXT_BOX: + field_model = TextField(type_key=field.key, **kwargs) + case FieldTypeEnum.URL: + field_model = UrlField( type_key=field.key, **kwargs) + case FieldTypeEnum.DATETIME: + field_model = DatetimeField(type_key=field.key, **kwargs) + case _: + raise NotImplementedError(f"field type not implemented: {field.type}") with Session(self.engine) as session: try: diff --git a/src/tagstudio/qt/mixed/url_widget.py b/src/tagstudio/qt/mixed/url_widget.py index 413c6d383..52f5ab0ef 100644 --- a/src/tagstudio/qt/mixed/url_widget.py +++ b/src/tagstudio/qt/mixed/url_widget.py @@ -10,7 +10,7 @@ def to_anchor(url_title: str | None, url_value: str) -> str: - if url_title is None or url_title == "": + if url_title is None: url_title = url_value return f'{url_title}' diff --git a/src/tagstudio/qt/views/edit_text_line_modal.py b/src/tagstudio/qt/views/edit_text_line_modal.py index 732235997..5e059789a 100644 --- a/src/tagstudio/qt/views/edit_text_line_modal.py +++ b/src/tagstudio/qt/views/edit_text_line_modal.py @@ -9,21 +9,24 @@ class EditTextLine(PanelWidget): - def __init__(self, text): + def __init__(self, text: str | None): super().__init__() self.setMinimumWidth(480) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(6, 0, 6, 0) self.text = text self.text_edit = QLineEdit() - self.text_edit.setText(text) + self.text_edit.setText(text or "") self.root_layout.addWidget(self.text_edit) - def get_content(self): - return self.text_edit.text() + def get_content(self) -> str | None: + text: str | None = self.text_edit.text() + if text == "": + text = None + return text def reset(self): - self.text_edit.setText(self.text) + self.text_edit.setText(self.text or "") def add_callback(self, callback: Callable, event: str = "returnPressed"): if event == "returnPressed": diff --git a/src/tagstudio/qt/views/edit_url_modal.py b/src/tagstudio/qt/views/edit_url_modal.py index ede414e13..e0506686d 100644 --- a/src/tagstudio/qt/views/edit_url_modal.py +++ b/src/tagstudio/qt/views/edit_url_modal.py @@ -10,7 +10,7 @@ class EditUrl(PanelWidget): - def __init__(self, url_title: str | None, url_value: str): + def __init__(self, url_title: str | None, url_value: str | None): super().__init__() self.url_title = url_title self.url_value = url_value @@ -36,7 +36,7 @@ def __init__(self, url_title: str | None, url_value: str): self.edit_url_label = QLabel(Translations["field.url.edit_url"]) self.edit_url_input = QLineEdit() - self.edit_url_input.setText(self.url_value) + self.edit_url_input.setText(self.url_value or "") self.edit_url_layout.addWidget(self.edit_url_label) self.edit_url_layout.addWidget(self.edit_url_input) @@ -50,11 +50,15 @@ def get_content(self): if url_title == "": url_title = None - return url_title, self.edit_url_input.text() + url_value: str | None = self.edit_url_input.text() + if url_value == "": + url_value = None + + return url_title, url_value def reset(self): self.edit_title_input.setText(self.url_title or "") - self.edit_url_input.setText(self.url_value) + self.edit_url_input.setText(self.url_value or "") def add_callback(self, callback: Callable, event: str = "returnPressed"): if event == "returnPressed": From be0c2d557b702a0618a4d747de9a6cea2a92dbcf Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Sun, 19 Oct 2025 15:24:52 -0400 Subject: [PATCH 12/12] Format --- src/tagstudio/core/library/alchemy/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index ccc6a53f3..09751b387 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1360,7 +1360,7 @@ def add_field_to_entry( case FieldTypeEnum.TEXT_LINE | FieldTypeEnum.TEXT_BOX: field_model = TextField(type_key=field.key, **kwargs) case FieldTypeEnum.URL: - field_model = UrlField( type_key=field.key, **kwargs) + field_model = UrlField(type_key=field.key, **kwargs) case FieldTypeEnum.DATETIME: field_model = DatetimeField(type_key=field.key, **kwargs) case _: