From af07d3982fbd19fbc01082b21a197f3d7cb44c76 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 11 Oct 2023 20:28:05 +0200 Subject: [PATCH 001/105] Add card-suspension options to deck config --- ankihub/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ankihub/settings.py b/ankihub/settings.py index 904de0408..6d5e91d40 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -83,6 +83,8 @@ class DeckConfig(DataClassJSONMixin): subdecks_enabled: bool = ( False # whether deck is organized into subdecks by the add-on ) + suspend_new_cards_of_new_notes: bool = False + suspend_new_cards_of_existing_notes: bool = True @dataclass From 86a9af87dce0fbc448c7f1139429da7bafb99533 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 11 Oct 2023 20:28:53 +0200 Subject: [PATCH 002/105] Set `suspend_new_cards_of_new_notes=True` when installing AnKing deck --- ankihub/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/settings.py b/ankihub/settings.py index 6d5e91d40..3d8223aa0 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -233,6 +233,7 @@ def add_deck( anki_id=DeckId(anki_did), user_relation=user_relation, subdecks_enabled=subdecks_enabled, + suspend_new_cards_of_new_notes=ankihub_did == ANKING_DECK_ID, ) # remove duplicates self.save_latest_deck_update(ankihub_did, latest_udpate) From 49d90ea23b8990d1700038b201bceb46b3cb802a Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 11 Oct 2023 20:29:25 +0200 Subject: [PATCH 003/105] Add migration to set `suspend_new_cards_of_new_notes=True` for installed AnKing deck --- ankihub/private_config_migrations.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ankihub/private_config_migrations.py b/ankihub/private_config_migrations.py index 9b898d32c..57dff77a3 100644 --- a/ankihub/private_config_migrations.py +++ b/ankihub/private_config_migrations.py @@ -21,6 +21,9 @@ def migrate_private_config(private_config_dict: Dict) -> None: """ maybe_rename_ankihub_deck_uuid_to_ah_did(private_config_dict) maybe_reset_media_update_timestamps(private_config_dict) + maybe_set_suspend_new_cards_of_new_notes_to_true_for_anking_deck( + private_config_dict + ) def maybe_reset_media_update_timestamps(private_config_dict: Dict) -> None: @@ -49,6 +52,23 @@ def maybe_rename_ankihub_deck_uuid_to_ah_did(private_config_dict: Dict) -> None: ) +def maybe_set_suspend_new_cards_of_new_notes_to_true_for_anking_deck( + private_config_dict: Dict, +) -> None: + """Set suspend_new_cards_of_new_notes to True in the DeckConfig of the AnKing deck if the field + doesn't exist yet.""" + from .settings import ANKING_DECK_ID + + field_name = "suspend_new_cards_of_new_notes" + decks = private_config_dict["decks"] + for ah_did, deck in decks.items(): + if ah_did == ANKING_DECK_ID and deck.get(field_name) is None: + deck[field_name] = True + LOGGER.info( + f"Set {field_name} to True for the previously installed AnKing deck." + ) + + def _is_api_version_on_last_sync_below_threshold( private_config_dict: Dict, version_threshold: float ) -> bool: From f1938c65365168ac9ed8a9ed7174283d09c44125 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:17:12 +0200 Subject: [PATCH 004/105] Change layout --- ankihub/gui/decks_dialog.py | 63 +++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index aa523fb26..0832915d2 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -40,7 +40,6 @@ class SubscribedDecksDialog(QDialog): def __init__(self): super(SubscribedDecksDialog, self).__init__() self.client = AnkiHubClient() - self.setWindowTitle("Subscribed AnkiHub Decks") self._setup_ui() self._on_item_selection_changed() self._refresh_decks_list() @@ -52,30 +51,64 @@ def __init__(self): self.show() def _setup_ui(self): - self.box_top = QVBoxLayout() - self.box_above = QHBoxLayout() - self.box_right = QVBoxLayout() + self.setWindowTitle("AnkiHub | Decks Managment") - self.decks_list = QListWidget() - qconnect(self.decks_list.itemSelectionChanged, self._on_item_selection_changed) + self.box_main = QVBoxLayout() + self.setLayout(self.box_main) + + self.box_top = self._setup_box_top() + self.box_main.addLayout(self.box_top) + + self.box_bottom = QHBoxLayout() + self.box_main.addLayout(self.box_bottom) + + self.box_bottom_left = self._setup_box_bottom_left() + self.box_bottom.addLayout(self.box_bottom_left) + + self.box_bottom_right = self._setup_box_bottom_right() + self.box_bottom.addLayout(self.box_bottom_right) + + def _setup_box_top(self) -> QVBoxLayout: + box = QVBoxLayout() + self.deck_operations_label = QLabel("Deck Operations") + box.addWidget(self.deck_operations_label) + + self.box_top_buttons = QHBoxLayout() + box.addLayout(self.box_top_buttons) self.browse_btn = QPushButton("Browse Decks") - self.box_right.addWidget(self.browse_btn) + self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) + box.addSpacing(10) + + return box + + def _setup_box_bottom_left(self) -> QVBoxLayout: + box = QVBoxLayout() + self.decks_list_label = QLabel("Subscribed AnkiHub Decks") + box.addWidget(self.decks_list_label) + + self.decks_list = QListWidget() + box.addWidget(self.decks_list) + qconnect(self.decks_list.itemSelectionChanged, self._on_item_selection_changed) + return box + + def _setup_box_bottom_right(self) -> QVBoxLayout: + box = QVBoxLayout() self.unsubscribe_btn = QPushButton("Unsubscribe") - self.box_right.addWidget(self.unsubscribe_btn) + box.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) self.open_web_btn = QPushButton("Open on AnkiHub") - self.box_right.addWidget(self.open_web_btn) + box.addWidget(self.open_web_btn) qconnect(self.open_web_btn.clicked, self._on_open_web) self.set_home_deck_btn = QPushButton("Set Home deck") self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") set_tooltip_icon(self.set_home_deck_btn) qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) - self.box_right.addWidget(self.set_home_deck_btn) + box.addWidget(self.set_home_deck_btn) self.toggle_subdecks_btn = QPushButton("Enable Subdecks") self.toggle_subdecks_btn.setToolTip( @@ -84,14 +117,10 @@ def _setup_ui(self): ) set_tooltip_icon(self.toggle_subdecks_btn) qconnect(self.toggle_subdecks_btn.clicked, self._on_toggle_subdecks) - self.box_right.addWidget(self.toggle_subdecks_btn) + box.addWidget(self.toggle_subdecks_btn) + box.addStretch(1) - self.box_right.addStretch(1) - - self.setLayout(self.box_top) - self.box_top.addLayout(self.box_above) - self.box_above.addWidget(self.decks_list) - self.box_above.addLayout(self.box_right) + return box def _refresh_decks_list(self) -> None: self.decks_list.clear() From 616159944145e1c3a47695a640dea6e5869368bc Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:27:58 +0200 Subject: [PATCH 005/105] Reorganize bottom right box --- ankihub/gui/decks_dialog.py | 44 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 0832915d2..fe6584b6e 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -96,19 +96,36 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: def _setup_box_bottom_right(self) -> QVBoxLayout: box = QVBoxLayout() - self.unsubscribe_btn = QPushButton("Unsubscribe") - box.addWidget(self.unsubscribe_btn) - qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) + + # Deck Actions + self.box_deck_actions = QVBoxLayout() + box.addLayout(self.box_deck_actions) + + self.deck_actions_label = QLabel("Deck Actions") + self.box_deck_actions.addWidget(self.deck_actions_label) + + self.box_deck_action_buttons = QHBoxLayout() + self.box_deck_actions.addLayout(self.box_deck_action_buttons) self.open_web_btn = QPushButton("Open on AnkiHub") - box.addWidget(self.open_web_btn) + self.box_deck_action_buttons.addWidget(self.open_web_btn) qconnect(self.open_web_btn.clicked, self._on_open_web) - self.set_home_deck_btn = QPushButton("Set Home deck") - self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") - set_tooltip_icon(self.set_home_deck_btn) - qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) - box.addWidget(self.set_home_deck_btn) + self.unsubscribe_btn = QPushButton("Unsubscribe") + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) + qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) + + box.addSpacing(10) + + # Deck Settings + self.box_deck_settings = QVBoxLayout() + box.addLayout(self.box_deck_settings) + + self.deck_settings_label = QLabel("Deck Settings") + self.box_deck_settings.addWidget(self.deck_settings_label) + + self.box_deck_settings_elements = QVBoxLayout() + self.box_deck_settings.addLayout(self.box_deck_settings_elements) self.toggle_subdecks_btn = QPushButton("Enable Subdecks") self.toggle_subdecks_btn.setToolTip( @@ -117,7 +134,14 @@ def _setup_box_bottom_right(self) -> QVBoxLayout: ) set_tooltip_icon(self.toggle_subdecks_btn) qconnect(self.toggle_subdecks_btn.clicked, self._on_toggle_subdecks) - box.addWidget(self.toggle_subdecks_btn) + self.box_deck_settings_elements.addWidget(self.toggle_subdecks_btn) + + self.set_home_deck_btn = QPushButton("Set Home deck") + self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") + set_tooltip_icon(self.set_home_deck_btn) + qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) + self.box_deck_settings_elements.addWidget(self.set_home_deck_btn) + box.addStretch(1) return box From 8927d6846ed489fd1463b919cc471809a82b089c Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:39:03 +0200 Subject: [PATCH 006/105] ref: Extract create_collaborative_deck operation --- ankihub/gui/menu.py | 160 +---------------------- ankihub/gui/operations/deck_creation.py | 167 ++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 156 deletions(-) create mode 100644 ankihub/gui/operations/deck_creation.py diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py index eaa454380..789f16942 100644 --- a/ankihub/gui/menu.py +++ b/ankihub/gui/menu.py @@ -2,42 +2,35 @@ import re from concurrent.futures import Future from dataclasses import dataclass -from datetime import datetime, timezone from pathlib import Path from typing import Optional import aqt from aqt import ( AnkiApp, - QCheckBox, QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QSizePolicy, QVBoxLayout, QWidget, ) -from aqt.operations import QueryOp from aqt.qt import QAction, QDialog, QKeySequence, QMenu, Qt, qconnect -from aqt.studydeck import StudyDeck from aqt.utils import openLink, showInfo, tooltip from .. import LOGGER from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient -from ..ankihub_client import AnkiHubHTTPError, get_media_names_from_notes_data -from ..ankihub_client.models import UserDeckRelation +from ..ankihub_client import AnkiHubHTTPError from ..db import ankihub_db -from ..main.deck_creation import DeckCreationResult, create_ankihub_deck -from ..main.subdecks import SUBDECK_TAG from ..media_import.ui import open_import_dialog -from ..settings import ADDON_VERSION, config, url_view_deck +from ..settings import ADDON_VERSION, config from .config_dialog import get_config_dialog_manager from .decks_dialog import SubscribedDecksDialog from .errors import upload_logs_and_data_in_background, upload_logs_in_background from .media_sync import media_sync from .operations.ankihub_sync import sync_with_ankihub +from .operations.deck_creation import create_collaborative_deck from .utils import ( ask_user, check_and_prompt_for_updates_on_main_window, @@ -192,154 +185,9 @@ def display_login(cls): return cls._window -class DeckCreationConfirmationDialog(QMessageBox): - def __init__(self): - super().__init__(parent=aqt.mw) - - self.setWindowTitle("Confirm AnkiHub Deck Creation") - self.setIcon(QMessageBox.Icon.Question) - self.setText( - "Are you sure you want to create a new collaborative deck?


" - 'Terms of use: https://www.ankihub.net/terms
' - 'Privacy Policy: https://www.ankihub.net/privacy
', - ) - self.setStandardButtons( - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel # type: ignore - ) - self.confirmation_cb = QCheckBox( - text=" by checking this checkbox you agree to the terms of use", - parent=self, - ) - self.setCheckBox(self.confirmation_cb) - - def run(self) -> bool: - clicked_ok = self.exec() == QMessageBox.StandardButton.Yes - if not clicked_ok: - return False - - if not self.confirmation_cb.isChecked(): - tooltip("You didn't agree to the terms of use.") - return False - - return True - - -def _create_collaborative_deck_action() -> None: - - confirm = DeckCreationConfirmationDialog().run() - if not confirm: - return - - LOGGER.info("Asking user to choose a deck to upload...") - deck_chooser = StudyDeck( - aqt.mw, - title="AnkiHub", - accept="Upload", - # Removes the "Add" button - buttons=[], - names=lambda: [ - d.name - for d in aqt.mw.col.decks.all_names_and_ids(include_filtered=False) - if "::" not in d.name and d.id != 1 - ], - parent=aqt.mw, - ) - LOGGER.info(f"Closed deck chooser dialog: {deck_chooser}") - LOGGER.info(f"Chosen deck name: {deck_chooser.name}") - deck_name = deck_chooser.name - if not deck_name: - return - - if len(aqt.mw.col.find_cards(f'deck:"{deck_name}"')) == 0: - showInfo("You can't upload an empty deck.") - return - - public = ask_user( - "Would you like to make this deck public?

" - 'If you chose "No" it will be private and only people with a link ' - "will be able to see it on the AnkiHub website." - ) - if public is None: - return - - private = public is False - - add_subdeck_tags = False - if aqt.mw.col.decks.children(aqt.mw.col.decks.id_for_name(deck_name)): - add_subdeck_tags = ask_user( - "Would you like to add a tag to each note in the deck that indicates which subdeck it belongs to?

" - "For example, if you have a deck named My Deck with a subdeck named My Deck::Subdeck, " - "each note in My Deck::Subdeck will have a tag " - f"{SUBDECK_TAG}::Subdeck added to it.

" - "This allows subscribers to have the same subdeck structure as you have." - ) - if add_subdeck_tags is None: - return - - confirm = ask_user( - "Uploading the deck to AnkiHub requires modifying notes and note types in " - f"{deck_name} and will require a full sync afterwards. Would you like to " - "continue?", - ) - if not confirm: - return - - should_upload_media = ask_user( - "Do you want to upload media for this deck as well? " - "This will take some extra time but it is required to display images " - "on AnkiHub and this way subscribers will be able to download media files " - "when installing the deck. " - ) - - def on_success(deck_creation_result: DeckCreationResult) -> None: - - # Upload all existing local media for this deck - # (media files that are referenced on Deck's notes) - if should_upload_media: - media_names = get_media_names_from_notes_data( - deck_creation_result.notes_data - ) - media_sync.start_media_upload(media_names, deck_creation_result.ankihub_did) - - # Add the deck to the list of decks the user owns - anki_did = aqt.mw.col.decks.id_for_name(deck_name) - creation_time = datetime.now(tz=timezone.utc) - config.add_deck( - deck_name, - deck_creation_result.ankihub_did, - anki_did, - user_relation=UserDeckRelation.OWNER, - latest_udpate=creation_time, - ) - - # Show a message to the user with a link to the deck on AnkiHub - deck_url = f"{url_view_deck()}{deck_creation_result.ankihub_did}" - showInfo( - "🎉 Deck upload successful!

" - "Link to the deck on AnkiHub:
" - f"{deck_url}" - ) - - def on_failure(exc: Exception): - aqt.mw.progress.finish() - raise exc - - op = QueryOp( - parent=aqt.mw, - op=lambda col: create_ankihub_deck( - deck_name, - private=private, - add_subdeck_tags=add_subdeck_tags, - ), - success=on_success, - ).failure(on_failure) - LOGGER.info("Instantiated QueryOp for creating collaborative deck") - op.with_progress(label="Creating collaborative deck").run_in_background() - - def _create_collaborative_deck_setup(parent: QMenu): q_action = QAction("🛠️ Create Collaborative Deck", parent=parent) - qconnect(q_action.triggered, _create_collaborative_deck_action) + qconnect(q_action.triggered, create_collaborative_deck) parent.addAction(q_action) diff --git a/ankihub/gui/operations/deck_creation.py b/ankihub/gui/operations/deck_creation.py new file mode 100644 index 000000000..03585e927 --- /dev/null +++ b/ankihub/gui/operations/deck_creation.py @@ -0,0 +1,167 @@ +from datetime import datetime, timezone + +import aqt +from aqt import QCheckBox, QMessageBox +from aqt.operations import QueryOp +from aqt.studydeck import StudyDeck +from aqt.utils import showInfo, tooltip + +from ... import LOGGER +from ...ankihub_client import get_media_names_from_notes_data +from ...ankihub_client.models import UserDeckRelation +from ...main.deck_creation import DeckCreationResult, create_ankihub_deck +from ...main.subdecks import SUBDECK_TAG +from ...settings import config, url_view_deck +from ..media_sync import media_sync +from ..utils import ask_user + + +def create_collaborative_deck() -> None: + """Creates a new collaborative deck and uploads it to AnkiHub. + + Asks the user to confirm, choose a deck to upload and for some additional options, + and then uploads the deck to AnkiHub. + When the upload is complete, shows a message to the user with a link to the deck on AnkiHub. + """ + + confirm = DeckCreationConfirmationDialog().run() + if not confirm: + return + + LOGGER.info("Asking user to choose a deck to upload...") + deck_chooser = StudyDeck( + aqt.mw, + title="AnkiHub", + accept="Upload", + # Removes the "Add" button + buttons=[], + names=lambda: [ + d.name + for d in aqt.mw.col.decks.all_names_and_ids(include_filtered=False) + if "::" not in d.name and d.id != 1 + ], + parent=aqt.mw, + ) + LOGGER.info(f"Closed deck chooser dialog: {deck_chooser}") + LOGGER.info(f"Chosen deck name: {deck_chooser.name}") + deck_name = deck_chooser.name + if not deck_name: + return + + if len(aqt.mw.col.find_cards(f'deck:"{deck_name}"')) == 0: + showInfo("You can't upload an empty deck.") + return + + public = ask_user( + "Would you like to make this deck public?

" + 'If you chose "No" it will be private and only people with a link ' + "will be able to see it on the AnkiHub website." + ) + if public is None: + return + + private = public is False + + add_subdeck_tags = False + if aqt.mw.col.decks.children(aqt.mw.col.decks.id_for_name(deck_name)): + add_subdeck_tags = ask_user( + "Would you like to add a tag to each note in the deck that indicates which subdeck it belongs to?

" + "For example, if you have a deck named My Deck with a subdeck named My Deck::Subdeck, " + "each note in My Deck::Subdeck will have a tag " + f"{SUBDECK_TAG}::Subdeck added to it.

" + "This allows subscribers to have the same subdeck structure as you have." + ) + if add_subdeck_tags is None: + return + + confirm = ask_user( + "Uploading the deck to AnkiHub requires modifying notes and note types in " + f"{deck_name} and will require a full sync afterwards. Would you like to " + "continue?", + ) + if not confirm: + return + + should_upload_media = ask_user( + "Do you want to upload media for this deck as well? " + "This will take some extra time but it is required to display images " + "on AnkiHub and this way subscribers will be able to download media files " + "when installing the deck. " + ) + + def on_success(deck_creation_result: DeckCreationResult) -> None: + + # Upload all existing local media for this deck + # (media files that are referenced on Deck's notes) + if should_upload_media: + media_names = get_media_names_from_notes_data( + deck_creation_result.notes_data + ) + media_sync.start_media_upload(media_names, deck_creation_result.ankihub_did) + + # Add the deck to the list of decks the user owns + anki_did = aqt.mw.col.decks.id_for_name(deck_name) + creation_time = datetime.now(tz=timezone.utc) + config.add_deck( + deck_name, + deck_creation_result.ankihub_did, + anki_did, + user_relation=UserDeckRelation.OWNER, + latest_udpate=creation_time, + ) + + # Show a message to the user with a link to the deck on AnkiHub + deck_url = f"{url_view_deck()}{deck_creation_result.ankihub_did}" + showInfo( + "🎉 Deck upload successful!

" + "Link to the deck on AnkiHub:
" + f"{deck_url}" + ) + + def on_failure(exc: Exception): + aqt.mw.progress.finish() + raise exc + + op = QueryOp( + parent=aqt.mw, + op=lambda col: create_ankihub_deck( + deck_name, + private=private, + add_subdeck_tags=add_subdeck_tags, + ), + success=on_success, + ).failure(on_failure) + LOGGER.info("Instantiated QueryOp for creating collaborative deck") + op.with_progress(label="Creating collaborative deck").run_in_background() + + +class DeckCreationConfirmationDialog(QMessageBox): + def __init__(self): + super().__init__(parent=aqt.mw) + + self.setWindowTitle("Confirm AnkiHub Deck Creation") + self.setIcon(QMessageBox.Icon.Question) + self.setText( + "Are you sure you want to create a new collaborative deck?


" + 'Terms of use: https://www.ankihub.net/terms
' + 'Privacy Policy: https://www.ankihub.net/privacy
', + ) + self.setStandardButtons( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel # type: ignore + ) + self.confirmation_cb = QCheckBox( + text=" by checking this checkbox you agree to the terms of use", + parent=self, + ) + self.setCheckBox(self.confirmation_cb) + + def run(self) -> bool: + clicked_ok = self.exec() == QMessageBox.StandardButton.Yes + if not clicked_ok: + return False + + if not self.confirmation_cb.isChecked(): + tooltip("You didn't agree to the terms of use.") + return False + + return True From 75758944c6d4aab12f1e95f3fedfa75a80c672bb Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:40:28 +0200 Subject: [PATCH 007/105] Add button for creating new deck --- ankihub/gui/decks_dialog.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index fe6584b6e..434bd842f 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -25,6 +25,7 @@ from aqt.utils import openLink, showInfo, showText, tooltip from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient +from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall from ..main.subdecks import SUBDECK_TAG from ..settings import config, url_deck_base, url_decks, url_help @@ -80,6 +81,10 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) + self.create_btn = QPushButton("Create Collaborative Deck") + self.box_top_buttons.addWidget(self.create_btn) + qconnect(self.create_btn.clicked, create_collaborative_deck) + box.addSpacing(10) return box From 28ff9decbc6c798231714a8c84ac98a1d9250bad Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:16:05 +0200 Subject: [PATCH 008/105] Refresh bottom right layout when deck is selected --- ankihub/gui/decks_dialog.py | 32 +++++++++++++++++++++++--------- ankihub/gui/utils.py | 13 +++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 434bd842f..a20022fba 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -31,7 +31,7 @@ from ..settings import config, url_deck_base, url_decks, url_help from .operations.deck_installation import download_and_install_decks from .operations.subdecks import confirm_and_toggle_subdecks -from .utils import ask_user, set_tooltip_icon +from .utils import ask_user, clear_layout, set_tooltip_icon class SubscribedDecksDialog(QDialog): @@ -42,7 +42,6 @@ def __init__(self): super(SubscribedDecksDialog, self).__init__() self.client = AnkiHubClient() self._setup_ui() - self._on_item_selection_changed() self._refresh_decks_list() if not config.is_logged_in(): @@ -66,7 +65,8 @@ def _setup_ui(self): self.box_bottom_left = self._setup_box_bottom_left() self.box_bottom.addLayout(self.box_bottom_left) - self.box_bottom_right = self._setup_box_bottom_right() + self.box_bottom_right = QVBoxLayout() + self._refresh_box_bottom_right(self.box_bottom_right, None) self.box_bottom.addLayout(self.box_bottom_right) def _setup_box_top(self) -> QVBoxLayout: @@ -96,11 +96,22 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: self.decks_list = QListWidget() box.addWidget(self.decks_list) - qconnect(self.decks_list.itemSelectionChanged, self._on_item_selection_changed) + qconnect(self.decks_list.itemSelectionChanged, self._on_deck_selection_changed) return box - def _setup_box_bottom_right(self) -> QVBoxLayout: - box = QVBoxLayout() + def _refresh_box_bottom_right( + self, box: QVBoxLayout, selected_ah_did: Optional[uuid.UUID] + ) -> None: + clear_layout(box) + + if selected_ah_did is None: + box.addSpacing(30) + + self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") + box.addWidget(self.no_deck_selected_label) + + box.addStretch(1) + return # Deck Actions self.box_deck_actions = QVBoxLayout() @@ -149,8 +160,6 @@ def _setup_box_bottom_right(self) -> QVBoxLayout: box.addStretch(1) - return box - def _refresh_decks_list(self) -> None: self.decks_list.clear() for deck in self.client.get_deck_subscriptions(): @@ -294,7 +303,7 @@ def _refresh_subdecks_button(self): "Disable Subdecks" if using_subdecks else "Enable Subdecks" ) - def _on_item_selection_changed(self) -> None: + def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() one_selected: bool = len(selection) == 1 is_deck_installed = False @@ -303,6 +312,11 @@ def _on_item_selection_changed(self) -> None: ankihub_did: UUID = selected.data(Qt.ItemDataRole.UserRole) is_deck_installed = bool(config.deck_config(ankihub_did)) + if one_selected: + self._refresh_box_bottom_right(self.box_bottom_right, ankihub_did) + else: + self._refresh_box_bottom_right(self.box_bottom_right, None) + self.unsubscribe_btn.setEnabled(one_selected) self.open_web_btn.setEnabled(one_selected) self.set_home_deck_btn.setEnabled(one_selected and is_deck_installed) diff --git a/ankihub/gui/utils.py b/ankihub/gui/utils.py index 41ae47f05..965c3db8e 100644 --- a/ankihub/gui/utils.py +++ b/ankihub/gui/utils.py @@ -9,6 +9,7 @@ QDialogButtonBox, QIcon, QLabel, + QLayout, QListWidget, QListWidgetItem, QMessageBox, @@ -271,3 +272,15 @@ def check_and_prompt_for_updates_on_main_window(): on_done=aqt.mw.on_updates_installed, requested_by_user=True, ) + + +def clear_layout(layout: QLayout) -> None: + """Remove all widgets from a layout and delete them.""" + while layout.count(): + child = layout.takeAt(0) + if child.widget(): + widget = child.widget() + widget.setParent(None) + widget.deleteLater() + elif child.layout(): + clear_layout(child.layout()) From 46627f029e583ebbff075d51bd172079f98ddba3 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:19:04 +0200 Subject: [PATCH 009/105] Set minimum width to 640 --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index a20022fba..0d6008af9 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -52,6 +52,7 @@ def __init__(self): def _setup_ui(self): self.setWindowTitle("AnkiHub | Decks Managment") + self.setMinimumWidth(640) self.box_main = QVBoxLayout() self.setLayout(self.box_main) From 3b52b085658719e32ca3875aa13c5974ca90927c Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:30:51 +0200 Subject: [PATCH 010/105] Share space equally between bottom layouts --- ankihub/gui/decks_dialog.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 0d6008af9..babc6f34a 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -93,6 +93,9 @@ def _setup_box_top(self) -> QVBoxLayout: def _setup_box_bottom_left(self) -> QVBoxLayout: box = QVBoxLayout() self.decks_list_label = QLabel("Subscribed AnkiHub Decks") + self.decks_list_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) box.addWidget(self.decks_list_label) self.decks_list = QListWidget() @@ -109,6 +112,9 @@ def _refresh_box_bottom_right( box.addSpacing(30) self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") + self.no_deck_selected_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) box.addWidget(self.no_deck_selected_label) box.addStretch(1) @@ -119,6 +125,9 @@ def _refresh_box_bottom_right( box.addLayout(self.box_deck_actions) self.deck_actions_label = QLabel("Deck Actions") + self.deck_actions_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) self.box_deck_actions.addWidget(self.deck_actions_label) self.box_deck_action_buttons = QHBoxLayout() From d7666a0b5a8e13b95bd66a013142ac550f82edc5 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:32:56 +0200 Subject: [PATCH 011/105] Set mimimum height --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index babc6f34a..924803be4 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -53,6 +53,7 @@ def __init__(self): def _setup_ui(self): self.setWindowTitle("AnkiHub | Decks Managment") self.setMinimumWidth(640) + self.setMinimumHeight(450) self.box_main = QVBoxLayout() self.setLayout(self.box_main) From 2ade68cb5643d8c663d86faa6516aa8890747ef4 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:37:39 +0200 Subject: [PATCH 012/105] Add spacing --- ankihub/gui/decks_dialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 924803be4..58ba43ef6 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -112,11 +112,16 @@ def _refresh_box_bottom_right( if selected_ah_did is None: box.addSpacing(30) + self.box_no_deck_selected = QHBoxLayout() + box.addLayout(self.box_no_deck_selected) + + self.box_no_deck_selected.addSpacing(5) + self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") self.no_deck_selected_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) - box.addWidget(self.no_deck_selected_label) + self.box_no_deck_selected.addWidget(self.no_deck_selected_label) box.addStretch(1) return From 41a0891d3aeff88da5daf09d1d1577cba60ee7b7 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 13:28:14 +0200 Subject: [PATCH 013/105] Replace subdecks button with checkbox --- ankihub/gui/decks_dialog.py | 30 +++++++++++++++--------------- ankihub/gui/utils.py | 7 ++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 58ba43ef6..1f90cc469 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -8,6 +8,7 @@ from anki.collection import OpChanges from aqt import gui_hooks from aqt.qt import ( + QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, @@ -159,14 +160,14 @@ def _refresh_box_bottom_right( self.box_deck_settings_elements = QVBoxLayout() self.box_deck_settings.addLayout(self.box_deck_settings_elements) - self.toggle_subdecks_btn = QPushButton("Enable Subdecks") - self.toggle_subdecks_btn.setToolTip( + self.toggle_subdecks_cb = QCheckBox("Enable Subdecks") + self.toggle_subdecks_cb.setToolTip( "Toggle between the deck being organized into subdecks or not.
" f"This will only have an effect if notes in the deck have {SUBDECK_TAG} tags." ) - set_tooltip_icon(self.toggle_subdecks_btn) - qconnect(self.toggle_subdecks_btn.clicked, self._on_toggle_subdecks) - self.box_deck_settings_elements.addWidget(self.toggle_subdecks_btn) + set_tooltip_icon(self.toggle_subdecks_cb) + qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) + self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) self.set_home_deck_btn = QPushButton("Set Home deck") self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") @@ -298,26 +299,25 @@ def _on_toggle_subdecks(self): confirm_and_toggle_subdecks(ankihub_id) - self._refresh_subdecks_button() + self._refresh_subdecks_checkbox() - def _refresh_subdecks_button(self): + def _refresh_subdecks_checkbox(self): selection = self.decks_list.selectedItems() one_selected: bool = len(selection) == 1 if not one_selected: - self.toggle_subdecks_btn.setEnabled(False) + self.toggle_subdecks_cb.setEnabled(False) return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - using_subdecks = False if deck_from_config := config.deck_config(ankihub_did): using_subdecks = deck_from_config.subdecks_enabled - self.toggle_subdecks_btn.setEnabled(True) + self.toggle_subdecks_cb.setEnabled(True) else: - self.toggle_subdecks_btn.setEnabled(False) - self.toggle_subdecks_btn.setText( - "Disable Subdecks" if using_subdecks else "Enable Subdecks" - ) + using_subdecks = False + self.toggle_subdecks_cb.setEnabled(False) + + self.toggle_subdecks_cb.setChecked(using_subdecks) def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() @@ -337,7 +337,7 @@ def _on_deck_selection_changed(self) -> None: self.open_web_btn.setEnabled(one_selected) self.set_home_deck_btn.setEnabled(one_selected and is_deck_installed) - self._refresh_subdecks_button() + self._refresh_subdecks_checkbox() @classmethod def display_subscribe_window(cls): diff --git a/ankihub/gui/utils.py b/ankihub/gui/utils.py index 965c3db8e..187fd3e5e 100644 --- a/ankihub/gui/utils.py +++ b/ankihub/gui/utils.py @@ -1,10 +1,11 @@ import uuid -from typing import Any, List, Optional +from typing import Any, List, Optional, Union import aqt from aqt.addons import check_and_prompt_for_updates from aqt.qt import ( QApplication, + QCheckBox, QDialog, QDialogButtonBox, QIcon, @@ -255,8 +256,8 @@ def ask_user( return None -def set_tooltip_icon(btn: QPushButton) -> None: - btn.setIcon( +def set_tooltip_icon(widget: Union[QPushButton, QCheckBox]) -> None: + widget.setIcon( QIcon( QApplication.style().standardIcon( QStyle.StandardPixmap.SP_MessageBoxInformation From 28872972e2540ffd47ea9988e415d64fb1523b78 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 13:33:42 +0200 Subject: [PATCH 014/105] Disable subdeck checkbox if no subdeck tags --- ankihub/gui/decks_dialog.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 1f90cc469..57cda47ce 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -28,7 +28,7 @@ from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall -from ..main.subdecks import SUBDECK_TAG +from ..main.subdecks import SUBDECK_TAG, deck_contains_subdeck_tags from ..settings import config, url_deck_base, url_decks, url_help from .operations.deck_installation import download_and_install_decks from .operations.subdecks import confirm_and_toggle_subdecks @@ -163,7 +163,7 @@ def _refresh_box_bottom_right( self.toggle_subdecks_cb = QCheckBox("Enable Subdecks") self.toggle_subdecks_cb.setToolTip( "Toggle between the deck being organized into subdecks or not.
" - f"This will only have an effect if notes in the deck have {SUBDECK_TAG} tags." + f"This option is only available if notes in the deck have {SUBDECK_TAG} tags." ) set_tooltip_icon(self.toggle_subdecks_cb) qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) @@ -310,9 +310,10 @@ def _refresh_subdecks_checkbox(self): return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - if deck_from_config := config.deck_config(ankihub_did): - using_subdecks = deck_from_config.subdecks_enabled - self.toggle_subdecks_cb.setEnabled(True) + if deck_config := config.deck_config(ankihub_did): + using_subdecks = deck_config.subdecks_enabled + has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) + self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) else: using_subdecks = False self.toggle_subdecks_cb.setEnabled(False) From 67807c7573c971f3aeb842893819fbc87e98d69e Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 13:59:07 +0200 Subject: [PATCH 015/105] Replace home_deck_btn with updates destination section --- ankihub/gui/decks_dialog.py | 56 ++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 57cda47ce..c51dcb4ff 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -169,14 +169,42 @@ def _refresh_box_bottom_right( qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) - self.set_home_deck_btn = QPushButton("Set Home deck") - self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") - set_tooltip_icon(self.set_home_deck_btn) - qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) - self.box_deck_settings_elements.addWidget(self.set_home_deck_btn) + box.addSpacing(10) + + # Updates Destination + self.box_updates_destination = QVBoxLayout() + box.addLayout(self.box_updates_destination) + + self.updates_destination_label = QLabel("Updates Destination") + self.box_updates_destination.addWidget(self.updates_destination_label) + + self.updates_destination_details_label = QLabel() + self.updates_destination_details_label.setWordWrap(True) + self._refresh_updates_destination_details_label(selected_ah_did) + self.box_updates_destination.addWidget(self.updates_destination_details_label) + + self.set_updates_destination_btn = QPushButton("Change updates destination") + qconnect( + self.set_updates_destination_btn.clicked, + self._on_change_updates_destination, + ) + self.box_updates_destination.addWidget(self.set_updates_destination_btn) box.addStretch(1) + def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: + deck_config = config.deck_config(ah_did) + destination_anki_did = deck_config.anki_id + if name := aqt.mw.col.decks.name_if_exists(destination_anki_did): + self.updates_destination_details_label.setText( + f"New cards will be added to: {name}." + ) + else: + # If the deck doesn't exitst, it will be re-created on next sync with the name from the config. + self.updates_destination_details_label.setText( + f"New cards will be added to {deck_config.name}." + ) + def _refresh_decks_list(self) -> None: self.decks_list.clear() for deck in self.client.get_deck_subscriptions(): @@ -247,18 +275,20 @@ def _on_open_web(self) -> None: ankihub_id: UUID = item.data(Qt.ItemDataRole.UserRole) openLink(f"{url_deck_base()}/{ankihub_id}") - def _on_set_home_deck(self): + def _on_change_updates_destination(self): deck_names = self.decks_list.selectedItems() if len(deck_names) == 0: return deck_name = deck_names[0] ankihub_id: UUID = deck_name.data(Qt.ItemDataRole.UserRole) - current_home_deck = aqt.mw.col.decks.get(config.deck_config(ankihub_id).anki_id) - if current_home_deck is None: + current_destination_deck = aqt.mw.col.decks.get( + config.deck_config(ankihub_id).anki_id + ) + if current_destination_deck is None: current = None else: - current = current_home_deck["name"] + current = current_destination_deck["name"] def update_deck_config(ret: StudyDeck): if not ret.name: @@ -266,14 +296,14 @@ def update_deck_config(ret: StudyDeck): anki_did = aqt.mw.col.decks.id(ret.name) config.set_home_deck(ankihub_did=ankihub_id, anki_did=anki_did) - tooltip("Home deck updated.", parent=self) + self._refresh_updates_destination_details_label(ankihub_id) # this lets the user pick a deck StudyDeckWithoutHelpButton( aqt.mw, current=current, - accept="Set Home Deck", - title="Change Home Deck", + accept="Accept", + title="Choose Updates Destination", parent=self, callback=update_deck_config, ) @@ -336,7 +366,7 @@ def _on_deck_selection_changed(self) -> None: self.unsubscribe_btn.setEnabled(one_selected) self.open_web_btn.setEnabled(one_selected) - self.set_home_deck_btn.setEnabled(one_selected and is_deck_installed) + self.set_updates_destination_btn.setEnabled(one_selected and is_deck_installed) self._refresh_subdecks_checkbox() From 5039e8257e14b6926c2996692464566707e9a386 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:01:01 +0200 Subject: [PATCH 016/105] Adjust spacing of box_bottom_right --- ankihub/gui/decks_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index c51dcb4ff..0432b83b0 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -110,8 +110,9 @@ def _refresh_box_bottom_right( ) -> None: clear_layout(box) + box.addSpacing(25) + if selected_ah_did is None: - box.addSpacing(30) self.box_no_deck_selected = QHBoxLayout() box.addLayout(self.box_no_deck_selected) From c2da7f08aeb68ac90ecec443b92274c15f762401 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:34:04 +0200 Subject: [PATCH 017/105] Style buttons --- ankihub/gui/decks_dialog.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 0432b83b0..12f468aff 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -23,6 +23,7 @@ qconnect, ) from aqt.studydeck import StudyDeck +from aqt.theme import theme_manager from aqt.utils import openLink, showInfo, showText, tooltip from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient @@ -80,11 +81,13 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons = QHBoxLayout() box.addLayout(self.box_top_buttons) - self.browse_btn = QPushButton("Browse Decks") + self.browse_btn = QPushButton("🔗 Browse Decks") + self.browse_btn.setStyleSheet("color: white; background-color: #306bec") + self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) - self.create_btn = QPushButton("Create Collaborative Deck") + self.create_btn = QPushButton("➕ Create Collaborative Deck") self.box_top_buttons.addWidget(self.create_btn) qconnect(self.create_btn.clicked, create_collaborative_deck) @@ -146,6 +149,11 @@ def _refresh_box_bottom_right( qconnect(self.open_web_btn.clicked, self._on_open_web) self.unsubscribe_btn = QPushButton("Unsubscribe") + if theme_manager.night_mode: + self.unsubscribe_btn.setStyleSheet("color: #e29792") + else: + self.unsubscribe_btn.setStyleSheet("color: #e2857f") + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) From 4c1be055202f0ac434d43e41353d9e9a93c871da Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:43:10 +0200 Subject: [PATCH 018/105] Add links to docs --- ankihub/gui/decks_dialog.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 12f468aff..e8406eeed 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -178,6 +178,16 @@ def _refresh_box_bottom_right( qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) + self.subdecks_docs_link_label = QLabel( + """ + + More about Subdecks + + """ + ) + self.subdecks_docs_link_label.setOpenExternalLinks(True) + self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) + box.addSpacing(10) # Updates Destination @@ -198,6 +208,17 @@ def _refresh_box_bottom_right( self._on_change_updates_destination, ) self.box_updates_destination.addWidget(self.set_updates_destination_btn) + self.box_updates_destination.addSpacing(7) + + self.destination_updates_docs_link_label = QLabel( + """ + + More about Updates Destinations + + """ + ) + self.destination_updates_docs_link_label.setOpenExternalLinks(True) + self.box_updates_destination.addWidget(self.destination_updates_docs_link_label) box.addStretch(1) From 76e0b74a81e304a0e315e5d347e0b4cb5590b65c Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:44:57 +0200 Subject: [PATCH 019/105] Adjust spacing --- ankihub/gui/decks_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index e8406eeed..ac4aad63d 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -157,7 +157,7 @@ def _refresh_box_bottom_right( self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) - box.addSpacing(10) + box.addSpacing(20) # Deck Settings self.box_deck_settings = QVBoxLayout() @@ -188,7 +188,7 @@ def _refresh_box_bottom_right( self.subdecks_docs_link_label.setOpenExternalLinks(True) self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) - box.addSpacing(10) + box.addSpacing(20) # Updates Destination self.box_updates_destination = QVBoxLayout() @@ -220,7 +220,7 @@ def _refresh_box_bottom_right( self.destination_updates_docs_link_label.setOpenExternalLinks(True) self.box_updates_destination.addWidget(self.destination_updates_docs_link_label) - box.addStretch(1) + box.addStretch() def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) From 511b7d59f71bdc458f052200ae6fe50572757075 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:55:01 +0200 Subject: [PATCH 020/105] Fix test --- tests/addon/test_integration.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index c68ec0a21..2c0421944 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2492,22 +2492,6 @@ def test_toggle_subdecks( operations.subdecks, "ask_user", lambda *args, **kwargs: True ) - # Mock get_deck_subscriptions to return an empty list - monkeypatch.setattr( - AnkiHubClient, - "get_deck_subscriptions", - lambda *args, **kwargs: [], - ) - - # Open the dialog - dialog = SubscribedDecksDialog() - qtbot.add_widget(dialog) - dialog.display_subscribe_window() - qtbot.wait(200) - - # The toggle subdecks button should be disabled because there are no decks - assert dialog.toggle_subdecks_btn.isEnabled() is False - # Install a deck with subdeck tags subdeck_name, anki_did, ah_did = self._install_deck_with_subdeck_tag( install_sample_ah_deck @@ -2533,15 +2517,15 @@ def test_toggle_subdecks( dialog.decks_list.setCurrentRow(0) qtbot.wait(200) - assert dialog.toggle_subdecks_btn.isEnabled() is True - dialog.toggle_subdecks_btn.click() + assert dialog.toggle_subdecks_cb.isEnabled() is True + dialog.toggle_subdecks_cb.click() qtbot.wait(200) # The subdeck should now exist assert mw.col.decks.by_name(subdeck_name) is not None # Click the toggle subdeck button again - dialog.toggle_subdecks_btn.click() + dialog.toggle_subdecks_cb.click() qtbot.wait(200) # The subdeck should not exist anymore From f32471adf3e62079fd69fc0a1dbe7018dea88e36 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 15:55:14 +0200 Subject: [PATCH 021/105] Remove unused SubscribeDialog --- ankihub/gui/decks_dialog.py | 109 +----------------- .../gui/operations/db_check/ah_db_check.py | 2 +- tests/addon/test_integration.py | 3 +- 3 files changed, 4 insertions(+), 110 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ac4aad63d..3b70f9f81 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -1,6 +1,5 @@ """Dialog for managing subscriptions to AnkiHub decks and deck-specific settings.""" import uuid -from concurrent.futures import Future from typing import Optional from uuid import UUID @@ -13,7 +12,6 @@ QDialogButtonBox, QHBoxLayout, QLabel, - QLineEdit, QListWidget, QListWidgetItem, QPushButton, @@ -30,8 +28,7 @@ from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall from ..main.subdecks import SUBDECK_TAG, deck_contains_subdeck_tags -from ..settings import config, url_deck_base, url_decks, url_help -from .operations.deck_installation import download_and_install_decks +from ..settings import config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks from .utils import ask_user, clear_layout, set_tooltip_icon @@ -256,12 +253,6 @@ def _refresh_anki(self) -> None: op.study_queues = True gui_hooks.operation_did_execute(op, handler=None) - def _on_add(self) -> None: - SubscribeDialog().exec() - - self._refresh_decks_list() - self._refresh_anki() - def _select_deck(self, ah_did: uuid.UUID): deck_item = next( ( @@ -419,101 +410,3 @@ def __init__(self, *args, **kwargs) -> None: self.form.buttonBox.removeButton( self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help) ) - - -class SubscribeDialog(QDialog): - silentlyClose = True - - def __init__(self): - super(SubscribeDialog, self).__init__() - - self.thread = None # type: ignore - self.box_top = QVBoxLayout() - self.box_mid = QHBoxLayout() - self.box_left = QVBoxLayout() - self.box_right = QVBoxLayout() - - self.deck_id_box = QHBoxLayout() - self.deck_id_box_label = QLabel("Deck ID:") - self.deck_id_box_text = QLineEdit("", self) - self.deck_id_box_text.setMinimumWidth(300) - self.deck_id_box.addWidget(self.deck_id_box_label) - self.deck_id_box.addWidget(self.deck_id_box_text) - self.box_left.addLayout(self.deck_id_box) - - self.box_mid.addLayout(self.box_left) - self.box_mid.addSpacing(20) - self.box_mid.addLayout(self.box_right) - - self.buttonbox = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel # type: ignore - ) - self.buttonbox.button(QDialogButtonBox.StandardButton.Ok).setText("Subscribe") - self.browse_btn = self.buttonbox.addButton( - "Browse Decks", QDialogButtonBox.ButtonRole.ActionRole - ) - qconnect(self.browse_btn.clicked, self._on_browse_deck) - qconnect(self.buttonbox.accepted, self._subscribe) - self.buttonbox.rejected.connect(self.close) - - self.instructions_label = QLabel( - "
Copy/Paste a Deck ID from AnkiHub.net/decks to subscribe.
" - ) - # Add all widgets to top layout. - self.box_top.addWidget(self.instructions_label) - self.box_top.addSpacing(10) - self.box_top.addLayout(self.box_mid) - self.box_top.addStretch(1) - self.box_top.addWidget(self.buttonbox) - self.setLayout(self.box_top) - - self.setMinimumWidth(500) - self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) - self.setWindowTitle("Subscribe to AnkiHub Deck") - - self.client = AnkiHubClient() - if not config.is_logged_in(): - showText("Oops! Please make sure you are logged into AnkiHub!") - self.close() - else: - self.show() - - def _subscribe(self) -> None: - ah_did_str = self.deck_id_box_text.text().strip() - - try: - ah_did = uuid.UUID(ah_did_str) - except ValueError: - showInfo( - "The format of the Deck ID is invalid. Please make sure you copied the Deck ID correctly." - ) - return - - if ah_did in config.deck_ids(): - showText( - f"You've already subscribed to deck {ah_did}. " - "Syncing with AnkiHub will happen automatically everytime you " - "restart Anki. You can manually sync with AnkiHub from the AnkiHub " - f"menu. See {url_help()} for more details." - ) - self.close() - return - - confirmed = ask_user( - f"Would you like to proceed with downloading and installing the deck? " - f"Your personal collection will be modified.

" - f"See {url_help()} for details.", - title="Please confirm to proceed.", - ) - if not confirmed: - return - - def on_done(future: Future) -> None: - future.result() - - self.accept() - - download_and_install_decks([ah_did], on_done=on_done) - - def _on_browse_deck(self) -> None: - openLink(url_decks()) diff --git a/ankihub/gui/operations/db_check/ah_db_check.py b/ankihub/gui/operations/db_check/ah_db_check.py index 53976298e..91740a7a9 100644 --- a/ankihub/gui/operations/db_check/ah_db_check.py +++ b/ankihub/gui/operations/db_check/ah_db_check.py @@ -7,8 +7,8 @@ from ....db import ankihub_db from ....main.deck_unsubscribtion import uninstall_deck from ....settings import config -from ...decks_dialog import download_and_install_decks from ...exceptions import DeckDownloadAndInstallError, RemoteDeckNotFoundError +from ...operations.deck_installation import download_and_install_decks from ...utils import ask_user diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 2c0421944..94ec539ac 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -115,12 +115,13 @@ setup_config_dialog_manager, ) from ankihub.gui.deck_updater import _AnkiHubDeckUpdater, ah_deck_updater -from ankihub.gui.decks_dialog import SubscribedDecksDialog, download_and_install_decks +from ankihub.gui.decks_dialog import SubscribedDecksDialog from ankihub.gui.editor import _on_suggestion_button_press, _refresh_buttons from ankihub.gui.errors import upload_logs_and_data_in_background from ankihub.gui.media_sync import media_sync from ankihub.gui.menu import menu_state from ankihub.gui.operations import ankihub_sync +from ankihub.gui.operations.deck_installation import download_and_install_decks from ankihub.gui.operations.new_deck_subscriptions import ( check_and_install_new_deck_subscriptions, ) From 2fa529d1e808e9e67406c3df86c1f6c5224eb7fe Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 16:03:23 +0200 Subject: [PATCH 022/105] Add test --- tests/addon/test_integration.py | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 94ec539ac..feecdf579 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -40,6 +40,7 @@ from aqt.browser.sidebar.tree import SidebarTreeView from aqt.importing import AnkiPackageImporter from aqt.qt import QAction, Qt +from aqt.theme import theme_manager from pytest import MonkeyPatch, fixture from pytest_anki import AnkiSession from pytestqt.qtbot import QtBot # type: ignore @@ -2474,6 +2475,43 @@ def assert_note_has_expected_tag(): class TestSubscribedDecksDialog: + @pytest.mark.parametrize( + "nightmode", + [True, False], + ) + def test_basic( + self, + anki_session_with_addon_data: AnkiSession, + install_sample_ah_deck: InstallSampleAHDeck, + qtbot: QtBot, + monkeypatch: MonkeyPatch, + nightmode: bool, + ): + with anki_session_with_addon_data.profile_loaded(): + + # Mock the config to return that the user is logged in + monkeypatch.setattr(config, "is_logged_in", lambda: True) + + # Mock the ask_user function to always return True + monkeypatch.setattr( + operations.subdecks, "ask_user", lambda *args, **kwargs: True + ) + + ah_did, anki_did = install_sample_ah_deck() + + # Mock get_deck_subscriptions to return the deck + monkeypatch.setattr( + AnkiHubClient, + "get_deck_subscriptions", + lambda *args: [DeckFactory.create(ah_did=ah_did, anki_did=anki_did)], + ) + + theme_manager.night_mode = nightmode + + dialog = SubscribedDecksDialog() + qtbot.add_widget(dialog) + dialog.display_subscribe_window() + def test_toggle_subdecks( self, anki_session_with_addon_data: AnkiSession, From 05624a9fd4c2c8b95294765910221121a1b86e7b Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 16:06:20 +0200 Subject: [PATCH 023/105] Only display installed decks in list --- ankihub/gui/decks_dialog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3b70f9f81..d2cf3e13e 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -234,7 +234,13 @@ def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: def _refresh_decks_list(self) -> None: self.decks_list.clear() - for deck in self.client.get_deck_subscriptions(): + + subscribed_decks = self.client.get_deck_subscriptions() + installed_ah_dids = config.deck_ids() + subscribed_and_installed_decks = [ + deck for deck in subscribed_decks if deck.ah_did in installed_ah_dids + ] + for deck in subscribed_and_installed_decks: name = deck.name if deck.is_user_relation_owner: item = QListWidgetItem(f"{name} (Created by you)") From e815eddd5089478e8bad356bfa435fc95f4397c4 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 16:08:24 +0200 Subject: [PATCH 024/105] Remove unused code --- ankihub/gui/decks_dialog.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index d2cf3e13e..05d79d59a 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -367,15 +367,11 @@ def _refresh_subdecks_checkbox(self): return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - if deck_config := config.deck_config(ankihub_did): - using_subdecks = deck_config.subdecks_enabled - has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) - self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) - else: - using_subdecks = False - self.toggle_subdecks_cb.setEnabled(False) + deck_config = config.deck_config(ankihub_did) - self.toggle_subdecks_cb.setChecked(using_subdecks) + has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) + self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) + self.toggle_subdecks_cb.setChecked(deck_config.subdecks_enabled) def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() From 5022ca00278d17bd1f97bcf320c3e78d35b1f947 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 17:51:03 +0200 Subject: [PATCH 025/105] ref: Move call to `_refresh_subdecks_checkbox` --- ankihub/gui/decks_dialog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 05d79d59a..a40722355 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -172,6 +172,7 @@ def _refresh_box_bottom_right( f"This option is only available if notes in the deck have {SUBDECK_TAG} tags." ) set_tooltip_icon(self.toggle_subdecks_cb) + self._refresh_subdecks_checkbox() qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) @@ -391,8 +392,6 @@ def _on_deck_selection_changed(self) -> None: self.open_web_btn.setEnabled(one_selected) self.set_updates_destination_btn.setEnabled(one_selected and is_deck_installed) - self._refresh_subdecks_checkbox() - @classmethod def display_subscribe_window(cls): if cls._window is None: From f91d0d0417d08b69e767ea14947c8d621b3e5504 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 18:10:12 +0200 Subject: [PATCH 026/105] Refactor tests --- tests/addon/test_integration.py | 46 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index feecdf579..49d3b8d66 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2489,17 +2489,10 @@ def test_basic( ): with anki_session_with_addon_data.profile_loaded(): - # Mock the config to return that the user is logged in - monkeypatch.setattr(config, "is_logged_in", lambda: True) + self._mock_dependencies(monkeypatch) - # Mock the ask_user function to always return True - monkeypatch.setattr( - operations.subdecks, "ask_user", lambda *args, **kwargs: True - ) - - ah_did, anki_did = install_sample_ah_deck() + anki_did, ah_did = install_sample_ah_deck() - # Mock get_deck_subscriptions to return the deck monkeypatch.setattr( AnkiHubClient, "get_deck_subscriptions", @@ -2509,9 +2502,14 @@ def test_basic( theme_manager.night_mode = nightmode dialog = SubscribedDecksDialog() - qtbot.add_widget(dialog) dialog.display_subscribe_window() + assert dialog.decks_list.count() == 1 + + # Select a deck from the list + dialog.decks_list.setCurrentRow(0) + qtbot.wait(200) + def test_toggle_subdecks( self, anki_session_with_addon_data: AnkiSession, @@ -2519,17 +2517,9 @@ def test_toggle_subdecks( install_sample_ah_deck: InstallSampleAHDeck, monkeypatch: MonkeyPatch, ): - anki_session = anki_session_with_addon_data - with anki_session.profile_loaded(): - mw = anki_session.mw - - # Mock the config to return that the user is logged in - monkeypatch.setattr(config, "is_logged_in", lambda: True) + with anki_session_with_addon_data.profile_loaded(): - # Mock the ask_user function to always return True - monkeypatch.setattr( - operations.subdecks, "ask_user", lambda *args, **kwargs: True - ) + self._mock_dependencies(monkeypatch) # Install a deck with subdeck tags subdeck_name, anki_did, ah_did = self._install_deck_with_subdeck_tag( @@ -2545,9 +2535,8 @@ def test_toggle_subdecks( lambda *args: [DeckFactory.create(ah_did=ah_did, anki_did=anki_did)], ) - # Refresh the dialog + # Open the dialog dialog = SubscribedDecksDialog() - qtbot.add_widget(dialog) dialog.display_subscribe_window() qtbot.wait(200) @@ -2561,14 +2550,14 @@ def test_toggle_subdecks( qtbot.wait(200) # The subdeck should now exist - assert mw.col.decks.by_name(subdeck_name) is not None + assert aqt.mw.col.decks.by_name(subdeck_name) is not None # Click the toggle subdeck button again dialog.toggle_subdecks_cb.click() qtbot.wait(200) # The subdeck should not exist anymore - assert mw.col.decks.by_name(subdeck_name) is None + assert aqt.mw.col.decks.by_name(subdeck_name) is None def _install_deck_with_subdeck_tag( self, @@ -2583,6 +2572,15 @@ def _install_deck_with_subdeck_tag( note.flush() return subdeck_name, anki_did, ah_did + def _mock_dependencies(self, monkeypatch: MonkeyPatch) -> None: + # Mock the config to return that the user is logged in + monkeypatch.setattr(config, "is_logged_in", lambda: True) + + # Mock the ask_user function to always return True + monkeypatch.setattr( + operations.subdecks, "ask_user", lambda *args, **kwargs: True + ) + class TestBuildSubdecksAndMoveCardsToThem: def test_basic( From fafd3561f931b57b4ea24829435911e3185311f9 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 18:46:15 +0200 Subject: [PATCH 027/105] Add test for updates destination setting --- tests/addon/conftest.py | 1 + tests/addon/test_integration.py | 69 +++++++++++++++++++++++++++++++++ tests/fixtures.py | 59 ++++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/tests/addon/conftest.py b/tests/addon/conftest.py index adaa17f43..15b7bc777 100644 --- a/tests/addon/conftest.py +++ b/tests/addon/conftest.py @@ -14,6 +14,7 @@ ankihub_basic_note_type, import_ah_note, import_ah_note_type, + install_ah_deck, mock_all_feature_flags_to_default_values, mock_download_and_install_deck_dependencies, mock_function, diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 49d3b8d66..fd0bde857 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -63,6 +63,7 @@ from ..factories import DeckFactory, DeckMediaFactory, NoteInfoFactory from ..fixtures import ( ImportAHNote, + InstallAHDeck, MockDownloadAndInstallDeckDependencies, MockFunction, create_or_get_ah_version_of_note_type, @@ -2572,6 +2573,74 @@ def _install_deck_with_subdeck_tag( note.flush() return subdeck_name, anki_did, ah_did + def test_change_updates_destination( + self, + anki_session_with_addon_data: AnkiSession, + qtbot: QtBot, + install_ah_deck: InstallAHDeck, + monkeypatch: MonkeyPatch, + ): + with anki_session_with_addon_data.profile_loaded(): + ah_did = install_ah_deck() + + self._mock_dependencies(monkeypatch) + + # Mock get_deck_subscriptions to return the deck + monkeypatch.setattr( + AnkiHubClient, + "get_deck_subscriptions", + lambda *args: [ + DeckFactory.create( + ah_did=ah_did, anki_did=config.deck_config(ah_did).anki_id + ) + ], + ) + + # Mock the dialog that asks the user for the destination deck + new_destination_deck_name = "New Deck" + install_ah_deck(anki_deck_name=new_destination_deck_name) + new_home_deck_anki_id = aqt.mw.col.decks.id_for_name( + new_destination_deck_name + ) + self._mock_new_cards_destination_dialog( + new_destination_deck_name, monkeypatch + ) + + # Open the dialog + dialog = SubscribedDecksDialog() + dialog.display_subscribe_window() + qtbot.wait(200) + + # Select the deck and click the Set Updates Destination button + dialog.decks_list.setCurrentRow(0) + qtbot.wait(200) + + dialog.set_updates_destination_btn.click() + qtbot.wait(200) + + # Assert that the destination deck was updated + assert config.deck_config(ah_did).anki_id == new_home_deck_anki_id + + def _mock_new_cards_destination_dialog( + self, + destination_deck_name: str, + monkeypatch: MonkeyPatch, + ) -> None: + """Sets the destination deck for new cards to the deck with the given name.""" + study_deck_mock = Mock() + + def study_deck_mock_side_effect(*args, **kwargs): + callback = kwargs["callback"] + cb_study_deck_mock = Mock() + cb_study_deck_mock.name = destination_deck_name + callback(cb_study_deck_mock) + + study_deck_mock.side_effect = study_deck_mock_side_effect + monkeypatch.setattr( + "ankihub.gui.decks_dialog.StudyDeckWithoutHelpButton", + study_deck_mock, + ) + def _mock_dependencies(self, monkeypatch: MonkeyPatch) -> None: # Mock the config to return that the user is logged in monkeypatch.setattr(config, "is_logged_in", lambda: True) diff --git a/tests/fixtures.py b/tests/fixtures.py index d524ba8fe..297a136a4 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -21,12 +21,13 @@ from ankihub.ankihub_client import NoteInfo from ankihub.ankihub_client.ankihub_client import AnkiHubClient -from ankihub.ankihub_client.models import Deck +from ankihub.ankihub_client.models import Deck, UserDeckRelation from ankihub.feature_flags import setup_feature_flags from ankihub.gui import operations from ankihub.gui.media_sync import _AnkiHubMediaSync from ankihub.main.importing import AnkiHubImporter from ankihub.main.utils import modify_note_type +from ankihub.settings import config @fixture @@ -174,7 +175,8 @@ def __call__( note_data: Optional[NoteInfo] = None, ah_nid: Optional[uuid.UUID] = None, mid: Optional[NotetypeId] = None, - ah_did: uuid.UUID = None, + ah_did: Optional[uuid.UUID] = None, + anki_did: Optional[DeckId] = None, ) -> NoteInfo: ... @@ -203,7 +205,8 @@ def _import_ah_note( note_data: Optional[NoteInfo] = None, ah_nid: Optional[uuid.UUID] = None, mid: Optional[NotetypeId] = None, - ah_did: uuid.UUID = default_ah_did, + ah_did: Optional[uuid.UUID] = default_ah_did, + anki_did: Optional[DeckId] = None, ) -> NoteInfo: if mid is None: ah_basic_note_type = create_or_get_ah_version_of_note_type( @@ -238,7 +241,8 @@ def _import_ah_note( protected_fields={}, protected_tags=[], deck_name=deck_name, - is_first_import_of_deck=True, + is_first_import_of_deck=anki_did is None, + anki_did=anki_did, ) return note_data @@ -322,6 +326,53 @@ def new_note_with_note_type_inner( return new_note_with_note_type_inner +class InstallAHDeck(Protocol): + def __call__( + self, + ah_did: Optional[uuid.UUID] = None, + ah_deck_name: Optional[str] = None, + anki_did: Optional[DeckId] = None, + anki_deck_name: Optional[str] = None, + ) -> uuid.UUID: + ... + + +@pytest.fixture +def install_ah_deck( + next_deterministic_uuid: Callable[[], uuid.UUID], + import_ah_note: ImportAHNote, +) -> InstallAHDeck: + """Installs a deck with the given AnkiHub and Anki names and ids. + The deck is imported and added to the private config. + Returns the AnkiHub deck id.""" + default_ah_did = next_deterministic_uuid() + default_ah_deck_name = "Test Deck" + # 1 is the id of the default deck, we don't want to use that here + default_anki_did = DeckId(2) + default_anki_deck_name = "Test Deck" + + def install_ah_deck_inner( + ah_did: Optional[uuid.UUID] = default_ah_did, + ah_deck_name: Optional[str] = default_ah_deck_name, + anki_did: Optional[DeckId] = default_anki_did, + anki_deck_name: Optional[str] = default_anki_deck_name, + ) -> uuid.UUID: + # Add deck to the config + config.add_deck( + name=ah_deck_name, + ankihub_did=ah_did, + anki_did=anki_did, + user_relation=UserDeckRelation.SUBSCRIBER, + ) + + # Create deck by importing a note for it + import_ah_note(ah_did=ah_did, anki_did=anki_did) + aqt.mw.col.decks.rename(aqt.mw.col.decks.get(anki_did), anki_deck_name) + return ah_did + + return install_ah_deck_inner + + class MockDownloadAndInstallDeckDependencies(Protocol): def __call__( self, From 16dc24ca687402199d570a60434b98ecd6450a45 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:39:03 +0200 Subject: [PATCH 028/105] ref: Extract create_collaborative_deck operation --- ankihub/gui/menu.py | 160 +---------------------- ankihub/gui/operations/deck_creation.py | 167 ++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 156 deletions(-) create mode 100644 ankihub/gui/operations/deck_creation.py diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py index eaa454380..789f16942 100644 --- a/ankihub/gui/menu.py +++ b/ankihub/gui/menu.py @@ -2,42 +2,35 @@ import re from concurrent.futures import Future from dataclasses import dataclass -from datetime import datetime, timezone from pathlib import Path from typing import Optional import aqt from aqt import ( AnkiApp, - QCheckBox, QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QSizePolicy, QVBoxLayout, QWidget, ) -from aqt.operations import QueryOp from aqt.qt import QAction, QDialog, QKeySequence, QMenu, Qt, qconnect -from aqt.studydeck import StudyDeck from aqt.utils import openLink, showInfo, tooltip from .. import LOGGER from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient -from ..ankihub_client import AnkiHubHTTPError, get_media_names_from_notes_data -from ..ankihub_client.models import UserDeckRelation +from ..ankihub_client import AnkiHubHTTPError from ..db import ankihub_db -from ..main.deck_creation import DeckCreationResult, create_ankihub_deck -from ..main.subdecks import SUBDECK_TAG from ..media_import.ui import open_import_dialog -from ..settings import ADDON_VERSION, config, url_view_deck +from ..settings import ADDON_VERSION, config from .config_dialog import get_config_dialog_manager from .decks_dialog import SubscribedDecksDialog from .errors import upload_logs_and_data_in_background, upload_logs_in_background from .media_sync import media_sync from .operations.ankihub_sync import sync_with_ankihub +from .operations.deck_creation import create_collaborative_deck from .utils import ( ask_user, check_and_prompt_for_updates_on_main_window, @@ -192,154 +185,9 @@ def display_login(cls): return cls._window -class DeckCreationConfirmationDialog(QMessageBox): - def __init__(self): - super().__init__(parent=aqt.mw) - - self.setWindowTitle("Confirm AnkiHub Deck Creation") - self.setIcon(QMessageBox.Icon.Question) - self.setText( - "Are you sure you want to create a new collaborative deck?


" - 'Terms of use: https://www.ankihub.net/terms
' - 'Privacy Policy: https://www.ankihub.net/privacy
', - ) - self.setStandardButtons( - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel # type: ignore - ) - self.confirmation_cb = QCheckBox( - text=" by checking this checkbox you agree to the terms of use", - parent=self, - ) - self.setCheckBox(self.confirmation_cb) - - def run(self) -> bool: - clicked_ok = self.exec() == QMessageBox.StandardButton.Yes - if not clicked_ok: - return False - - if not self.confirmation_cb.isChecked(): - tooltip("You didn't agree to the terms of use.") - return False - - return True - - -def _create_collaborative_deck_action() -> None: - - confirm = DeckCreationConfirmationDialog().run() - if not confirm: - return - - LOGGER.info("Asking user to choose a deck to upload...") - deck_chooser = StudyDeck( - aqt.mw, - title="AnkiHub", - accept="Upload", - # Removes the "Add" button - buttons=[], - names=lambda: [ - d.name - for d in aqt.mw.col.decks.all_names_and_ids(include_filtered=False) - if "::" not in d.name and d.id != 1 - ], - parent=aqt.mw, - ) - LOGGER.info(f"Closed deck chooser dialog: {deck_chooser}") - LOGGER.info(f"Chosen deck name: {deck_chooser.name}") - deck_name = deck_chooser.name - if not deck_name: - return - - if len(aqt.mw.col.find_cards(f'deck:"{deck_name}"')) == 0: - showInfo("You can't upload an empty deck.") - return - - public = ask_user( - "Would you like to make this deck public?

" - 'If you chose "No" it will be private and only people with a link ' - "will be able to see it on the AnkiHub website." - ) - if public is None: - return - - private = public is False - - add_subdeck_tags = False - if aqt.mw.col.decks.children(aqt.mw.col.decks.id_for_name(deck_name)): - add_subdeck_tags = ask_user( - "Would you like to add a tag to each note in the deck that indicates which subdeck it belongs to?

" - "For example, if you have a deck named My Deck with a subdeck named My Deck::Subdeck, " - "each note in My Deck::Subdeck will have a tag " - f"{SUBDECK_TAG}::Subdeck added to it.

" - "This allows subscribers to have the same subdeck structure as you have." - ) - if add_subdeck_tags is None: - return - - confirm = ask_user( - "Uploading the deck to AnkiHub requires modifying notes and note types in " - f"{deck_name} and will require a full sync afterwards. Would you like to " - "continue?", - ) - if not confirm: - return - - should_upload_media = ask_user( - "Do you want to upload media for this deck as well? " - "This will take some extra time but it is required to display images " - "on AnkiHub and this way subscribers will be able to download media files " - "when installing the deck. " - ) - - def on_success(deck_creation_result: DeckCreationResult) -> None: - - # Upload all existing local media for this deck - # (media files that are referenced on Deck's notes) - if should_upload_media: - media_names = get_media_names_from_notes_data( - deck_creation_result.notes_data - ) - media_sync.start_media_upload(media_names, deck_creation_result.ankihub_did) - - # Add the deck to the list of decks the user owns - anki_did = aqt.mw.col.decks.id_for_name(deck_name) - creation_time = datetime.now(tz=timezone.utc) - config.add_deck( - deck_name, - deck_creation_result.ankihub_did, - anki_did, - user_relation=UserDeckRelation.OWNER, - latest_udpate=creation_time, - ) - - # Show a message to the user with a link to the deck on AnkiHub - deck_url = f"{url_view_deck()}{deck_creation_result.ankihub_did}" - showInfo( - "🎉 Deck upload successful!

" - "Link to the deck on AnkiHub:
" - f"{deck_url}" - ) - - def on_failure(exc: Exception): - aqt.mw.progress.finish() - raise exc - - op = QueryOp( - parent=aqt.mw, - op=lambda col: create_ankihub_deck( - deck_name, - private=private, - add_subdeck_tags=add_subdeck_tags, - ), - success=on_success, - ).failure(on_failure) - LOGGER.info("Instantiated QueryOp for creating collaborative deck") - op.with_progress(label="Creating collaborative deck").run_in_background() - - def _create_collaborative_deck_setup(parent: QMenu): q_action = QAction("🛠️ Create Collaborative Deck", parent=parent) - qconnect(q_action.triggered, _create_collaborative_deck_action) + qconnect(q_action.triggered, create_collaborative_deck) parent.addAction(q_action) diff --git a/ankihub/gui/operations/deck_creation.py b/ankihub/gui/operations/deck_creation.py new file mode 100644 index 000000000..03585e927 --- /dev/null +++ b/ankihub/gui/operations/deck_creation.py @@ -0,0 +1,167 @@ +from datetime import datetime, timezone + +import aqt +from aqt import QCheckBox, QMessageBox +from aqt.operations import QueryOp +from aqt.studydeck import StudyDeck +from aqt.utils import showInfo, tooltip + +from ... import LOGGER +from ...ankihub_client import get_media_names_from_notes_data +from ...ankihub_client.models import UserDeckRelation +from ...main.deck_creation import DeckCreationResult, create_ankihub_deck +from ...main.subdecks import SUBDECK_TAG +from ...settings import config, url_view_deck +from ..media_sync import media_sync +from ..utils import ask_user + + +def create_collaborative_deck() -> None: + """Creates a new collaborative deck and uploads it to AnkiHub. + + Asks the user to confirm, choose a deck to upload and for some additional options, + and then uploads the deck to AnkiHub. + When the upload is complete, shows a message to the user with a link to the deck on AnkiHub. + """ + + confirm = DeckCreationConfirmationDialog().run() + if not confirm: + return + + LOGGER.info("Asking user to choose a deck to upload...") + deck_chooser = StudyDeck( + aqt.mw, + title="AnkiHub", + accept="Upload", + # Removes the "Add" button + buttons=[], + names=lambda: [ + d.name + for d in aqt.mw.col.decks.all_names_and_ids(include_filtered=False) + if "::" not in d.name and d.id != 1 + ], + parent=aqt.mw, + ) + LOGGER.info(f"Closed deck chooser dialog: {deck_chooser}") + LOGGER.info(f"Chosen deck name: {deck_chooser.name}") + deck_name = deck_chooser.name + if not deck_name: + return + + if len(aqt.mw.col.find_cards(f'deck:"{deck_name}"')) == 0: + showInfo("You can't upload an empty deck.") + return + + public = ask_user( + "Would you like to make this deck public?

" + 'If you chose "No" it will be private and only people with a link ' + "will be able to see it on the AnkiHub website." + ) + if public is None: + return + + private = public is False + + add_subdeck_tags = False + if aqt.mw.col.decks.children(aqt.mw.col.decks.id_for_name(deck_name)): + add_subdeck_tags = ask_user( + "Would you like to add a tag to each note in the deck that indicates which subdeck it belongs to?

" + "For example, if you have a deck named My Deck with a subdeck named My Deck::Subdeck, " + "each note in My Deck::Subdeck will have a tag " + f"{SUBDECK_TAG}::Subdeck added to it.

" + "This allows subscribers to have the same subdeck structure as you have." + ) + if add_subdeck_tags is None: + return + + confirm = ask_user( + "Uploading the deck to AnkiHub requires modifying notes and note types in " + f"{deck_name} and will require a full sync afterwards. Would you like to " + "continue?", + ) + if not confirm: + return + + should_upload_media = ask_user( + "Do you want to upload media for this deck as well? " + "This will take some extra time but it is required to display images " + "on AnkiHub and this way subscribers will be able to download media files " + "when installing the deck. " + ) + + def on_success(deck_creation_result: DeckCreationResult) -> None: + + # Upload all existing local media for this deck + # (media files that are referenced on Deck's notes) + if should_upload_media: + media_names = get_media_names_from_notes_data( + deck_creation_result.notes_data + ) + media_sync.start_media_upload(media_names, deck_creation_result.ankihub_did) + + # Add the deck to the list of decks the user owns + anki_did = aqt.mw.col.decks.id_for_name(deck_name) + creation_time = datetime.now(tz=timezone.utc) + config.add_deck( + deck_name, + deck_creation_result.ankihub_did, + anki_did, + user_relation=UserDeckRelation.OWNER, + latest_udpate=creation_time, + ) + + # Show a message to the user with a link to the deck on AnkiHub + deck_url = f"{url_view_deck()}{deck_creation_result.ankihub_did}" + showInfo( + "🎉 Deck upload successful!

" + "Link to the deck on AnkiHub:
" + f"{deck_url}" + ) + + def on_failure(exc: Exception): + aqt.mw.progress.finish() + raise exc + + op = QueryOp( + parent=aqt.mw, + op=lambda col: create_ankihub_deck( + deck_name, + private=private, + add_subdeck_tags=add_subdeck_tags, + ), + success=on_success, + ).failure(on_failure) + LOGGER.info("Instantiated QueryOp for creating collaborative deck") + op.with_progress(label="Creating collaborative deck").run_in_background() + + +class DeckCreationConfirmationDialog(QMessageBox): + def __init__(self): + super().__init__(parent=aqt.mw) + + self.setWindowTitle("Confirm AnkiHub Deck Creation") + self.setIcon(QMessageBox.Icon.Question) + self.setText( + "Are you sure you want to create a new collaborative deck?


" + 'Terms of use: https://www.ankihub.net/terms
' + 'Privacy Policy: https://www.ankihub.net/privacy
', + ) + self.setStandardButtons( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel # type: ignore + ) + self.confirmation_cb = QCheckBox( + text=" by checking this checkbox you agree to the terms of use", + parent=self, + ) + self.setCheckBox(self.confirmation_cb) + + def run(self) -> bool: + clicked_ok = self.exec() == QMessageBox.StandardButton.Yes + if not clicked_ok: + return False + + if not self.confirmation_cb.isChecked(): + tooltip("You didn't agree to the terms of use.") + return False + + return True From 85aa1c833371911a7eec7c571e9ccee1dcafcac9 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 15:55:14 +0200 Subject: [PATCH 029/105] Remove unused SubscribeDialog --- ankihub/gui/decks_dialog.py | 111 +----------------- .../gui/operations/db_check/ah_db_check.py | 2 +- tests/addon/test_integration.py | 3 +- 3 files changed, 4 insertions(+), 112 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index aa523fb26..3bd3d312b 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -1,6 +1,5 @@ """Dialog for managing subscriptions to AnkiHub decks and deck-specific settings.""" import uuid -from concurrent.futures import Future from typing import Optional from uuid import UUID @@ -11,12 +10,9 @@ QDialog, QDialogButtonBox, QHBoxLayout, - QLabel, - QLineEdit, QListWidget, QListWidgetItem, QPushButton, - QSizePolicy, Qt, QVBoxLayout, qconnect, @@ -27,8 +23,7 @@ from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall from ..main.subdecks import SUBDECK_TAG -from ..settings import config, url_deck_base, url_decks, url_help -from .operations.deck_installation import download_and_install_decks +from ..settings import config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks from .utils import ask_user, set_tooltip_icon @@ -114,12 +109,6 @@ def _refresh_anki(self) -> None: op.study_queues = True gui_hooks.operation_did_execute(op, handler=None) - def _on_add(self) -> None: - SubscribeDialog().exec() - - self._refresh_decks_list() - self._refresh_anki() - def _select_deck(self, ah_did: uuid.UUID): deck_item = next( ( @@ -270,101 +259,3 @@ def __init__(self, *args, **kwargs) -> None: self.form.buttonBox.removeButton( self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help) ) - - -class SubscribeDialog(QDialog): - silentlyClose = True - - def __init__(self): - super(SubscribeDialog, self).__init__() - - self.thread = None # type: ignore - self.box_top = QVBoxLayout() - self.box_mid = QHBoxLayout() - self.box_left = QVBoxLayout() - self.box_right = QVBoxLayout() - - self.deck_id_box = QHBoxLayout() - self.deck_id_box_label = QLabel("Deck ID:") - self.deck_id_box_text = QLineEdit("", self) - self.deck_id_box_text.setMinimumWidth(300) - self.deck_id_box.addWidget(self.deck_id_box_label) - self.deck_id_box.addWidget(self.deck_id_box_text) - self.box_left.addLayout(self.deck_id_box) - - self.box_mid.addLayout(self.box_left) - self.box_mid.addSpacing(20) - self.box_mid.addLayout(self.box_right) - - self.buttonbox = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel # type: ignore - ) - self.buttonbox.button(QDialogButtonBox.StandardButton.Ok).setText("Subscribe") - self.browse_btn = self.buttonbox.addButton( - "Browse Decks", QDialogButtonBox.ButtonRole.ActionRole - ) - qconnect(self.browse_btn.clicked, self._on_browse_deck) - qconnect(self.buttonbox.accepted, self._subscribe) - self.buttonbox.rejected.connect(self.close) - - self.instructions_label = QLabel( - "
Copy/Paste a Deck ID from AnkiHub.net/decks to subscribe.
" - ) - # Add all widgets to top layout. - self.box_top.addWidget(self.instructions_label) - self.box_top.addSpacing(10) - self.box_top.addLayout(self.box_mid) - self.box_top.addStretch(1) - self.box_top.addWidget(self.buttonbox) - self.setLayout(self.box_top) - - self.setMinimumWidth(500) - self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) - self.setWindowTitle("Subscribe to AnkiHub Deck") - - self.client = AnkiHubClient() - if not config.is_logged_in(): - showText("Oops! Please make sure you are logged into AnkiHub!") - self.close() - else: - self.show() - - def _subscribe(self) -> None: - ah_did_str = self.deck_id_box_text.text().strip() - - try: - ah_did = uuid.UUID(ah_did_str) - except ValueError: - showInfo( - "The format of the Deck ID is invalid. Please make sure you copied the Deck ID correctly." - ) - return - - if ah_did in config.deck_ids(): - showText( - f"You've already subscribed to deck {ah_did}. " - "Syncing with AnkiHub will happen automatically everytime you " - "restart Anki. You can manually sync with AnkiHub from the AnkiHub " - f"menu. See {url_help()} for more details." - ) - self.close() - return - - confirmed = ask_user( - f"Would you like to proceed with downloading and installing the deck? " - f"Your personal collection will be modified.

" - f"See {url_help()} for details.", - title="Please confirm to proceed.", - ) - if not confirmed: - return - - def on_done(future: Future) -> None: - future.result() - - self.accept() - - download_and_install_decks([ah_did], on_done=on_done) - - def _on_browse_deck(self) -> None: - openLink(url_decks()) diff --git a/ankihub/gui/operations/db_check/ah_db_check.py b/ankihub/gui/operations/db_check/ah_db_check.py index 53976298e..91740a7a9 100644 --- a/ankihub/gui/operations/db_check/ah_db_check.py +++ b/ankihub/gui/operations/db_check/ah_db_check.py @@ -7,8 +7,8 @@ from ....db import ankihub_db from ....main.deck_unsubscribtion import uninstall_deck from ....settings import config -from ...decks_dialog import download_and_install_decks from ...exceptions import DeckDownloadAndInstallError, RemoteDeckNotFoundError +from ...operations.deck_installation import download_and_install_decks from ...utils import ask_user diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index c68ec0a21..d0d7e4b09 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -115,12 +115,13 @@ setup_config_dialog_manager, ) from ankihub.gui.deck_updater import _AnkiHubDeckUpdater, ah_deck_updater -from ankihub.gui.decks_dialog import SubscribedDecksDialog, download_and_install_decks +from ankihub.gui.decks_dialog import SubscribedDecksDialog from ankihub.gui.editor import _on_suggestion_button_press, _refresh_buttons from ankihub.gui.errors import upload_logs_and_data_in_background from ankihub.gui.media_sync import media_sync from ankihub.gui.menu import menu_state from ankihub.gui.operations import ankihub_sync +from ankihub.gui.operations.deck_installation import download_and_install_decks from ankihub.gui.operations.new_deck_subscriptions import ( check_and_install_new_deck_subscriptions, ) From ea559a32e448778c4060ba97b3f7e375bfaa1d94 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:17:12 +0200 Subject: [PATCH 030/105] Change layout --- ankihub/gui/decks_dialog.py | 63 +++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3bd3d312b..ef61c08e9 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -35,7 +35,6 @@ class SubscribedDecksDialog(QDialog): def __init__(self): super(SubscribedDecksDialog, self).__init__() self.client = AnkiHubClient() - self.setWindowTitle("Subscribed AnkiHub Decks") self._setup_ui() self._on_item_selection_changed() self._refresh_decks_list() @@ -47,30 +46,64 @@ def __init__(self): self.show() def _setup_ui(self): - self.box_top = QVBoxLayout() - self.box_above = QHBoxLayout() - self.box_right = QVBoxLayout() + self.setWindowTitle("AnkiHub | Decks Managment") - self.decks_list = QListWidget() - qconnect(self.decks_list.itemSelectionChanged, self._on_item_selection_changed) + self.box_main = QVBoxLayout() + self.setLayout(self.box_main) + + self.box_top = self._setup_box_top() + self.box_main.addLayout(self.box_top) + + self.box_bottom = QHBoxLayout() + self.box_main.addLayout(self.box_bottom) + + self.box_bottom_left = self._setup_box_bottom_left() + self.box_bottom.addLayout(self.box_bottom_left) + + self.box_bottom_right = self._setup_box_bottom_right() + self.box_bottom.addLayout(self.box_bottom_right) + + def _setup_box_top(self) -> QVBoxLayout: + box = QVBoxLayout() + self.deck_operations_label = QLabel("Deck Operations") + box.addWidget(self.deck_operations_label) + + self.box_top_buttons = QHBoxLayout() + box.addLayout(self.box_top_buttons) self.browse_btn = QPushButton("Browse Decks") - self.box_right.addWidget(self.browse_btn) + self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) + box.addSpacing(10) + + return box + + def _setup_box_bottom_left(self) -> QVBoxLayout: + box = QVBoxLayout() + self.decks_list_label = QLabel("Subscribed AnkiHub Decks") + box.addWidget(self.decks_list_label) + + self.decks_list = QListWidget() + box.addWidget(self.decks_list) + qconnect(self.decks_list.itemSelectionChanged, self._on_item_selection_changed) + return box + + def _setup_box_bottom_right(self) -> QVBoxLayout: + box = QVBoxLayout() self.unsubscribe_btn = QPushButton("Unsubscribe") - self.box_right.addWidget(self.unsubscribe_btn) + box.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) self.open_web_btn = QPushButton("Open on AnkiHub") - self.box_right.addWidget(self.open_web_btn) + box.addWidget(self.open_web_btn) qconnect(self.open_web_btn.clicked, self._on_open_web) self.set_home_deck_btn = QPushButton("Set Home deck") self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") set_tooltip_icon(self.set_home_deck_btn) qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) - self.box_right.addWidget(self.set_home_deck_btn) + box.addWidget(self.set_home_deck_btn) self.toggle_subdecks_btn = QPushButton("Enable Subdecks") self.toggle_subdecks_btn.setToolTip( @@ -79,14 +112,10 @@ def _setup_ui(self): ) set_tooltip_icon(self.toggle_subdecks_btn) qconnect(self.toggle_subdecks_btn.clicked, self._on_toggle_subdecks) - self.box_right.addWidget(self.toggle_subdecks_btn) - - self.box_right.addStretch(1) + box.addWidget(self.toggle_subdecks_btn) + box.addStretch(1) - self.setLayout(self.box_top) - self.box_top.addLayout(self.box_above) - self.box_above.addWidget(self.decks_list) - self.box_above.addLayout(self.box_right) + return box def _refresh_decks_list(self) -> None: self.decks_list.clear() From 25dbade2a41d577884fe1cb1a79cdc254e65a994 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:27:58 +0200 Subject: [PATCH 031/105] Reorganize bottom right box --- ankihub/gui/decks_dialog.py | 44 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ef61c08e9..9da48a6ec 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -91,19 +91,36 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: def _setup_box_bottom_right(self) -> QVBoxLayout: box = QVBoxLayout() - self.unsubscribe_btn = QPushButton("Unsubscribe") - box.addWidget(self.unsubscribe_btn) - qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) + + # Deck Actions + self.box_deck_actions = QVBoxLayout() + box.addLayout(self.box_deck_actions) + + self.deck_actions_label = QLabel("Deck Actions") + self.box_deck_actions.addWidget(self.deck_actions_label) + + self.box_deck_action_buttons = QHBoxLayout() + self.box_deck_actions.addLayout(self.box_deck_action_buttons) self.open_web_btn = QPushButton("Open on AnkiHub") - box.addWidget(self.open_web_btn) + self.box_deck_action_buttons.addWidget(self.open_web_btn) qconnect(self.open_web_btn.clicked, self._on_open_web) - self.set_home_deck_btn = QPushButton("Set Home deck") - self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") - set_tooltip_icon(self.set_home_deck_btn) - qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) - box.addWidget(self.set_home_deck_btn) + self.unsubscribe_btn = QPushButton("Unsubscribe") + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) + qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) + + box.addSpacing(10) + + # Deck Settings + self.box_deck_settings = QVBoxLayout() + box.addLayout(self.box_deck_settings) + + self.deck_settings_label = QLabel("Deck Settings") + self.box_deck_settings.addWidget(self.deck_settings_label) + + self.box_deck_settings_elements = QVBoxLayout() + self.box_deck_settings.addLayout(self.box_deck_settings_elements) self.toggle_subdecks_btn = QPushButton("Enable Subdecks") self.toggle_subdecks_btn.setToolTip( @@ -112,7 +129,14 @@ def _setup_box_bottom_right(self) -> QVBoxLayout: ) set_tooltip_icon(self.toggle_subdecks_btn) qconnect(self.toggle_subdecks_btn.clicked, self._on_toggle_subdecks) - box.addWidget(self.toggle_subdecks_btn) + self.box_deck_settings_elements.addWidget(self.toggle_subdecks_btn) + + self.set_home_deck_btn = QPushButton("Set Home deck") + self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") + set_tooltip_icon(self.set_home_deck_btn) + qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) + self.box_deck_settings_elements.addWidget(self.set_home_deck_btn) + box.addStretch(1) return box From 3140806603486762186f5b9a9a5991d5c682ac94 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 21:40:28 +0200 Subject: [PATCH 032/105] Add button for creating new deck --- ankihub/gui/decks_dialog.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 9da48a6ec..1eb7ae546 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -21,6 +21,7 @@ from aqt.utils import openLink, showInfo, showText, tooltip from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient +from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall from ..main.subdecks import SUBDECK_TAG from ..settings import config, url_deck_base, url_decks @@ -75,6 +76,10 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) + self.create_btn = QPushButton("Create Collaborative Deck") + self.box_top_buttons.addWidget(self.create_btn) + qconnect(self.create_btn.clicked, create_collaborative_deck) + box.addSpacing(10) return box From 258083cb1467467b35982f5c18d6c17d4d9aa821 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:16:05 +0200 Subject: [PATCH 033/105] Refresh bottom right layout when deck is selected --- ankihub/gui/decks_dialog.py | 32 +++++++++++++++++++++++--------- ankihub/gui/utils.py | 13 +++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 1eb7ae546..800869ec2 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -26,7 +26,7 @@ from ..main.subdecks import SUBDECK_TAG from ..settings import config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks -from .utils import ask_user, set_tooltip_icon +from .utils import ask_user, clear_layout, set_tooltip_icon class SubscribedDecksDialog(QDialog): @@ -37,7 +37,6 @@ def __init__(self): super(SubscribedDecksDialog, self).__init__() self.client = AnkiHubClient() self._setup_ui() - self._on_item_selection_changed() self._refresh_decks_list() if not config.is_logged_in(): @@ -61,7 +60,8 @@ def _setup_ui(self): self.box_bottom_left = self._setup_box_bottom_left() self.box_bottom.addLayout(self.box_bottom_left) - self.box_bottom_right = self._setup_box_bottom_right() + self.box_bottom_right = QVBoxLayout() + self._refresh_box_bottom_right(self.box_bottom_right, None) self.box_bottom.addLayout(self.box_bottom_right) def _setup_box_top(self) -> QVBoxLayout: @@ -91,11 +91,22 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: self.decks_list = QListWidget() box.addWidget(self.decks_list) - qconnect(self.decks_list.itemSelectionChanged, self._on_item_selection_changed) + qconnect(self.decks_list.itemSelectionChanged, self._on_deck_selection_changed) return box - def _setup_box_bottom_right(self) -> QVBoxLayout: - box = QVBoxLayout() + def _refresh_box_bottom_right( + self, box: QVBoxLayout, selected_ah_did: Optional[uuid.UUID] + ) -> None: + clear_layout(box) + + if selected_ah_did is None: + box.addSpacing(30) + + self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") + box.addWidget(self.no_deck_selected_label) + + box.addStretch(1) + return # Deck Actions self.box_deck_actions = QVBoxLayout() @@ -144,8 +155,6 @@ def _setup_box_bottom_right(self) -> QVBoxLayout: box.addStretch(1) - return box - def _refresh_decks_list(self) -> None: self.decks_list.clear() for deck in self.client.get_deck_subscriptions(): @@ -283,7 +292,7 @@ def _refresh_subdecks_button(self): "Disable Subdecks" if using_subdecks else "Enable Subdecks" ) - def _on_item_selection_changed(self) -> None: + def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() one_selected: bool = len(selection) == 1 is_deck_installed = False @@ -292,6 +301,11 @@ def _on_item_selection_changed(self) -> None: ankihub_did: UUID = selected.data(Qt.ItemDataRole.UserRole) is_deck_installed = bool(config.deck_config(ankihub_did)) + if one_selected: + self._refresh_box_bottom_right(self.box_bottom_right, ankihub_did) + else: + self._refresh_box_bottom_right(self.box_bottom_right, None) + self.unsubscribe_btn.setEnabled(one_selected) self.open_web_btn.setEnabled(one_selected) self.set_home_deck_btn.setEnabled(one_selected and is_deck_installed) diff --git a/ankihub/gui/utils.py b/ankihub/gui/utils.py index 41ae47f05..965c3db8e 100644 --- a/ankihub/gui/utils.py +++ b/ankihub/gui/utils.py @@ -9,6 +9,7 @@ QDialogButtonBox, QIcon, QLabel, + QLayout, QListWidget, QListWidgetItem, QMessageBox, @@ -271,3 +272,15 @@ def check_and_prompt_for_updates_on_main_window(): on_done=aqt.mw.on_updates_installed, requested_by_user=True, ) + + +def clear_layout(layout: QLayout) -> None: + """Remove all widgets from a layout and delete them.""" + while layout.count(): + child = layout.takeAt(0) + if child.widget(): + widget = child.widget() + widget.setParent(None) + widget.deleteLater() + elif child.layout(): + clear_layout(child.layout()) From c3058f51a1750f469f7434b46eb3ade0d33cefa2 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:19:04 +0200 Subject: [PATCH 034/105] Set minimum width to 640 --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 800869ec2..f0bcc4203 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -47,6 +47,7 @@ def __init__(self): def _setup_ui(self): self.setWindowTitle("AnkiHub | Decks Managment") + self.setMinimumWidth(640) self.box_main = QVBoxLayout() self.setLayout(self.box_main) From ac3467c49b83358c3547707c29a13b8cf8b410ed Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:30:51 +0200 Subject: [PATCH 035/105] Share space equally between bottom layouts --- ankihub/gui/decks_dialog.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index f0bcc4203..5ff5d3930 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -88,6 +88,9 @@ def _setup_box_top(self) -> QVBoxLayout: def _setup_box_bottom_left(self) -> QVBoxLayout: box = QVBoxLayout() self.decks_list_label = QLabel("Subscribed AnkiHub Decks") + self.decks_list_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) box.addWidget(self.decks_list_label) self.decks_list = QListWidget() @@ -104,6 +107,9 @@ def _refresh_box_bottom_right( box.addSpacing(30) self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") + self.no_deck_selected_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) box.addWidget(self.no_deck_selected_label) box.addStretch(1) @@ -114,6 +120,9 @@ def _refresh_box_bottom_right( box.addLayout(self.box_deck_actions) self.deck_actions_label = QLabel("Deck Actions") + self.deck_actions_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) self.box_deck_actions.addWidget(self.deck_actions_label) self.box_deck_action_buttons = QHBoxLayout() From 33ed791b396e8555235e568a635a37eb317f0bde Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:32:56 +0200 Subject: [PATCH 036/105] Set mimimum height --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 5ff5d3930..7adb0073d 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -48,6 +48,7 @@ def __init__(self): def _setup_ui(self): self.setWindowTitle("AnkiHub | Decks Managment") self.setMinimumWidth(640) + self.setMinimumHeight(450) self.box_main = QVBoxLayout() self.setLayout(self.box_main) From 3eae64c3861fba447f099ab2fd4a6751f45e30d5 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sun, 15 Oct 2023 22:37:39 +0200 Subject: [PATCH 037/105] Add spacing --- ankihub/gui/decks_dialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 7adb0073d..ce4d1f1bd 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -107,11 +107,16 @@ def _refresh_box_bottom_right( if selected_ah_did is None: box.addSpacing(30) + self.box_no_deck_selected = QHBoxLayout() + box.addLayout(self.box_no_deck_selected) + + self.box_no_deck_selected.addSpacing(5) + self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") self.no_deck_selected_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) - box.addWidget(self.no_deck_selected_label) + self.box_no_deck_selected.addWidget(self.no_deck_selected_label) box.addStretch(1) return From ca03f61ee1004e3c6b26cabca2e5812f6b4e7c20 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 13:28:14 +0200 Subject: [PATCH 038/105] Replace subdecks button with checkbox --- ankihub/gui/decks_dialog.py | 30 +++++++++++++++--------------- ankihub/gui/utils.py | 7 ++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ce4d1f1bd..25fd03a19 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -7,6 +7,7 @@ from anki.collection import OpChanges from aqt import gui_hooks from aqt.qt import ( + QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, @@ -154,14 +155,14 @@ def _refresh_box_bottom_right( self.box_deck_settings_elements = QVBoxLayout() self.box_deck_settings.addLayout(self.box_deck_settings_elements) - self.toggle_subdecks_btn = QPushButton("Enable Subdecks") - self.toggle_subdecks_btn.setToolTip( + self.toggle_subdecks_cb = QCheckBox("Enable Subdecks") + self.toggle_subdecks_cb.setToolTip( "Toggle between the deck being organized into subdecks or not.
" f"This will only have an effect if notes in the deck have {SUBDECK_TAG} tags." ) - set_tooltip_icon(self.toggle_subdecks_btn) - qconnect(self.toggle_subdecks_btn.clicked, self._on_toggle_subdecks) - self.box_deck_settings_elements.addWidget(self.toggle_subdecks_btn) + set_tooltip_icon(self.toggle_subdecks_cb) + qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) + self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) self.set_home_deck_btn = QPushButton("Set Home deck") self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") @@ -287,26 +288,25 @@ def _on_toggle_subdecks(self): confirm_and_toggle_subdecks(ankihub_id) - self._refresh_subdecks_button() + self._refresh_subdecks_checkbox() - def _refresh_subdecks_button(self): + def _refresh_subdecks_checkbox(self): selection = self.decks_list.selectedItems() one_selected: bool = len(selection) == 1 if not one_selected: - self.toggle_subdecks_btn.setEnabled(False) + self.toggle_subdecks_cb.setEnabled(False) return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - using_subdecks = False if deck_from_config := config.deck_config(ankihub_did): using_subdecks = deck_from_config.subdecks_enabled - self.toggle_subdecks_btn.setEnabled(True) + self.toggle_subdecks_cb.setEnabled(True) else: - self.toggle_subdecks_btn.setEnabled(False) - self.toggle_subdecks_btn.setText( - "Disable Subdecks" if using_subdecks else "Enable Subdecks" - ) + using_subdecks = False + self.toggle_subdecks_cb.setEnabled(False) + + self.toggle_subdecks_cb.setChecked(using_subdecks) def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() @@ -326,7 +326,7 @@ def _on_deck_selection_changed(self) -> None: self.open_web_btn.setEnabled(one_selected) self.set_home_deck_btn.setEnabled(one_selected and is_deck_installed) - self._refresh_subdecks_button() + self._refresh_subdecks_checkbox() @classmethod def display_subscribe_window(cls): diff --git a/ankihub/gui/utils.py b/ankihub/gui/utils.py index 965c3db8e..187fd3e5e 100644 --- a/ankihub/gui/utils.py +++ b/ankihub/gui/utils.py @@ -1,10 +1,11 @@ import uuid -from typing import Any, List, Optional +from typing import Any, List, Optional, Union import aqt from aqt.addons import check_and_prompt_for_updates from aqt.qt import ( QApplication, + QCheckBox, QDialog, QDialogButtonBox, QIcon, @@ -255,8 +256,8 @@ def ask_user( return None -def set_tooltip_icon(btn: QPushButton) -> None: - btn.setIcon( +def set_tooltip_icon(widget: Union[QPushButton, QCheckBox]) -> None: + widget.setIcon( QIcon( QApplication.style().standardIcon( QStyle.StandardPixmap.SP_MessageBoxInformation From efdb74b1724741386855857b53a2c3462aa53da7 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 13:33:42 +0200 Subject: [PATCH 039/105] Disable subdeck checkbox if no subdeck tags --- ankihub/gui/decks_dialog.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 25fd03a19..78bd40783 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -11,9 +11,11 @@ QDialog, QDialogButtonBox, QHBoxLayout, + QLabel, QListWidget, QListWidgetItem, QPushButton, + QSizePolicy, Qt, QVBoxLayout, qconnect, @@ -24,7 +26,7 @@ from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall -from ..main.subdecks import SUBDECK_TAG +from ..main.subdecks import SUBDECK_TAG, deck_contains_subdeck_tags from ..settings import config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks from .utils import ask_user, clear_layout, set_tooltip_icon @@ -158,7 +160,7 @@ def _refresh_box_bottom_right( self.toggle_subdecks_cb = QCheckBox("Enable Subdecks") self.toggle_subdecks_cb.setToolTip( "Toggle between the deck being organized into subdecks or not.
" - f"This will only have an effect if notes in the deck have {SUBDECK_TAG} tags." + f"This option is only available if notes in the deck have {SUBDECK_TAG} tags." ) set_tooltip_icon(self.toggle_subdecks_cb) qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) @@ -299,9 +301,10 @@ def _refresh_subdecks_checkbox(self): return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - if deck_from_config := config.deck_config(ankihub_did): - using_subdecks = deck_from_config.subdecks_enabled - self.toggle_subdecks_cb.setEnabled(True) + if deck_config := config.deck_config(ankihub_did): + using_subdecks = deck_config.subdecks_enabled + has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) + self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) else: using_subdecks = False self.toggle_subdecks_cb.setEnabled(False) From b224bb963ea9b5caa7a447bb2e97e10adffafdad Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 13:59:07 +0200 Subject: [PATCH 040/105] Replace home_deck_btn with updates destination section --- ankihub/gui/decks_dialog.py | 56 ++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 78bd40783..b57057253 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -166,14 +166,42 @@ def _refresh_box_bottom_right( qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) - self.set_home_deck_btn = QPushButton("Set Home deck") - self.set_home_deck_btn.setToolTip("New cards will be added to this deck.") - set_tooltip_icon(self.set_home_deck_btn) - qconnect(self.set_home_deck_btn.clicked, self._on_set_home_deck) - self.box_deck_settings_elements.addWidget(self.set_home_deck_btn) + box.addSpacing(10) + + # Updates Destination + self.box_updates_destination = QVBoxLayout() + box.addLayout(self.box_updates_destination) + + self.updates_destination_label = QLabel("Updates Destination") + self.box_updates_destination.addWidget(self.updates_destination_label) + + self.updates_destination_details_label = QLabel() + self.updates_destination_details_label.setWordWrap(True) + self._refresh_updates_destination_details_label(selected_ah_did) + self.box_updates_destination.addWidget(self.updates_destination_details_label) + + self.set_updates_destination_btn = QPushButton("Change updates destination") + qconnect( + self.set_updates_destination_btn.clicked, + self._on_change_updates_destination, + ) + self.box_updates_destination.addWidget(self.set_updates_destination_btn) box.addStretch(1) + def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: + deck_config = config.deck_config(ah_did) + destination_anki_did = deck_config.anki_id + if name := aqt.mw.col.decks.name_if_exists(destination_anki_did): + self.updates_destination_details_label.setText( + f"New cards will be added to: {name}." + ) + else: + # If the deck doesn't exitst, it will be re-created on next sync with the name from the config. + self.updates_destination_details_label.setText( + f"New cards will be added to {deck_config.name}." + ) + def _refresh_decks_list(self) -> None: self.decks_list.clear() for deck in self.client.get_deck_subscriptions(): @@ -238,18 +266,20 @@ def _on_open_web(self) -> None: ankihub_id: UUID = item.data(Qt.ItemDataRole.UserRole) openLink(f"{url_deck_base()}/{ankihub_id}") - def _on_set_home_deck(self): + def _on_change_updates_destination(self): deck_names = self.decks_list.selectedItems() if len(deck_names) == 0: return deck_name = deck_names[0] ankihub_id: UUID = deck_name.data(Qt.ItemDataRole.UserRole) - current_home_deck = aqt.mw.col.decks.get(config.deck_config(ankihub_id).anki_id) - if current_home_deck is None: + current_destination_deck = aqt.mw.col.decks.get( + config.deck_config(ankihub_id).anki_id + ) + if current_destination_deck is None: current = None else: - current = current_home_deck["name"] + current = current_destination_deck["name"] def update_deck_config(ret: StudyDeck): if not ret.name: @@ -257,14 +287,14 @@ def update_deck_config(ret: StudyDeck): anki_did = aqt.mw.col.decks.id(ret.name) config.set_home_deck(ankihub_did=ankihub_id, anki_did=anki_did) - tooltip("Home deck updated.", parent=self) + self._refresh_updates_destination_details_label(ankihub_id) # this lets the user pick a deck StudyDeckWithoutHelpButton( aqt.mw, current=current, - accept="Set Home Deck", - title="Change Home Deck", + accept="Accept", + title="Choose Updates Destination", parent=self, callback=update_deck_config, ) @@ -327,7 +357,7 @@ def _on_deck_selection_changed(self) -> None: self.unsubscribe_btn.setEnabled(one_selected) self.open_web_btn.setEnabled(one_selected) - self.set_home_deck_btn.setEnabled(one_selected and is_deck_installed) + self.set_updates_destination_btn.setEnabled(one_selected and is_deck_installed) self._refresh_subdecks_checkbox() From 199310c79926928154fc2f9de6ef4117993b3da5 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:01:01 +0200 Subject: [PATCH 041/105] Adjust spacing of box_bottom_right --- ankihub/gui/decks_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index b57057253..655177604 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -107,8 +107,9 @@ def _refresh_box_bottom_right( ) -> None: clear_layout(box) + box.addSpacing(25) + if selected_ah_did is None: - box.addSpacing(30) self.box_no_deck_selected = QHBoxLayout() box.addLayout(self.box_no_deck_selected) From 2eef221640124efcd7a3b56e003d9180b5401e8d Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:34:04 +0200 Subject: [PATCH 042/105] Style buttons --- ankihub/gui/decks_dialog.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 655177604..34f2b5e36 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -21,6 +21,7 @@ qconnect, ) from aqt.studydeck import StudyDeck +from aqt.theme import theme_manager from aqt.utils import openLink, showInfo, showText, tooltip from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient @@ -77,11 +78,13 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons = QHBoxLayout() box.addLayout(self.box_top_buttons) - self.browse_btn = QPushButton("Browse Decks") + self.browse_btn = QPushButton("🔗 Browse Decks") + self.browse_btn.setStyleSheet("color: white; background-color: #306bec") + self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) - self.create_btn = QPushButton("Create Collaborative Deck") + self.create_btn = QPushButton("➕ Create Collaborative Deck") self.box_top_buttons.addWidget(self.create_btn) qconnect(self.create_btn.clicked, create_collaborative_deck) @@ -143,6 +146,11 @@ def _refresh_box_bottom_right( qconnect(self.open_web_btn.clicked, self._on_open_web) self.unsubscribe_btn = QPushButton("Unsubscribe") + if theme_manager.night_mode: + self.unsubscribe_btn.setStyleSheet("color: #e29792") + else: + self.unsubscribe_btn.setStyleSheet("color: #e2857f") + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) From 03d3043bab285223fe09a7d28e10c0ae0a0de018 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:43:10 +0200 Subject: [PATCH 043/105] Add links to docs --- ankihub/gui/decks_dialog.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 34f2b5e36..38e77707a 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -175,6 +175,16 @@ def _refresh_box_bottom_right( qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) + self.subdecks_docs_link_label = QLabel( + """ + + More about Subdecks + + """ + ) + self.subdecks_docs_link_label.setOpenExternalLinks(True) + self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) + box.addSpacing(10) # Updates Destination @@ -195,6 +205,17 @@ def _refresh_box_bottom_right( self._on_change_updates_destination, ) self.box_updates_destination.addWidget(self.set_updates_destination_btn) + self.box_updates_destination.addSpacing(7) + + self.destination_updates_docs_link_label = QLabel( + """ + + More about Updates Destinations + + """ + ) + self.destination_updates_docs_link_label.setOpenExternalLinks(True) + self.box_updates_destination.addWidget(self.destination_updates_docs_link_label) box.addStretch(1) From 1d4edb6baefd166a49d755d6b10e79f41e255966 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:44:57 +0200 Subject: [PATCH 044/105] Adjust spacing --- ankihub/gui/decks_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 38e77707a..3b70f9f81 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -154,7 +154,7 @@ def _refresh_box_bottom_right( self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) - box.addSpacing(10) + box.addSpacing(20) # Deck Settings self.box_deck_settings = QVBoxLayout() @@ -185,7 +185,7 @@ def _refresh_box_bottom_right( self.subdecks_docs_link_label.setOpenExternalLinks(True) self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) - box.addSpacing(10) + box.addSpacing(20) # Updates Destination self.box_updates_destination = QVBoxLayout() @@ -217,7 +217,7 @@ def _refresh_box_bottom_right( self.destination_updates_docs_link_label.setOpenExternalLinks(True) self.box_updates_destination.addWidget(self.destination_updates_docs_link_label) - box.addStretch(1) + box.addStretch() def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) From 54b9a4f3404f98257c7a42bdfc869e316d7bfa64 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 14:55:01 +0200 Subject: [PATCH 045/105] Fix test --- tests/addon/test_integration.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index d0d7e4b09..94ec539ac 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2493,22 +2493,6 @@ def test_toggle_subdecks( operations.subdecks, "ask_user", lambda *args, **kwargs: True ) - # Mock get_deck_subscriptions to return an empty list - monkeypatch.setattr( - AnkiHubClient, - "get_deck_subscriptions", - lambda *args, **kwargs: [], - ) - - # Open the dialog - dialog = SubscribedDecksDialog() - qtbot.add_widget(dialog) - dialog.display_subscribe_window() - qtbot.wait(200) - - # The toggle subdecks button should be disabled because there are no decks - assert dialog.toggle_subdecks_btn.isEnabled() is False - # Install a deck with subdeck tags subdeck_name, anki_did, ah_did = self._install_deck_with_subdeck_tag( install_sample_ah_deck @@ -2534,15 +2518,15 @@ def test_toggle_subdecks( dialog.decks_list.setCurrentRow(0) qtbot.wait(200) - assert dialog.toggle_subdecks_btn.isEnabled() is True - dialog.toggle_subdecks_btn.click() + assert dialog.toggle_subdecks_cb.isEnabled() is True + dialog.toggle_subdecks_cb.click() qtbot.wait(200) # The subdeck should now exist assert mw.col.decks.by_name(subdeck_name) is not None # Click the toggle subdeck button again - dialog.toggle_subdecks_btn.click() + dialog.toggle_subdecks_cb.click() qtbot.wait(200) # The subdeck should not exist anymore From 039a58658d09d1128c250f8fe08fb975492b6c44 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 16:03:23 +0200 Subject: [PATCH 046/105] Add test --- tests/addon/test_integration.py | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 94ec539ac..feecdf579 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -40,6 +40,7 @@ from aqt.browser.sidebar.tree import SidebarTreeView from aqt.importing import AnkiPackageImporter from aqt.qt import QAction, Qt +from aqt.theme import theme_manager from pytest import MonkeyPatch, fixture from pytest_anki import AnkiSession from pytestqt.qtbot import QtBot # type: ignore @@ -2474,6 +2475,43 @@ def assert_note_has_expected_tag(): class TestSubscribedDecksDialog: + @pytest.mark.parametrize( + "nightmode", + [True, False], + ) + def test_basic( + self, + anki_session_with_addon_data: AnkiSession, + install_sample_ah_deck: InstallSampleAHDeck, + qtbot: QtBot, + monkeypatch: MonkeyPatch, + nightmode: bool, + ): + with anki_session_with_addon_data.profile_loaded(): + + # Mock the config to return that the user is logged in + monkeypatch.setattr(config, "is_logged_in", lambda: True) + + # Mock the ask_user function to always return True + monkeypatch.setattr( + operations.subdecks, "ask_user", lambda *args, **kwargs: True + ) + + ah_did, anki_did = install_sample_ah_deck() + + # Mock get_deck_subscriptions to return the deck + monkeypatch.setattr( + AnkiHubClient, + "get_deck_subscriptions", + lambda *args: [DeckFactory.create(ah_did=ah_did, anki_did=anki_did)], + ) + + theme_manager.night_mode = nightmode + + dialog = SubscribedDecksDialog() + qtbot.add_widget(dialog) + dialog.display_subscribe_window() + def test_toggle_subdecks( self, anki_session_with_addon_data: AnkiSession, From 2672094f6bccd6a3e95dd61ace95c31891d6f1b9 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 16:06:20 +0200 Subject: [PATCH 047/105] Only display installed decks in list --- ankihub/gui/decks_dialog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3b70f9f81..d2cf3e13e 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -234,7 +234,13 @@ def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: def _refresh_decks_list(self) -> None: self.decks_list.clear() - for deck in self.client.get_deck_subscriptions(): + + subscribed_decks = self.client.get_deck_subscriptions() + installed_ah_dids = config.deck_ids() + subscribed_and_installed_decks = [ + deck for deck in subscribed_decks if deck.ah_did in installed_ah_dids + ] + for deck in subscribed_and_installed_decks: name = deck.name if deck.is_user_relation_owner: item = QListWidgetItem(f"{name} (Created by you)") From bccce3de0c0ef33f755542688e751061becf2296 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 16:08:24 +0200 Subject: [PATCH 048/105] Remove unused code --- ankihub/gui/decks_dialog.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index d2cf3e13e..05d79d59a 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -367,15 +367,11 @@ def _refresh_subdecks_checkbox(self): return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - if deck_config := config.deck_config(ankihub_did): - using_subdecks = deck_config.subdecks_enabled - has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) - self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) - else: - using_subdecks = False - self.toggle_subdecks_cb.setEnabled(False) + deck_config = config.deck_config(ankihub_did) - self.toggle_subdecks_cb.setChecked(using_subdecks) + has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) + self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) + self.toggle_subdecks_cb.setChecked(deck_config.subdecks_enabled) def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() From 09bd595085e1951709d802c45ad0d71ddddfdd03 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 17:51:03 +0200 Subject: [PATCH 049/105] ref: Move call to `_refresh_subdecks_checkbox` --- ankihub/gui/decks_dialog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 05d79d59a..a40722355 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -172,6 +172,7 @@ def _refresh_box_bottom_right( f"This option is only available if notes in the deck have {SUBDECK_TAG} tags." ) set_tooltip_icon(self.toggle_subdecks_cb) + self._refresh_subdecks_checkbox() qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) @@ -391,8 +392,6 @@ def _on_deck_selection_changed(self) -> None: self.open_web_btn.setEnabled(one_selected) self.set_updates_destination_btn.setEnabled(one_selected and is_deck_installed) - self._refresh_subdecks_checkbox() - @classmethod def display_subscribe_window(cls): if cls._window is None: From a6fdb3b1de16efa3fecdbf7337f0a57539f488b6 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 18:10:12 +0200 Subject: [PATCH 050/105] Refactor tests --- tests/addon/test_integration.py | 46 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index feecdf579..49d3b8d66 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2489,17 +2489,10 @@ def test_basic( ): with anki_session_with_addon_data.profile_loaded(): - # Mock the config to return that the user is logged in - monkeypatch.setattr(config, "is_logged_in", lambda: True) + self._mock_dependencies(monkeypatch) - # Mock the ask_user function to always return True - monkeypatch.setattr( - operations.subdecks, "ask_user", lambda *args, **kwargs: True - ) - - ah_did, anki_did = install_sample_ah_deck() + anki_did, ah_did = install_sample_ah_deck() - # Mock get_deck_subscriptions to return the deck monkeypatch.setattr( AnkiHubClient, "get_deck_subscriptions", @@ -2509,9 +2502,14 @@ def test_basic( theme_manager.night_mode = nightmode dialog = SubscribedDecksDialog() - qtbot.add_widget(dialog) dialog.display_subscribe_window() + assert dialog.decks_list.count() == 1 + + # Select a deck from the list + dialog.decks_list.setCurrentRow(0) + qtbot.wait(200) + def test_toggle_subdecks( self, anki_session_with_addon_data: AnkiSession, @@ -2519,17 +2517,9 @@ def test_toggle_subdecks( install_sample_ah_deck: InstallSampleAHDeck, monkeypatch: MonkeyPatch, ): - anki_session = anki_session_with_addon_data - with anki_session.profile_loaded(): - mw = anki_session.mw - - # Mock the config to return that the user is logged in - monkeypatch.setattr(config, "is_logged_in", lambda: True) + with anki_session_with_addon_data.profile_loaded(): - # Mock the ask_user function to always return True - monkeypatch.setattr( - operations.subdecks, "ask_user", lambda *args, **kwargs: True - ) + self._mock_dependencies(monkeypatch) # Install a deck with subdeck tags subdeck_name, anki_did, ah_did = self._install_deck_with_subdeck_tag( @@ -2545,9 +2535,8 @@ def test_toggle_subdecks( lambda *args: [DeckFactory.create(ah_did=ah_did, anki_did=anki_did)], ) - # Refresh the dialog + # Open the dialog dialog = SubscribedDecksDialog() - qtbot.add_widget(dialog) dialog.display_subscribe_window() qtbot.wait(200) @@ -2561,14 +2550,14 @@ def test_toggle_subdecks( qtbot.wait(200) # The subdeck should now exist - assert mw.col.decks.by_name(subdeck_name) is not None + assert aqt.mw.col.decks.by_name(subdeck_name) is not None # Click the toggle subdeck button again dialog.toggle_subdecks_cb.click() qtbot.wait(200) # The subdeck should not exist anymore - assert mw.col.decks.by_name(subdeck_name) is None + assert aqt.mw.col.decks.by_name(subdeck_name) is None def _install_deck_with_subdeck_tag( self, @@ -2583,6 +2572,15 @@ def _install_deck_with_subdeck_tag( note.flush() return subdeck_name, anki_did, ah_did + def _mock_dependencies(self, monkeypatch: MonkeyPatch) -> None: + # Mock the config to return that the user is logged in + monkeypatch.setattr(config, "is_logged_in", lambda: True) + + # Mock the ask_user function to always return True + monkeypatch.setattr( + operations.subdecks, "ask_user", lambda *args, **kwargs: True + ) + class TestBuildSubdecksAndMoveCardsToThem: def test_basic( From 53ad08b6d65c868f0c74172318ddbb182c988409 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 18:46:15 +0200 Subject: [PATCH 051/105] Add test for updates destination setting --- tests/addon/conftest.py | 1 + tests/addon/test_integration.py | 69 +++++++++++++++++++++++++++++++++ tests/fixtures.py | 59 ++++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/tests/addon/conftest.py b/tests/addon/conftest.py index adaa17f43..15b7bc777 100644 --- a/tests/addon/conftest.py +++ b/tests/addon/conftest.py @@ -14,6 +14,7 @@ ankihub_basic_note_type, import_ah_note, import_ah_note_type, + install_ah_deck, mock_all_feature_flags_to_default_values, mock_download_and_install_deck_dependencies, mock_function, diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 49d3b8d66..fd0bde857 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -63,6 +63,7 @@ from ..factories import DeckFactory, DeckMediaFactory, NoteInfoFactory from ..fixtures import ( ImportAHNote, + InstallAHDeck, MockDownloadAndInstallDeckDependencies, MockFunction, create_or_get_ah_version_of_note_type, @@ -2572,6 +2573,74 @@ def _install_deck_with_subdeck_tag( note.flush() return subdeck_name, anki_did, ah_did + def test_change_updates_destination( + self, + anki_session_with_addon_data: AnkiSession, + qtbot: QtBot, + install_ah_deck: InstallAHDeck, + monkeypatch: MonkeyPatch, + ): + with anki_session_with_addon_data.profile_loaded(): + ah_did = install_ah_deck() + + self._mock_dependencies(monkeypatch) + + # Mock get_deck_subscriptions to return the deck + monkeypatch.setattr( + AnkiHubClient, + "get_deck_subscriptions", + lambda *args: [ + DeckFactory.create( + ah_did=ah_did, anki_did=config.deck_config(ah_did).anki_id + ) + ], + ) + + # Mock the dialog that asks the user for the destination deck + new_destination_deck_name = "New Deck" + install_ah_deck(anki_deck_name=new_destination_deck_name) + new_home_deck_anki_id = aqt.mw.col.decks.id_for_name( + new_destination_deck_name + ) + self._mock_new_cards_destination_dialog( + new_destination_deck_name, monkeypatch + ) + + # Open the dialog + dialog = SubscribedDecksDialog() + dialog.display_subscribe_window() + qtbot.wait(200) + + # Select the deck and click the Set Updates Destination button + dialog.decks_list.setCurrentRow(0) + qtbot.wait(200) + + dialog.set_updates_destination_btn.click() + qtbot.wait(200) + + # Assert that the destination deck was updated + assert config.deck_config(ah_did).anki_id == new_home_deck_anki_id + + def _mock_new_cards_destination_dialog( + self, + destination_deck_name: str, + monkeypatch: MonkeyPatch, + ) -> None: + """Sets the destination deck for new cards to the deck with the given name.""" + study_deck_mock = Mock() + + def study_deck_mock_side_effect(*args, **kwargs): + callback = kwargs["callback"] + cb_study_deck_mock = Mock() + cb_study_deck_mock.name = destination_deck_name + callback(cb_study_deck_mock) + + study_deck_mock.side_effect = study_deck_mock_side_effect + monkeypatch.setattr( + "ankihub.gui.decks_dialog.StudyDeckWithoutHelpButton", + study_deck_mock, + ) + def _mock_dependencies(self, monkeypatch: MonkeyPatch) -> None: # Mock the config to return that the user is logged in monkeypatch.setattr(config, "is_logged_in", lambda: True) diff --git a/tests/fixtures.py b/tests/fixtures.py index d524ba8fe..297a136a4 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -21,12 +21,13 @@ from ankihub.ankihub_client import NoteInfo from ankihub.ankihub_client.ankihub_client import AnkiHubClient -from ankihub.ankihub_client.models import Deck +from ankihub.ankihub_client.models import Deck, UserDeckRelation from ankihub.feature_flags import setup_feature_flags from ankihub.gui import operations from ankihub.gui.media_sync import _AnkiHubMediaSync from ankihub.main.importing import AnkiHubImporter from ankihub.main.utils import modify_note_type +from ankihub.settings import config @fixture @@ -174,7 +175,8 @@ def __call__( note_data: Optional[NoteInfo] = None, ah_nid: Optional[uuid.UUID] = None, mid: Optional[NotetypeId] = None, - ah_did: uuid.UUID = None, + ah_did: Optional[uuid.UUID] = None, + anki_did: Optional[DeckId] = None, ) -> NoteInfo: ... @@ -203,7 +205,8 @@ def _import_ah_note( note_data: Optional[NoteInfo] = None, ah_nid: Optional[uuid.UUID] = None, mid: Optional[NotetypeId] = None, - ah_did: uuid.UUID = default_ah_did, + ah_did: Optional[uuid.UUID] = default_ah_did, + anki_did: Optional[DeckId] = None, ) -> NoteInfo: if mid is None: ah_basic_note_type = create_or_get_ah_version_of_note_type( @@ -238,7 +241,8 @@ def _import_ah_note( protected_fields={}, protected_tags=[], deck_name=deck_name, - is_first_import_of_deck=True, + is_first_import_of_deck=anki_did is None, + anki_did=anki_did, ) return note_data @@ -322,6 +326,53 @@ def new_note_with_note_type_inner( return new_note_with_note_type_inner +class InstallAHDeck(Protocol): + def __call__( + self, + ah_did: Optional[uuid.UUID] = None, + ah_deck_name: Optional[str] = None, + anki_did: Optional[DeckId] = None, + anki_deck_name: Optional[str] = None, + ) -> uuid.UUID: + ... + + +@pytest.fixture +def install_ah_deck( + next_deterministic_uuid: Callable[[], uuid.UUID], + import_ah_note: ImportAHNote, +) -> InstallAHDeck: + """Installs a deck with the given AnkiHub and Anki names and ids. + The deck is imported and added to the private config. + Returns the AnkiHub deck id.""" + default_ah_did = next_deterministic_uuid() + default_ah_deck_name = "Test Deck" + # 1 is the id of the default deck, we don't want to use that here + default_anki_did = DeckId(2) + default_anki_deck_name = "Test Deck" + + def install_ah_deck_inner( + ah_did: Optional[uuid.UUID] = default_ah_did, + ah_deck_name: Optional[str] = default_ah_deck_name, + anki_did: Optional[DeckId] = default_anki_did, + anki_deck_name: Optional[str] = default_anki_deck_name, + ) -> uuid.UUID: + # Add deck to the config + config.add_deck( + name=ah_deck_name, + ankihub_did=ah_did, + anki_did=anki_did, + user_relation=UserDeckRelation.SUBSCRIBER, + ) + + # Create deck by importing a note for it + import_ah_note(ah_did=ah_did, anki_did=anki_did) + aqt.mw.col.decks.rename(aqt.mw.col.decks.get(anki_did), anki_deck_name) + return ah_did + + return install_ah_deck_inner + + class MockDownloadAndInstallDeckDependencies(Protocol): def __call__( self, From 3cf473f179db2c2b38b6e91b54455766a299e60b Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Mon, 16 Oct 2023 22:55:05 +0200 Subject: [PATCH 052/105] Fix typo in comment --- ankihub/gui/decks_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index a40722355..3c5368fe2 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -228,7 +228,7 @@ def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: f"New cards will be added to: {name}." ) else: - # If the deck doesn't exitst, it will be re-created on next sync with the name from the config. + # If the deck doesn't exist, it will be re-created on next sync with the name from the config. self.updates_destination_details_label.setText( f"New cards will be added to {deck_config.name}." ) From 1acbd458ca1e358655b5078fc1fb1f652d65ef23 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 18:48:40 +0200 Subject: [PATCH 053/105] Add test --- tests/addon/test_unit.py | 76 +++++++++++++++++++++++++++++++++++++--- tests/fixtures.py | 15 ++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/tests/addon/test_unit.py b/tests/addon/test_unit.py index 4f50a0790..98ca5b010 100644 --- a/tests/addon/test_unit.py +++ b/tests/addon/test_unit.py @@ -21,15 +21,14 @@ from pytestqt.qtbot import QtBot # type: ignore from requests import Response # type: ignore -from ankihub.gui import errors -from ankihub.gui.operations.utils import future_with_exception, future_with_result - from ..factories import DeckMediaFactory, NoteInfoFactory from ..fixtures import ( # type: ignore ImportAHNoteType, MockFunction, NewNoteWithNoteType, SetFeatureFlagState, + add_basic_anki_note_to_deck, + create_anki_deck, ) from .test_integration import ImportAHNote @@ -41,7 +40,7 @@ from ankihub.db.db import _AnkiHubDB from ankihub.db.exceptions import IntegrityError from ankihub.feature_flags import _FeatureFlags, feature_flags -from ankihub.gui import suggestion_dialog +from ankihub.gui import errors, suggestion_dialog from ankihub.gui.error_dialog import ErrorDialog from ankihub.gui.errors import ( OUTDATED_CLIENT_ERROR_REASON, @@ -50,6 +49,12 @@ _try_handle_exception, ) from ankihub.gui.menu import AnkiHubLogin +from ankihub.gui.operations import deck_creation +from ankihub.gui.operations.deck_creation import ( + DeckCreationConfirmationDialog, + create_collaborative_deck, +) +from ankihub.gui.operations.utils import future_with_exception, future_with_result from ankihub.gui.suggestion_dialog import ( SourceType, SuggestionDialog, @@ -62,7 +67,10 @@ ) from ankihub.gui.threading_utils import rate_limited from ankihub.main import suggestions -from ankihub.main.deck_creation import _note_type_name_without_ankihub_modifications +from ankihub.main.deck_creation import ( + DeckCreationResult, + _note_type_name_without_ankihub_modifications, +) from ankihub.main.exporting import _prepared_field_html from ankihub.main.importing import _updated_tags from ankihub.main.note_conversion import ( @@ -1493,3 +1501,61 @@ def test_combined( nids = [nid_1, nid_2, nid_3] assert retain_nids_with_ah_note_type(nids) == [nid_1, nid_2] + + +class TestCreateCollaborativeDeck: + def test_basic( + self, + anki_session_with_addon_data: AnkiSession, + mock_function: MockFunction, + next_deterministic_uuid: Callable[[], uuid.UUID], + qtbot: QtBot, + ) -> None: + with anki_session_with_addon_data.profile_loaded(): + # Setup Anki deck with a note. + deck_name = "test" + anki_did = create_anki_deck(deck_name=deck_name) + add_basic_anki_note_to_deck(anki_did) + + # Mock all the UI interactions. + mock_function(DeckCreationConfirmationDialog, "run", return_value=True) + + study_deck_mock = Mock + study_deck_mock.name = deck_name + mock_function(deck_creation, "StudyDeck", return_value=study_deck_mock) + + mock_function(deck_creation, "ask_user", return_value=True) + + ah_did = next_deterministic_uuid() + notes_data = [NoteInfoFactory.create()] + create_ankihub_deck_mock = mock_function( + deck_creation, + "create_ankihub_deck", + return_value=DeckCreationResult( + ankihub_did=ah_did, + notes_data=notes_data, + ), + ) + + get_media_names_from_notes_data_mock = mock_function( + deck_creation, + "get_media_names_from_notes_data", + return_value=[], + ) + start_media_upload_mock = mock_function( + deck_creation.media_sync, "start_media_upload" + ) + showInfo_mock = mock_function(deck_creation, "showInfo") + + # Create the collaborative deck. + create_collaborative_deck() + + qtbot.wait_until(lambda: showInfo_mock.called) + + # Assert that the correct functions were called. + create_ankihub_deck_mock.assert_called_once_with( + deck_name, private=False, add_subdeck_tags=False + ) + + get_media_names_from_notes_data_mock.assert_called_once_with(notes_data) + start_media_upload_mock.assert_called_once() diff --git a/tests/fixtures.py b/tests/fixtures.py index d524ba8fe..9a52f7062 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -374,3 +374,18 @@ def add_mock(object, func_name: str, return_value: Any = None): return mocks return mock_install_deck_dependencies + + +def create_anki_deck(deck_name: str) -> DeckId: + """Creates an Anki deck with the given name and returns the id.""" + deck = aqt.mw.col.decks.new_deck() + deck.name = deck_name + changes = aqt.mw.col.decks.add_deck(deck) + return DeckId(changes.id) + + +def add_basic_anki_note_to_deck(anki_did: DeckId) -> None: + """Adds a basic Anki note to the given deck.""" + note = aqt.mw.col.new_note(aqt.mw.col.models.by_name("Basic")) + note["Front"] = "some text" + aqt.mw.col.add_note(note, anki_did) From 5931f9d57c16d1f358a7dd20a506be0fd2839864 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 19:15:41 +0200 Subject: [PATCH 054/105] Increase coverage --- tests/addon/test_unit.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/addon/test_unit.py b/tests/addon/test_unit.py index 98ca5b010..c9413c12b 100644 --- a/tests/addon/test_unit.py +++ b/tests/addon/test_unit.py @@ -1503,13 +1503,19 @@ def test_combined( assert retain_nids_with_ah_note_type(nids) == [nid_1, nid_2] +@pytest.mark.parametrize( + "creating_deck_fails", + [True, False], +) class TestCreateCollaborativeDeck: + @pytest.mark.qt_no_exception_capture def test_basic( self, anki_session_with_addon_data: AnkiSession, mock_function: MockFunction, next_deterministic_uuid: Callable[[], uuid.UUID], qtbot: QtBot, + creating_deck_fails: bool, ) -> None: with anki_session_with_addon_data.profile_loaded(): # Setup Anki deck with a note. @@ -1526,6 +1532,9 @@ def test_basic( mock_function(deck_creation, "ask_user", return_value=True) + def raise_exception(*args, **kwargs) -> None: + raise Exception("test") + ah_did = next_deterministic_uuid() notes_data = [NoteInfoFactory.create()] create_ankihub_deck_mock = mock_function( @@ -1535,6 +1544,7 @@ def test_basic( ankihub_did=ah_did, notes_data=notes_data, ), + side_effect=raise_exception if creating_deck_fails else None, ) get_media_names_from_notes_data_mock = mock_function( @@ -1548,14 +1558,19 @@ def test_basic( showInfo_mock = mock_function(deck_creation, "showInfo") # Create the collaborative deck. - create_collaborative_deck() + if creating_deck_fails: + create_collaborative_deck() + qtbot.wait(500) + showInfo_mock.assert_not_called() + else: + create_collaborative_deck() - qtbot.wait_until(lambda: showInfo_mock.called) + qtbot.wait_until(lambda: showInfo_mock.called) - # Assert that the correct functions were called. - create_ankihub_deck_mock.assert_called_once_with( - deck_name, private=False, add_subdeck_tags=False - ) + # Assert that the correct functions were called. + create_ankihub_deck_mock.assert_called_once_with( + deck_name, private=False, add_subdeck_tags=False + ) - get_media_names_from_notes_data_mock.assert_called_once_with(notes_data) - start_media_upload_mock.assert_called_once() + get_media_names_from_notes_data_mock.assert_called_once_with(notes_data) + start_media_upload_mock.assert_called_once() From 49b7fdb75a1e3744eea8572daf38f7f29a639c66 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 20:05:12 +0200 Subject: [PATCH 055/105] Fix `install_ah_deck` fixture --- tests/fixtures.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 588b51c91..8151e286b 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -340,23 +340,28 @@ def __call__( @pytest.fixture def install_ah_deck( next_deterministic_uuid: Callable[[], uuid.UUID], + next_deterministic_id: Callable[[], int], import_ah_note: ImportAHNote, ) -> InstallAHDeck: """Installs a deck with the given AnkiHub and Anki names and ids. The deck is imported and added to the private config. Returns the AnkiHub deck id.""" - default_ah_did = next_deterministic_uuid() - default_ah_deck_name = "Test Deck" - # 1 is the id of the default deck, we don't want to use that here - default_anki_did = DeckId(2) - default_anki_deck_name = "Test Deck" def install_ah_deck_inner( - ah_did: Optional[uuid.UUID] = default_ah_did, - ah_deck_name: Optional[str] = default_ah_deck_name, - anki_did: Optional[DeckId] = default_anki_did, - anki_deck_name: Optional[str] = default_anki_deck_name, + ah_did: Optional[uuid.UUID] = None, + ah_deck_name: Optional[str] = None, + anki_did: Optional[DeckId] = None, + anki_deck_name: Optional[str] = None, ) -> uuid.UUID: + if not ah_did: + ah_did = next_deterministic_uuid() + if not anki_did: + anki_did = DeckId(next_deterministic_id() + 1) # 1 is the default deck + if not anki_deck_name: + anki_deck_name = f"Deck {anki_did}" + if not ah_deck_name: + ah_deck_name = f"Deck {ah_did}" + # Add deck to the config config.add_deck( name=ah_deck_name, From 5e93120e687f19e5c37c771d9e75cbf5d1fbab98 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 20:08:47 +0200 Subject: [PATCH 056/105] Refactor test --- tests/addon/conftest.py | 1 + tests/addon/test_integration.py | 29 ++++++----------------------- tests/fixtures.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/tests/addon/conftest.py b/tests/addon/conftest.py index 15b7bc777..b8a10ac79 100644 --- a/tests/addon/conftest.py +++ b/tests/addon/conftest.py @@ -18,6 +18,7 @@ mock_all_feature_flags_to_default_values, mock_download_and_install_deck_dependencies, mock_function, + mock_study_deck_dialog_with_cb, new_note_with_note_type, next_deterministic_id, next_deterministic_uuid, diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index fd0bde857..7e6d1b056 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2579,6 +2579,7 @@ def test_change_updates_destination( qtbot: QtBot, install_ah_deck: InstallAHDeck, monkeypatch: MonkeyPatch, + mock_study_deck_dialog_with_cb, ): with anki_session_with_addon_data.profile_loaded(): ah_did = install_ah_deck() @@ -2596,14 +2597,16 @@ def test_change_updates_destination( ], ) - # Mock the dialog that asks the user for the destination deck + # Mock the dialog that asks the user for the destination deck to choose + # a new deck. new_destination_deck_name = "New Deck" install_ah_deck(anki_deck_name=new_destination_deck_name) new_home_deck_anki_id = aqt.mw.col.decks.id_for_name( new_destination_deck_name ) - self._mock_new_cards_destination_dialog( - new_destination_deck_name, monkeypatch + mock_study_deck_dialog_with_cb( + "ankihub.gui.decks_dialog.StudyDeckWithoutHelpButton", + deck_name=new_destination_deck_name, ) # Open the dialog @@ -2621,26 +2624,6 @@ def test_change_updates_destination( # Assert that the destination deck was updated assert config.deck_config(ah_did).anki_id == new_home_deck_anki_id - def _mock_new_cards_destination_dialog( - self, - destination_deck_name: str, - monkeypatch: MonkeyPatch, - ) -> None: - """Sets the destination deck for new cards to the deck with the given name.""" - study_deck_mock = Mock() - - def study_deck_mock_side_effect(*args, **kwargs): - callback = kwargs["callback"] - cb_study_deck_mock = Mock() - cb_study_deck_mock.name = destination_deck_name - callback(cb_study_deck_mock) - - study_deck_mock.side_effect = study_deck_mock_side_effect - monkeypatch.setattr( - "ankihub.gui.decks_dialog.StudyDeckWithoutHelpButton", - study_deck_mock, - ) - def _mock_dependencies(self, monkeypatch: MonkeyPatch) -> None: # Mock the config to return that the user is logged in monkeypatch.setattr(config, "is_logged_in", lambda: True) diff --git a/tests/fixtures.py b/tests/fixtures.py index 8151e286b..64f66acb9 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -445,3 +445,31 @@ def add_basic_anki_note_to_deck(anki_did: DeckId) -> None: note = aqt.mw.col.new_note(aqt.mw.col.models.by_name("Basic")) note["Front"] = "some text" aqt.mw.col.add_note(note, anki_did) + + +@fixture +def mock_study_deck_dialog_with_cb( + monkeypatch: MonkeyPatch, +): + """Mocks the aqt.studydeck.StudyDeck dialog to call the callback with the provided deck name + instead of showing the dialog.""" + + def mock_study_deck_dialog_inner( + target_object: Any, + deck_name: str, + ) -> None: + dialog_mock = Mock() + + def study_deck_mock_side_effect(*args, **kwargs) -> None: + callback = kwargs["callback"] + cb_study_deck_mock = Mock() + cb_study_deck_mock.name = deck_name + callback(cb_study_deck_mock) + + dialog_mock.side_effect = study_deck_mock_side_effect + monkeypatch.setattr( + target_object, + dialog_mock, + ) + + return mock_study_deck_dialog_inner From 3a003e1e0324bfe4ea71678ae384e233629a164d Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:27:42 +0200 Subject: [PATCH 057/105] Remove Deck Operations label --- ankihub/gui/decks_dialog.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3c5368fe2..803bb51dd 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -72,8 +72,6 @@ def _setup_ui(self): def _setup_box_top(self) -> QVBoxLayout: box = QVBoxLayout() - self.deck_operations_label = QLabel("Deck Operations") - box.addWidget(self.deck_operations_label) self.box_top_buttons = QHBoxLayout() box.addLayout(self.box_top_buttons) From 79d189edd7cbd524c917e09def334519e2650293 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:28:50 +0200 Subject: [PATCH 058/105] Replace "Deck Settings" with "Deck Options" --- ankihub/gui/decks_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 803bb51dd..ba468f617 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -154,11 +154,11 @@ def _refresh_box_bottom_right( box.addSpacing(20) - # Deck Settings + # Deck Options self.box_deck_settings = QVBoxLayout() box.addLayout(self.box_deck_settings) - self.deck_settings_label = QLabel("Deck Settings") + self.deck_settings_label = QLabel("Deck Options") self.box_deck_settings.addWidget(self.deck_settings_label) self.box_deck_settings_elements = QVBoxLayout() From 5beed10518e233a671e782de37a4f6955d946744 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:42:38 +0200 Subject: [PATCH 059/105] Rename "Update destination" to "Destination for New Cards" --- ankihub/gui/decks_dialog.py | 54 +++++++++++++++++++-------------- tests/addon/test_integration.py | 4 +-- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ba468f617..1e6d2d39a 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -187,34 +187,40 @@ def _refresh_box_bottom_right( box.addSpacing(20) # Updates Destination - self.box_updates_destination = QVBoxLayout() - box.addLayout(self.box_updates_destination) + self.box_new_cards_destination = QVBoxLayout() + box.addLayout(self.box_new_cards_destination) - self.updates_destination_label = QLabel("Updates Destination") - self.box_updates_destination.addWidget(self.updates_destination_label) + self.new_cards_destination = QLabel("Destination for New Cards") + self.box_new_cards_destination.addWidget(self.new_cards_destination) - self.updates_destination_details_label = QLabel() - self.updates_destination_details_label.setWordWrap(True) + self.new_cards_destination_details_label = QLabel() + self.new_cards_destination_details_label.setWordWrap(True) self._refresh_updates_destination_details_label(selected_ah_did) - self.box_updates_destination.addWidget(self.updates_destination_details_label) + self.box_new_cards_destination.addWidget( + self.new_cards_destination_details_label + ) - self.set_updates_destination_btn = QPushButton("Change updates destination") + self.set_new_cards_destination_btn = QPushButton( + "Change Destination for New Cards" + ) qconnect( - self.set_updates_destination_btn.clicked, - self._on_change_updates_destination, + self.set_new_cards_destination_btn.clicked, + self._on_new_cards_destination_btn_clicked, ) - self.box_updates_destination.addWidget(self.set_updates_destination_btn) - self.box_updates_destination.addSpacing(7) + self.box_new_cards_destination.addWidget(self.set_new_cards_destination_btn) + self.box_new_cards_destination.addSpacing(7) - self.destination_updates_docs_link_label = QLabel( + self.new_cards_destination_docs_link_label = QLabel( """ - More about Updates Destinations + More about destinations for new cards """ ) - self.destination_updates_docs_link_label.setOpenExternalLinks(True) - self.box_updates_destination.addWidget(self.destination_updates_docs_link_label) + self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) + self.box_new_cards_destination.addWidget( + self.new_cards_destination_docs_link_label + ) box.addStretch() @@ -222,13 +228,13 @@ def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) destination_anki_did = deck_config.anki_id if name := aqt.mw.col.decks.name_if_exists(destination_anki_did): - self.updates_destination_details_label.setText( - f"New cards will be added to: {name}." + self.new_cards_destination_details_label.setText( + f"New cards are saved to: {name}." ) else: # If the deck doesn't exist, it will be re-created on next sync with the name from the config. - self.updates_destination_details_label.setText( - f"New cards will be added to {deck_config.name}." + self.new_cards_destination_details_label.setText( + f"New cards are saved to: {deck_config.name}." ) def _refresh_decks_list(self) -> None: @@ -301,7 +307,7 @@ def _on_open_web(self) -> None: ankihub_id: UUID = item.data(Qt.ItemDataRole.UserRole) openLink(f"{url_deck_base()}/{ankihub_id}") - def _on_change_updates_destination(self): + def _on_new_cards_destination_btn_clicked(self): deck_names = self.decks_list.selectedItems() if len(deck_names) == 0: return @@ -329,7 +335,7 @@ def update_deck_config(ret: StudyDeck): aqt.mw, current=current, accept="Accept", - title="Choose Updates Destination", + title="Select Destination for New Cards", parent=self, callback=update_deck_config, ) @@ -388,7 +394,9 @@ def _on_deck_selection_changed(self) -> None: self.unsubscribe_btn.setEnabled(one_selected) self.open_web_btn.setEnabled(one_selected) - self.set_updates_destination_btn.setEnabled(one_selected and is_deck_installed) + self.set_new_cards_destination_btn.setEnabled( + one_selected and is_deck_installed + ) @classmethod def display_subscribe_window(cls): diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 7e6d1b056..bcb0c8328 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2573,7 +2573,7 @@ def _install_deck_with_subdeck_tag( note.flush() return subdeck_name, anki_did, ah_did - def test_change_updates_destination( + def test_change_destination_for_new_cards( self, anki_session_with_addon_data: AnkiSession, qtbot: QtBot, @@ -2618,7 +2618,7 @@ def test_change_updates_destination( dialog.decks_list.setCurrentRow(0) qtbot.wait(200) - dialog.set_updates_destination_btn.click() + dialog.set_new_cards_destination_btn.click() qtbot.wait(200) # Assert that the destination deck was updated From 86e7c45ec59e32dd682c45d7ff03fb1cd9f44155 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:44:36 +0200 Subject: [PATCH 060/105] Adjust case --- ankihub/gui/decks_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 1e6d2d39a..98b884507 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -177,7 +177,7 @@ def _refresh_box_bottom_right( self.subdecks_docs_link_label = QLabel( """ - More about Subdecks + More about subdecks """ ) From 55cdca315c589356441c7b1e08b59ca3bed48422 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:50:15 +0200 Subject: [PATCH 061/105] Display selected deck name above options --- ankihub/gui/decks_dialog.py | 7 ++++--- tests/addon/test_integration.py | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 98b884507..ed26e1897 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -130,11 +130,12 @@ def _refresh_box_bottom_right( self.box_deck_actions = QVBoxLayout() box.addLayout(self.box_deck_actions) - self.deck_actions_label = QLabel("Deck Actions") - self.deck_actions_label.setSizePolicy( + deck_name = config.deck_config(selected_ah_did).name + self.deck_name_label = QLabel(f"

{deck_name}

") + self.deck_name_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) - self.box_deck_actions.addWidget(self.deck_actions_label) + self.box_deck_actions.addWidget(self.deck_name_label) self.box_deck_action_buttons = QHBoxLayout() self.box_deck_actions.addLayout(self.box_deck_action_buttons) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index bcb0c8328..4d0f8743a 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2511,6 +2511,9 @@ def test_basic( dialog.decks_list.setCurrentRow(0) qtbot.wait(200) + deck_name = config.deck_config(ah_did).name + assert deck_name in dialog.deck_name_label.text() + def test_toggle_subdecks( self, anki_session_with_addon_data: AnkiSession, From 4fe0e4571f1e3975f4581676669fe18031cb5b68 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:53:03 +0200 Subject: [PATCH 062/105] Remove the Add button from the deck chooser dialog --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ed26e1897..c5b58f7b5 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -339,6 +339,7 @@ def update_deck_config(ret: StudyDeck): title="Select Destination for New Cards", parent=self, callback=update_deck_config, + buttons=[], # This removes the "Add" button ) def _on_toggle_subdecks(self): From cccfdb81476ce9a23ec162950a8078c7444ee616 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 22:58:08 +0200 Subject: [PATCH 063/105] Adjust spacing --- ankihub/gui/decks_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index c5b58f7b5..922532d9c 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -66,6 +66,8 @@ def _setup_ui(self): self.box_bottom_left = self._setup_box_bottom_left() self.box_bottom.addLayout(self.box_bottom_left) + self.box_bottom.addSpacing(10) + self.box_bottom_right = QVBoxLayout() self._refresh_box_bottom_right(self.box_bottom_right, None) self.box_bottom.addLayout(self.box_bottom_right) @@ -98,6 +100,8 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: ) box.addWidget(self.decks_list_label) + box.addSpacing(5) + self.decks_list = QListWidget() box.addWidget(self.decks_list) qconnect(self.decks_list.itemSelectionChanged, self._on_deck_selection_changed) From 82ff9ec2a504f4d662dde41c33245551bf6ca7c8 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 23:01:07 +0200 Subject: [PATCH 064/105] Rename dialog to `DeckManagementDialog` --- ankihub/gui/decks_dialog.py | 8 ++++---- ankihub/gui/menu.py | 4 ++-- tests/addon/test_integration.py | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 922532d9c..5573d63a6 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -33,12 +33,12 @@ from .utils import ask_user, clear_layout, set_tooltip_icon -class SubscribedDecksDialog(QDialog): - _window: Optional["SubscribedDecksDialog"] = None +class DeckManagementDialog(QDialog): + _window: Optional["DeckManagementDialog"] = None silentlyClose = True def __init__(self): - super(SubscribedDecksDialog, self).__init__() + super(DeckManagementDialog, self).__init__() self.client = AnkiHubClient() self._setup_ui() self._refresh_decks_list() @@ -50,7 +50,7 @@ def __init__(self): self.show() def _setup_ui(self): - self.setWindowTitle("AnkiHub | Decks Managment") + self.setWindowTitle("AnkiHub | Deck Management") self.setMinimumWidth(640) self.setMinimumHeight(450) diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py index 789f16942..dd50f16db 100644 --- a/ankihub/gui/menu.py +++ b/ankihub/gui/menu.py @@ -26,7 +26,7 @@ from ..media_import.ui import open_import_dialog from ..settings import ADDON_VERSION, config from .config_dialog import get_config_dialog_manager -from .decks_dialog import SubscribedDecksDialog +from .decks_dialog import DeckManagementDialog from .errors import upload_logs_and_data_in_background, upload_logs_in_background from .media_sync import media_sync from .operations.ankihub_sync import sync_with_ankihub @@ -263,7 +263,7 @@ def _ankihub_login_setup(parent: QMenu): def _subscribed_decks_setup(parent: QMenu): q_action = QAction("📚 Subscribed Decks", aqt.mw) - qconnect(q_action.triggered, SubscribedDecksDialog.display_subscribe_window) + qconnect(q_action.triggered, DeckManagementDialog.display_subscribe_window) parent.addAction(q_action) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 4d0f8743a..0c5b5938d 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -117,7 +117,7 @@ setup_config_dialog_manager, ) from ankihub.gui.deck_updater import _AnkiHubDeckUpdater, ah_deck_updater -from ankihub.gui.decks_dialog import SubscribedDecksDialog +from ankihub.gui.decks_dialog import DeckManagementDialog from ankihub.gui.editor import _on_suggestion_button_press, _refresh_buttons from ankihub.gui.errors import upload_logs_and_data_in_background from ankihub.gui.media_sync import media_sync @@ -1779,7 +1779,7 @@ def test_unsubscribe_from_deck( } ], ) - dialog = SubscribedDecksDialog() + dialog = DeckManagementDialog() qtbot.wait(500) decks_list = dialog.decks_list @@ -2475,7 +2475,7 @@ def assert_note_has_expected_tag(): qtbot.wait_until(assert_note_has_expected_tag) -class TestSubscribedDecksDialog: +class TestDeckManagementDialog: @pytest.mark.parametrize( "nightmode", [True, False], @@ -2502,7 +2502,7 @@ def test_basic( theme_manager.night_mode = nightmode - dialog = SubscribedDecksDialog() + dialog = DeckManagementDialog() dialog.display_subscribe_window() assert dialog.decks_list.count() == 1 @@ -2540,7 +2540,7 @@ def test_toggle_subdecks( ) # Open the dialog - dialog = SubscribedDecksDialog() + dialog = DeckManagementDialog() dialog.display_subscribe_window() qtbot.wait(200) @@ -2613,7 +2613,7 @@ def test_change_destination_for_new_cards( ) # Open the dialog - dialog = SubscribedDecksDialog() + dialog = DeckManagementDialog() dialog.display_subscribe_window() qtbot.wait(200) From 0bb4e981db0f9a2c559380325a7095540003a88c Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 23:03:50 +0200 Subject: [PATCH 065/105] Refactor tests --- tests/addon/test_integration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 0c5b5938d..489a6166f 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2549,7 +2549,7 @@ def test_toggle_subdecks( dialog.decks_list.setCurrentRow(0) qtbot.wait(200) - assert dialog.toggle_subdecks_cb.isEnabled() is True + assert dialog.toggle_subdecks_cb.isEnabled() dialog.toggle_subdecks_cb.click() qtbot.wait(200) @@ -2585,10 +2585,10 @@ def test_change_destination_for_new_cards( mock_study_deck_dialog_with_cb, ): with anki_session_with_addon_data.profile_loaded(): - ah_did = install_ah_deck() - self._mock_dependencies(monkeypatch) + ah_did = install_ah_deck() + # Mock get_deck_subscriptions to return the deck monkeypatch.setattr( AnkiHubClient, From 0376b090e7f3292eff23f3230106c2a4e81388ad Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 23:06:06 +0200 Subject: [PATCH 066/105] Rename to `Deck Management` in the menu --- ankihub/gui/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py index dd50f16db..83b90814b 100644 --- a/ankihub/gui/menu.py +++ b/ankihub/gui/menu.py @@ -262,7 +262,7 @@ def _ankihub_login_setup(parent: QMenu): def _subscribed_decks_setup(parent: QMenu): - q_action = QAction("📚 Subscribed Decks", aqt.mw) + q_action = QAction("📚 Deck Management", aqt.mw) qconnect(q_action.triggered, DeckManagementDialog.display_subscribe_window) parent.addAction(q_action) From c6bd39537f5b21efb8c25df3da46522c0c7b711d Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 23:39:49 +0200 Subject: [PATCH 067/105] Move tooltip icon to the right of the text --- ankihub/gui/decks_dialog.py | 26 +++++++++++++++++--------- ankihub/gui/utils.py | 13 +++++-------- tests/addon/test_integration.py | 6 +++--- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 5573d63a6..668899a29 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -30,7 +30,7 @@ from ..main.subdecks import SUBDECK_TAG, deck_contains_subdeck_tags from ..settings import config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks -from .utils import ask_user, clear_layout, set_tooltip_icon +from .utils import ask_user, clear_layout, tooltip_icon class DeckManagementDialog(QDialog): @@ -169,15 +169,23 @@ def _refresh_box_bottom_right( self.box_deck_settings_elements = QVBoxLayout() self.box_deck_settings.addLayout(self.box_deck_settings_elements) - self.toggle_subdecks_cb = QCheckBox("Enable Subdecks") - self.toggle_subdecks_cb.setToolTip( + self.subdecks_cb_row = QHBoxLayout() + self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) + + subdecks_tooltip_message = ( "Toggle between the deck being organized into subdecks or not.
" f"This option is only available if notes in the deck have {SUBDECK_TAG} tags." ) - set_tooltip_icon(self.toggle_subdecks_cb) + self.subdecks_cb = QCheckBox("Enable Subdecks") + self.subdecks_cb.setToolTip(subdecks_tooltip_message) self._refresh_subdecks_checkbox() - qconnect(self.toggle_subdecks_cb.clicked, self._on_toggle_subdecks) - self.box_deck_settings_elements.addWidget(self.toggle_subdecks_cb) + qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) + self.subdecks_cb_row.addWidget(self.subdecks_cb) + + self.subdeck_cb_icon_label = QLabel() + self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) + self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) + self.subdecks_cb_row.addWidget(self.subdeck_cb_icon_label) self.subdecks_docs_link_label = QLabel( """ @@ -374,15 +382,15 @@ def _refresh_subdecks_checkbox(self): one_selected: bool = len(selection) == 1 if not one_selected: - self.toggle_subdecks_cb.setEnabled(False) + self.subdecks_cb.setEnabled(False) return ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) deck_config = config.deck_config(ankihub_did) has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) - self.toggle_subdecks_cb.setEnabled(has_subdeck_tags) - self.toggle_subdecks_cb.setChecked(deck_config.subdecks_enabled) + self.subdecks_cb.setEnabled(has_subdeck_tags) + self.subdecks_cb.setChecked(deck_config.subdecks_enabled) def _on_deck_selection_changed(self) -> None: selection = self.decks_list.selectedItems() diff --git a/ankihub/gui/utils.py b/ankihub/gui/utils.py index 187fd3e5e..11ffa5a8e 100644 --- a/ankihub/gui/utils.py +++ b/ankihub/gui/utils.py @@ -1,11 +1,10 @@ import uuid -from typing import Any, List, Optional, Union +from typing import Any, List, Optional import aqt from aqt.addons import check_and_prompt_for_updates from aqt.qt import ( QApplication, - QCheckBox, QDialog, QDialogButtonBox, QIcon, @@ -256,12 +255,10 @@ def ask_user( return None -def set_tooltip_icon(widget: Union[QPushButton, QCheckBox]) -> None: - widget.setIcon( - QIcon( - QApplication.style().standardIcon( - QStyle.StandardPixmap.SP_MessageBoxInformation - ) +def tooltip_icon() -> QIcon: + return QIcon( + QApplication.style().standardIcon( + QStyle.StandardPixmap.SP_MessageBoxInformation ) ) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 489a6166f..2bfa1d69d 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -2549,15 +2549,15 @@ def test_toggle_subdecks( dialog.decks_list.setCurrentRow(0) qtbot.wait(200) - assert dialog.toggle_subdecks_cb.isEnabled() - dialog.toggle_subdecks_cb.click() + assert dialog.subdecks_cb.isEnabled() + dialog.subdecks_cb.click() qtbot.wait(200) # The subdeck should now exist assert aqt.mw.col.decks.by_name(subdeck_name) is not None # Click the toggle subdeck button again - dialog.toggle_subdecks_cb.click() + dialog.subdecks_cb.click() qtbot.wait(200) # The subdeck should not exist anymore From 1b8cf3472936232eb73a492021fc2f014156b88b Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 17 Oct 2023 23:41:15 +0200 Subject: [PATCH 068/105] Adjust comment --- ankihub/gui/decks_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 668899a29..ef3628e10 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -199,7 +199,7 @@ def _refresh_box_bottom_right( box.addSpacing(20) - # Updates Destination + # Destination for new cards self.box_new_cards_destination = QVBoxLayout() box.addLayout(self.box_new_cards_destination) From 66e9c3e4568a7c719abe8943b3603243c16017f1 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 12:30:43 +0200 Subject: [PATCH 069/105] fixup updates destination --- ankihub/gui/decks_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ef3628e10..1d08103c5 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -208,7 +208,7 @@ def _refresh_box_bottom_right( self.new_cards_destination_details_label = QLabel() self.new_cards_destination_details_label.setWordWrap(True) - self._refresh_updates_destination_details_label(selected_ah_did) + self._refresh_new_cards_destination_details_label(selected_ah_did) self.box_new_cards_destination.addWidget( self.new_cards_destination_details_label ) @@ -237,7 +237,7 @@ def _refresh_box_bottom_right( box.addStretch() - def _refresh_updates_destination_details_label(self, ah_did: uuid.UUID) -> None: + def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) destination_anki_did = deck_config.anki_id if name := aqt.mw.col.decks.name_if_exists(destination_anki_did): @@ -341,7 +341,7 @@ def update_deck_config(ret: StudyDeck): anki_did = aqt.mw.col.decks.id(ret.name) config.set_home_deck(ankihub_did=ankihub_id, anki_did=anki_did) - self._refresh_updates_destination_details_label(ankihub_id) + self._refresh_new_cards_destination_details_label(ankihub_id) # this lets the user pick a deck StudyDeckWithoutHelpButton( From 3922fe7ae0cabffb5d4accf4e91dfd123a96ca1f Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 12:33:17 +0200 Subject: [PATCH 070/105] Remove unused code --- ankihub/gui/decks_dialog.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 1d08103c5..d12dd0156 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -379,12 +379,6 @@ def _on_toggle_subdecks(self): def _refresh_subdecks_checkbox(self): selection = self.decks_list.selectedItems() - one_selected: bool = len(selection) == 1 - - if not one_selected: - self.subdecks_cb.setEnabled(False) - return - ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) deck_config = config.deck_config(ankihub_did) From 8652eec93264f403a019bb3b190dc9b876d8b9eb Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 12:36:14 +0200 Subject: [PATCH 071/105] Refactor fixture --- tests/addon/test_integration.py | 3 ++- tests/fixtures.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 2bfa1d69d..8e494ef9b 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -66,6 +66,7 @@ InstallAHDeck, MockDownloadAndInstallDeckDependencies, MockFunction, + MockStudyDeckDialogWithCB, create_or_get_ah_version_of_note_type, ) from .conftest import TEST_PROFILE_ID @@ -2582,7 +2583,7 @@ def test_change_destination_for_new_cards( qtbot: QtBot, install_ah_deck: InstallAHDeck, monkeypatch: MonkeyPatch, - mock_study_deck_dialog_with_cb, + mock_study_deck_dialog_with_cb: MockStudyDeckDialogWithCB, ): with anki_session_with_addon_data.profile_loaded(): self._mock_dependencies(monkeypatch) diff --git a/tests/fixtures.py b/tests/fixtures.py index 64f66acb9..d98a657e6 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -447,10 +447,19 @@ def add_basic_anki_note_to_deck(anki_did: DeckId) -> None: aqt.mw.col.add_note(note, anki_did) +class MockStudyDeckDialogWithCB(Protocol): + def __call__( + self, + target_object: Any, + deck_name: str, + ) -> None: + ... + + @fixture def mock_study_deck_dialog_with_cb( monkeypatch: MonkeyPatch, -): +) -> MockStudyDeckDialogWithCB: """Mocks the aqt.studydeck.StudyDeck dialog to call the callback with the provided deck name instead of showing the dialog.""" @@ -460,13 +469,13 @@ def mock_study_deck_dialog_inner( ) -> None: dialog_mock = Mock() - def study_deck_mock_side_effect(*args, **kwargs) -> None: + def dialog_mock_side_effect(*args, **kwargs) -> None: callback = kwargs["callback"] cb_study_deck_mock = Mock() cb_study_deck_mock.name = deck_name callback(cb_study_deck_mock) - dialog_mock.side_effect = study_deck_mock_side_effect + dialog_mock.side_effect = dialog_mock_side_effect monkeypatch.setattr( target_object, dialog_mock, From a4f91e481e3be85536adf4da500729450cdc9389 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 13:02:07 +0200 Subject: [PATCH 072/105] Refactor code related to selected decks --- ankihub/gui/decks_dialog.py | 111 +++++++++++++----------------------- 1 file changed, 40 insertions(+), 71 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index d12dd0156..4bf1ba3f4 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -69,7 +69,7 @@ def _setup_ui(self): self.box_bottom.addSpacing(10) self.box_bottom_right = QVBoxLayout() - self._refresh_box_bottom_right(self.box_bottom_right, None) + self._refresh_box_bottom_right() self.box_bottom.addLayout(self.box_bottom_right) def _setup_box_top(self) -> QVBoxLayout: @@ -104,20 +104,19 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: self.decks_list = QListWidget() box.addWidget(self.decks_list) - qconnect(self.decks_list.itemSelectionChanged, self._on_deck_selection_changed) + qconnect(self.decks_list.itemSelectionChanged, self._refresh_box_bottom_right) return box - def _refresh_box_bottom_right( - self, box: QVBoxLayout, selected_ah_did: Optional[uuid.UUID] - ) -> None: - clear_layout(box) + def _refresh_box_bottom_right(self) -> None: + clear_layout(self.box_bottom_right) - box.addSpacing(25) + self.box_bottom_right.addSpacing(25) + selected_ah_did = self._selected_ah_did() if selected_ah_did is None: self.box_no_deck_selected = QHBoxLayout() - box.addLayout(self.box_no_deck_selected) + self.box_bottom_right.addLayout(self.box_no_deck_selected) self.box_no_deck_selected.addSpacing(5) @@ -127,12 +126,12 @@ def _refresh_box_bottom_right( ) self.box_no_deck_selected.addWidget(self.no_deck_selected_label) - box.addStretch(1) + self.box_bottom_right.addStretch(1) return # Deck Actions self.box_deck_actions = QVBoxLayout() - box.addLayout(self.box_deck_actions) + self.box_bottom_right.addLayout(self.box_deck_actions) deck_name = config.deck_config(selected_ah_did).name self.deck_name_label = QLabel(f"

{deck_name}

") @@ -157,11 +156,11 @@ def _refresh_box_bottom_right( self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) - box.addSpacing(20) + self.box_bottom_right.addSpacing(20) # Deck Options self.box_deck_settings = QVBoxLayout() - box.addLayout(self.box_deck_settings) + self.box_bottom_right.addLayout(self.box_deck_settings) self.deck_settings_label = QLabel("Deck Options") self.box_deck_settings.addWidget(self.deck_settings_label) @@ -197,11 +196,11 @@ def _refresh_box_bottom_right( self.subdecks_docs_link_label.setOpenExternalLinks(True) self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) - box.addSpacing(20) + self.box_bottom_right.addSpacing(20) # Destination for new cards self.box_new_cards_destination = QVBoxLayout() - box.addLayout(self.box_new_cards_destination) + self.box_bottom_right.addLayout(self.box_new_cards_destination) self.new_cards_destination = QLabel("Destination for New Cards") self.box_new_cards_destination.addWidget(self.new_cards_destination) @@ -235,7 +234,7 @@ def _refresh_box_bottom_right( self.new_cards_destination_docs_link_label ) - box.addStretch() + self.box_bottom_right.addStretch() def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) @@ -269,6 +268,14 @@ def _refresh_decks_list(self) -> None: item.setData(Qt.ItemDataRole.UserRole, deck.ah_did) self.decks_list.addItem(item) + def _selected_ah_did(self) -> Optional[UUID]: + selection = self.decks_list.selectedItems() + if len(selection) != 1: + return None + + result = selection[0].data(Qt.ItemDataRole.UserRole) + return result + def _refresh_anki(self) -> None: op = OpChanges() op.deck = True @@ -291,44 +298,32 @@ def _select_deck(self, ah_did: uuid.UUID): self.decks_list.setCurrentItem(deck_item) def _on_unsubscribe(self) -> None: - items = self.decks_list.selectedItems() - if len(items) == 0: - return - deck_names = [item.text() for item in items] - deck_names_text = ", ".join(deck_names) + ah_did = self._selected_ah_did() + deck_name = config.deck_config(ah_did).name confirm = ask_user( - f"Unsubscribe from deck {deck_names_text}?\n\n" + f"Unsubscribe from deck {deck_name}?\n\n" "The deck will remain in your collection, but it will no longer sync with AnkiHub.", title="Unsubscribe AnkiHub Deck", ) if not confirm: return - for item in items: - ankihub_did: UUID = item.data(Qt.ItemDataRole.UserRole) - unsubscribe_from_deck_and_uninstall(ankihub_did) + unsubscribe_from_deck_and_uninstall(ah_did) tooltip("Unsubscribed from AnkiHub Deck.", parent=aqt.mw) self._refresh_decks_list() def _on_open_web(self) -> None: - items = self.decks_list.selectedItems() - if len(items) == 0: + ah_did = self._selected_ah_did() + if ah_did is None: return - for item in items: - ankihub_id: UUID = item.data(Qt.ItemDataRole.UserRole) - openLink(f"{url_deck_base()}/{ankihub_id}") + openLink(f"{url_deck_base()}/{ah_did}") def _on_new_cards_destination_btn_clicked(self): - deck_names = self.decks_list.selectedItems() - if len(deck_names) == 0: - return - - deck_name = deck_names[0] - ankihub_id: UUID = deck_name.data(Qt.ItemDataRole.UserRole) + ah_did = self._selected_ah_did() current_destination_deck = aqt.mw.col.decks.get( - config.deck_config(ankihub_id).anki_id + config.deck_config(ah_did).anki_id ) if current_destination_deck is None: current = None @@ -340,8 +335,8 @@ def update_deck_config(ret: StudyDeck): return anki_did = aqt.mw.col.decks.id(ret.name) - config.set_home_deck(ankihub_did=ankihub_id, anki_did=anki_did) - self._refresh_new_cards_destination_details_label(ankihub_id) + config.set_home_deck(ankihub_did=ah_did, anki_did=anki_did) + self._refresh_new_cards_destination_details_label(ah_did) # this lets the user pick a deck StudyDeckWithoutHelpButton( @@ -355,14 +350,8 @@ def update_deck_config(ret: StudyDeck): ) def _on_toggle_subdecks(self): - deck_items = self.decks_list.selectedItems() - if len(deck_items) == 0: - return - - deck_item = deck_items[0] - ankihub_id: UUID = deck_item.data(Qt.ItemDataRole.UserRole) - - deck_config = config.deck_config(ankihub_id) + ah_did = self._selected_ah_did() + deck_config = config.deck_config(ah_did) if aqt.mw.col.decks.name_if_exists(deck_config.anki_id) is None: showInfo( ( @@ -373,38 +362,18 @@ def _on_toggle_subdecks(self): ) return - confirm_and_toggle_subdecks(ankihub_id) + confirm_and_toggle_subdecks(ah_did) self._refresh_subdecks_checkbox() def _refresh_subdecks_checkbox(self): - selection = self.decks_list.selectedItems() - ankihub_did: UUID = selection[0].data(Qt.ItemDataRole.UserRole) - deck_config = config.deck_config(ankihub_did) + ah_did = self._selected_ah_did() - has_subdeck_tags = deck_contains_subdeck_tags(ankihub_did) + has_subdeck_tags = deck_contains_subdeck_tags(ah_did) self.subdecks_cb.setEnabled(has_subdeck_tags) - self.subdecks_cb.setChecked(deck_config.subdecks_enabled) - def _on_deck_selection_changed(self) -> None: - selection = self.decks_list.selectedItems() - one_selected: bool = len(selection) == 1 - is_deck_installed = False - if one_selected: - selected = selection[0] - ankihub_did: UUID = selected.data(Qt.ItemDataRole.UserRole) - is_deck_installed = bool(config.deck_config(ankihub_did)) - - if one_selected: - self._refresh_box_bottom_right(self.box_bottom_right, ankihub_did) - else: - self._refresh_box_bottom_right(self.box_bottom_right, None) - - self.unsubscribe_btn.setEnabled(one_selected) - self.open_web_btn.setEnabled(one_selected) - self.set_new_cards_destination_btn.setEnabled( - one_selected and is_deck_installed - ) + deck_config = config.deck_config(ah_did) + self.subdecks_cb.setChecked(deck_config.subdecks_enabled) @classmethod def display_subscribe_window(cls): From a68de0290ef085eac5ccf7c49d4d0444440935e2 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 13:09:06 +0200 Subject: [PATCH 073/105] Make subdecks checkbox text gray when disabled --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 4bf1ba3f4..89e4e415e 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -371,6 +371,7 @@ def _refresh_subdecks_checkbox(self): has_subdeck_tags = deck_contains_subdeck_tags(ah_did) self.subdecks_cb.setEnabled(has_subdeck_tags) + self.subdecks_cb.setStyleSheet("color: grey" if not has_subdeck_tags else "") deck_config = config.deck_config(ah_did) self.subdecks_cb.setChecked(deck_config.subdecks_enabled) From f4c55d71659d1cc3eea7df1e47626462c7625099 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 13:25:56 +0200 Subject: [PATCH 074/105] Enabble word wrap for deck_name_label --- ankihub/gui/decks_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 89e4e415e..a2a5de9bb 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -135,6 +135,7 @@ def _refresh_box_bottom_right(self) -> None: deck_name = config.deck_config(selected_ah_did).name self.deck_name_label = QLabel(f"

{deck_name}

") + self.deck_name_label.setWordWrap(True) self.deck_name_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) From 73bc97a7f3725f739f91d021b91adfcd7f411fc8 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 13:26:08 +0200 Subject: [PATCH 075/105] Adjust spacing --- ankihub/gui/decks_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index a2a5de9bb..3c3f20409 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -110,7 +110,7 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: def _refresh_box_bottom_right(self) -> None: clear_layout(self.box_bottom_right) - self.box_bottom_right.addSpacing(25) + self.box_bottom_right.addSpacing(30) selected_ah_did = self._selected_ah_did() if selected_ah_did is None: From 36205e5f75b41d8c8f8a0894c6f566d841bd81e2 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 14:01:00 +0200 Subject: [PATCH 076/105] Use enum for `suspend_new_cards_of_existing_notes` --- ankihub/settings.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ankihub/settings.py b/ankihub/settings.py index 3d8223aa0..e09f8fabc 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -61,6 +61,12 @@ def _deserialize_datetime(x: str) -> Optional[datetime]: return datetime.strptime(x, ANKIHUB_DATETIME_FORMAT_STR) if x else None +class SuspendNewCardsOfExistingNotes(Enum): + ALWAYS = "Always" + NEVER = "Never" + IF_SIBLINGS_SUSPENDED = "If siblings are suspended" + + @dataclass class DeckConfig(DataClassJSONMixin): anki_id: DeckId @@ -84,7 +90,9 @@ class DeckConfig(DataClassJSONMixin): False # whether deck is organized into subdecks by the add-on ) suspend_new_cards_of_new_notes: bool = False - suspend_new_cards_of_existing_notes: bool = True + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes = ( + SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED + ) @dataclass From 76c018a63463deac8dfad961075045bb984857ee Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 14:05:20 +0200 Subject: [PATCH 077/105] Remove suspension settings from global config --- ankihub/config.json | 2 -- ankihub/gui/config_dialog.py | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/ankihub/config.json b/ankihub/config.json index d32f7af49..97436132c 100644 --- a/ankihub/config.json +++ b/ankihub/config.json @@ -3,8 +3,6 @@ "report_errors": true, "sync_hotkey": "Ctrl+Shift+h", "auto_sync": "on_ankiweb_sync", - "suspend_new_cards_of_existing_notes": "if_siblings_are_suspended", - "suspend_new_cards_of_new_notes": "always", "debug_level_logs": false, "use_staging": false } diff --git a/ankihub/gui/config_dialog.py b/ankihub/gui/config_dialog.py index 968554eef..44039c608 100644 --- a/ankihub/gui/config_dialog.py +++ b/ankihub/gui/config_dialog.py @@ -49,18 +49,6 @@ def _general_tab(conf_window) -> None: values=["on_ankiweb_sync", "on_startup", "never"], description="Auto Sync with AnkiHub", ) - tab.dropdown( - "suspend_new_cards_of_existing_notes", - labels=["If sibling cards are suspended", "Always", "Never"], - values=["if_siblings_are_suspended", "always", "never"], - description="Suspend new cards of existing notes", - ) - tab.dropdown( - "suspend_new_cards_of_new_notes", - labels=["Always", "Never"], - values=["always", "never"], - description="Suspend new cards of new notes", - ) tab.hseparator() tab.space(8) From f4687ffe329bc4e4786b8e91da6782c471bd6aa5 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 14:40:15 +0200 Subject: [PATCH 078/105] Add suspension options to decks dialog --- ankihub/gui/decks_dialog.py | 63 +++++++++++++++++++++++++++++++++++-- ankihub/settings.py | 10 ++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3c3f20409..a59228b4b 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -8,6 +8,7 @@ from aqt import gui_hooks from aqt.qt import ( QCheckBox, + QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, @@ -28,7 +29,7 @@ from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall from ..main.subdecks import SUBDECK_TAG, deck_contains_subdeck_tags -from ..settings import config, url_deck_base, url_decks +from ..settings import SuspendNewCardsOfExistingNotes, config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks from .utils import ask_user, clear_layout, tooltip_icon @@ -52,7 +53,7 @@ def __init__(self): def _setup_ui(self): self.setWindowTitle("AnkiHub | Deck Management") self.setMinimumWidth(640) - self.setMinimumHeight(450) + self.setMinimumHeight(500) self.box_main = QVBoxLayout() self.setLayout(self.box_main) @@ -169,6 +170,64 @@ def _refresh_box_bottom_right(self) -> None: self.box_deck_settings_elements = QVBoxLayout() self.box_deck_settings.addLayout(self.box_deck_settings_elements) + # ... Suspend new cards of existing notes + deck_config = config.deck_config(selected_ah_did) + + self.box_suspend_new_cards_of_existing_notes = QVBoxLayout() + self.box_deck_settings_elements.addLayout( + self.box_suspend_new_cards_of_existing_notes + ) + + self.suspend_new_cards_of_existing_notes_label = QLabel( + "Suspend new cards of existing notes" + ) + self.box_suspend_new_cards_of_existing_notes.addWidget( + self.suspend_new_cards_of_existing_notes_label + ) + + self.suspend_new_cards_of_existing_notes = QComboBox() + self.box_suspend_new_cards_of_existing_notes.addWidget( + self.suspend_new_cards_of_existing_notes + ) + + self.suspend_new_cards_of_existing_notes.insertItems( + 0, [option.value for option in SuspendNewCardsOfExistingNotes] + ) + self.suspend_new_cards_of_existing_notes.setCurrentText( + deck_config.suspend_new_cards_of_existing_notes.name + ) + qconnect( + self.suspend_new_cards_of_existing_notes.currentTextChanged, + lambda: config.set_suspend_new_cards_of_existing_notes( + selected_ah_did, + SuspendNewCardsOfExistingNotes( + self.suspend_new_cards_of_existing_notes.currentText() + ), + ), + ) + + self.box_deck_settings_elements.addSpacing(10) + + # ... Suspend new cards of new notes + self.suspend_new_cards_of_new_notes_cb = QCheckBox( + "Suspend new cards of new notes" + ) + self.suspend_new_cards_of_new_notes_cb.setChecked( + deck_config.suspend_new_cards_of_new_notes + ) + qconnect( + self.suspend_new_cards_of_new_notes_cb.toggled, + lambda: config.set_suspend_new_cards_of_new_notes( + selected_ah_did, self.suspend_new_cards_of_new_notes_cb.isChecked() + ), + ) + self.box_deck_settings_elements.addWidget( + self.suspend_new_cards_of_new_notes_cb + ) + + self.box_deck_settings_elements.addSpacing(10) + + # ... Subdecks self.subdecks_cb_row = QHBoxLayout() self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) diff --git a/ankihub/settings.py b/ankihub/settings.py index e09f8fabc..0bd0f0cca 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -226,6 +226,16 @@ def set_subdecks(self, ankihub_did: uuid.UUID, subdecks: bool): self.deck_config(ankihub_did).subdecks_enabled = subdecks self._update_private_config() + def set_suspend_new_cards_of_new_notes(self, ankihub_did: uuid.UUID, suspend: bool): + self.deck_config(ankihub_did).suspend_new_cards_of_new_notes = suspend + self._update_private_config() + + def set_suspend_new_cards_of_existing_notes( + self, ankihub_did: uuid.UUID, suspend: SuspendNewCardsOfExistingNotes + ): + self.deck_config(ankihub_did).suspend_new_cards_of_existing_notes = suspend + self._update_private_config() + def add_deck( self, name: str, From ee6d03e0737c4228166f3785f721392ae09b6be8 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 14:46:39 +0200 Subject: [PATCH 079/105] Change suspension logic in importer --- ankihub/main/importing.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/ankihub/main/importing.py b/ankihub/main/importing.py index bb8ff98e5..50847b015 100644 --- a/ankihub/main/importing.py +++ b/ankihub/main/importing.py @@ -17,7 +17,7 @@ from .. import LOGGER, settings from ..ankihub_client import Field, NoteInfo from ..db import ankihub_db -from ..settings import config +from ..settings import SuspendNewCardsOfExistingNotes, config from .note_conversion import ( TAG_FOR_PROTECTING_ALL_FIELDS, get_fields_protected_by_tags, @@ -274,35 +274,29 @@ def suspend_new_cards() -> None: card.queue = QUEUE_TYPE_SUSPENDED card.flush() + deck_config = config.deck_config(self._ankihub_did) if cards_before_changes: # If there were cards before the changes, the note already existed in Anki. - config_key = "suspend_new_cards_of_existing_notes" - config_value = config.public_config[config_key] - if config_value == "never": + option = deck_config.suspend_new_cards_of_existing_notes + if option == SuspendNewCardsOfExistingNotes.NEVER: return - elif config_value == "always": + elif option == SuspendNewCardsOfExistingNotes.ALWAYS: suspend_new_cards() - elif config_value == "if_siblings_are_suspended": + elif option == SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED: if all( card.queue == QUEUE_TYPE_SUSPENDED for card in cards_before_changes ): suspend_new_cards() else: raise ValueError( - f"Invalid value for {config_key}: {config_value}" + f"Unknown value for {str(SuspendNewCardsOfExistingNotes)}" ) # pragma: no cover else: # If there were no cards before the changes, the note didn't exist in Anki before. - config_key = "suspend_new_cards_of_new_notes" - config_value = config.public_config[config_key] - if config_value == "never": - return - elif config_value == "always": + if deck_config.suspend_new_cards_of_new_notes: suspend_new_cards() else: - raise ValueError( - f"Invalid value for {config_key}: {config_value}" - ) # pragma: no cover + return def _update_or_create_note_inner( self, From 850b598fd5c24df5ce146060a6cc0a3610a11fc7 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 17:18:38 +0200 Subject: [PATCH 080/105] Pass in suspension options as parameters to importer --- ankihub/gui/deck_updater.py | 2 + ankihub/gui/operations/deck_installation.py | 6 ++- ankihub/main/importing.py | 51 ++++++++++++++++----- ankihub/main/reset_local_changes.py | 2 + ankihub/settings.py | 15 +++++- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/ankihub/gui/deck_updater.py b/ankihub/gui/deck_updater.py index de292eb78..09e0fc6cb 100644 --- a/ankihub/gui/deck_updater.py +++ b/ankihub/gui/deck_updater.py @@ -126,6 +126,8 @@ def _download_updates_for_deck(self, ankihub_did) -> bool: protected_fields=chunk.protected_fields, protected_tags=chunk.protected_tags, subdecks=deck_config.subdecks_enabled, + suspend_new_cards_of_new_notes=deck_config.suspend_new_cards_of_new_notes, + suspend_new_cards_of_existing_notes=deck_config.suspend_new_cards_of_existing_notes, ) self._import_results.append(import_result) diff --git a/ankihub/gui/operations/deck_installation.py b/ankihub/gui/operations/deck_installation.py index 4a112099b..c1af1c43e 100644 --- a/ankihub/gui/operations/deck_installation.py +++ b/ankihub/gui/operations/deck_installation.py @@ -19,7 +19,7 @@ from ...main.note_types import fetch_note_types_based_on_notes from ...main.subdecks import deck_contains_subdeck_tags from ...main.utils import create_backup -from ...settings import config +from ...settings import DeckConfig, config from ..exceptions import DeckDownloadAndInstallError, RemoteDeckNotFoundError from ..media_sync import media_sync from ..messages import messages @@ -170,6 +170,10 @@ def _install_deck( is_first_import_of_deck=True, protected_fields=protected_fields, protected_tags=protected_tags, + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ankihub_did + ), ) config.add_deck( diff --git a/ankihub/main/importing.py b/ankihub/main/importing.py index 50847b015..7b387f8ca 100644 --- a/ankihub/main/importing.py +++ b/ankihub/main/importing.py @@ -17,7 +17,7 @@ from .. import LOGGER, settings from ..ankihub_client import Field, NoteInfo from ..db import ankihub_db -from ..settings import SuspendNewCardsOfExistingNotes, config +from ..settings import SuspendNewCardsOfExistingNotes from .note_conversion import ( TAG_FOR_PROTECTING_ALL_FIELDS, get_fields_protected_by_tags, @@ -71,6 +71,8 @@ def import_ankihub_deck( protected_tags: List[str], deck_name: str, # name that will be used for a deck if a new one gets created is_first_import_of_deck: bool, + suspend_new_cards_of_new_notes: bool, + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes, anki_did: Optional[ # did that new notes should be put into if importing not for the first time DeckId ] = None, @@ -114,7 +116,9 @@ def import_ankihub_deck( self._import_note_types(note_types=note_types) - dids = self._import_notes(notes_data=notes) + dids = self._import_notes( + notes, suspend_new_cards_of_new_notes, suspend_new_cards_of_existing_notes + ) if self._is_first_import_of_deck: self._local_did = self._cleanup_first_time_deck_import( @@ -155,7 +159,12 @@ def _import_note_types_into_ankihub_db( ankihub_did=self._ankihub_did, note_type=note_type ) - def _import_notes(self, notes_data: List[NoteInfo]) -> Set[DeckId]: + def _import_notes( + self, + notes_data: List[NoteInfo], + suspend_new_cards_of_new_notes: bool, + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes, + ) -> Set[DeckId]: # returns set of ids of decks notes were imported into upserted_notes, skipped_notes = ankihub_db.upsert_notes_data( @@ -172,6 +181,8 @@ def _import_notes(self, notes_data: List[NoteInfo]) -> Set[DeckId]: anki_did=self._local_did, protected_fields=self._protected_fields, protected_tags=self._protected_tags, + suspend_new_cards_of_new_notes=suspend_new_cards_of_new_notes, + suspend_new_cards_of_existing_notes=suspend_new_cards_of_existing_notes, ) dids_for_note = set(c.did for c in note.cards()) dids = dids | dids_for_note @@ -230,6 +241,8 @@ def _update_or_create_note( note_data: NoteInfo, protected_fields: Dict[int, List[str]], protected_tags: List[str], + suspend_new_cards_of_new_notes: bool, + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes, anki_did: Optional[DeckId] = None, ) -> Note: LOGGER.debug( @@ -253,12 +266,21 @@ def _update_or_create_note( anki_did=anki_did, ) - self._maybe_suspend_new_cards(note, cards_before_changes) + self._maybe_suspend_new_cards( + note=note, + cards_before_changes=cards_before_changes, + suspend_new_cards_of_new_notes=suspend_new_cards_of_new_notes, + suspend_new_cards_of_existing_notes=suspend_new_cards_of_existing_notes, + ) return note def _maybe_suspend_new_cards( - self, note: Note, cards_before_changes: List[Card] + self, + note: Note, + cards_before_changes: List[Card], + suspend_new_cards_of_new_notes: bool, + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes, ) -> None: def new_cards() -> Set[Card]: cids_before_changes = {c.id for c in cards_before_changes} @@ -274,15 +296,22 @@ def suspend_new_cards() -> None: card.queue = QUEUE_TYPE_SUSPENDED card.flush() - deck_config = config.deck_config(self._ankihub_did) if cards_before_changes: # If there were cards before the changes, the note already existed in Anki. - option = deck_config.suspend_new_cards_of_existing_notes - if option == SuspendNewCardsOfExistingNotes.NEVER: + if ( + suspend_new_cards_of_existing_notes + == SuspendNewCardsOfExistingNotes.NEVER + ): return - elif option == SuspendNewCardsOfExistingNotes.ALWAYS: + elif ( + suspend_new_cards_of_existing_notes + == SuspendNewCardsOfExistingNotes.ALWAYS + ): suspend_new_cards() - elif option == SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED: + elif ( + suspend_new_cards_of_existing_notes + == SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED + ): if all( card.queue == QUEUE_TYPE_SUSPENDED for card in cards_before_changes ): @@ -293,7 +322,7 @@ def suspend_new_cards() -> None: ) # pragma: no cover else: # If there were no cards before the changes, the note didn't exist in Anki before. - if deck_config.suspend_new_cards_of_new_notes: + if suspend_new_cards_of_new_notes: suspend_new_cards() else: return diff --git a/ankihub/main/reset_local_changes.py b/ankihub/main/reset_local_changes.py index 6f3b96838..80246a629 100644 --- a/ankihub/main/reset_local_changes.py +++ b/ankihub/main/reset_local_changes.py @@ -46,6 +46,8 @@ def reset_local_changes_to_notes( protected_tags=protected_tags, # we don't move existing notes between decks here, users might not want that subdecks_for_new_notes_only=deck_config.subdecks_enabled, + suspend_new_cards_of_new_notes=deck_config.suspend_new_cards_of_new_notes, + suspend_new_cards_of_existing_notes=deck_config.suspend_new_cards_of_existing_notes, ) # this way the notes won't be marked as "changed after sync" anymore diff --git a/ankihub/settings.py b/ankihub/settings.py index 0bd0f0cca..895da44a1 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -90,10 +90,19 @@ class DeckConfig(DataClassJSONMixin): False # whether deck is organized into subdecks by the add-on ) suspend_new_cards_of_new_notes: bool = False - suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes = ( + suspend_new_cards_of_existing_notes = ( SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED ) + @staticmethod + def suspend_new_cards_of_new_notes_default(ah_did: uuid.UUID) -> bool: + result = ah_did == ANKING_DECK_ID + return result + + @staticmethod + def suspend_new_cards_of_existing_notes_default() -> SuspendNewCardsOfExistingNotes: + return SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED + @dataclass class DeckExtensionConfig(DataClassJSONMixin): @@ -251,7 +260,9 @@ def add_deck( anki_id=DeckId(anki_did), user_relation=user_relation, subdecks_enabled=subdecks_enabled, - suspend_new_cards_of_new_notes=ankihub_did == ANKING_DECK_ID, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ankihub_did + ), ) # remove duplicates self.save_latest_deck_update(ankihub_did, latest_udpate) From 28d0eef7b2a3f4815d788c7e1309a197549e09b2 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 17:17:15 +0200 Subject: [PATCH 081/105] Make factory generate unique anki ids --- tests/factories.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/factories.py b/tests/factories.py index 929c76993..d7fe45c07 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -24,12 +24,22 @@ def create(cls, **kwargs) -> T: return super().create(**kwargs) +def _next_anki_nid() -> int: + # Returns a new nid for each call. + # The purpose of this is to make sure that the nids used by the NoteInfoFactory are unique. + _next_anki_nid.nid += 1 # type: ignore + return _next_anki_nid.nid # type: ignore + + +_next_anki_nid.nid = 0 # type: ignore + + class NoteInfoFactory(BaseFactory[NoteInfo]): class Meta: model = NoteInfo - ah_nid = factory.LazyFunction(uuid.uuid4) - anki_nid = 1 + ah_nid: uuid.UUID = factory.LazyFunction(uuid.uuid4) # type: ignore + anki_nid: int = factory.LazyFunction(_next_anki_nid) # type: ignore mid = 1 fields: List[Field] = factory.LazyAttribute( # type: ignore lambda _: [ From 8afe8fb4d263f8276e2fd0edb220a097cbeb6fd2 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 17:19:36 +0200 Subject: [PATCH 082/105] Fix tests --- .../performance/test_ankihub_importer.py | 3 + tests/addon/performance/test_exporting.py | 3 + tests/addon/test_integration.py | 94 ++++++++++++++++--- tests/fixtures.py | 21 ++++- 4 files changed, 105 insertions(+), 16 deletions(-) diff --git a/tests/addon/performance/test_ankihub_importer.py b/tests/addon/performance/test_ankihub_importer.py index ddf7386c2..3df9efce3 100644 --- a/tests/addon/performance/test_ankihub_importer.py +++ b/tests/addon/performance/test_ankihub_importer.py @@ -7,6 +7,7 @@ from pytest_anki import AnkiSession from ankihub.ankihub_client import NoteInfo +from ankihub.settings import DeckConfig from .conftest import Profile @@ -38,6 +39,8 @@ def test_anking_deck_first_time_import( note_types=anking_note_types, protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=False, + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) ) print(f"Importing {len(notes_data)} notes took {duration} seconds") diff --git a/tests/addon/performance/test_exporting.py b/tests/addon/performance/test_exporting.py index 3b6e89201..77c42b6eb 100644 --- a/tests/addon/performance/test_exporting.py +++ b/tests/addon/performance/test_exporting.py @@ -7,6 +7,7 @@ from pytest_anki import AnkiSession from ankihub.ankihub_client import NoteInfo +from ankihub.settings import DeckConfig from .conftest import Profile @@ -43,6 +44,8 @@ def test_anking_export_without_changes( note_types=anking_note_types, protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=False, + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) # Assert that exporting the notes takes less than 0.3 seconds diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py index 8e494ef9b..5186a571b 100644 --- a/tests/addon/test_integration.py +++ b/tests/addon/test_integration.py @@ -166,8 +166,10 @@ from ankihub.settings import ( ANKIHUB_NOTE_TYPE_FIELD_NAME, AnkiHubCommands, + DeckConfig, DeckExtension, DeckExtensionConfig, + SuspendNewCardsOfExistingNotes, config, profile_files_path, ) @@ -228,6 +230,10 @@ def import_sample_ankihub_deck( protected_fields={}, protected_tags=[], note_types=SAMPLE_NOTE_TYPES, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ankihub_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ).anki_did new_dids = all_dids() - dids_before_import @@ -1194,6 +1200,10 @@ def test_import_new_deck( note_types=SAMPLE_NOTE_TYPES, protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1234,6 +1244,10 @@ def test_import_existing_deck_1( note_types=SAMPLE_NOTE_TYPES, protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1278,6 +1292,10 @@ def test_import_existing_deck_2( note_types=SAMPLE_NOTE_TYPES, protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1330,6 +1348,10 @@ def test_import_existing_deck_3( note_types=SAMPLE_NOTE_TYPES, protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1366,6 +1388,10 @@ def test_update_deck( protected_fields={}, protected_tags=[], anki_did=first_local_did, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) second_anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1411,6 +1437,10 @@ def test_update_deck_when_it_was_deleted( protected_fields={}, protected_tags=[], anki_did=first_local_did, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) second_anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1456,6 +1486,10 @@ def test_update_deck_with_subdecks( protected_tags=[], anki_did=anki_did, subdecks=True, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) second_anki_did = import_result.anki_did new_dids = all_dids() - dids_before_import @@ -1515,6 +1549,10 @@ def test_import_deck_and_check_that_values_are_saved_to_databases( protected_fields={note_type_id: [protected_field_name]}, protected_tags=["protected_tag"], note_types=SAMPLE_NOTE_TYPES, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) # assert that the fields are saved correctly in the Anki DB (protected) @@ -1560,6 +1598,10 @@ def test_conflicting_notes_dont_get_imported( protected_tags=[], deck_name="test", is_first_import_of_deck=True, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did_1 + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) assert import_result.created_nids == [anki_nid] assert import_result.updated_nids == [] @@ -1584,6 +1626,10 @@ def test_conflicting_notes_dont_get_imported( protected_tags=[], deck_name="test", is_first_import_of_deck=True, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did_2 + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) assert import_result.created_nids == [] assert import_result.updated_nids == [] @@ -1622,33 +1668,36 @@ class TestAnkiHubImporterSuspendNewCardsOfExistingNotesOption: "option_value, existing_card_suspended, expected_new_card_suspended", [ # Always suspend new cards - ("always", False, True), - ("always", True, True), + (SuspendNewCardsOfExistingNotes.ALWAYS, False, True), + (SuspendNewCardsOfExistingNotes.ALWAYS, True, True), # Never suspend new cards - ("never", True, False), - ("never", False, False), + (SuspendNewCardsOfExistingNotes.NEVER, True, False), + (SuspendNewCardsOfExistingNotes.NEVER, False, False), # Suspend new cards if existing sibling cards are suspended - ("if_siblings_are_suspended", True, True), - ("if_siblings_are_suspended", False, False), + (SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED, True, True), + (SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED, False, False), ], ) def test_suspend_new_cards_of_existing_notes_option( self, anki_session_with_addon_data: AnkiSession, next_deterministic_uuid: Callable[[], uuid.UUID], - option_value: str, + install_ah_deck: InstallAHDeck, + option_value: SuspendNewCardsOfExistingNotes, existing_card_suspended: bool, expected_new_card_suspended: bool, ): anki_session = anki_session_with_addon_data with anki_session.profile_loaded(): - config.public_config["suspend_new_cards_of_existing_notes"] = option_value + ah_did = install_ah_deck() + config.set_suspend_new_cards_of_existing_notes(ah_did, option_value) ah_nid = next_deterministic_uuid() old_card, new_card = self._create_and_update_note_with_new_card( existing_card_suspended=existing_card_suspended, ah_nid=ah_nid, + suspend_new_cards_of_existing_notes=option_value, ) # Assert the old card has the same suspension state as before @@ -1665,6 +1714,7 @@ def _create_and_update_note_with_new_card( self, existing_card_suspended: bool, ah_nid: uuid.UUID, + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes, ) -> Tuple[Card, Card]: # Create a cloze note with one card, optionally suspend the existing card, # then update the note using AnkiHubImporter adding a new cloze @@ -1699,6 +1749,8 @@ def _create_and_update_note_with_new_card( anki_did=DeckId(0), protected_fields={}, protected_tags=[], + suspend_new_cards_of_new_notes=False, + suspend_new_cards_of_existing_notes=suspend_new_cards_of_existing_notes, ) assert len(updated_note.cards()) == 2 # one existing and one new card @@ -1713,23 +1765,25 @@ class TestAnkiHubImporterSuspendNewCardsOfNewNotesOption: @pytest.mark.parametrize( "option_value, expected_new_card_suspended", [ - ("always", True), - ("never", False), + (True, True), + (False, False), ], ) def test_suspend_new_cards_of_new_notes_option( self, anki_session_with_addon_data: AnkiSession, + install_ah_deck: InstallAHDeck, import_ah_note: ImportAHNote, - option_value: str, + option_value: bool, expected_new_card_suspended: bool, ): anki_session = anki_session_with_addon_data with anki_session.profile_loaded(): - config.public_config["suspend_new_cards_of_new_notes"] = option_value + ah_did = install_ah_deck() + config.set_suspend_new_cards_of_new_notes(ah_did, option_value) - note_info = import_ah_note() + note_info = import_ah_note(ah_did=ah_did) note = aqt.mw.col.get_note(NoteId(note_info.anki_nid)) assert len(note.cards()) == 1 @@ -2157,14 +2211,19 @@ def test_NewNoteSearchNode( ankihub_models = { m["id"]: m for m in mw.col.models.all() if "/" in m["name"] } + ah_did = next_deterministic_uuid() AnkiHubImporter().import_ankihub_deck( - ankihub_did=next_deterministic_uuid(), + ankihub_did=ah_did, notes=notes_data, note_types=ankihub_models, protected_fields={}, protected_tags=[], deck_name="Test-Deck", is_first_import_of_deck=True, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) all_nids = mw.col.find_notes("") @@ -2196,13 +2255,18 @@ def test_SuggestionTypeSearchNode( ankihub_models = { m["id"]: m for m in mw.col.models.all() if "/" in m["name"] } + ah_did = next_deterministic_uuid() AnkiHubImporter().import_ankihub_deck( - ankihub_did=next_deterministic_uuid(), + ankihub_did=ah_did, notes=notes_data, note_types=ankihub_models, protected_fields={}, protected_tags=[], deck_name="Test-Deck", + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), is_first_import_of_deck=True, ) diff --git a/tests/fixtures.py b/tests/fixtures.py index d98a657e6..c650ed30a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -27,7 +27,7 @@ from ankihub.gui.media_sync import _AnkiHubMediaSync from ankihub.main.importing import AnkiHubImporter from ankihub.main.utils import modify_note_type -from ankihub.settings import config +from ankihub.settings import DeckConfig, config @fixture @@ -234,6 +234,19 @@ def _import_ah_note( f"\tNote data: {note_data.fields}, note type: {field_names_of_note_type}" ) + if deck_config := config.deck_config(ah_did): + suspend_new_cards_of_new_notes = deck_config.suspend_new_cards_of_new_notes + suspend_new_cards_of_existing_notes = ( + deck_config.suspend_new_cards_of_existing_notes + ) + else: + suspend_new_cards_of_new_notes = ( + DeckConfig.suspend_new_cards_of_new_notes_default(ah_did) + ) + suspend_new_cards_of_existing_notes = ( + DeckConfig.suspend_new_cards_of_existing_notes_default() + ) + AnkiHubImporter().import_ankihub_deck( ankihub_did=ah_did, notes=[note_data], @@ -243,6 +256,8 @@ def _import_ah_note( deck_name=deck_name, is_first_import_of_deck=anki_did is None, anki_did=anki_did, + suspend_new_cards_of_new_notes=suspend_new_cards_of_new_notes, + suspend_new_cards_of_existing_notes=suspend_new_cards_of_existing_notes, ) return note_data @@ -295,6 +310,10 @@ def import_ah_note_type_inner( protected_fields={}, protected_tags=[], is_first_import_of_deck=False, + suspend_new_cards_of_new_notes=DeckConfig.suspend_new_cards_of_new_notes_default( + ah_did + ), + suspend_new_cards_of_existing_notes=DeckConfig.suspend_new_cards_of_existing_notes_default(), ) return note_type From 5830a52977b2d6f79360f1103233094a85e00d9f Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 17:59:54 +0200 Subject: [PATCH 083/105] Truncate long deck names --- ankihub/gui/decks_dialog.py | 11 +++++++---- ankihub/main/utils.py | 5 +++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3c3f20409..7d65ccdc5 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -28,6 +28,7 @@ from ..gui.operations.deck_creation import create_collaborative_deck from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall from ..main.subdecks import SUBDECK_TAG, deck_contains_subdeck_tags +from ..main.utils import truncate_string from ..settings import config, url_deck_base, url_decks from .operations.subdecks import confirm_and_toggle_subdecks from .utils import ask_user, clear_layout, tooltip_icon @@ -134,7 +135,9 @@ def _refresh_box_bottom_right(self) -> None: self.box_bottom_right.addLayout(self.box_deck_actions) deck_name = config.deck_config(selected_ah_did).name - self.deck_name_label = QLabel(f"

{deck_name}

") + self.deck_name_label = QLabel( + f"

{truncate_string(deck_name, limit=70)}

" + ) self.deck_name_label.setWordWrap(True) self.deck_name_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred @@ -240,14 +243,14 @@ def _refresh_box_bottom_right(self) -> None: def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) destination_anki_did = deck_config.anki_id - if name := aqt.mw.col.decks.name_if_exists(destination_anki_did): + if deck_name := aqt.mw.col.decks.name_if_exists(destination_anki_did): self.new_cards_destination_details_label.setText( - f"New cards are saved to: {name}." + f"New cards are saved to: {truncate_string(deck_name, limit=90)}." ) else: # If the deck doesn't exist, it will be re-created on next sync with the name from the config. self.new_cards_destination_details_label.setText( - f"New cards are saved to: {deck_config.name}." + f"New cards are saved to: {truncate_string(deck_config.name, limit=90)}." ) def _refresh_decks_list(self) -> None: diff --git a/ankihub/main/utils.py b/ankihub/main/utils.py index 9ec91ff72..db9f2ba05 100644 --- a/ankihub/main/utils.py +++ b/ankihub/main/utils.py @@ -479,3 +479,8 @@ def md5_file_hash(media_path: Path) -> str: file_content_hash = hashlib.md5(media_file.read()) result = file_content_hash.hexdigest() return result + + +def truncate_string(string: str, limit: int) -> str: + assert limit > 0 + return string[:limit] + "..." if len(string) > limit else string From 78a14a3130da0cf6e74aa2dbe81672b93da2f54d Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 18:12:13 +0200 Subject: [PATCH 084/105] Fix json serialization of `DeckConfig` --- ankihub/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/settings.py b/ankihub/settings.py index 895da44a1..edfb0220e 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -90,7 +90,7 @@ class DeckConfig(DataClassJSONMixin): False # whether deck is organized into subdecks by the add-on ) suspend_new_cards_of_new_notes: bool = False - suspend_new_cards_of_existing_notes = ( + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes = ( SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED ) From a0f5d2c039305bebf5cda1af2be2983c73cb4e56 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 19:24:47 +0200 Subject: [PATCH 085/105] Adjust subdecks tooltip message --- ankihub/gui/decks_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 7d65ccdc5..2f13a6fa7 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -176,8 +176,8 @@ def _refresh_box_bottom_right(self) -> None: self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) subdecks_tooltip_message = ( - "Toggle between the deck being organized into subdecks or not.
" - f"This option is only available if notes in the deck have {SUBDECK_TAG} tags." + "Activates organizing the deck into subdecks. " + f"Applies only to decks with {SUBDECK_TAG} tags." ) self.subdecks_cb = QCheckBox("Enable Subdecks") self.subdecks_cb.setToolTip(subdecks_tooltip_message) From 5ffbd011b18d7eef776a747dad423c912cce39b5 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 20:03:30 +0200 Subject: [PATCH 086/105] Add tooltip for `suspend_new_cards_of_new_notes` checkbox --- ankihub/gui/decks_dialog.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 0d4c52953..14464fcc6 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -212,9 +212,26 @@ def _refresh_box_bottom_right(self) -> None: self.box_deck_settings_elements.addSpacing(10) # ... Suspend new cards of new notes + self.suspend_new_cards_of_new_notes_row = QHBoxLayout() + self.box_deck_settings_elements.addLayout( + self.suspend_new_cards_of_new_notes_row + ) + self.suspend_new_cards_of_new_notes_cb = QCheckBox( "Suspend new cards of new notes" ) + self.suspend_new_cards_of_new_notes_row.addWidget( + self.suspend_new_cards_of_new_notes_cb + ) + + suspend_new_cards_of_new_notes_tooltip_message = ( + "Will automatically suspend all
" + "the cards of new notes added to
" + "the deck in future updates." + ) + self.suspend_new_cards_of_new_notes_cb.setToolTip( + suspend_new_cards_of_new_notes_tooltip_message + ) self.suspend_new_cards_of_new_notes_cb.setChecked( deck_config.suspend_new_cards_of_new_notes ) @@ -224,8 +241,16 @@ def _refresh_box_bottom_right(self) -> None: selected_ah_did, self.suspend_new_cards_of_new_notes_cb.isChecked() ), ) - self.box_deck_settings_elements.addWidget( - self.suspend_new_cards_of_new_notes_cb + + self.suspend_new_cards_of_new_notes_cb_icon_label = QLabel() + self.suspend_new_cards_of_new_notes_cb_icon_label.setPixmap( + tooltip_icon().pixmap(16, 16) + ) + self.suspend_new_cards_of_new_notes_cb_icon_label.setToolTip( + suspend_new_cards_of_new_notes_tooltip_message + ) + self.suspend_new_cards_of_new_notes_row.addWidget( + self.suspend_new_cards_of_new_notes_cb_icon_label ) self.box_deck_settings_elements.addSpacing(10) From 0b6c0529ed3ff94f980ac41e7650537247f344f5 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 20:11:47 +0200 Subject: [PATCH 087/105] Add tooltip for `suspend_new_cards_of_existing_notes` --- ankihub/gui/decks_dialog.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 14464fcc6..e7c2e7615 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -181,12 +181,37 @@ def _refresh_box_bottom_right(self) -> None: self.box_suspend_new_cards_of_existing_notes ) + self.suspend_new_cards_of_existing_notes_row = QHBoxLayout() + self.box_suspend_new_cards_of_existing_notes.addLayout( + self.suspend_new_cards_of_existing_notes_row + ) + + suspend_cards_of_existing_notes_tooltip_message = ( + "Will automatically suspend
" + "the cards of existing notes in
" + "the deck in future updates
" + "according to the chosen option." + ) self.suspend_new_cards_of_existing_notes_label = QLabel( "Suspend new cards of existing notes" ) - self.box_suspend_new_cards_of_existing_notes.addWidget( + self.suspend_new_cards_of_existing_notes_row.addWidget( self.suspend_new_cards_of_existing_notes_label ) + self.suspend_new_cards_of_existing_notes_label.setToolTip( + suspend_cards_of_existing_notes_tooltip_message + ) + + self.suspend_new_cards_of_existing_notes_cb_icon_label = QLabel() + self.suspend_new_cards_of_existing_notes_row.addWidget( + self.suspend_new_cards_of_existing_notes_cb_icon_label + ) + self.suspend_new_cards_of_existing_notes_cb_icon_label.setPixmap( + tooltip_icon().pixmap(16, 16) + ) + self.suspend_new_cards_of_existing_notes_cb_icon_label.setToolTip( + suspend_cards_of_existing_notes_tooltip_message + ) self.suspend_new_cards_of_existing_notes = QComboBox() self.box_suspend_new_cards_of_existing_notes.addWidget( From a4a3789bb54f239359849c288d33e06031cff5cf Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Wed, 18 Oct 2023 20:25:10 +0200 Subject: [PATCH 088/105] Extract ui setup for suspension options into methods --- ankihub/gui/decks_dialog.py | 196 +++++++++++++++++++----------------- 1 file changed, 104 insertions(+), 92 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index e7c2e7615..df7802f94 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -7,6 +7,7 @@ from anki.collection import OpChanges from aqt import gui_hooks from aqt.qt import ( + QBoxLayout, QCheckBox, QComboBox, QDialog, @@ -174,17 +175,103 @@ def _refresh_box_bottom_right(self) -> None: self.box_deck_settings.addLayout(self.box_deck_settings_elements) # ... Suspend new cards of existing notes - deck_config = config.deck_config(selected_ah_did) - - self.box_suspend_new_cards_of_existing_notes = QVBoxLayout() + self.box_suspend_new_cards_of_existing_notes = ( + self._setup_suspend_new_cards_of_existing_notes(selected_ah_did) + ) self.box_deck_settings_elements.addLayout( self.box_suspend_new_cards_of_existing_notes ) - self.suspend_new_cards_of_existing_notes_row = QHBoxLayout() - self.box_suspend_new_cards_of_existing_notes.addLayout( - self.suspend_new_cards_of_existing_notes_row + self.box_deck_settings_elements.addSpacing(10) + + # ... Suspend new cards of new notes + self.box_suspend_new_cards_of_new_notes = ( + self._setup_suspend_new_cards_of_new_notes(selected_ah_did) + ) + self.box_deck_settings_elements.addLayout( + self.box_suspend_new_cards_of_new_notes + ) + + self.box_deck_settings_elements.addSpacing(10) + + # ... Subdecks + self.subdecks_cb_row = QHBoxLayout() + self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) + + subdecks_tooltip_message = ( + "Activates organizing the deck into subdecks. " + f"Applies only to decks with {SUBDECK_TAG} tags." + ) + self.subdecks_cb = QCheckBox("Enable Subdecks") + self.subdecks_cb.setToolTip(subdecks_tooltip_message) + self._refresh_subdecks_checkbox() + qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) + self.subdecks_cb_row.addWidget(self.subdecks_cb) + + self.subdeck_cb_icon_label = QLabel() + self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) + self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) + self.subdecks_cb_row.addWidget(self.subdeck_cb_icon_label) + + self.subdecks_docs_link_label = QLabel( + """ + + More about subdecks + + """ + ) + self.subdecks_docs_link_label.setOpenExternalLinks(True) + self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) + + self.box_bottom_right.addSpacing(20) + + # Destination for new cards + self.box_new_cards_destination = QVBoxLayout() + self.box_bottom_right.addLayout(self.box_new_cards_destination) + + self.new_cards_destination = QLabel("Destination for New Cards") + self.box_new_cards_destination.addWidget(self.new_cards_destination) + + self.new_cards_destination_details_label = QLabel() + self.new_cards_destination_details_label.setWordWrap(True) + self._refresh_new_cards_destination_details_label(selected_ah_did) + self.box_new_cards_destination.addWidget( + self.new_cards_destination_details_label + ) + + self.set_new_cards_destination_btn = QPushButton( + "Change Destination for New Cards" + ) + qconnect( + self.set_new_cards_destination_btn.clicked, + self._on_new_cards_destination_btn_clicked, + ) + self.box_new_cards_destination.addWidget(self.set_new_cards_destination_btn) + self.box_new_cards_destination.addSpacing(7) + + self.new_cards_destination_docs_link_label = QLabel( + """ + + More about destinations for new cards + + """ ) + self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) + self.box_new_cards_destination.addWidget( + self.new_cards_destination_docs_link_label + ) + + self.box_bottom_right.addStretch() + + def _setup_suspend_new_cards_of_existing_notes( + self, selected_ah_did: uuid.UUID + ) -> QBoxLayout: + deck_config = config.deck_config(selected_ah_did) + + box = QVBoxLayout() + + self.suspend_new_cards_of_existing_notes_row = QHBoxLayout() + box.addLayout(self.suspend_new_cards_of_existing_notes_row) suspend_cards_of_existing_notes_tooltip_message = ( "Will automatically suspend
" @@ -214,9 +301,7 @@ def _refresh_box_bottom_right(self) -> None: ) self.suspend_new_cards_of_existing_notes = QComboBox() - self.box_suspend_new_cards_of_existing_notes.addWidget( - self.suspend_new_cards_of_existing_notes - ) + box.addWidget(self.suspend_new_cards_of_existing_notes) self.suspend_new_cards_of_existing_notes.insertItems( 0, [option.value for option in SuspendNewCardsOfExistingNotes] @@ -233,21 +318,20 @@ def _refresh_box_bottom_right(self) -> None: ), ), ) + return box - self.box_deck_settings_elements.addSpacing(10) + def _setup_suspend_new_cards_of_new_notes( + self, + selected_ah_did: uuid.UUID, + ) -> QBoxLayout: + deck_config = config.deck_config(selected_ah_did) - # ... Suspend new cards of new notes - self.suspend_new_cards_of_new_notes_row = QHBoxLayout() - self.box_deck_settings_elements.addLayout( - self.suspend_new_cards_of_new_notes_row - ) + box = QHBoxLayout() self.suspend_new_cards_of_new_notes_cb = QCheckBox( "Suspend new cards of new notes" ) - self.suspend_new_cards_of_new_notes_row.addWidget( - self.suspend_new_cards_of_new_notes_cb - ) + box.addWidget(self.suspend_new_cards_of_new_notes_cb) suspend_new_cards_of_new_notes_tooltip_message = ( "Will automatically suspend all
" @@ -274,80 +358,8 @@ def _refresh_box_bottom_right(self) -> None: self.suspend_new_cards_of_new_notes_cb_icon_label.setToolTip( suspend_new_cards_of_new_notes_tooltip_message ) - self.suspend_new_cards_of_new_notes_row.addWidget( - self.suspend_new_cards_of_new_notes_cb_icon_label - ) - - self.box_deck_settings_elements.addSpacing(10) - - # ... Subdecks - self.subdecks_cb_row = QHBoxLayout() - self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) - - subdecks_tooltip_message = ( - "Activates organizing the deck into subdecks. " - f"Applies only to decks with {SUBDECK_TAG} tags." - ) - self.subdecks_cb = QCheckBox("Enable Subdecks") - self.subdecks_cb.setToolTip(subdecks_tooltip_message) - self._refresh_subdecks_checkbox() - qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) - self.subdecks_cb_row.addWidget(self.subdecks_cb) - - self.subdeck_cb_icon_label = QLabel() - self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) - self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) - self.subdecks_cb_row.addWidget(self.subdeck_cb_icon_label) - - self.subdecks_docs_link_label = QLabel( - """ - - More about subdecks - - """ - ) - self.subdecks_docs_link_label.setOpenExternalLinks(True) - self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) - - self.box_bottom_right.addSpacing(20) - - # Destination for new cards - self.box_new_cards_destination = QVBoxLayout() - self.box_bottom_right.addLayout(self.box_new_cards_destination) - - self.new_cards_destination = QLabel("Destination for New Cards") - self.box_new_cards_destination.addWidget(self.new_cards_destination) - - self.new_cards_destination_details_label = QLabel() - self.new_cards_destination_details_label.setWordWrap(True) - self._refresh_new_cards_destination_details_label(selected_ah_did) - self.box_new_cards_destination.addWidget( - self.new_cards_destination_details_label - ) - - self.set_new_cards_destination_btn = QPushButton( - "Change Destination for New Cards" - ) - qconnect( - self.set_new_cards_destination_btn.clicked, - self._on_new_cards_destination_btn_clicked, - ) - self.box_new_cards_destination.addWidget(self.set_new_cards_destination_btn) - self.box_new_cards_destination.addSpacing(7) - - self.new_cards_destination_docs_link_label = QLabel( - """ - - More about destinations for new cards - - """ - ) - self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) - self.box_new_cards_destination.addWidget( - self.new_cards_destination_docs_link_label - ) - - self.box_bottom_right.addStretch() + box.addWidget(self.suspend_new_cards_of_new_notes_cb_icon_label) + return box def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) From a73e45a04eded2ee6179bd373ec00ff00bd62ab9 Mon Sep 17 00:00:00 2001 From: Jakub Fidler <31575114+RisingOrange@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:58:23 +0200 Subject: [PATCH 089/105] =?UTF-8?q?feat:=20Define=20defaults=20for=C2=A0th?= =?UTF-8?q?e=20Suspend=20new=20cards=20of=20new=20notes=20option=20(#776)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add card-suspension options to deck config * Set `suspend_new_cards_of_new_notes=True` when installing AnKing deck * Add migration to set `suspend_new_cards_of_new_notes=True` for installed AnKing deck * Use enum for `suspend_new_cards_of_existing_notes` --- ankihub/private_config_migrations.py | 20 ++++++++++++++++++++ ankihub/settings.py | 11 +++++++++++ 2 files changed, 31 insertions(+) diff --git a/ankihub/private_config_migrations.py b/ankihub/private_config_migrations.py index 9b898d32c..57dff77a3 100644 --- a/ankihub/private_config_migrations.py +++ b/ankihub/private_config_migrations.py @@ -21,6 +21,9 @@ def migrate_private_config(private_config_dict: Dict) -> None: """ maybe_rename_ankihub_deck_uuid_to_ah_did(private_config_dict) maybe_reset_media_update_timestamps(private_config_dict) + maybe_set_suspend_new_cards_of_new_notes_to_true_for_anking_deck( + private_config_dict + ) def maybe_reset_media_update_timestamps(private_config_dict: Dict) -> None: @@ -49,6 +52,23 @@ def maybe_rename_ankihub_deck_uuid_to_ah_did(private_config_dict: Dict) -> None: ) +def maybe_set_suspend_new_cards_of_new_notes_to_true_for_anking_deck( + private_config_dict: Dict, +) -> None: + """Set suspend_new_cards_of_new_notes to True in the DeckConfig of the AnKing deck if the field + doesn't exist yet.""" + from .settings import ANKING_DECK_ID + + field_name = "suspend_new_cards_of_new_notes" + decks = private_config_dict["decks"] + for ah_did, deck in decks.items(): + if ah_did == ANKING_DECK_ID and deck.get(field_name) is None: + deck[field_name] = True + LOGGER.info( + f"Set {field_name} to True for the previously installed AnKing deck." + ) + + def _is_api_version_on_last_sync_below_threshold( private_config_dict: Dict, version_threshold: float ) -> bool: diff --git a/ankihub/settings.py b/ankihub/settings.py index 904de0408..e09f8fabc 100644 --- a/ankihub/settings.py +++ b/ankihub/settings.py @@ -61,6 +61,12 @@ def _deserialize_datetime(x: str) -> Optional[datetime]: return datetime.strptime(x, ANKIHUB_DATETIME_FORMAT_STR) if x else None +class SuspendNewCardsOfExistingNotes(Enum): + ALWAYS = "Always" + NEVER = "Never" + IF_SIBLINGS_SUSPENDED = "If siblings are suspended" + + @dataclass class DeckConfig(DataClassJSONMixin): anki_id: DeckId @@ -83,6 +89,10 @@ class DeckConfig(DataClassJSONMixin): subdecks_enabled: bool = ( False # whether deck is organized into subdecks by the add-on ) + suspend_new_cards_of_new_notes: bool = False + suspend_new_cards_of_existing_notes: SuspendNewCardsOfExistingNotes = ( + SuspendNewCardsOfExistingNotes.IF_SIBLINGS_SUSPENDED + ) @dataclass @@ -231,6 +241,7 @@ def add_deck( anki_id=DeckId(anki_did), user_relation=user_relation, subdecks_enabled=subdecks_enabled, + suspend_new_cards_of_new_notes=ankihub_did == ANKING_DECK_ID, ) # remove duplicates self.save_latest_deck_update(ankihub_did, latest_udpate) From e84964c955e177406d1259ad2a46bf2040b9b976 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 17:06:50 +0200 Subject: [PATCH 090/105] Adjust spacing --- ankihub/gui/decks_dialog.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 2f13a6fa7..f936f9c70 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -58,27 +58,39 @@ def _setup_ui(self): self.box_main = QVBoxLayout() self.setLayout(self.box_main) + self.box_main.addSpacing(10) + self.box_top = self._setup_box_top() self.box_main.addLayout(self.box_top) + self.box_main.addSpacing(20) + self.box_bottom = QHBoxLayout() self.box_main.addLayout(self.box_bottom) + self.box_bottom.addSpacing(10) + self.box_bottom_left = self._setup_box_bottom_left() + self.box_bottom_left.addSpacing(10) self.box_bottom.addLayout(self.box_bottom_left) self.box_bottom.addSpacing(10) self.box_bottom_right = QVBoxLayout() self._refresh_box_bottom_right() + self.box_bottom_right.addSpacing(10) self.box_bottom.addLayout(self.box_bottom_right) + self.box_bottom.addSpacing(10) + def _setup_box_top(self) -> QVBoxLayout: box = QVBoxLayout() self.box_top_buttons = QHBoxLayout() box.addLayout(self.box_top_buttons) + self.box_top_buttons.addSpacing(10) + self.browse_btn = QPushButton("🔗 Browse Decks") self.browse_btn.setStyleSheet("color: white; background-color: #306bec") @@ -89,7 +101,7 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons.addWidget(self.create_btn) qconnect(self.create_btn.clicked, create_collaborative_deck) - box.addSpacing(10) + self.box_top_buttons.addSpacing(10) return box From abd02fcc67b1d53f8961efaddb073af9ef214bd2 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 17:13:50 +0200 Subject: [PATCH 091/105] Adjust spacing --- ankihub/gui/decks_dialog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index f936f9c70..78d11a2b4 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -97,6 +97,8 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) + self.box_top_buttons.addSpacing(10) + self.create_btn = QPushButton("➕ Create Collaborative Deck") self.box_top_buttons.addWidget(self.create_btn) qconnect(self.create_btn.clicked, create_collaborative_deck) From 9aab76c2ecb9c7f25046707bcf638ee403f70ff1 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 17:38:41 +0200 Subject: [PATCH 092/105] Change copy to "Create AnkiHub Deck" --- ankihub/gui/decks_dialog.py | 2 +- ankihub/gui/menu.py | 2 +- ankihub/gui/operations/deck_creation.py | 8 ++++---- ankihub/main/deck_creation.py | 2 +- tests/addon/test_unit.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 78d11a2b4..42d2443fb 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -99,7 +99,7 @@ def _setup_box_top(self) -> QVBoxLayout: self.box_top_buttons.addSpacing(10) - self.create_btn = QPushButton("➕ Create Collaborative Deck") + self.create_btn = QPushButton("➕ Create AnkiHub Deck") self.box_top_buttons.addWidget(self.create_btn) qconnect(self.create_btn.clicked, create_collaborative_deck) diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py index 83b90814b..50d2de8e6 100644 --- a/ankihub/gui/menu.py +++ b/ankihub/gui/menu.py @@ -186,7 +186,7 @@ def display_login(cls): def _create_collaborative_deck_setup(parent: QMenu): - q_action = QAction("🛠️ Create Collaborative Deck", parent=parent) + q_action = QAction("🛠️ Create AnkiHub Deck", parent=parent) qconnect(q_action.triggered, create_collaborative_deck) parent.addAction(q_action) diff --git a/ankihub/gui/operations/deck_creation.py b/ankihub/gui/operations/deck_creation.py index 03585e927..ab885f703 100644 --- a/ankihub/gui/operations/deck_creation.py +++ b/ankihub/gui/operations/deck_creation.py @@ -17,7 +17,7 @@ def create_collaborative_deck() -> None: - """Creates a new collaborative deck and uploads it to AnkiHub. + """Creates a new AnkiHub deck and uploads it to AnkiHub. Asks the user to confirm, choose a deck to upload and for some additional options, and then uploads the deck to AnkiHub. @@ -131,8 +131,8 @@ def on_failure(exc: Exception): ), success=on_success, ).failure(on_failure) - LOGGER.info("Instantiated QueryOp for creating collaborative deck") - op.with_progress(label="Creating collaborative deck").run_in_background() + LOGGER.info("Instantiated QueryOp for creating an AnkiHub deck") + op.with_progress(label="Creating AnkiHub deck").run_in_background() class DeckCreationConfirmationDialog(QMessageBox): @@ -142,7 +142,7 @@ def __init__(self): self.setWindowTitle("Confirm AnkiHub Deck Creation") self.setIcon(QMessageBox.Icon.Question) self.setText( - "Are you sure you want to create a new collaborative deck?


" + "Are you sure you want to create a new AnkiHub deck?


" 'Terms of use: https://www.ankihub.net/terms
' 'Privacy Policy: https://www.ankihub.net/privacy
', ) diff --git a/ankihub/main/deck_creation.py b/ankihub/main/deck_creation.py index c3b0f659a..a99eb9b9c 100644 --- a/ankihub/main/deck_creation.py +++ b/ankihub/main/deck_creation.py @@ -37,7 +37,7 @@ def create_ankihub_deck( private: bool, add_subdeck_tags: bool = False, ) -> DeckCreationResult: - LOGGER.info("Creating collaborative deck") + LOGGER.info("Creating AnkiHub deck") create_backup() diff --git a/tests/addon/test_unit.py b/tests/addon/test_unit.py index c9413c12b..5b92017af 100644 --- a/tests/addon/test_unit.py +++ b/tests/addon/test_unit.py @@ -1557,7 +1557,7 @@ def raise_exception(*args, **kwargs) -> None: ) showInfo_mock = mock_function(deck_creation, "showInfo") - # Create the collaborative deck. + # Create the AnkiHub deck. if creating_deck_fails: create_collaborative_deck() qtbot.wait(500) From ace182b03e0091b4a53952296d3d27952846d8a0 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 19:47:37 +0200 Subject: [PATCH 093/105] ref: Reorganize ui setup statements --- ankihub/gui/decks_dialog.py | 48 ++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 42d2443fb..4834bb3b8 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -56,70 +56,68 @@ def _setup_ui(self): self.setMinimumHeight(450) self.box_main = QVBoxLayout() - self.setLayout(self.box_main) - - self.box_main.addSpacing(10) + # Set up the top layout and add it to the main layout self.box_top = self._setup_box_top() + self.box_main.addSpacing(10) self.box_main.addLayout(self.box_top) - self.box_main.addSpacing(20) self.box_bottom = QHBoxLayout() - self.box_main.addLayout(self.box_bottom) - - self.box_bottom.addSpacing(10) + # Set up the bottom-left layout and add it to the bottom layout self.box_bottom_left = self._setup_box_bottom_left() self.box_bottom_left.addSpacing(10) - self.box_bottom.addLayout(self.box_bottom_left) - self.box_bottom.addSpacing(10) + self.box_bottom.addLayout(self.box_bottom_left) + # Set up the bottom-right layout and add it to the bottom layout self.box_bottom_right = QVBoxLayout() self._refresh_box_bottom_right() self.box_bottom_right.addSpacing(10) + self.box_bottom.addSpacing(10) self.box_bottom.addLayout(self.box_bottom_right) - self.box_bottom.addSpacing(10) - def _setup_box_top(self) -> QVBoxLayout: - box = QVBoxLayout() + self.box_main.addLayout(self.box_bottom) - self.box_top_buttons = QHBoxLayout() - box.addLayout(self.box_top_buttons) + self.setLayout(self.box_main) - self.box_top_buttons.addSpacing(10) + def _setup_box_top(self) -> QVBoxLayout: + self.box_top_buttons = QHBoxLayout() + # Set up the browse button, connect its signal, and add it to the top buttons layout self.browse_btn = QPushButton("🔗 Browse Decks") self.browse_btn.setStyleSheet("color: white; background-color: #306bec") - - self.box_top_buttons.addWidget(self.browse_btn) qconnect(self.browse_btn.clicked, lambda: openLink(url_decks())) - + self.box_top_buttons.addSpacing(10) + self.box_top_buttons.addWidget(self.browse_btn) self.box_top_buttons.addSpacing(10) + # Set up the create button, connect its signal, and add it to the top buttons layout self.create_btn = QPushButton("➕ Create AnkiHub Deck") - self.box_top_buttons.addWidget(self.create_btn) qconnect(self.create_btn.clicked, create_collaborative_deck) - + self.box_top_buttons.addWidget(self.create_btn) self.box_top_buttons.addSpacing(10) + box = QVBoxLayout() + box.addLayout(self.box_top_buttons) + return box def _setup_box_bottom_left(self) -> QVBoxLayout: - box = QVBoxLayout() self.decks_list_label = QLabel("Subscribed AnkiHub Decks") self.decks_list_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) - box.addWidget(self.decks_list_label) - - box.addSpacing(5) self.decks_list = QListWidget() - box.addWidget(self.decks_list) qconnect(self.decks_list.itemSelectionChanged, self._refresh_box_bottom_right) + + box = QVBoxLayout() + box.addWidget(self.decks_list_label) + box.addSpacing(5) + box.addWidget(self.decks_list) return box def _refresh_box_bottom_right(self) -> None: From 97b4bc82be7a3e4f9310193f48b3438604de6c1c Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 19:53:11 +0200 Subject: [PATCH 094/105] ref: Remove unused `_refresh_anki` method --- ankihub/gui/decks_dialog.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 4834bb3b8..ba0f5e461 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -4,8 +4,6 @@ from uuid import UUID import aqt -from anki.collection import OpChanges -from aqt import gui_hooks from aqt.qt import ( QCheckBox, QDialog, @@ -292,14 +290,6 @@ def _selected_ah_did(self) -> Optional[UUID]: result = selection[0].data(Qt.ItemDataRole.UserRole) return result - def _refresh_anki(self) -> None: - op = OpChanges() - op.deck = True - op.browser_table = True - op.browser_sidebar = True - op.study_queues = True - gui_hooks.operation_did_execute(op, handler=None) - def _select_deck(self, ah_did: uuid.UUID): deck_item = next( ( From 289178e0560d7e6a1ce18543eaf968d3ad33f8d0 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 22:57:40 +0200 Subject: [PATCH 095/105] Fix suspend new cards of new notes option --- ankihub/gui/decks_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 82ce5149b..34f157e3a 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -317,7 +317,7 @@ def _setup_suspend_new_cards_of_existing_notes( 0, [option.value for option in SuspendNewCardsOfExistingNotes] ) self.suspend_new_cards_of_existing_notes.setCurrentText( - deck_config.suspend_new_cards_of_existing_notes.name + deck_config.suspend_new_cards_of_existing_notes.value ) qconnect( self.suspend_new_cards_of_existing_notes.currentTextChanged, From 97f817ba32fe906d30b26fc1c8f9205bccd45bc9 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:26:47 +0200 Subject: [PATCH 096/105] Extract `_setup_box_deck_actions` --- ankihub/gui/decks_dialog.py | 64 +++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index ba0f5e461..3050b2ec8 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -118,6 +118,41 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: box.addWidget(self.decks_list) return box + def _setup_box_deck_actions(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: + # Initialize and setup the deck name label + deck_name = config.deck_config(selected_ah_did).name + self.deck_name_label = QLabel( + f"

{truncate_string(deck_name, limit=70)}

" + ) + self.deck_name_label.setWordWrap(True) + self.deck_name_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) + + # Initialize and setup the open web button + self.open_web_btn = QPushButton("Open on AnkiHub") + qconnect(self.open_web_btn.clicked, self._on_open_web) + + # Initialize and setup the unsubscribe button + self.unsubscribe_btn = QPushButton("Unsubscribe") + if theme_manager.night_mode: + self.unsubscribe_btn.setStyleSheet("color: #e29792") + else: + self.unsubscribe_btn.setStyleSheet("color: #e2857f") + qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) + + # Add widgets to the action buttons layout + self.box_deck_action_buttons = QHBoxLayout() + self.box_deck_action_buttons.addWidget(self.open_web_btn) + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) + + # Add everything to the main deck actions layout + box = QVBoxLayout() + box.addWidget(self.deck_name_label) + box.addLayout(self.box_deck_action_buttons) + + return box + def _refresh_box_bottom_right(self) -> None: clear_layout(self.box_bottom_right) @@ -141,35 +176,8 @@ def _refresh_box_bottom_right(self) -> None: return # Deck Actions - self.box_deck_actions = QVBoxLayout() + self.box_deck_actions = self._setup_box_deck_actions(selected_ah_did) self.box_bottom_right.addLayout(self.box_deck_actions) - - deck_name = config.deck_config(selected_ah_did).name - self.deck_name_label = QLabel( - f"

{truncate_string(deck_name, limit=70)}

" - ) - self.deck_name_label.setWordWrap(True) - self.deck_name_label.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred - ) - self.box_deck_actions.addWidget(self.deck_name_label) - - self.box_deck_action_buttons = QHBoxLayout() - self.box_deck_actions.addLayout(self.box_deck_action_buttons) - - self.open_web_btn = QPushButton("Open on AnkiHub") - self.box_deck_action_buttons.addWidget(self.open_web_btn) - qconnect(self.open_web_btn.clicked, self._on_open_web) - - self.unsubscribe_btn = QPushButton("Unsubscribe") - if theme_manager.night_mode: - self.unsubscribe_btn.setStyleSheet("color: #e29792") - else: - self.unsubscribe_btn.setStyleSheet("color: #e2857f") - - self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) - qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) - self.box_bottom_right.addSpacing(20) # Deck Options From 6916e2a9f5c3bcdcef704489acbc1e8d5b06f1f1 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:33:16 +0200 Subject: [PATCH 097/105] Rename `box_deck_settings` to `box_deck_options` --- ankihub/gui/decks_dialog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 3050b2ec8..cf43a3649 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -181,14 +181,14 @@ def _refresh_box_bottom_right(self) -> None: self.box_bottom_right.addSpacing(20) # Deck Options - self.box_deck_settings = QVBoxLayout() - self.box_bottom_right.addLayout(self.box_deck_settings) + self.box_deck_options = QVBoxLayout() + self.box_bottom_right.addLayout(self.box_deck_options) self.deck_settings_label = QLabel("Deck Options") - self.box_deck_settings.addWidget(self.deck_settings_label) + self.box_deck_options.addWidget(self.deck_settings_label) self.box_deck_settings_elements = QVBoxLayout() - self.box_deck_settings.addLayout(self.box_deck_settings_elements) + self.box_deck_options.addLayout(self.box_deck_settings_elements) self.subdecks_cb_row = QHBoxLayout() self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) From 55241114c247f67c91c6bb240aed18e2f6ec20a7 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:40:17 +0200 Subject: [PATCH 098/105] Extract `_setup_deck_options` --- ankihub/gui/decks_dialog.py | 84 +++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index cf43a3649..10e5ef83e 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -181,47 +181,12 @@ def _refresh_box_bottom_right(self) -> None: self.box_bottom_right.addSpacing(20) # Deck Options - self.box_deck_options = QVBoxLayout() + self.box_deck_options = self._setup_deck_options() self.box_bottom_right.addLayout(self.box_deck_options) - self.deck_settings_label = QLabel("Deck Options") - self.box_deck_options.addWidget(self.deck_settings_label) - - self.box_deck_settings_elements = QVBoxLayout() - self.box_deck_options.addLayout(self.box_deck_settings_elements) - - self.subdecks_cb_row = QHBoxLayout() - self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) - - subdecks_tooltip_message = ( - "Activates organizing the deck into subdecks. " - f"Applies only to decks with {SUBDECK_TAG} tags." - ) - self.subdecks_cb = QCheckBox("Enable Subdecks") - self.subdecks_cb.setToolTip(subdecks_tooltip_message) - self._refresh_subdecks_checkbox() - qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) - self.subdecks_cb_row.addWidget(self.subdecks_cb) - - self.subdeck_cb_icon_label = QLabel() - self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) - self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) - self.subdecks_cb_row.addWidget(self.subdeck_cb_icon_label) - - self.subdecks_docs_link_label = QLabel( - """ - - More about subdecks - - """ - ) - self.subdecks_docs_link_label.setOpenExternalLinks(True) - self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) - - self.box_bottom_right.addSpacing(20) - # Destination for new cards self.box_new_cards_destination = QVBoxLayout() + self.box_bottom_right.addSpacing(20) self.box_bottom_right.addLayout(self.box_new_cards_destination) self.new_cards_destination = QLabel("Destination for New Cards") @@ -258,6 +223,51 @@ def _refresh_box_bottom_right(self) -> None: self.box_bottom_right.addStretch() + def _setup_deck_options(self) -> QVBoxLayout: + self.deck_options_label = QLabel("Deck Options") + + # Initialize and set up the subdecks checkbox + subdecks_tooltip_message = ( + "Activates organizing the deck into subdecks. " + f"Applies only to decks with {SUBDECK_TAG} tags." + ) + self.subdecks_cb = QCheckBox("Enable Subdecks") + self.subdecks_cb.setToolTip(subdecks_tooltip_message) + self._refresh_subdecks_checkbox() + qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) + + # Initialize and set up the subdeck icon label + self.subdeck_cb_icon_label = QLabel() + self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) + self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) + + # Add widgets to the subdecks checkbox row layout + self.subdecks_cb_row = QHBoxLayout() + self.subdecks_cb_row.addWidget(self.subdecks_cb) + self.subdecks_cb_row.addWidget(self.subdeck_cb_icon_label) + + # Initialize and set up the subdecks documentation link label + self.subdecks_docs_link_label = QLabel( + """ + + More about subdecks + + """ + ) + self.subdecks_docs_link_label.setOpenExternalLinks(True) + + # Add everything to the deck settings elements layout + self.box_deck_options_elements = QVBoxLayout() + self.box_deck_options_elements.addLayout(self.subdecks_cb_row) + self.box_deck_options_elements.addWidget(self.subdecks_docs_link_label) + + # Add everything to the main deck options layout + box = QVBoxLayout() + box.addWidget(self.deck_options_label) + box.addLayout(self.box_deck_options_elements) + + return box + def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) destination_anki_did = deck_config.anki_id From 8063519bc13ef62a49ce4ed23adaca99a8d9fa82 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:49:48 +0200 Subject: [PATCH 099/105] Extract `_setup_destination_for_new_cards` --- ankihub/gui/decks_dialog.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 10e5ef83e..7ffd8cb75 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -183,22 +183,27 @@ def _refresh_box_bottom_right(self) -> None: # Deck Options self.box_deck_options = self._setup_deck_options() self.box_bottom_right.addLayout(self.box_deck_options) + self.box_bottom_right.addSpacing(20) # Destination for new cards - self.box_new_cards_destination = QVBoxLayout() - self.box_bottom_right.addSpacing(20) + self.box_new_cards_destination = self._setup_destination_for_new_cards( + selected_ah_did + ) self.box_bottom_right.addLayout(self.box_new_cards_destination) + self.box_bottom_right.addStretch() + + def _setup_destination_for_new_cards( + self, selected_ah_did: uuid.UUID + ) -> QVBoxLayout: self.new_cards_destination = QLabel("Destination for New Cards") - self.box_new_cards_destination.addWidget(self.new_cards_destination) + # Initialize and set up the destination details label self.new_cards_destination_details_label = QLabel() self.new_cards_destination_details_label.setWordWrap(True) self._refresh_new_cards_destination_details_label(selected_ah_did) - self.box_new_cards_destination.addWidget( - self.new_cards_destination_details_label - ) + # Initialize and set up the change destination button self.set_new_cards_destination_btn = QPushButton( "Change Destination for New Cards" ) @@ -206,9 +211,8 @@ def _refresh_box_bottom_right(self) -> None: self.set_new_cards_destination_btn.clicked, self._on_new_cards_destination_btn_clicked, ) - self.box_new_cards_destination.addWidget(self.set_new_cards_destination_btn) - self.box_new_cards_destination.addSpacing(7) + # Initialize and set up the documentation link label self.new_cards_destination_docs_link_label = QLabel( """ @@ -217,11 +221,16 @@ def _refresh_box_bottom_right(self) -> None: """ ) self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) - self.box_new_cards_destination.addWidget( - self.new_cards_destination_docs_link_label - ) - self.box_bottom_right.addStretch() + # Add everything to the result layout + box = QVBoxLayout() + box.addWidget(self.new_cards_destination) + box.addWidget(self.new_cards_destination_details_label) + box.addWidget(self.set_new_cards_destination_btn) + box.addSpacing(5) + box.addWidget(self.new_cards_destination_docs_link_label) + + return box def _setup_deck_options(self) -> QVBoxLayout: self.deck_options_label = QLabel("Deck Options") From d3a7ec900836aab87976d8f6fc64f996236f994b Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:50:32 +0200 Subject: [PATCH 100/105] Edit comment --- ankihub/gui/decks_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 7ffd8cb75..7efd5d6fc 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -146,7 +146,7 @@ def _setup_box_deck_actions(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: self.box_deck_action_buttons.addWidget(self.open_web_btn) self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) - # Add everything to the main deck actions layout + # Add everything to the result layout box = QVBoxLayout() box.addWidget(self.deck_name_label) box.addLayout(self.box_deck_action_buttons) @@ -270,7 +270,7 @@ def _setup_deck_options(self) -> QVBoxLayout: self.box_deck_options_elements.addLayout(self.subdecks_cb_row) self.box_deck_options_elements.addWidget(self.subdecks_docs_link_label) - # Add everything to the main deck options layout + # Add everything to the result layout box = QVBoxLayout() box.addWidget(self.deck_options_label) box.addLayout(self.box_deck_options_elements) From 09486cdc0677e42ceb12fb2ac99d1b46651dad0e Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:52:34 +0200 Subject: [PATCH 101/105] Reorder methods --- ankihub/gui/decks_dialog.py | 136 ++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 7efd5d6fc..2088130fd 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -118,41 +118,6 @@ def _setup_box_bottom_left(self) -> QVBoxLayout: box.addWidget(self.decks_list) return box - def _setup_box_deck_actions(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: - # Initialize and setup the deck name label - deck_name = config.deck_config(selected_ah_did).name - self.deck_name_label = QLabel( - f"

{truncate_string(deck_name, limit=70)}

" - ) - self.deck_name_label.setWordWrap(True) - self.deck_name_label.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred - ) - - # Initialize and setup the open web button - self.open_web_btn = QPushButton("Open on AnkiHub") - qconnect(self.open_web_btn.clicked, self._on_open_web) - - # Initialize and setup the unsubscribe button - self.unsubscribe_btn = QPushButton("Unsubscribe") - if theme_manager.night_mode: - self.unsubscribe_btn.setStyleSheet("color: #e29792") - else: - self.unsubscribe_btn.setStyleSheet("color: #e2857f") - qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) - - # Add widgets to the action buttons layout - self.box_deck_action_buttons = QHBoxLayout() - self.box_deck_action_buttons.addWidget(self.open_web_btn) - self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) - - # Add everything to the result layout - box = QVBoxLayout() - box.addWidget(self.deck_name_label) - box.addLayout(self.box_deck_action_buttons) - - return box - def _refresh_box_bottom_right(self) -> None: clear_layout(self.box_bottom_right) @@ -181,58 +146,54 @@ def _refresh_box_bottom_right(self) -> None: self.box_bottom_right.addSpacing(20) # Deck Options - self.box_deck_options = self._setup_deck_options() + self.box_deck_options = self._setup_box_deck_options() self.box_bottom_right.addLayout(self.box_deck_options) self.box_bottom_right.addSpacing(20) # Destination for new cards - self.box_new_cards_destination = self._setup_destination_for_new_cards( + self.box_new_cards_destination = self._setup_box_new_cards_destination( selected_ah_did ) self.box_bottom_right.addLayout(self.box_new_cards_destination) self.box_bottom_right.addStretch() - def _setup_destination_for_new_cards( - self, selected_ah_did: uuid.UUID - ) -> QVBoxLayout: - self.new_cards_destination = QLabel("Destination for New Cards") - - # Initialize and set up the destination details label - self.new_cards_destination_details_label = QLabel() - self.new_cards_destination_details_label.setWordWrap(True) - self._refresh_new_cards_destination_details_label(selected_ah_did) - - # Initialize and set up the change destination button - self.set_new_cards_destination_btn = QPushButton( - "Change Destination for New Cards" + def _setup_box_deck_actions(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: + # Initialize and setup the deck name label + deck_name = config.deck_config(selected_ah_did).name + self.deck_name_label = QLabel( + f"

{truncate_string(deck_name, limit=70)}

" ) - qconnect( - self.set_new_cards_destination_btn.clicked, - self._on_new_cards_destination_btn_clicked, + self.deck_name_label.setWordWrap(True) + self.deck_name_label.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) - # Initialize and set up the documentation link label - self.new_cards_destination_docs_link_label = QLabel( - """ -
- More about destinations for new cards - - """ - ) - self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) + # Initialize and setup the open web button + self.open_web_btn = QPushButton("Open on AnkiHub") + qconnect(self.open_web_btn.clicked, self._on_open_web) + + # Initialize and setup the unsubscribe button + self.unsubscribe_btn = QPushButton("Unsubscribe") + if theme_manager.night_mode: + self.unsubscribe_btn.setStyleSheet("color: #e29792") + else: + self.unsubscribe_btn.setStyleSheet("color: #e2857f") + qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) + + # Add widgets to the action buttons layout + self.box_deck_action_buttons = QHBoxLayout() + self.box_deck_action_buttons.addWidget(self.open_web_btn) + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) # Add everything to the result layout box = QVBoxLayout() - box.addWidget(self.new_cards_destination) - box.addWidget(self.new_cards_destination_details_label) - box.addWidget(self.set_new_cards_destination_btn) - box.addSpacing(5) - box.addWidget(self.new_cards_destination_docs_link_label) + box.addWidget(self.deck_name_label) + box.addLayout(self.box_deck_action_buttons) return box - def _setup_deck_options(self) -> QVBoxLayout: + def _setup_box_deck_options(self) -> QVBoxLayout: self.deck_options_label = QLabel("Deck Options") # Initialize and set up the subdecks checkbox @@ -277,6 +238,45 @@ def _setup_deck_options(self) -> QVBoxLayout: return box + def _setup_box_new_cards_destination( + self, selected_ah_did: uuid.UUID + ) -> QVBoxLayout: + self.new_cards_destination = QLabel("Destination for New Cards") + + # Initialize and set up the destination details label + self.new_cards_destination_details_label = QLabel() + self.new_cards_destination_details_label.setWordWrap(True) + self._refresh_new_cards_destination_details_label(selected_ah_did) + + # Initialize and set up the change destination button + self.set_new_cards_destination_btn = QPushButton( + "Change Destination for New Cards" + ) + qconnect( + self.set_new_cards_destination_btn.clicked, + self._on_new_cards_destination_btn_clicked, + ) + + # Initialize and set up the documentation link label + self.new_cards_destination_docs_link_label = QLabel( + """ + + More about destinations for new cards + + """ + ) + self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) + + # Add everything to the result layout + box = QVBoxLayout() + box.addWidget(self.new_cards_destination) + box.addWidget(self.new_cards_destination_details_label) + box.addWidget(self.set_new_cards_destination_btn) + box.addSpacing(5) + box.addWidget(self.new_cards_destination_docs_link_label) + + return box + def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) destination_anki_did = deck_config.anki_id From 290b815fc8a8b4f5ac3ad5f7af5aea1d95ce7d18 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 19 Oct 2023 23:54:00 +0200 Subject: [PATCH 102/105] Reorder statements --- ankihub/gui/decks_dialog.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 2088130fd..912ff2501 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -125,18 +125,16 @@ def _refresh_box_bottom_right(self) -> None: selected_ah_did = self._selected_ah_did() if selected_ah_did is None: - - self.box_no_deck_selected = QHBoxLayout() - self.box_bottom_right.addLayout(self.box_no_deck_selected) - - self.box_no_deck_selected.addSpacing(5) - self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") self.no_deck_selected_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) + + self.box_no_deck_selected = QHBoxLayout() + self.box_no_deck_selected.addSpacing(5) self.box_no_deck_selected.addWidget(self.no_deck_selected_label) + self.box_bottom_right.addLayout(self.box_no_deck_selected) self.box_bottom_right.addStretch(1) return From 511751b1f2d5a36a9a9b1b4b89ca243a26255794 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Fri, 20 Oct 2023 00:22:54 +0200 Subject: [PATCH 103/105] Merge branch 'feat/redesign-decks-dialog' into feat/make-suspension-settings-per-deck --- ankihub/gui/decks_dialog.py | 226 ++++++++++++++++++++---------------- 1 file changed, 129 insertions(+), 97 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 34f157e3a..7ee4c5bdc 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -127,25 +127,39 @@ def _refresh_box_bottom_right(self) -> None: selected_ah_did = self._selected_ah_did() if selected_ah_did is None: - - self.box_no_deck_selected = QHBoxLayout() - self.box_bottom_right.addLayout(self.box_no_deck_selected) - - self.box_no_deck_selected.addSpacing(5) - self.no_deck_selected_label = QLabel("Choose deck to adjust settings.") self.no_deck_selected_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) + + self.box_no_deck_selected = QHBoxLayout() + self.box_no_deck_selected.addSpacing(5) self.box_no_deck_selected.addWidget(self.no_deck_selected_label) + self.box_bottom_right.addLayout(self.box_no_deck_selected) self.box_bottom_right.addStretch(1) return # Deck Actions - self.box_deck_actions = QVBoxLayout() + self.box_deck_actions = self._setup_box_deck_actions(selected_ah_did) self.box_bottom_right.addLayout(self.box_deck_actions) + self.box_bottom_right.addSpacing(20) + # Deck Options + self.box_deck_options = self._setup_box_deck_options(selected_ah_did) + self.box_bottom_right.addLayout(self.box_deck_options) + self.box_bottom_right.addSpacing(20) + + # Destination for new cards + self.box_new_cards_destination = self._setup_box_new_cards_destination( + selected_ah_did + ) + self.box_bottom_right.addLayout(self.box_new_cards_destination) + + self.box_bottom_right.addStretch() + + def _setup_box_deck_actions(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: + # Initialize and setup the deck name label deck_name = config.deck_config(selected_ah_did).name self.deck_name_label = QLabel( f"

{truncate_string(deck_name, limit=70)}

" @@ -154,124 +168,64 @@ def _refresh_box_bottom_right(self) -> None: self.deck_name_label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred ) - self.box_deck_actions.addWidget(self.deck_name_label) - - self.box_deck_action_buttons = QHBoxLayout() - self.box_deck_actions.addLayout(self.box_deck_action_buttons) + # Initialize and setup the open web button self.open_web_btn = QPushButton("Open on AnkiHub") - self.box_deck_action_buttons.addWidget(self.open_web_btn) qconnect(self.open_web_btn.clicked, self._on_open_web) + # Initialize and setup the unsubscribe button self.unsubscribe_btn = QPushButton("Unsubscribe") if theme_manager.night_mode: self.unsubscribe_btn.setStyleSheet("color: #e29792") else: self.unsubscribe_btn.setStyleSheet("color: #e2857f") - - self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) qconnect(self.unsubscribe_btn.clicked, self._on_unsubscribe) - self.box_bottom_right.addSpacing(20) + # Add widgets to the action buttons layout + self.box_deck_action_buttons = QHBoxLayout() + self.box_deck_action_buttons.addWidget(self.open_web_btn) + self.box_deck_action_buttons.addWidget(self.unsubscribe_btn) - # Deck Options - self.box_deck_settings = QVBoxLayout() - self.box_bottom_right.addLayout(self.box_deck_settings) + # Add everything to the result layout + box = QVBoxLayout() + box.addWidget(self.deck_name_label) + box.addLayout(self.box_deck_action_buttons) - self.deck_settings_label = QLabel("Deck Options") - self.box_deck_settings.addWidget(self.deck_settings_label) + return box - self.box_deck_settings_elements = QVBoxLayout() - self.box_deck_settings.addLayout(self.box_deck_settings_elements) + def _setup_box_deck_options(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: + self.deck_options_label = QLabel("Deck Options") - # ... Suspend new cards of existing notes + # Setup "Suspend new cards of existing notes" self.box_suspend_new_cards_of_existing_notes = ( self._setup_suspend_new_cards_of_existing_notes(selected_ah_did) ) - self.box_deck_settings_elements.addLayout( - self.box_suspend_new_cards_of_existing_notes - ) - - self.box_deck_settings_elements.addSpacing(10) - # ... Suspend new cards of new notes + # Setup "Suspend new cards of new notes" self.box_suspend_new_cards_of_new_notes = ( self._setup_suspend_new_cards_of_new_notes(selected_ah_did) ) - self.box_deck_settings_elements.addLayout( - self.box_suspend_new_cards_of_new_notes - ) - self.box_deck_settings_elements.addSpacing(10) + # Setup "Subdecks enabled" + self.box_subdecks_enabled = self._setup_box_subdecks_enabled() - # ... Subdecks - self.subdecks_cb_row = QHBoxLayout() - self.box_deck_settings_elements.addLayout(self.subdecks_cb_row) - - subdecks_tooltip_message = ( - "Activates organizing the deck into subdecks. " - f"Applies only to decks with {SUBDECK_TAG} tags." - ) - self.subdecks_cb = QCheckBox("Enable Subdecks") - self.subdecks_cb.setToolTip(subdecks_tooltip_message) - self._refresh_subdecks_checkbox() - qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) - self.subdecks_cb_row.addWidget(self.subdecks_cb) - - self.subdeck_cb_icon_label = QLabel() - self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) - self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) - self.subdecks_cb_row.addWidget(self.subdeck_cb_icon_label) - - self.subdecks_docs_link_label = QLabel( - """ - - More about subdecks - - """ - ) - self.subdecks_docs_link_label.setOpenExternalLinks(True) - self.box_deck_settings_elements.addWidget(self.subdecks_docs_link_label) - - self.box_bottom_right.addSpacing(20) - - # Destination for new cards - self.box_new_cards_destination = QVBoxLayout() - self.box_bottom_right.addLayout(self.box_new_cards_destination) - - self.new_cards_destination = QLabel("Destination for New Cards") - self.box_new_cards_destination.addWidget(self.new_cards_destination) - - self.new_cards_destination_details_label = QLabel() - self.new_cards_destination_details_label.setWordWrap(True) - self._refresh_new_cards_destination_details_label(selected_ah_did) - self.box_new_cards_destination.addWidget( - self.new_cards_destination_details_label - ) - - self.set_new_cards_destination_btn = QPushButton( - "Change Destination for New Cards" + # Add individual elements to the deck options elements box + self.box_deck_options_elements = QVBoxLayout() + self.box_deck_options_elements.addLayout( + self.box_suspend_new_cards_of_existing_notes ) - qconnect( - self.set_new_cards_destination_btn.clicked, - self._on_new_cards_destination_btn_clicked, + self.box_deck_options_elements.addSpacing(10) + self.box_deck_options_elements.addLayout( + self.box_suspend_new_cards_of_new_notes ) - self.box_new_cards_destination.addWidget(self.set_new_cards_destination_btn) - self.box_new_cards_destination.addSpacing(7) + self.box_deck_options_elements.addLayout(self.box_subdecks_enabled) - self.new_cards_destination_docs_link_label = QLabel( - """ - - More about destinations for new cards - - """ - ) - self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) - self.box_new_cards_destination.addWidget( - self.new_cards_destination_docs_link_label - ) + # Add everything to the result layout + box = QVBoxLayout() + box.addWidget(self.deck_options_label) + box.addLayout(self.box_deck_options_elements) - self.box_bottom_right.addStretch() + return box def _setup_suspend_new_cards_of_existing_notes( self, selected_ah_did: uuid.UUID @@ -371,6 +325,84 @@ def _setup_suspend_new_cards_of_new_notes( box.addWidget(self.suspend_new_cards_of_new_notes_cb_icon_label) return box + def _setup_box_subdecks_enabled(self) -> QVBoxLayout: + subdecks_tooltip_message = ( + "Activates organizing the deck into subdecks. " + f"Applies only to decks with {SUBDECK_TAG} tags." + ) + + # Set up the subdecks checkbox + self.subdecks_cb = QCheckBox("Enable Subdecks") + self.subdecks_cb.setToolTip(subdecks_tooltip_message) + self._refresh_subdecks_checkbox() + qconnect(self.subdecks_cb.clicked, self._on_toggle_subdecks) + + # Initialize and set up the subdeck icon label + self.subdeck_cb_icon_label = QLabel() + self.subdeck_cb_icon_label.setPixmap(tooltip_icon().pixmap(16, 16)) + self.subdeck_cb_icon_label.setToolTip(subdecks_tooltip_message) + + # Add widgets to the subdecks checkbox row layout + self.subdecks_enabled_row = QHBoxLayout() + self.subdecks_enabled_row.addWidget(self.subdecks_cb) + self.subdecks_enabled_row.addWidget(self.subdeck_cb_icon_label) + + # Initialize and set up the subdecks documentation link label + self.subdecks_docs_link_label = QLabel( + """ + + More about subdecks + + """ + ) + self.subdecks_docs_link_label.setOpenExternalLinks(True) + + # Add everything to the result layout + box = QVBoxLayout() + box.addLayout(self.subdecks_enabled_row) + box.addWidget(self.subdecks_docs_link_label) + + return box + + def _setup_box_new_cards_destination( + self, selected_ah_did: uuid.UUID + ) -> QVBoxLayout: + self.new_cards_destination = QLabel("Destination for New Cards") + + # Initialize and set up the destination details label + self.new_cards_destination_details_label = QLabel() + self.new_cards_destination_details_label.setWordWrap(True) + self._refresh_new_cards_destination_details_label(selected_ah_did) + + # Initialize and set up the change destination button + self.set_new_cards_destination_btn = QPushButton( + "Change Destination for New Cards" + ) + qconnect( + self.set_new_cards_destination_btn.clicked, + self._on_new_cards_destination_btn_clicked, + ) + + # Initialize and set up the documentation link label + self.new_cards_destination_docs_link_label = QLabel( + """ + + More about destinations for new cards + + """ + ) + self.new_cards_destination_docs_link_label.setOpenExternalLinks(True) + + # Add everything to the result layout + box = QVBoxLayout() + box.addWidget(self.new_cards_destination) + box.addWidget(self.new_cards_destination_details_label) + box.addWidget(self.set_new_cards_destination_btn) + box.addSpacing(5) + box.addWidget(self.new_cards_destination_docs_link_label) + + return box + def _refresh_new_cards_destination_details_label(self, ah_did: uuid.UUID) -> None: deck_config = config.deck_config(ah_did) destination_anki_did = deck_config.anki_id From b3f0e7b7c141051627abb2e9c20c8293c2e30e0b Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Fri, 20 Oct 2023 00:28:09 +0200 Subject: [PATCH 104/105] ref: Reorganize statements in `_setup_box_suspend_new_cards_of_existing_notes` --- ankihub/gui/decks_dialog.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 7ee4c5bdc..4f110a043 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -198,7 +198,7 @@ def _setup_box_deck_options(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: # Setup "Suspend new cards of existing notes" self.box_suspend_new_cards_of_existing_notes = ( - self._setup_suspend_new_cards_of_existing_notes(selected_ah_did) + self._setup_box_suspend_new_cards_of_existing_notes(selected_ah_did) ) # Setup "Suspend new cards of new notes" @@ -227,16 +227,12 @@ def _setup_box_deck_options(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: return box - def _setup_suspend_new_cards_of_existing_notes( + def _setup_box_suspend_new_cards_of_existing_notes( self, selected_ah_did: uuid.UUID ) -> QBoxLayout: deck_config = config.deck_config(selected_ah_did) - box = QVBoxLayout() - - self.suspend_new_cards_of_existing_notes_row = QHBoxLayout() - box.addLayout(self.suspend_new_cards_of_existing_notes_row) - + # Setup label suspend_cards_of_existing_notes_tooltip_message = ( "Will automatically suspend
" "the cards of existing notes in
" @@ -246,17 +242,12 @@ def _setup_suspend_new_cards_of_existing_notes( self.suspend_new_cards_of_existing_notes_label = QLabel( "Suspend new cards of existing notes" ) - self.suspend_new_cards_of_existing_notes_row.addWidget( - self.suspend_new_cards_of_existing_notes_label - ) self.suspend_new_cards_of_existing_notes_label.setToolTip( suspend_cards_of_existing_notes_tooltip_message ) + # Setup tooltip icon self.suspend_new_cards_of_existing_notes_cb_icon_label = QLabel() - self.suspend_new_cards_of_existing_notes_row.addWidget( - self.suspend_new_cards_of_existing_notes_cb_icon_label - ) self.suspend_new_cards_of_existing_notes_cb_icon_label.setPixmap( tooltip_icon().pixmap(16, 16) ) @@ -264,9 +255,17 @@ def _setup_suspend_new_cards_of_existing_notes( suspend_cards_of_existing_notes_tooltip_message ) - self.suspend_new_cards_of_existing_notes = QComboBox() - box.addWidget(self.suspend_new_cards_of_existing_notes) + # Add the label and tooltip icon to the row layout + self.suspend_new_cards_of_existing_notes_row = QHBoxLayout() + self.suspend_new_cards_of_existing_notes_row.addWidget( + self.suspend_new_cards_of_existing_notes_label + ) + self.suspend_new_cards_of_existing_notes_row.addWidget( + self.suspend_new_cards_of_existing_notes_cb_icon_label + ) + # Setup and configure the combo box for "Suspend new cards of existing notes" + self.suspend_new_cards_of_existing_notes = QComboBox() self.suspend_new_cards_of_existing_notes.insertItems( 0, [option.value for option in SuspendNewCardsOfExistingNotes] ) @@ -282,6 +281,12 @@ def _setup_suspend_new_cards_of_existing_notes( ), ), ) + + # Add the row and combo box to the result layout + box = QVBoxLayout() + box.addLayout(self.suspend_new_cards_of_existing_notes_row) + box.addWidget(self.suspend_new_cards_of_existing_notes) + return box def _setup_suspend_new_cards_of_new_notes( From a2de991f9007263dbf32edd9ae46993e049c9c23 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Fri, 20 Oct 2023 00:31:30 +0200 Subject: [PATCH 105/105] ref: Reorganize statements in `_setup_box_suspend_new_cards_of_new_notes` --- ankihub/gui/decks_dialog.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py index 4f110a043..0df06c9b9 100644 --- a/ankihub/gui/decks_dialog.py +++ b/ankihub/gui/decks_dialog.py @@ -203,7 +203,7 @@ def _setup_box_deck_options(self, selected_ah_did: uuid.UUID) -> QVBoxLayout: # Setup "Suspend new cards of new notes" self.box_suspend_new_cards_of_new_notes = ( - self._setup_suspend_new_cards_of_new_notes(selected_ah_did) + self._setup_box_suspend_new_cards_of_new_notes(selected_ah_did) ) # Setup "Subdecks enabled" @@ -289,24 +289,21 @@ def _setup_box_suspend_new_cards_of_existing_notes( return box - def _setup_suspend_new_cards_of_new_notes( + def _setup_box_suspend_new_cards_of_new_notes( self, selected_ah_did: uuid.UUID, ) -> QBoxLayout: deck_config = config.deck_config(selected_ah_did) - box = QHBoxLayout() - - self.suspend_new_cards_of_new_notes_cb = QCheckBox( - "Suspend new cards of new notes" - ) - box.addWidget(self.suspend_new_cards_of_new_notes_cb) - + # Setup checkbox suspend_new_cards_of_new_notes_tooltip_message = ( "Will automatically suspend all
" "the cards of new notes added to
" "the deck in future updates." ) + self.suspend_new_cards_of_new_notes_cb = QCheckBox( + "Suspend new cards of new notes" + ) self.suspend_new_cards_of_new_notes_cb.setToolTip( suspend_new_cards_of_new_notes_tooltip_message ) @@ -320,6 +317,7 @@ def _setup_suspend_new_cards_of_new_notes( ), ) + # Setup tooltip icon self.suspend_new_cards_of_new_notes_cb_icon_label = QLabel() self.suspend_new_cards_of_new_notes_cb_icon_label.setPixmap( tooltip_icon().pixmap(16, 16) @@ -327,7 +325,12 @@ def _setup_suspend_new_cards_of_new_notes( self.suspend_new_cards_of_new_notes_cb_icon_label.setToolTip( suspend_new_cards_of_new_notes_tooltip_message ) + + # Add the checkbox and tooltip icon to the result layout + box = QHBoxLayout() + box.addWidget(self.suspend_new_cards_of_new_notes_cb) box.addWidget(self.suspend_new_cards_of_new_notes_cb_icon_label) + return box def _setup_box_subdecks_enabled(self) -> QVBoxLayout: