feat: port copy/paste fields and tags to sql (#722)

* implement copy/paste for fields and tags

* fix tests

* fixed badge refresh on pasting

* renamed translation field

* ignore duplicate tags on insert

* fixes

* break into several statements to satisfy tests

* chore: format with ruff
This commit is contained in:
mashed5894
2025-02-01 00:38:01 +02:00
committed by GitHub
parent 496c87c7c3
commit 225fe98c20
4 changed files with 86 additions and 5 deletions

View File

@@ -186,6 +186,8 @@
"select.add_tag_to_selected": "Add Tag to Selected",
"select.all": "Select All",
"select.clear": "Clear Selection",
"edit.copy_fields": "Copy Fields",
"edit.paste_fields": "Paste Fields",
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
"settings.open_library_on_start": "Open Library on Start",
"settings.show_filenames_in_grid": "Show Filenames in Grid",

View File

@@ -1016,19 +1016,21 @@ class Library:
def add_tags_to_entry(self, entry_id: int, tag_ids: int | list[int] | set[int]) -> bool:
"""Add one or more tags to an entry."""
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
tag_ids = [tag_ids] if isinstance(tag_ids, int) else tag_ids
with Session(self.engine, expire_on_commit=False) as session:
try:
# TODO: Optimize this by using a single query to update.
for tag_id in tag_ids_:
for tag_id in tag_ids:
try:
session.add(TagEntry(tag_id=tag_id, entry_id=entry_id))
session.flush()
except IntegrityError:
session.rollback()
try:
session.commit()
return True
except IntegrityError as e:
logger.warning("[add_tags_to_entry]", warning=e)
session.rollback()
return False
return True
def remove_tags_from_entry(self, entry_id: int, tag_ids: int | list[int] | set[int]) -> bool:
"""Remove one or more tags from an entry."""

View File

@@ -426,6 +426,34 @@ class QtDriver(DriverMixin, QObject):
clear_select_action.setToolTip("Esc")
edit_menu.addAction(clear_select_action)
self.copy_buffer: dict = {"fields": [], "tags": []}
self.copy_fields_action = QAction(menu_bar)
Translations.translate_qobject(self.copy_fields_action, "edit.copy_fields")
self.copy_fields_action.triggered.connect(self.copy_fields_action_callback)
self.copy_fields_action.setShortcut(
QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
QtCore.Qt.Key.Key_C,
)
)
self.copy_fields_action.setToolTip("Ctrl+C")
self.copy_fields_action.setEnabled(False)
edit_menu.addAction(self.copy_fields_action)
self.paste_fields_action = QAction(menu_bar)
Translations.translate_qobject(self.paste_fields_action, "edit.paste_fields")
self.paste_fields_action.triggered.connect(self.paste_fields_action_callback)
self.paste_fields_action.setShortcut(
QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
QtCore.Qt.Key.Key_V,
)
)
self.paste_fields_action.setToolTip("Ctrl+V")
self.paste_fields_action.setEnabled(False)
edit_menu.addAction(self.paste_fields_action)
self.add_tag_to_selected_action = QAction(menu_bar)
Translations.translate_qobject(
self.add_tag_to_selected_action, "select.add_tag_to_selected"
@@ -814,7 +842,9 @@ class QtDriver(DriverMixin, QObject):
item.thumb_button.set_selected(True)
self.set_macro_menu_viability()
self.set_clipboard_menu_viability()
self.set_add_to_selected_visibility()
self.preview_panel.update_widgets(update_preview=False)
def clear_select_action_callback(self):
@@ -824,6 +854,7 @@ class QtDriver(DriverMixin, QObject):
item.thumb_button.set_selected(False)
self.set_macro_menu_viability()
self.set_clipboard_menu_viability()
self.preview_panel.update_widgets()
def add_tags_to_selected_callback(self, tag_ids: list[int]):
@@ -1100,6 +1131,36 @@ class QtDriver(DriverMixin, QObject):
sa.setWidgetResizable(True)
sa.setWidget(self.flow_container)
def copy_fields_action_callback(self):
if len(self.selected) > 0:
entry = self.lib.get_entry_full(self.selected[0])
if entry:
self.copy_buffer["fields"] = entry.fields
self.copy_buffer["tags"] = [tag.id for tag in entry.tags]
self.set_clipboard_menu_viability()
def paste_fields_action_callback(self):
for id in self.selected:
entry = self.lib.get_entry_full(id, with_fields=True, with_tags=False)
if not entry:
continue
existing_fields = entry.fields
for field in self.copy_buffer["fields"]:
exists = False
for e in existing_fields:
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)
self.lib.add_tags_to_entry(id, self.copy_buffer["tags"])
if len(self.selected) > 1:
if TAG_ARCHIVED in self.copy_buffer["tags"]:
self.update_badges({BadgeType.ARCHIVED: True}, origin_id=0, add_tags=False)
if TAG_FAVORITE in self.copy_buffer["tags"]:
self.update_badges({BadgeType.FAVORITE: True}, origin_id=0, add_tags=False)
else:
self.preview_panel.update_widgets()
def toggle_item_selection(self, item_id: int, append: bool, bridge: bool):
"""Toggle the selection of an item in the Thumbnail Grid.
@@ -1170,12 +1231,24 @@ class QtDriver(DriverMixin, QObject):
it.thumb_button.set_selected(False)
self.set_macro_menu_viability()
self.set_clipboard_menu_viability()
self.set_add_to_selected_visibility()
self.preview_panel.update_widgets()
def set_macro_menu_viability(self):
self.autofill_action.setDisabled(not self.selected)
def set_clipboard_menu_viability(self):
if len(self.selected) == 1:
self.copy_fields_action.setEnabled(True)
else:
self.copy_fields_action.setEnabled(False)
if self.selected and (self.copy_buffer["fields"] or self.copy_buffer["tags"]):
self.paste_fields_action.setEnabled(True)
else:
self.paste_fields_action.setEnabled(False)
def set_add_to_selected_visibility(self):
if not self.add_tag_to_selected_action:
return

View File

@@ -146,6 +146,10 @@ def qt_driver(qtbot, library):
driver.item_thumbs = []
driver.autofill_action = Mock()
driver.copy_buffer = {"fields": [], "tags": []}
driver.copy_fields_action = Mock()
driver.paste_fields_action = Mock()
driver.lib = library
# TODO - downsize this method and use it
# driver.start()