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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions plugin_source/auto_protect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from aqt import mw
from typing import Literal
import aqt.utils
from anki.cards import Card

from .var_defs import PREFIX_PROTECTED_FIELDS
from .utils import get_logger

logger = get_logger("ankicollab.auto_protect")

AUTO_PROTECT_TAG = f"{PREFIX_PROTECTED_FIELDS}::All"


def _is_auto_protect_on_review_enabled() -> bool:
config = mw.addonManager.getConfig(__name__)
if not config:
return False
return bool(config.get("settings", {}).get("auto_protect_on_review", False))


def _is_auto_protect_learned_enabled() -> bool:
config = mw.addonManager.getConfig(__name__)
if not config:
return False
return bool(config.get("settings", {}).get("auto_protect_learned", False))


def on_card_reviewed(reviewer, card: Card, ease: Literal[1, 2, 3, 4]) -> None:
if not _is_auto_protect_on_review_enabled():
return
if card.type != 2:
return

note = card.note()
if any(t.lower() == AUTO_PROTECT_TAG.lower() for t in note.tags):
return

note.tags.append(AUTO_PROTECT_TAG)
try:
undo_id = mw.col.add_custom_undo_entry("Auto-protect note")
mw.col.update_note(note)
mw.col.merge_undo_entries(undo_id)
logger.info(f"Auto-protected note {note.id}")
except Exception as e:
logger.error(f"Failed to auto-protect note {note.id}: {e}")


def protect_all_learned() -> None:
if not mw.col:
aqt.utils.showInfo("Collection not available.")
return

def _task():
card_ids = mw.col.find_cards("is:review")
note_ids = set()
for cid in card_ids:
card = mw.col.get_card(cid)
note_ids.add(card.nid)

notes_to_update = []
for nid in note_ids:
note = mw.col.get_note(nid)
if not any(t.lower() == AUTO_PROTECT_TAG.lower() for t in note.tags):
note.tags.append(AUTO_PROTECT_TAG)
notes_to_update.append(note)

if notes_to_update:
undo_id = mw.col.add_custom_undo_entry("Protect all learned notes")
mw.col.update_notes(notes_to_update)
mw.col.merge_undo_entries(undo_id)

return len(notes_to_update)

def _on_done(future):
try:
count = future.result()
aqt.utils.showInfo(
f"{count} notes protected with {AUTO_PROTECT_TAG}.\n"
f"the notes won't be erased with new card update."
)
except Exception as e:
aqt.utils.showInfo(f"Error : {e}")
logger.exception("Error in protect_all_learned")

mw.taskman.with_progress(
task=_task,
on_done=_on_done,
label="Protecting learned card",
)
17 changes: 9 additions & 8 deletions plugin_source/crowd_anki/representation/note.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,8 @@ def handle_import_config_changes(self, import_config, note_model, field_mapping=
if self.anki_object and hasattr(self.anki_object, 'tags'):
protected_tags = [
tag for tag in self.anki_object.tags
if tag.startswith(PREFIX_PROTECTED_FIELDS)
if tag.lower().startswith(PREFIX_PROTECTED_FIELDS.lower())
Comment thread
Dev-Zaius marked this conversation as resolved.
]

# Preserve DEFAULT_PROTECTED_TAGS (leech, marked, missing-media) from local note
# These tags are stripped during export but should be kept during import updates
preserved_default_tags = []
Expand Down Expand Up @@ -549,14 +548,16 @@ def remove_tags(self, tags): # Option to remove personal tags from notes before
return

for personal_tag in tags:
# Remove exact matches
if personal_tag in self.anki_object.tags:
self.anki_object.tags.remove(personal_tag)
# Remove tags that start with the personal_tag prefix (hierarchical tags)
# Remove exact matches (case-insensitive)
self.anki_object.tags = [
tag for tag in self.anki_object.tags
if tag.lower() != personal_tag.lower()
]
# Remove tags that start with the personal_tag prefix (case-insensitive)
self.anki_object.tags = [
tag for tag in self.anki_object.tags
if not tag.startswith(f"{personal_tag}::")
if not tag.lower().startswith(f"{personal_tag.lower()}::")
]

# Remove any tags that are just whitespace
self.anki_object.tags = [tag for tag in self.anki_object.tags if tag.strip()]
self.anki_object.tags = [tag for tag in self.anki_object.tags if tag.strip()]
4 changes: 4 additions & 0 deletions plugin_source/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .notifications_center import refresh_notifications, register_sync_refresh_hook
from .utils import get_logger
import requests
from .auto_protect import on_card_reviewed, protect_all_learned

logger = get_logger("ankicollab.hooks")

Expand Down Expand Up @@ -698,3 +699,6 @@ def hooks_init():
# Context Menus (callbacks have internal checks)
gui_hooks.browser_sidebar_will_show_context_menu.append(add_sidebar_context_menu)
gui_hooks.browser_will_show_context_menu.append(context_menu_bulk_suggest)

# Reviewer related
gui_hooks.reviewer_did_answer_card.append(on_card_reviewed)
35 changes: 33 additions & 2 deletions plugin_source/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from .import_manager import *
from .export_manager import handle_export
from .media_import import on_media_btn
from .hooks import async_update, update_hooks_for_login_state
from .hooks import async_update, update_hooks_for_login_state, protect_all_learned
from .dialogs import LoginDialog
from .auth_manager import auth_manager
from .notifications_center import (
Expand Down Expand Up @@ -916,7 +916,36 @@ def show_global_settings_dialog(parent_dialog):
error_reporting_cb.setStyleSheet(checkbox_style)
error_reporting_cb.setChecked(bool(settings.get("error_reporting_enabled", False)))
error_reporting_cb.setToolTip("Help us fix bugs faster - no personal data is collected")


auto_protect_separator = QLabel("─" * 30)
auto_protect_separator.setStyleSheet(f"color: {colors['border']};")
global_layout.addWidget(auto_protect_separator)

auto_protect_review_cb = QCheckBox("Protects new card from change automatically")
auto_protect_review_cb.setStyleSheet(checkbox_style)
auto_protect_review_cb.setChecked(bool(settings.get("auto_protect_on_review", False)))
auto_protect_review_cb.setToolTip(
"Add Protect All Fields tag to your card immediately after you reviewed it "
"so the note won't be erased on updates"
Comment on lines +924 to +929
)
global_layout.addWidget(auto_protect_review_cb)

auto_protect_learned_cb = QCheckBox("Protect all Learned cards from change")
auto_protect_learned_cb.setStyleSheet(checkbox_style)
auto_protect_learned_cb.setChecked(bool(settings.get("auto_protect_learned", False)))
auto_protect_learned_cb.setToolTip(
"Add a button to protect all reviewed cards"
)
global_layout.addWidget(auto_protect_learned_cb)

protect_now_btn = QPushButton("Protect now all learned cards")
protect_now_btn.setStyleSheet(get_button_style('neutral', 'small'))
protect_now_btn.setToolTip("Launch the protection tag on all learned cards")
protect_now_btn.setEnabled(bool(settings.get("auto_protect_learned", False)))
auto_protect_learned_cb.toggled.connect(protect_now_btn.setEnabled)
protect_now_btn.clicked.connect(protect_all_learned)
global_layout.addWidget(protect_now_btn)

global_layout.addWidget(pull_on_startup_cb)
global_layout.addWidget(suspend_new_cards_cb)
global_layout.addWidget(move_cards_cb)
Expand Down Expand Up @@ -1002,6 +1031,8 @@ def save_global_settings():
settings["auto_move_cards"] = move_cards_cb.isChecked()
settings["keep_empty_subdecks"] = keep_empty_subdecks_cb.isChecked()
settings["error_reporting_enabled"] = error_reporting_cb.isChecked()
settings["auto_protect_on_review"] = auto_protect_review_cb.isChecked()
settings["auto_protect_learned"] = auto_protect_learned_cb.isChecked()
mw.addonManager.writeConfig(__name__, strings_data)
auth_manager.set_auto_approve(auto_approve_cb.isChecked())
# Apply telemetry setting immediately
Expand Down