mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-06-24 16:11:53 +00:00
feat: add basic field template editor
This commit is contained in:
@@ -30,6 +30,7 @@ from sqlalchemy import (
|
||||
Engine,
|
||||
NullPool,
|
||||
ScalarResult,
|
||||
Update,
|
||||
and_,
|
||||
asc,
|
||||
create_engine,
|
||||
@@ -1313,6 +1314,113 @@ class Library:
|
||||
|
||||
return direct_tags, descendant_tags
|
||||
|
||||
def add_field_template(self, field_template: BaseFieldTemplate) -> bool:
|
||||
"""Add a new field template to the library."""
|
||||
if not (isinstance(field_template, (TextFieldTemplate, DatetimeFieldTemplate))):
|
||||
logger.error("[Library] BaseFieldTemplate attempted to be added to the library.")
|
||||
return False
|
||||
|
||||
with Session(self.engine) as session:
|
||||
try:
|
||||
session.add(field_template)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
logger.error(e)
|
||||
session.rollback()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_field_template(self, old_field_type: str, field_template: BaseFieldTemplate) -> bool:
|
||||
"""Update a field template in the library.
|
||||
|
||||
old_field_class:str
|
||||
field_template: BaseFieldTemplate
|
||||
"""
|
||||
with Session(self.engine) as session:
|
||||
logger.warning(f"Updating old type {old_field_type} to new {field_template.class_name}")
|
||||
is_same_type: bool = old_field_type == field_template.class_name
|
||||
try:
|
||||
update_stmt: Update | None = None
|
||||
# If the template is changing type, remove the old one and add the updated
|
||||
# template to the proper table.
|
||||
if not is_same_type:
|
||||
old_template: BaseFieldTemplate | None = None
|
||||
if old_field_type == "TextFieldTemplate":
|
||||
old_template = session.scalar(
|
||||
select(TextFieldTemplate)
|
||||
.where(TextFieldTemplate.id == field_template.id)
|
||||
.limit(1)
|
||||
)
|
||||
elif old_field_type == "DatetimeFieldTemplate":
|
||||
old_template = session.scalar(
|
||||
select(DatetimeFieldTemplate)
|
||||
.where(DatetimeFieldTemplate.id == field_template.id)
|
||||
.limit(1)
|
||||
)
|
||||
if old_template is None:
|
||||
logger.error("[Library] old_template is None")
|
||||
return False
|
||||
session.delete(old_template)
|
||||
session.flush()
|
||||
field_template.id = None # The id should not transfer between tables
|
||||
session.add(field_template)
|
||||
session.commit()
|
||||
# Otherwise, update the existing template in-place
|
||||
elif isinstance(field_template, TextFieldTemplate):
|
||||
update_stmt = (
|
||||
update(TextFieldTemplate)
|
||||
.where(TextFieldTemplate.id == field_template.id)
|
||||
.values(name=field_template.name, is_multiline=field_template.is_multiline)
|
||||
)
|
||||
elif isinstance(field_template, DatetimeFieldTemplate):
|
||||
update_stmt = (
|
||||
update(DatetimeFieldTemplate)
|
||||
.where(DatetimeFieldTemplate.id == field_template.id)
|
||||
.values(name=field_template.name)
|
||||
)
|
||||
if is_same_type:
|
||||
if update_stmt is None:
|
||||
return False
|
||||
session.execute(update_stmt)
|
||||
session.commit()
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.error(e)
|
||||
session.rollback()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def remove_field_template(self, field_template: BaseFieldTemplate) -> bool:
|
||||
"""Remove a field template from the library."""
|
||||
with Session(self.engine) as session:
|
||||
try:
|
||||
session_item: BaseFieldTemplate | None = None
|
||||
if isinstance(field_template, TextFieldTemplate):
|
||||
session_item = session.scalar(
|
||||
select(TextFieldTemplate)
|
||||
.where(TextFieldTemplate.id == field_template.id)
|
||||
.limit(1)
|
||||
)
|
||||
elif isinstance(field_template, DatetimeFieldTemplate):
|
||||
session_item = session.scalar(
|
||||
select(DatetimeFieldTemplate)
|
||||
.where(DatetimeFieldTemplate.id == field_template.id)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if session_item is not None:
|
||||
session.delete(session_item)
|
||||
session.commit()
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.error(e)
|
||||
session.rollback()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def search_field_templates(self, name: str | None, limit: int = 100) -> list[BaseFieldTemplate]:
|
||||
"""Return field template rows matching the query, detached from the session."""
|
||||
if limit <= 0:
|
||||
@@ -1320,7 +1428,7 @@ class Library:
|
||||
|
||||
search_query: str = name.lower() if name else ""
|
||||
|
||||
def sort_key(template: BaseFieldTemplate) -> tuple:
|
||||
def sort_key(template: BaseFieldTemplate) -> tuple[str] | tuple[bool, int, str]:
|
||||
text = template.name.lower()
|
||||
if not search_query:
|
||||
return (text,)
|
||||
|
||||
116
src/tagstudio/qt/controllers/edit_field_template_modal.py
Normal file
116
src/tagstudio/qt/controllers/edit_field_template_modal.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
BaseFieldTemplate,
|
||||
DatetimeFieldTemplate,
|
||||
TextFieldTemplate,
|
||||
)
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.edit_field_template_modal_view import EditFieldTemplateModalView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class EditFieldTemplateModal(EditFieldTemplateModalView):
|
||||
field_type_map: dict[str, str] = {
|
||||
"TextFieldTemplate": Translations["field_type.text"],
|
||||
"DatetimeFieldTemplate": Translations["field_type.datetime"],
|
||||
}
|
||||
|
||||
def __init__(self, field_template: BaseFieldTemplate | None = None) -> None:
|
||||
super().__init__()
|
||||
self.__field_id: int = field_template.id if field_template else -1
|
||||
self.__field_name: str = ""
|
||||
self.__field_type: str | None = field_template.class_name if field_template else None
|
||||
self.__text_field_is_multiline: bool = False
|
||||
self.old_field_type: str = ""
|
||||
|
||||
for k, v in EditFieldTemplateModal.field_type_map.items():
|
||||
self._type_combobox.addItem(v, k)
|
||||
|
||||
self.__connect_callbacks()
|
||||
self.set_field_template(field_template)
|
||||
|
||||
def __connect_callbacks(self) -> None:
|
||||
self.name_field.textChanged.connect(self.__on_name_changed)
|
||||
self._type_combobox.currentIndexChanged.connect(self.__on_type_changed)
|
||||
|
||||
def set_field_template(self, field_template: BaseFieldTemplate | None = None) -> None:
|
||||
"""Populate the modal with pre-existing field template values, or fallback to defaults."""
|
||||
logger.info("[EditFieldTemplate] Setting Field Template", field_template=field_template)
|
||||
|
||||
# Indicates a new template, set default values
|
||||
if field_template is None:
|
||||
self.__field_name = Translations["field_template.new"]
|
||||
self.__field_type = None
|
||||
return
|
||||
# Populate common values for any field type
|
||||
else:
|
||||
self.__field_name = field_template.name
|
||||
self.__field_type = field_template.class_name
|
||||
self.old_field_type = field_template.class_name # Only set on init
|
||||
|
||||
# Update widgets
|
||||
self.name_field.setText(self.__field_name)
|
||||
self._type_combobox.setCurrentIndex(
|
||||
list(EditFieldTemplateModal.field_type_map.keys()).index(field_template.class_name)
|
||||
)
|
||||
|
||||
# Populate values for specific field types
|
||||
if isinstance(field_template, TextFieldTemplate):
|
||||
self.__text_field_is_multiline = field_template.is_multiline
|
||||
|
||||
def __on_name_changed(self):
|
||||
is_empty = not self.name_field.text().strip()
|
||||
|
||||
self.name_field.setStyleSheet(
|
||||
f"border: 1px solid {get_ui_color(ColorType.PRIMARY, UiColor.RED)}; border-radius: 2px"
|
||||
if is_empty
|
||||
else ""
|
||||
)
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_empty)
|
||||
|
||||
def __on_type_changed(self, index: int):
|
||||
self.__field_type = list(EditFieldTemplateModal.field_type_map.keys())[index]
|
||||
|
||||
def build_field_template(self) -> BaseFieldTemplate:
|
||||
if self.__field_type == "TextFieldTemplate":
|
||||
return TextFieldTemplate(
|
||||
id=self.__field_id,
|
||||
name=self.name_field.text(),
|
||||
is_multiline=self.__text_field_is_multiline,
|
||||
)
|
||||
elif self.__field_type == "DatetimeFieldTemplate":
|
||||
return DatetimeFieldTemplate(
|
||||
id=self.__field_id,
|
||||
name=self.name_field.text(),
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[EditFieldTemplateModal] Unknown field, falling back to TextFieldTemplate",
|
||||
field_type=self.__field_type,
|
||||
example=TextFieldTemplate,
|
||||
)
|
||||
return TextFieldTemplate(
|
||||
name=self.name_field.text(),
|
||||
is_multiline=self.__text_field_is_multiline,
|
||||
)
|
||||
|
||||
# def parent_post_init(self):
|
||||
# self.setTabOrder(self.name_field, self.shorthand_field)
|
||||
# self.setTabOrder(self.shorthand_field, self.aliases_add_button)
|
||||
# self.setTabOrder(self.aliases_add_button, self.parent_tags_add_button)
|
||||
# self.setTabOrder(self.parent_tags_add_button, self.color_button)
|
||||
# self.setTabOrder(self.color_button, unwrap(self.panel_cancel_button))
|
||||
# self.setTabOrder(unwrap(self.panel_cancel_button), unwrap(self.panel_save_button))
|
||||
# self.setTabOrder(unwrap(self.panel_save_button), self.aliases_table.cellWidget(0, 1))
|
||||
# self.name_field.selectAll()
|
||||
# self.name_field.setFocus()
|
||||
# self._set_aliases()
|
||||
@@ -2,13 +2,17 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
@@ -23,9 +27,9 @@ class FieldTemplateSearchModal(PanelModal):
|
||||
self,
|
||||
library: Library,
|
||||
is_field_template_chooser: bool = True,
|
||||
done_callback=None,
|
||||
save_callback=None,
|
||||
has_save=False,
|
||||
done_callback: Callable[..., None] | None = None,
|
||||
save_callback: Callable[..., None] | None = None,
|
||||
has_save: bool = False,
|
||||
) -> None:
|
||||
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
|
||||
library,
|
||||
@@ -60,34 +64,79 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
self._unlimited_limit_item_label = Translations["field_template.all_field_templates"]
|
||||
self._create_and_add_button_label_key = "field_template.create_add"
|
||||
|
||||
@override
|
||||
def _get_max_limit(self) -> int:
|
||||
return len(self.__lib.field_templates)
|
||||
|
||||
@override
|
||||
def on_item_create(self) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
@override
|
||||
def on_item_edit(self, item: BaseFieldTemplate) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
panel: EditFieldTemplateModal = EditFieldTemplateModal(item)
|
||||
modal: PanelModal = PanelModal(
|
||||
panel,
|
||||
item.name,
|
||||
Translations["field_template.edit"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
modal.saved.connect(lambda: self.edit_item(panel))
|
||||
modal.show()
|
||||
|
||||
@override
|
||||
def _on_item_remove(self, item: BaseFieldTemplate) -> None:
|
||||
if self.is_chooser:
|
||||
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Icon.Question,
|
||||
Translations["field_template.delete"],
|
||||
Translations.format("field_template.confirm_delete", field_template_name=item.name),
|
||||
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
|
||||
result = message_box.exec()
|
||||
|
||||
if result != QMessageBox.StandardButton.Ok:
|
||||
return
|
||||
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
self.__lib.remove_field_template(item)
|
||||
self.update_items(self.get_search_query())
|
||||
|
||||
@override
|
||||
def on_item_create_and_add(self) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
"""Opens "Create Field Template" panel to create a new field template.
|
||||
|
||||
Populates name field using current search query.
|
||||
"""
|
||||
query: str = self.get_search_query()
|
||||
logger.info("[FieldTemplateSearch] Create and Add Field Template", name=query)
|
||||
|
||||
panel: EditFieldTemplateModal = EditFieldTemplateModal()
|
||||
modal: PanelModal = PanelModal(
|
||||
panel,
|
||||
Translations["field_template.new"],
|
||||
Translations["field.add"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
if query.strip():
|
||||
panel.name_field.setText(query)
|
||||
|
||||
modal.saved.connect(lambda: self.create_item(panel, choose_item=True))
|
||||
modal.show()
|
||||
|
||||
@override
|
||||
def _on_item_chosen(self, item: BaseFieldTemplate) -> None:
|
||||
self.field_template_chosen.emit(item)
|
||||
|
||||
@override
|
||||
def search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseFieldTemplate]]:
|
||||
return self.__lib.search_field_templates(name=query, limit=self._get_limit()[1]), []
|
||||
|
||||
@override
|
||||
def set_item_widget(self, item: BaseFieldTemplate | None, index: int) -> None:
|
||||
"""Set the field template of a field template widget at a specific index."""
|
||||
field_template_widget: FieldTemplateWidget = self.get_item_widget(index, self.__lib)
|
||||
@@ -97,25 +146,39 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
if item is None:
|
||||
return
|
||||
|
||||
# field_template_widget.has_remove = not self.is_chooser
|
||||
|
||||
# Disconnect previous callbacks
|
||||
with catch_warnings(record=True):
|
||||
# tag_widget.on_edit.disconnect()
|
||||
# tag_widget.on_remove.disconnect()
|
||||
field_template_widget.on_edit.disconnect()
|
||||
field_template_widget.on_remove.disconnect()
|
||||
field_template_widget.on_click.disconnect()
|
||||
|
||||
# Connect callbacks
|
||||
# tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
|
||||
# tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
|
||||
field_template_widget.on_edit.connect(lambda item_=item: self.on_item_edit(item_))
|
||||
field_template_widget.on_remove.connect(lambda item_=item: self._on_item_remove(item_))
|
||||
field_template_widget.on_click.connect(
|
||||
lambda checked=False, tag=item: self._on_item_chosen(tag)
|
||||
lambda checked=False, item_=item: self._on_item_chosen(item_)
|
||||
)
|
||||
|
||||
def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
@override
|
||||
def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None:
|
||||
|
||||
if isinstance(edit_item_panel, EditFieldTemplateModal):
|
||||
template: BaseFieldTemplate = edit_item_panel.build_field_template()
|
||||
self.__lib.add_field_template(template)
|
||||
|
||||
if choose_item:
|
||||
self._on_item_chosen(template)
|
||||
self.clear_search_query()
|
||||
|
||||
edit_item_panel.hide()
|
||||
self.on_search_query_changed(self.get_search_query())
|
||||
|
||||
@override
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
if not isinstance(edit_item_panel, EditFieldTemplateModal):
|
||||
return
|
||||
|
||||
self.__lib.update_field_template(
|
||||
edit_item_panel.old_field_type, edit_item_panel.build_field_template()
|
||||
)
|
||||
self.update_items(self.search_field.text())
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from typing import override
|
||||
|
||||
from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QAction, QEnterEvent
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.field_template_widget_view import FieldTemplateWidgetView
|
||||
@@ -12,6 +17,14 @@ class FieldTemplateWidget(FieldTemplateWidgetView):
|
||||
|
||||
self.__field_template: BaseFieldTemplate | None = None
|
||||
|
||||
# Add actions
|
||||
edit_action = QAction(self)
|
||||
edit_action.setText(Translations["generic.edit"])
|
||||
edit_action.triggered.connect(self.on_edit.emit)
|
||||
self.addAction(edit_action)
|
||||
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
|
||||
def set_field_template(self, field_template: BaseFieldTemplate | None) -> None:
|
||||
self.__field_template = field_template
|
||||
|
||||
@@ -20,3 +33,15 @@ class FieldTemplateWidget(FieldTemplateWidgetView):
|
||||
|
||||
field_name_key: str = FIELD_TYPE_KEYS.get(field_template.class_name, "field_type.unknown")
|
||||
self._bg_button.setText(f"{field_template.name} ({Translations[field_name_key]})")
|
||||
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
self._delete_button.setHidden(False)
|
||||
self.update()
|
||||
return super().enterEvent(event)
|
||||
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
self._delete_button.setHidden(True)
|
||||
self.update()
|
||||
return super().leaveEvent(event)
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtWidgets import QVBoxLayout
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -22,7 +23,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def _item_id(item: object) -> int:
|
||||
item_id: Any = getattr(item, "id") # noqa: B009
|
||||
item_id: Any = getattr(item, "id") # noqa: B009 # pyright: ignore[reportExplicitAny]
|
||||
|
||||
if isinstance(item_id, int):
|
||||
return item_id
|
||||
@@ -31,7 +32,7 @@ def _item_id(item: object) -> int:
|
||||
|
||||
|
||||
def _item_name(item: object) -> str:
|
||||
item_name: Any = getattr(item, "name") # noqa: B009
|
||||
item_name: Any = getattr(item, "name") # noqa: B009 # pyright: ignore[reportExplicitAny]
|
||||
|
||||
if isinstance(item_name, str):
|
||||
return item_name
|
||||
@@ -93,15 +94,13 @@ class SearchPanel[T](PanelWidget):
|
||||
def clear_search_query(self) -> None:
|
||||
self.view.clear_search_query()
|
||||
|
||||
def get_item_widget(self, index: int, library: Any):
|
||||
def get_item_widget(self, index: int, library: Library):
|
||||
return self.view.get_item_widget(index, library)
|
||||
|
||||
def set_driver(self, driver: "QtDriver") -> None:
|
||||
self._driver = driver
|
||||
|
||||
def on_limit_changed(self, index: int) -> None:
|
||||
logger.info("[SearchPanel] Updating limit")
|
||||
|
||||
# Method was called outside the limit_combobox callback
|
||||
if index != self.view.get_limit_index():
|
||||
self.view.set_limit_index(index)
|
||||
@@ -130,8 +129,8 @@ class SearchPanel[T](PanelWidget):
|
||||
# Focus search field if no query
|
||||
if not query:
|
||||
self.search_field.setFocus()
|
||||
parent = self.parentWidget()
|
||||
if parent is not None:
|
||||
parent: QWidget | None = self.parentWidget()
|
||||
if parent is not None: # pyright: ignore[reportUnnecessaryComparison]
|
||||
parent.hide()
|
||||
return
|
||||
|
||||
@@ -147,16 +146,16 @@ class SearchPanel[T](PanelWidget):
|
||||
def on_item_create(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_item_edit(self, item: T) -> None:
|
||||
def on_item_edit(self, item: T) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_item_remove(self, item: T) -> None:
|
||||
def _on_item_remove(self, item: T) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_item_create_and_add(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_item_chosen(self, item: T) -> None:
|
||||
def _on_item_chosen(self, item: T) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def _is_excluded(self, item: T) -> bool:
|
||||
@@ -215,18 +214,20 @@ class SearchPanel[T](PanelWidget):
|
||||
if query and query.strip():
|
||||
self.view.add_create_and_add_button()
|
||||
|
||||
def search_items(self, query: str) -> tuple[list[T], list[T]]:
|
||||
def search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_item_widget(self, item: T | None, index: int) -> None:
|
||||
def set_item_widget(self, item: T | None, index: int) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def showEvent(self, event: QShowEvent) -> None: # noqa N802
|
||||
self.update_items()
|
||||
self.view.scroll_to(0)
|
||||
self.view.clear_search_query()
|
||||
return super().showEvent(event)
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
# When Escape is pressed, focus back on the search box.
|
||||
# If focus is already on the search box, close the modal.
|
||||
@@ -236,8 +237,8 @@ class SearchPanel[T](PanelWidget):
|
||||
else:
|
||||
self.view.focus_search_box(select_all=True)
|
||||
|
||||
def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
|
||||
def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
@@ -33,9 +34,9 @@ class TagSearchModal(PanelModal):
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
done_callback=None,
|
||||
save_callback=None,
|
||||
has_save=False,
|
||||
done_callback: Callable[..., None] | None = None,
|
||||
save_callback: Callable[..., None] | None = None,
|
||||
has_save: bool = False,
|
||||
):
|
||||
self.tsp = TagSearchPanel(
|
||||
library,
|
||||
@@ -70,28 +71,31 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
self._unlimited_limit_item_label = Translations["tag.all_tags"]
|
||||
self._create_and_add_button_label_key = "tag.create_add"
|
||||
|
||||
@override
|
||||
def _get_max_limit(self) -> int:
|
||||
return len(self.__lib.tags)
|
||||
|
||||
@override
|
||||
def on_item_create(self) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
query: str = self.get_search_query()
|
||||
|
||||
build_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
build_tag_modal: PanelModal = PanelModal(
|
||||
build_tag_panel,
|
||||
panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
modal: PanelModal = PanelModal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
if query.strip():
|
||||
build_tag_panel.name_field.setText(query)
|
||||
panel.name_field.setText(query)
|
||||
|
||||
build_tag_modal.saved.connect(lambda: self.create_item(build_tag_modal))
|
||||
build_tag_modal.show()
|
||||
modal.saved.connect(lambda: self.create_item(panel))
|
||||
modal.show()
|
||||
|
||||
@override
|
||||
def on_item_edit(self, item: Tag) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
@@ -107,6 +111,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
edit_tag_modal.saved.connect(lambda: self.edit_item(edit_tag_panel))
|
||||
edit_tag_modal.show()
|
||||
|
||||
@override
|
||||
def _on_item_remove(self, item: Tag) -> None:
|
||||
if self.is_chooser:
|
||||
return
|
||||
@@ -115,22 +120,26 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
return
|
||||
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Question, # type: ignore
|
||||
QMessageBox.Icon.Question,
|
||||
Translations["tag.remove"],
|
||||
Translations.format("tag.confirm_delete", tag_name=self.__lib.tag_display_name(item)),
|
||||
QMessageBox.Ok | QMessageBox.Cancel, # type: ignore
|
||||
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
|
||||
result = message_box.exec()
|
||||
|
||||
if result != QMessageBox.Ok: # type: ignore
|
||||
if result != QMessageBox.StandardButton.Ok:
|
||||
return
|
||||
|
||||
self.__lib.remove_tag(item.id)
|
||||
self.update_items(self.get_search_query())
|
||||
|
||||
@override
|
||||
def on_item_create_and_add(self) -> None:
|
||||
"""Opens "Create Tag" panel to create and add a new tag with given name."""
|
||||
"""Opens "Create Tag" panel to create and add a new tag.
|
||||
|
||||
Populates name field using current search query.
|
||||
"""
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
@@ -138,26 +147,29 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
|
||||
logger.info("Create and Add Tag", name=query)
|
||||
|
||||
build_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
build_tag_modal: PanelModal = PanelModal(
|
||||
build_tag_panel,
|
||||
panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
modal: PanelModal = PanelModal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
Translations["tag.add"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
if query.strip():
|
||||
build_tag_panel.name_field.setText(query)
|
||||
panel.name_field.setText(query)
|
||||
|
||||
build_tag_modal.saved.connect(lambda: self.create_item(build_tag_modal, choose_item=True))
|
||||
build_tag_modal.show()
|
||||
modal.saved.connect(lambda: self.create_item(panel, choose_item=True))
|
||||
modal.show()
|
||||
|
||||
@override
|
||||
def _on_item_chosen(self, item: Tag) -> None:
|
||||
self.item_chosen.emit(item.id)
|
||||
|
||||
@override
|
||||
def search_items(self, query: str) -> tuple[list[Tag], list[Tag]]:
|
||||
return self.__lib.search_tags(name=query, limit=self._get_limit()[1])
|
||||
|
||||
@override
|
||||
def set_item_widget(self, item: Tag | None, index: int) -> None:
|
||||
"""Set the tag of a tag widget at a specific index."""
|
||||
tag_widget: TagWidget = self.get_item_widget(index, self.__lib)
|
||||
@@ -195,39 +207,41 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
else:
|
||||
tag_widget.search_for_tag_action.setEnabled(False)
|
||||
|
||||
def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
|
||||
@override
|
||||
def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
if isinstance(build_item_modal.widget, BuildTagPanel):
|
||||
tag: Tag = build_item_modal.widget.build_tag()
|
||||
if isinstance(edit_item_panel, BuildTagPanel):
|
||||
tag: Tag = edit_item_panel.build_tag()
|
||||
self.__lib.add_tag(
|
||||
tag,
|
||||
parent_ids=build_item_modal.widget.parent_ids,
|
||||
alias_names=build_item_modal.widget.alias_names,
|
||||
alias_ids=build_item_modal.widget.alias_ids,
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
alias_names=edit_item_panel.alias_names,
|
||||
alias_ids=edit_item_panel.alias_ids,
|
||||
)
|
||||
|
||||
if choose_item:
|
||||
self._on_item_chosen(tag)
|
||||
self.clear_search_query()
|
||||
|
||||
build_item_modal.hide()
|
||||
edit_item_panel.hide()
|
||||
self.on_search_query_changed(self.get_search_query())
|
||||
|
||||
@override
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
if not isinstance(edit_item_panel, BuildTagPanel):
|
||||
return
|
||||
|
||||
self.__lib.update_tag(
|
||||
tag=edit_item_panel.build_tag(),
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
alias_names=edit_item_panel.alias_names,
|
||||
alias_ids=edit_item_panel.alias_ids,
|
||||
)
|
||||
|
||||
self.update_items(self.search_field.text())
|
||||
|
||||
def search_for_tag(self, tag_id: int) -> None:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import cast, override
|
||||
|
||||
import structlog
|
||||
@@ -47,13 +48,19 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class CustomTableItem(QLineEdit):
|
||||
def __init__(self, text, on_return, on_backspace, parent=None):
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
on_return: Callable[..., None],
|
||||
on_backspace: Callable[..., None],
|
||||
parent: QWidget | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setText(text)
|
||||
self.on_return = on_return
|
||||
self.on_backspace = on_backspace
|
||||
self.on_return: Callable[..., None] = on_return
|
||||
self.on_backspace: Callable[..., None] = on_backspace
|
||||
|
||||
def set_id(self, id):
|
||||
def set_id(self, id: int):
|
||||
self.id = id
|
||||
|
||||
@override
|
||||
@@ -301,7 +308,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.parent_ids: set[int] = set()
|
||||
self.alias_ids: list[int] = []
|
||||
self.alias_names: list[str] = []
|
||||
self.new_alias_names: dict = {}
|
||||
self.new_alias_names: dict[int, str] = {}
|
||||
self.new_item_id = sys.maxsize
|
||||
|
||||
self.set_tag(tag or Tag(name=Translations["tag.new"]))
|
||||
@@ -317,7 +324,7 @@ class BuildTagPanel(PanelWidget):
|
||||
item = self.aliases_table.cellWidget(i, 1)
|
||||
if (
|
||||
isinstance(item, CustomTableItem)
|
||||
and cast(CustomTableItem, item).id == cast(CustomTableItem, focused_widget).id
|
||||
and item.id == cast(CustomTableItem, focused_widget).id
|
||||
):
|
||||
cast(QPushButton, self.aliases_table.cellWidget(i, 0)).click()
|
||||
remove_row = i
|
||||
@@ -359,7 +366,7 @@ class BuildTagPanel(PanelWidget):
|
||||
item = self.aliases_table.cellWidget(row, 1)
|
||||
item.setFocus()
|
||||
|
||||
def remove_alias_callback(self, alias_name: str, alias_id: int):
|
||||
def remove_alias_callback(self, alias_id: int):
|
||||
logger.info("remove_alias_callback")
|
||||
|
||||
self.alias_ids.remove(alias_id)
|
||||
@@ -530,7 +537,7 @@ class BuildTagPanel(PanelWidget):
|
||||
for alias_id in self.alias_ids:
|
||||
alias = self.lib.get_alias(self.tag.id, alias_id)
|
||||
|
||||
alias_name = alias.name if alias else self.new_alias_names[alias_id]
|
||||
alias_name: str = alias.name if alias else self.new_alias_names[alias_id]
|
||||
|
||||
# handel when an alias name changes
|
||||
if alias_id in self.new_alias_names:
|
||||
@@ -539,9 +546,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.alias_names.append(alias_name)
|
||||
|
||||
remove_btn = QPushButton("-")
|
||||
remove_btn.clicked.connect(
|
||||
lambda a=alias_name, id=alias_id: self.remove_alias_callback(a, id)
|
||||
)
|
||||
remove_btn.clicked.connect(lambda id=alias_id: self.remove_alias_callback(id))
|
||||
|
||||
row = self.aliases_table.rowCount()
|
||||
new_item = CustomTableItem(alias_name, self.enter, self.backspace)
|
||||
@@ -619,6 +624,7 @@ class BuildTagPanel(PanelWidget):
|
||||
logger.info("built tag", tag=tag)
|
||||
return tag
|
||||
|
||||
@override
|
||||
def parent_post_init(self):
|
||||
self.setTabOrder(self.name_field, self.shorthand_field)
|
||||
self.setTabOrder(self.shorthand_field, self.aliases_add_button)
|
||||
|
||||
@@ -156,15 +156,15 @@ class TagWidget(QWidget):
|
||||
self.inner_layout.setObjectName("innerLayout")
|
||||
self.inner_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.remove_button = QPushButton(self)
|
||||
self.remove_button.setFlat(True)
|
||||
self.remove_button.setText("–")
|
||||
self.remove_button.setHidden(True)
|
||||
self.remove_button.setMinimumSize(22, 22)
|
||||
self.remove_button.setMaximumSize(22, 22)
|
||||
self.remove_button.clicked.connect(self.on_remove.emit)
|
||||
self.remove_button.setHidden(True)
|
||||
self.inner_layout.addWidget(self.remove_button)
|
||||
self._delete_button = QPushButton(self)
|
||||
self._delete_button.setFlat(True)
|
||||
self._delete_button.setText("–")
|
||||
self._delete_button.setHidden(True)
|
||||
self._delete_button.setMinimumSize(22, 22)
|
||||
self._delete_button.setMaximumSize(22, 22)
|
||||
self._delete_button.clicked.connect(self.on_remove.emit)
|
||||
self._delete_button.setHidden(True)
|
||||
self.inner_layout.addWidget(self._delete_button)
|
||||
self.inner_layout.addStretch(1)
|
||||
|
||||
self.bg_button.setLayout(self.inner_layout)
|
||||
@@ -236,7 +236,7 @@ class TagWidget(QWidget):
|
||||
f"}}"
|
||||
)
|
||||
|
||||
self.remove_button.setStyleSheet(
|
||||
self._delete_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"color: rgba{primary_color.toTuple()};"
|
||||
f"background: rgba{text_color.toTuple()};"
|
||||
@@ -275,14 +275,14 @@ class TagWidget(QWidget):
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
if self.has_remove:
|
||||
self.remove_button.setHidden(False)
|
||||
self._delete_button.setHidden(False)
|
||||
self.update()
|
||||
return super().enterEvent(event)
|
||||
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
if self.has_remove:
|
||||
self.remove_button.setHidden(True)
|
||||
self._delete_button.setHidden(True)
|
||||
self.update()
|
||||
return super().leaveEvent(event)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class Translator:
|
||||
for k, v in self._strings.items():
|
||||
self._strings[k] = remove_mnemonic_marker(v)
|
||||
|
||||
def __format(self, text: str, **kwargs) -> str:
|
||||
def __format(self, text: str, **kwargs: ...) -> str:
|
||||
try:
|
||||
return text.format(**kwargs)
|
||||
except (KeyError, ValueError):
|
||||
@@ -93,11 +93,11 @@ class Translator:
|
||||
kwargs=kwargs,
|
||||
language=self.__lang,
|
||||
)
|
||||
params: defaultdict[str, Any] = defaultdict(lambda: "{unknown_key}")
|
||||
params: defaultdict[str, Any] = defaultdict(lambda: "{unknown_key}") # pyright: ignore[reportExplicitAny]
|
||||
params.update(kwargs)
|
||||
return text.format_map(params)
|
||||
|
||||
def format(self, key: str, **kwargs) -> str:
|
||||
def format(self, key: str, **kwargs: ...) -> str:
|
||||
return self.__format(self[key], **kwargs)
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
|
||||
59
src/tagstudio/qt/views/edit_field_template_modal_view.py
Normal file
59
src/tagstudio/qt/views/edit_field_template_modal_view.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class EditFieldTemplateModalView(PanelWidget):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Layout Init
|
||||
self.setMinimumSize(460, 200)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Field Name -------------------------------------------------------------------------------
|
||||
self.__name_widget = QWidget()
|
||||
self.__name_layout = QVBoxLayout(self.__name_widget)
|
||||
self.__name_layout.setStretch(1, 1)
|
||||
self.__name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.__name_layout.setSpacing(0)
|
||||
self.__name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.__name_title = QLabel(Translations["field.name"])
|
||||
self.__name_layout.addWidget(self.__name_title)
|
||||
self.name_field = QLineEdit()
|
||||
self.name_field.setFixedHeight(24)
|
||||
self.name_field.setPlaceholderText(Translations["field.field_name_required"])
|
||||
self.__name_layout.addWidget(self.name_field)
|
||||
|
||||
# Field Type -------------------------------------------------------------------------------
|
||||
self.__type_widget = QWidget()
|
||||
self.__type_layout = QVBoxLayout(self.__type_widget)
|
||||
self.__type_layout.setStretch(1, 1)
|
||||
self.__type_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.__type_layout.setSpacing(0)
|
||||
self.__type_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.__type_title = QLabel(Translations["field.type"])
|
||||
self.__type_layout.addWidget(self.__type_title)
|
||||
self._type_combobox = QComboBox()
|
||||
self.__type_layout.addWidget(self._type_combobox)
|
||||
|
||||
# Add Widgets to Layout ====================================================================
|
||||
self.root_layout.addWidget(self.__name_widget)
|
||||
self.root_layout.addWidget(self.__type_widget)
|
||||
@@ -72,18 +72,18 @@ class FieldTemplateWidgetView(QWidget):
|
||||
self.__inner_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Remove button
|
||||
self.__remove_button = QPushButton(self)
|
||||
self.__remove_button.setFlat(True)
|
||||
self.__remove_button.setText("–")
|
||||
self.__remove_button.setHidden(True)
|
||||
self.__remove_button.setMinimumSize(22, 22)
|
||||
self.__remove_button.setMaximumSize(22, 22)
|
||||
self._delete_button = QPushButton(self)
|
||||
self._delete_button.setFlat(True)
|
||||
self._delete_button.setText("–")
|
||||
self._delete_button.setHidden(True)
|
||||
self._delete_button.setMinimumSize(22, 22)
|
||||
self._delete_button.setMaximumSize(22, 22)
|
||||
|
||||
self.__inner_layout.addWidget(self.__remove_button)
|
||||
self.__inner_layout.addWidget(self._delete_button)
|
||||
self.__inner_layout.addStretch(1)
|
||||
|
||||
self.__connect_callbacks()
|
||||
|
||||
def __connect_callbacks(self) -> None:
|
||||
self._bg_button.clicked.connect(self.on_click.emit)
|
||||
self.__remove_button.clicked.connect(self.on_remove.emit)
|
||||
self._delete_button.clicked.connect(self.on_remove.emit)
|
||||
|
||||
@@ -25,8 +25,8 @@ class PanelModal(QWidget):
|
||||
widget: "PanelWidget",
|
||||
title: str = "",
|
||||
window_title: str | None = None,
|
||||
done_callback: Callable[[], None] | None = None,
|
||||
save_callback: Callable[[str], None] | None = None,
|
||||
done_callback: Callable[..., None] | None = None,
|
||||
save_callback: Callable[..., None] | None = None,
|
||||
has_save: bool = False,
|
||||
):
|
||||
# [Done]
|
||||
@@ -128,7 +128,7 @@ class PanelWidget(QWidget):
|
||||
def parent_post_init(self) -> None:
|
||||
pass
|
||||
|
||||
def add_callback(self, callback: Callable[[], None], event: str = "returnPressed"):
|
||||
def add_callback(self, callback: Callable[..., None], event: str = "returnPressed"): # pyright: ignore[reportUnusedParameter]
|
||||
logger.warning(f"[PanelModal] add_callback not implemented for {self.__class__.__name__}")
|
||||
|
||||
@override
|
||||
|
||||
@@ -130,7 +130,7 @@ class SearchPanelView(PanelWidget):
|
||||
def scroll_area(self) -> QScrollArea:
|
||||
return self.__scroll_area
|
||||
|
||||
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None:
|
||||
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
|
||||
self.limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
|
||||
|
||||
self.search_field.textChanged.connect(controller.on_search_query_changed)
|
||||
@@ -171,7 +171,7 @@ class SearchPanelView(PanelWidget):
|
||||
def scroll_to(self, position: int) -> None:
|
||||
self.__scroll_area.verticalScrollBar().setValue(position)
|
||||
|
||||
def get_item_widget(self, index: int, library: Library | None) -> Any:
|
||||
def get_item_widget(self, index: int, library: Library | None) -> Any: # pyright: ignore[reportUnusedParameter, reportExplicitAny]
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_create_and_add_button(self) -> None:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Library '{library_dir}'",
|
||||
"color_manager.title": "Manage Tag Colors",
|
||||
"color.color_border": "Use Secondary Color for Border",
|
||||
"color.confirm_delete": "Are you sure you want to delete the color \"{color_name}\"?",
|
||||
"color.delete": "Delete Tag",
|
||||
@@ -22,7 +23,6 @@
|
||||
"color.primary_required": "Primary Color (Required)",
|
||||
"color.secondary": "Secondary Color",
|
||||
"color.title.no_color": "No Color",
|
||||
"color_manager.title": "Manage Tag Colors",
|
||||
"dependency.missing.title": "{dependency} Not Found",
|
||||
"drop_import.description": "The following files match file paths that already exist in the library",
|
||||
"drop_import.duplicates_choice.plural": "The following {count} files match file paths that already exist in the library.",
|
||||
@@ -34,7 +34,6 @@
|
||||
"drop_import.title": "Conflicting File(s)",
|
||||
"edit.color_manager": "Manage Tag Colors",
|
||||
"edit.copy_fields": "Copy Fields",
|
||||
"edit.field_template_manager": "Manage Field Templates",
|
||||
"edit.paste_fields": "Paste Fields",
|
||||
"edit.tag_manager": "Manage Tags",
|
||||
"entries.duplicate.merge": "Merge Duplicate Entries",
|
||||
@@ -72,21 +71,28 @@
|
||||
"entries.unlinked.unlinked_count": "Unlinked Entries: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg and/or FFprobe were not found. FFmpeg is required for multimedia playback and thumbnails.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field_template_manager.title": "Library Field Templates",
|
||||
"field_template.all_field_templates": "All Field Templates",
|
||||
"field_template.confirm_delete": "Are you sure you want to delete the field template \"{field_template_name}\"?",
|
||||
"field_template.create": "Create Field Template",
|
||||
"field_template.create_add": "Create && Add \"{query}\"",
|
||||
"field_template.delete": "Delete Field Template",
|
||||
"field_template.edit": "Edit Field Template",
|
||||
"field_template.new": "New Field Template",
|
||||
"field_type.datetime": "Datetime",
|
||||
"field_type.text": "Text",
|
||||
"field_type.unknown": "Unknown Type",
|
||||
"field.add": "Add Field",
|
||||
"field.add.plural": "Add Fields",
|
||||
"field.confirm_remove": "Are you sure you want to remove this \"{name}\" field?",
|
||||
"field.copy": "Copy Field",
|
||||
"field.edit": "Edit Field",
|
||||
"field.field_name_required": "Field Name (Required)",
|
||||
"field.mixed_data": "Mixed Data",
|
||||
"field.name": "Name",
|
||||
"field.paste": "Paste Field",
|
||||
"field.remove": "Remove Field",
|
||||
"field_template.all_field_templates": "All Field Templates",
|
||||
"field_template.create": "Create Field Template",
|
||||
"field_template.create_add": "Create && Add \"{query}\"",
|
||||
"field_template_manager.title": "Library Field Templates",
|
||||
"field_type.datetime": "Datetime",
|
||||
"field_type.text": "Text",
|
||||
"field_type.unknown": "Unknown Type",
|
||||
"field.type": "Type",
|
||||
"file.date_added": "Date Added",
|
||||
"file.date_created": "Date Created",
|
||||
"file.date_modified": "Date Modified",
|
||||
@@ -100,8 +106,8 @@
|
||||
"file.duplicates.fix": "Fix Duplicate Files",
|
||||
"file.duplicates.matches": "Duplicate File Matches: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Duplicate File Matches: N/A",
|
||||
"file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.",
|
||||
"file.duplicates.mirror_entries": "&Mirror Entries",
|
||||
"file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.",
|
||||
"file.duration": "Length",
|
||||
"file.not_found": "File Not Found",
|
||||
"file.open_file": "Open file",
|
||||
@@ -150,11 +156,11 @@
|
||||
"generic.skip_alt": "&Skip",
|
||||
"generic.yes": "Yes",
|
||||
"home.search": "Search",
|
||||
"home.search.view_limit": "View Limit:",
|
||||
"home.search_entries": "Search Entries",
|
||||
"home.search_field_templates": "Search Field Templates",
|
||||
"home.search_library": "Search Library",
|
||||
"home.search_tags": "Search Tags",
|
||||
"home.search.view_limit": "View Limit:",
|
||||
"home.show_hidden_entries": "Show Hidden Entries",
|
||||
"home.thumbnail_size": "Thumbnail Size",
|
||||
"home.thumbnail_size.extra_large": "Extra Large Thumbnails",
|
||||
@@ -187,13 +193,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Library</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Library</h2>",
|
||||
"landing.open_create_library": "Open/Create Library {shortcut}",
|
||||
"library.missing": "Library Location is Missing",
|
||||
"library.name": "Library",
|
||||
"library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found",
|
||||
"library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found",
|
||||
"library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...",
|
||||
"library.refresh.title": "Refreshing Directories",
|
||||
"library.scan_library.title": "Scanning Library",
|
||||
"library_info.cleanup": "Cleanup",
|
||||
"library_info.cleanup.backups": "Library Backups:",
|
||||
"library_info.cleanup.dupe_files": "Duplicate Files:",
|
||||
@@ -213,6 +212,13 @@
|
||||
"library_object.name_required": "Name (Required)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library_object.slug_required": "ID Slug (Required)",
|
||||
"library.missing": "Library Location is Missing",
|
||||
"library.name": "Library",
|
||||
"library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...",
|
||||
"library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found",
|
||||
"library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found",
|
||||
"library.refresh.title": "Refreshing Directories",
|
||||
"library.scan_library.title": "Scanning Library",
|
||||
"macros.running.dialog.new_entries": "Running Configured Macros on {count}/{total} New File Entries...",
|
||||
"macros.running.dialog.title": "Running Macros on New Entries",
|
||||
"media_player.autoplay": "Autoplay",
|
||||
@@ -323,11 +329,12 @@
|
||||
"status.library_version_found": "Found:",
|
||||
"status.library_version_mismatch": "Library Version Mismatch!",
|
||||
"status.results": "Results",
|
||||
"status.results.invalid_syntax": "Invalid Search Syntax:",
|
||||
"status.results_found": "{count} Results Found ({time_span})",
|
||||
"status.results.invalid_syntax": "Invalid Search Syntax:",
|
||||
"tag_manager.title": "Library Tags",
|
||||
"tag.add": "Add Tag",
|
||||
"tag.add.plural": "Add Tags",
|
||||
"tag.add_to_search": "Add to Search",
|
||||
"tag.add.plural": "Add Tags",
|
||||
"tag.aliases": "Aliases",
|
||||
"tag.all_tags": "All Tags",
|
||||
"tag.choose_color": "Choose Tag Color",
|
||||
@@ -348,7 +355,6 @@
|
||||
"tag.search_for_tag": "Search for Tag",
|
||||
"tag.shorthand": "Shorthand",
|
||||
"tag.tag_name_required": "Tag Name (Required)",
|
||||
"tag_manager.title": "Library Tags",
|
||||
"trash.context.ambiguous": "Move file(s) to {trash_term}",
|
||||
"trash.context.plural": "Move files to {trash_term}",
|
||||
"trash.context.singular": "Move file to {trash_term}",
|
||||
|
||||
@@ -80,7 +80,7 @@ def test_build_tag_panel_remove_alias_callback(
|
||||
|
||||
alias: TagAlias = unwrap(library.get_alias(tag.id, tag.alias_ids[0]))
|
||||
|
||||
panel.remove_alias_callback(alias.name, alias.id)
|
||||
panel.remove_alias_callback(alias.id)
|
||||
|
||||
assert len(panel.alias_ids) == 1
|
||||
assert len(panel.alias_names) == 1
|
||||
|
||||
Reference in New Issue
Block a user