From 0337ae3236da7be2f99b6cc45d36c33a2a5b0daf Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 5 Dec 2023 22:16:17 +0100 Subject: [PATCH 01/47] refactor: move side effects out of files --- src/__init__.py | 7 ++++++- src/editor.py | 18 +++++------------- src/inplace_editor.py | 2 -- src/note_type_dialogs.py | 4 ---- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index e4a1192..cb97b7b 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,7 +1,6 @@ from platform import platform import sys import os -import platform import aqt from aqt.qt import * @@ -71,5 +70,11 @@ def setup_menu(): aqt.mw.form.menubar.insertMenu(aqt.mw.form.menuHelp.menuAction(), menu) +def setup_hooks(): + aqt.gui_hooks.models_did_init_buttons.append(note_type_dialogs.setup_note_editor) + aqt.gui_hooks.editor_did_load_note.append(editor.editor_note_changed) + aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) + aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) + setup_menu() anki_version.check_anki_version_dialog() diff --git a/src/editor.py b/src/editor.py index 1a4a5b5..9df8ed0 100644 --- a/src/editor.py +++ b/src/editor.py @@ -1,20 +1,18 @@ import os from typing import List -import json import aqt from aqt.editor import Editor -from . import note_type_mgr -from . import util -from .util import addon_path +from .note_type_mgr import nt_get_lang +from .util import show_critical def editor_get_lang(editor: Editor): if editor.note: nt = editor.note.note_type() if nt: - return note_type_mgr.nt_get_lang(nt) + return nt_get_lang(nt) return None @@ -27,7 +25,7 @@ def editor_generate_syntax(editor: Editor): return if not aqt.mw.migaku_connection.is_connected(): - util.show_critical("Anki is not connected to the Browser Extension.") + show_critical("Anki is not connected to the Browser Extension.") return note_id = editor.note.id @@ -54,7 +52,7 @@ def handle_syntax(syntax_data): [{note_id_key: text}], lang.code, on_done=handle_syntax, - on_error=lambda msg: util.show_critical(msg, parent=editor.parentWindow), + on_error=lambda msg: show_critical(msg, parent=editor.parentWindow), callback_on_main_thread=True, timeout=10, ) @@ -110,9 +108,6 @@ def setup_editor_buttons(buttons: List[str], editor: Editor): return buttons -aqt.gui_hooks.editor_did_init_buttons.append(setup_editor_buttons) - - def editor_note_changed(editor: Editor): lang = editor_get_lang(editor) if lang is None: @@ -135,6 +130,3 @@ def editor_note_changed(editor: Editor): document.getElementById('migaku_btn_syntax_remove').style.display = ''; """ editor.web.eval(js) - - -aqt.gui_hooks.editor_did_load_note.append(editor_note_changed) diff --git a/src/inplace_editor.py b/src/inplace_editor.py index 1ccb711..b3276cb 100644 --- a/src/inplace_editor.py +++ b/src/inplace_editor.py @@ -216,8 +216,6 @@ def reviewer_reshow(reviewer: Reviewer, mute=False, reload_card=True) -> None: # Copied from aqt.editor.Editor and aqt.editor.EditorWebView with minimal changes - - class PasteHandler: pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico") diff --git a/src/note_type_dialogs.py b/src/note_type_dialogs.py index 152d049..8d908d2 100644 --- a/src/note_type_dialogs.py +++ b/src/note_type_dialogs.py @@ -1,5 +1,4 @@ from typing import Optional -import re import anki import aqt @@ -301,6 +300,3 @@ def setup_note_editor(buttons, notes_editor: aqt.models.Models): buttons.append(("Migaku Options", lambda: on_manage_migaku(notes_editor))) buttons.append(("Add Migaku\nNote Type", lambda: on_add_migaku(notes_editor))) return buttons - - -aqt.gui_hooks.models_did_init_buttons.append(setup_note_editor) From 353003bd16375f70d417faa57e1418e44c655220 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 5 Dec 2023 22:25:05 +0100 Subject: [PATCH 02/47] refactor: move editor_py to folder --- src/__init__.py | 52 +++++++++++++-------------- src/{editor.py => editor/__init__.py} | 0 src/migaku_connection/srs_util.py | 15 ++++++-- src/welcome_wizard.py | 1 + 4 files changed, 39 insertions(+), 29 deletions(-) rename src/{editor.py => editor/__init__.py} (100%) diff --git a/src/__init__.py b/src/__init__.py index cb97b7b..0081ca9 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -6,10 +6,31 @@ from aqt.qt import * import anki - -# insert librairies into sys.path +# Initialize sub modules +from . import ( + anki_version, + balance_scheduler, + browser, + balance_scheduler_vacation_window, + balance_scheduler_dayoff_window, + card_layout, + card_type_selector, + click_play_audio, + ease_reset, + editor, + inplace_editor, + migaku_connection, + note_type_dialogs, + note_type_mgr, + retirement, + reviewer, + settings_window, + webview_contextmenu, + welcome_wizard, +) +# insert librairies into sys.path def add_sys_path(*path_parts): sys.path.insert( 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib", *path_parts) @@ -32,31 +53,6 @@ def add_sys_path(*path_parts): # Allow webviews to access necessary resources aqt.mw.addonManager.setWebExports(__name__, r"(languages/.*?\.svg|inplace_editor.css)") -# Initialize sub modules -from . import ( - note_type_dialogs, - card_type_selector, - note_type_mgr, - reviewer, - editor, - inplace_editor, - click_play_audio, - browser, - card_layout, - welcome_wizard, - webview_contextmenu, - settings_window, - ease_reset, - retirement, - balance_scheduler, - balance_scheduler_vacation_window, - balance_scheduler_dayoff_window, - anki_version, -) - - -from . import migaku_connection - def setup_menu(): menu = QMenu("Migaku", aqt.mw) @@ -76,5 +72,7 @@ def setup_hooks(): aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) + setup_menu() +setup_hooks() anki_version.check_anki_version_dialog() diff --git a/src/editor.py b/src/editor/__init__.py similarity index 100% rename from src/editor.py rename to src/editor/__init__.py diff --git a/src/migaku_connection/srs_util.py b/src/migaku_connection/srs_util.py index ec1e8fd..8774ff0 100644 --- a/src/migaku_connection/srs_util.py +++ b/src/migaku_connection/srs_util.py @@ -229,7 +229,13 @@ def _upload_media_single_attempt(fname, user_token, is_audio=False): out_path = tmp_path(fname) # The arguments to ffmpeg are the same as in MM r = aqt.mw.migaku_connection.ffmpeg.call( - "-y", "-i", in_path, "-vn", "-b:a", "128k", out_path, + "-y", + "-i", + in_path, + "-vn", + "-b:a", + "128k", + out_path, ) if r != 0: # ignore failed conversions, most likely bad audio @@ -245,7 +251,12 @@ def _upload_media_single_attempt(fname, user_token, is_audio=False): # The arguments to ffmpeg are the same as in MM r = aqt.mw.migaku_connection.ffmpeg.call( - "-y", "-i", in_path, "-vf", "scale='min(800,iw)':-1", out_path, + "-y", + "-i", + in_path, + "-vf", + "scale='min(800,iw)':-1", + out_path, ) if r != 0: # ignore failed conversions, most likely bad audio diff --git a/src/welcome_wizard.py b/src/welcome_wizard.py index ad2863d..9e8e6bd 100644 --- a/src/welcome_wizard.py +++ b/src/welcome_wizard.py @@ -5,6 +5,7 @@ from . import config from .settings_widgets import TUTORIAL_WIDGETS + class WelcomeWizard(QWizard): INITIAL_SIZE = (625, 440) From b459e909b55c87d7e94088344dc55ac5626d4720 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Wed, 6 Dec 2023 14:28:35 +0100 Subject: [PATCH 03/47] feat: load editor code into webview --- src/__init__.py | 34 ++++++++++------------------------ src/editor/__init__.py | 40 +++++++++++++++++++--------------------- src/editor/editor.js | 21 +++++++++++++++++++++ src/note_type_mgr.py | 3 --- src/sys_libraries.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 src/editor/editor.js create mode 100644 src/sys_libraries.py diff --git a/src/__init__.py b/src/__init__.py index 0081ca9..1e984fa 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -6,6 +6,10 @@ from aqt.qt import * import anki +from .sys_libraries import init_sys_libs + +init_sys_libs() + # Initialize sub modules from . import ( anki_version, @@ -30,30 +34,6 @@ ) -# insert librairies into sys.path -def add_sys_path(*path_parts): - sys.path.insert( - 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib", *path_parts) - ) - - -add_sys_path("shared") -if anki.utils.is_lin: - add_sys_path("linux") -elif anki.utils.is_mac and sys.version_info.major >= 3 and sys.version_info.minor >= 11: - add_sys_path("macos_311") -elif anki.utils.is_mac and sys.version_info.major >= 3 and sys.version_info.minor >= 10: - add_sys_path("macos_310") -elif anki.utils.is_mac: - add_sys_path("macos_39") -elif anki.utils.is_win: - add_sys_path("windows") - - -# Allow webviews to access necessary resources -aqt.mw.addonManager.setWebExports(__name__, r"(languages/.*?\.svg|inplace_editor.css)") - - def setup_menu(): menu = QMenu("Migaku", aqt.mw) menu.addAction(settings_window.action) @@ -67,7 +47,13 @@ def setup_menu(): def setup_hooks(): + # Allow webviews to access necessary resources + aqt.mw.addonManager.setWebExports( + __name__, r"(languages/.*?\.svg|inplace_editor.css)" + ) + aqt.gui_hooks.models_did_init_buttons.append(note_type_dialogs.setup_note_editor) + aqt.gui_hooks.editor_web_view_did_init.append(editor.editor_webview_did_init) aqt.gui_hooks.editor_did_load_note.append(editor.editor_note_changed) aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 9df8ed0..aac5868 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -4,8 +4,9 @@ import aqt from aqt.editor import Editor -from .note_type_mgr import nt_get_lang -from .util import show_critical +from ..languages import Language +from ..note_type_mgr import nt_get_lang +from ..util import addon_path, show_critical def editor_get_lang(editor: Editor): @@ -108,25 +109,22 @@ def setup_editor_buttons(buttons: List[str], editor: Editor): return buttons +def editor_get_js_by_lang(lang: Language): + add_icon_path = lang.web_uri("icons", "generate.svg") + remove_icon_path = lang.web_uri("icons", "remove.svg") + no_icon_invert = os.path.exists(lang.file_path("icons", "no_invert")) + img_filter = "invert(0)" if no_icon_invert else "" + + return f"MigakuEditor.initButtons('{add_icon_path}', '{remove_icon_path}', '{img_filter}');" + + def editor_note_changed(editor: Editor): lang = editor_get_lang(editor) - if lang is None: - js = """ - document.getElementById('migaku_btn_syntax_generate').style.display = 'none'; - document.getElementById('migaku_btn_syntax_remove').style.display = 'none'; - """ - else: - add_icon_path = lang.web_uri("icons", "generate.svg") - remove_icon_path = lang.web_uri("icons", "remove.svg") - no_icon_invert = os.path.exists(lang.file_path("icons", "no_invert")) - img_filter = "invert(0)" if no_icon_invert else "" - - js = f""" - document.querySelector('#migaku_btn_syntax_generate img').src = '{add_icon_path}'; - document.querySelector('#migaku_btn_syntax_generate img').style.filter = '{img_filter}'; - document.querySelector('#migaku_btn_syntax_remove img').src = '{remove_icon_path}'; - document.querySelector('#migaku_btn_syntax_remove img').style.filter = '{img_filter}'; - document.getElementById('migaku_btn_syntax_generate').style.display = ''; - document.getElementById('migaku_btn_syntax_remove').style.display = ''; - """ + js = "MigakuEditor.hideButtons();" if lang is None else editor_get_js_by_lang(lang) + editor.web.eval(js) + + +def editor_webview_did_init(web: aqt.editor.EditorWebView): + with open(addon_path("editor/editor.js"), "r", encoding="utf-8") as editor_file: + web.eval(editor_file.read()) diff --git a/src/editor/editor.js b/src/editor/editor.js new file mode 100644 index 0000000..2ed19fb --- /dev/null +++ b/src/editor/editor.js @@ -0,0 +1,21 @@ +var MigakuEditor = {} + +/** + * @param {string} add_icon_path + * @param {string} remove_icon_path + * @param {string} img_filter + */ +MigakuEditor.initButtons = function (add_icon_path, remove_icon_path, img_filter) { + document.querySelector('#migaku_btn_syntax_generate img').src = add_icon_path; + document.querySelector('#migaku_btn_syntax_generate img').style.filter = img_filter; + document.querySelector('#migaku_btn_syntax_remove img').src = remove_icon_path; + document.querySelector('#migaku_btn_syntax_remove img').style.filter = img_filter; + document.getElementById('migaku_btn_syntax_generate').style.display = ''; + document.getElementById('migaku_btn_syntax_remove').style.display = ''; +} + + +MigakuEditor.hideButtons = function () { + document.getElementById('migaku_btn_syntax_generate').style.display = 'none'; + document.getElementById('migaku_btn_syntax_remove').style.display = 'none'; +} diff --git a/src/note_type_mgr.py b/src/note_type_mgr.py index a2b81a5..aeeddb8 100644 --- a/src/note_type_mgr.py +++ b/src/note_type_mgr.py @@ -288,6 +288,3 @@ def update_all_installed() -> None: if is_installed(lang): nt = nt_mgr.by_name(NOTE_TYPE_PREFIX + lang.name_en) nt_update(nt, lang) - - -aqt.gui_hooks.profile_did_open.append(update_all_installed) diff --git a/src/sys_libraries.py b/src/sys_libraries.py new file mode 100644 index 0000000..bf02b97 --- /dev/null +++ b/src/sys_libraries.py @@ -0,0 +1,32 @@ +# insert librairies into sys.path +import anki +import os +import sys + + +def add_sys_path(*path_parts): + sys.path.insert( + 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib", *path_parts) + ) + + +def init_sys_libs(): + add_sys_path("shared") + if anki.utils.is_lin: + add_sys_path("linux") + elif ( + anki.utils.is_mac + and sys.version_info.major >= 3 + and sys.version_info.minor >= 11 + ): + add_sys_path("macos_311") + elif ( + anki.utils.is_mac + and sys.version_info.major >= 3 + and sys.version_info.minor >= 10 + ): + add_sys_path("macos_310") + elif anki.utils.is_mac: + add_sys_path("macos_39") + elif anki.utils.is_win: + add_sys_path("windows") From 22ff15cc5fb2e19ce1146cf782c8d12a636bb77d Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 7 Dec 2023 20:17:34 +0100 Subject: [PATCH 04/47] feat: create types --- src/__init__.py | 7 +++- src/card_types.py | 33 +++++++++++++++ src/editor/__init__.py | 77 ++++++++++++++++++++++++++++++++-- src/editor/editor.js | 94 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 src/card_types.py diff --git a/src/__init__.py b/src/__init__.py index 1e984fa..c83d7f9 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -53,8 +53,11 @@ def setup_hooks(): ) aqt.gui_hooks.models_did_init_buttons.append(note_type_dialogs.setup_note_editor) - aqt.gui_hooks.editor_web_view_did_init.append(editor.editor_webview_did_init) - aqt.gui_hooks.editor_did_load_note.append(editor.editor_note_changed) + aqt.editor.Editor.onBridgeCmd = anki.hooks.wrap( + aqt.editor.Editor.onBridgeCmd, editor.on_migaku_bridge_cmds, "around" + ) + aqt.gui_hooks.editor_did_init.append(editor.editor_did_init) + aqt.gui_hooks.editor_did_load_note.append(editor.editor_did_load_note) aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) diff --git a/src/card_types.py b/src/card_types.py new file mode 100644 index 0000000..3fc9528 --- /dev/null +++ b/src/card_types.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass +from typing import Union, Optional + +# From media.types + +AudioInput = Union[str, 'HTMLAudioElement'] + +@dataclass +class AudioAsset(): + id: str + title: str + input: AudioInput + r2Url: Optional[str] + +@dataclass +class ImageAsset(): + id: str + title: str + src: str + alt: str + r2Url: Optional[str] + +@dataclass +class CardFields(): + targetWord: str + sentence: str + translation: str + definitions: str + sentenceAudio: AudioAsset[] + wordAudio: AudioAsset[] + images: ImageAsset[] + exampleSentences: str + notes: str diff --git a/src/editor/__init__.py b/src/editor/__init__.py index aac5868..fd094fe 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -1,9 +1,13 @@ import os +import re +import json from typing import List import aqt +import anki from aqt.editor import Editor +from ..config import get, set from ..languages import Language from ..note_type_mgr import nt_get_lang from ..util import addon_path, show_critical @@ -85,8 +89,51 @@ def do_edit(): editor.call_after_note_saved(callback=do_edit, keepFocus=True) +def infer_migaku_type(name: str) -> str: + if re.match(r'(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)', name, re.IGNORECASE): return 'word' + if re.match(r'(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)', name, re.IGNORECASE): return 'image' + if re.match(r'(sentence|文|句|문장|frase|phrase|satz|frase)', name, re.IGNORECASE): return 'sentence' + if re.match(r'(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)', name, re.IGNORECASE): return 'sentence_translation' + if re.match(r'(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)', name, re.IGNORECASE): return 'example_sentence' + + if re.match(r'(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)', name, re.IGNORECASE): + if re.match(r'(sentence|文|句|문장|frase|phrase|satz|frase)', name, re.IGNORECASE): return 'sentence_audio' + else: return 'word_audio' + + if re.match(r'(definition|定義|定义|정의|definición|definição|définition|definition|definição)', name, re.IGNORECASE): return 'definition' + if re.match(r'(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)', name, re.IGNORECASE): return 'notes' + return 'none' + +def toggle_migaku_mode(editor: Editor): + migaku_fields = get("migakuFields", {}) + data = migaku_fields.get(editor.note.mid, {}) + + nt = editor.note.note_type() + + field_names = [field["name"] for field in nt["flds"]] + + for field_name in field_names: + if field_name not in data: + data[field_name] = infer_migaku_type(field_name) + + for field_name in data.keys(): + if field_name not in field_names: + del data[field_name] + + editor.web.eval(f"MigakuEditor.toggleMode({json.dumps(data)});") + + def setup_editor_buttons(buttons: List[str], editor: Editor): added_buttons = [ + editor.addButton( + label="Migaku Mode", + icon=None, + id="migaku_btn_toggle_mode", + cmd="migaku_toggle_mode", + toggleable=True, + disables=False, + func=toggle_migaku_mode, + ), editor.addButton( icon="tmp", id="migaku_btn_syntax_generate", @@ -118,13 +165,37 @@ def editor_get_js_by_lang(lang: Language): return f"MigakuEditor.initButtons('{add_icon_path}', '{remove_icon_path}', '{img_filter}');" -def editor_note_changed(editor: Editor): +def editor_did_load_note(editor: Editor): lang = editor_get_lang(editor) js = "MigakuEditor.hideButtons();" if lang is None else editor_get_js_by_lang(lang) editor.web.eval(js) -def editor_webview_did_init(web: aqt.editor.EditorWebView): +def on_migaku_bridge_cmds(self: Editor, cmd: str, _old): + if cmd.startswith("migakuSelectChange"): + (_, migaku_type, field_name) = cmd.split(":", 2) + migakuFields = get("migakuFields", {}) + print("migakuFields", migakuFields) + set( + "migakuFields", + { + **migakuFields, + self.note.mid: { + **( + migakuFields[self.note.mid] + if self.note.mid in migakuFields + else {} + ), + field_name: migaku_type, + }, + }, + True, + ) + else: + _old(self, cmd) + + +def editor_did_init(editor: Editor): with open(addon_path("editor/editor.js"), "r", encoding="utf-8") as editor_file: - web.eval(editor_file.read()) + editor.web.eval(editor_file.read()) diff --git a/src/editor/editor.js b/src/editor/editor.js index 2ed19fb..40b9ca5 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -1,4 +1,4 @@ -var MigakuEditor = {} +function MigakuEditor() { } /** * @param {string} add_icon_path @@ -14,8 +14,94 @@ MigakuEditor.initButtons = function (add_icon_path, remove_icon_path, img_filter document.getElementById('migaku_btn_syntax_remove').style.display = ''; } - MigakuEditor.hideButtons = function () { - document.getElementById('migaku_btn_syntax_generate').style.display = 'none'; - document.getElementById('migaku_btn_syntax_remove').style.display = 'none'; + if (!document.getElementById('migaku_btn_syntax_generate')) setTimeout(() => { + document.getElementById('migaku_btn_syntax_generate').style.display = 'none'; + document.getElementById('migaku_btn_syntax_remove').style.display = 'none'; + }, 100) + else { + document.getElementById('migaku_btn_syntax_generate').style.display = 'none'; + document.getElementById('migaku_btn_syntax_remove').style.display = 'none'; + } +} + +const selectorOptions = [ + { value: 'none', text: '(None)' }, + { value: 'sentence', text: 'Sentence' }, + { value: 'word', text: 'Word' }, + { value: 'sentence_translation', text: 'Sentence Translation' }, + { value: 'sentence_audio', text: 'Sentence Audio' }, + { value: 'word_audio', text: 'Word Audio' }, + { value: 'image', text: 'Image' }, + { value: 'definition', text: 'Definitions' }, + { value: 'example_sentence', text: 'Example sentences' }, + { value: 'notes', text: 'Notes' }, +] + +function getSelectorField(editorField, settings) { + const field = document.createElement('div'); + field.classList.add('migaku-field-selector'); + + const select = document.createElement('select'); + select.style.margin = '2px'; + field.append(select); + + for (const option of selectorOptions) { + const optionElement = document.createElement('option'); + optionElement.value = option.value; + optionElement.text = option.text; + select.append(optionElement); + } + + const fieldContainer = editorField.parentElement.parentElement + const labelName = fieldContainer.querySelector('.label-name').innerText + select.value = settings[labelName] ?? 'none' + + select.addEventListener('change', (selectTarget) => { + + const cmd = `migakuSelectChange:${selectTarget.currentTarget.value}:${labelName}` + bridgeCommand(cmd) + }) + + return field +} + +const hiddenButtonCategories = [ + 'settings', + 'inlineFormatting', + 'blockFormatting', + 'template', + 'cloze', + 'image-occlusion-button', +] + +MigakuEditor.toggleMode = function (settings) { + if (document.querySelector('.migaku-field-selector')) { + resetMigakuEditor(); + } else { + setupMigakuEditor(settings); + } +} + +// New Migaku Editor +function setupMigakuEditor(settings) { + console.log('data', settings) + + document.querySelectorAll('.editing-area').forEach((field) => field.style.display = 'none'); + document.querySelectorAll('.plain-text-badge').forEach((field) => field.style.display = 'none'); + document.querySelectorAll('svg#mdi-pin-outline').forEach((field) => field.parentElement.parentElement.parentElement.style.display = 'none'); + hiddenButtonCategories.forEach((category) => document.querySelector(`.item#${category}`).style.display = 'none'); + + for (const field of document.querySelectorAll('.editor-field')) { + field.append(getSelectorField(field, settings)) + } +} + +function resetMigakuEditor() { + document.querySelectorAll('.editing-area').forEach((field) => field.style.display = ''); + document.querySelectorAll('.plain-text-badge').forEach((field) => field.style.display = ''); + document.querySelectorAll('svg#mdi-pin-outline').forEach((field) => field.parentElement.parentElement.parentElement.style.display = ''); + hiddenButtonCategories.forEach((category) => document.querySelector(`.item#${category}`).style.display = ''); + + document.querySelectorAll('.migaku-field-selector').forEach((selector) => selector.remove()); } From d167b19ca0f8f399c2ceea9560d36bb22ed1c192 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Mon, 11 Dec 2023 05:08:32 +0100 Subject: [PATCH 05/47] feat: add /receive endpoint --- src/__init__.py | 6 ++ src/card_types.py | 47 ++++++------- src/editor/__init__.py | 56 +++++++++++---- src/editor/current_editor.py | 34 +++++++++ src/migaku_connection/__init__.py | 3 +- src/migaku_connection/card_creator.py | 35 +--------- src/migaku_connection/card_receiver.py | 72 ++++++++++++++++++++ src/migaku_connection/migaku_http_handler.py | 4 +- 8 files changed, 185 insertions(+), 72 deletions(-) create mode 100644 src/editor/current_editor.py create mode 100644 src/migaku_connection/card_receiver.py diff --git a/src/__init__.py b/src/__init__.py index c83d7f9..59ad2ea 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -59,6 +59,12 @@ def setup_hooks(): aqt.gui_hooks.editor_did_init.append(editor.editor_did_init) aqt.gui_hooks.editor_did_load_note.append(editor.editor_did_load_note) aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) + + aqt.gui_hooks.editor_did_init.append(editor.current_editor.set_current_editor) + aqt.editor.Editor.cleanup = anki.hooks.wrap( + aqt.editor.Editor.cleanup, editor.current_editor.remove_editor, "before" + ) + aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) diff --git a/src/card_types.py b/src/card_types.py index 3fc9528..a1b1105 100644 --- a/src/card_types.py +++ b/src/card_types.py @@ -1,33 +1,34 @@ from dataclasses import dataclass -from typing import Union, Optional +from typing import Optional # From media.types -AudioInput = Union[str, 'HTMLAudioElement'] @dataclass -class AudioAsset(): - id: str - title: str - input: AudioInput - r2Url: Optional[str] +class AudioAsset: + id: str + title: str + input: str + r2Url: Optional[str] + @dataclass -class ImageAsset(): - id: str - title: str - src: str - alt: str - r2Url: Optional[str] +class ImageAsset: + id: str + title: str + src: str + alt: str + r2Url: Optional[str] + @dataclass -class CardFields(): - targetWord: str - sentence: str - translation: str - definitions: str - sentenceAudio: AudioAsset[] - wordAudio: AudioAsset[] - images: ImageAsset[] - exampleSentences: str - notes: str +class CardFields: + targetWord: str + sentence: str + translation: str + definitions: str + sentenceAudio: list[AudioAsset] + wordAudio: list[AudioAsset] + images: list[ImageAsset] + exampleSentences: str + notes: str diff --git a/src/editor/__init__.py b/src/editor/__init__.py index fd094fe..e2cbbfa 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -90,19 +90,49 @@ def do_edit(): def infer_migaku_type(name: str) -> str: - if re.match(r'(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)', name, re.IGNORECASE): return 'word' - if re.match(r'(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)', name, re.IGNORECASE): return 'image' - if re.match(r'(sentence|文|句|문장|frase|phrase|satz|frase)', name, re.IGNORECASE): return 'sentence' - if re.match(r'(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)', name, re.IGNORECASE): return 'sentence_translation' - if re.match(r'(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)', name, re.IGNORECASE): return 'example_sentence' + if re.match( + r"(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)", name, re.IGNORECASE + ): + return "word" + if re.match( + r"(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE + ): + return "image" + if re.match(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): + return "sentence" + if re.match( + r"(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)", + name, + re.IGNORECASE, + ): + return "sentence_translation" + if re.match( + r"(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)", + name, + re.IGNORECASE, + ): + return "example_sentence" + + if re.match( + r"(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)", name, re.IGNORECASE + ): + if re.match(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): + return "sentence_audio" + else: + return "word_audio" + + if re.match( + r"(definition|定義|定义|정의|definición|definição|définition|definition|definição)", + name, + re.IGNORECASE, + ): + return "definition" + if re.match( + r"(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)", name, re.IGNORECASE + ): + return "notes" + return "none" - if re.match(r'(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)', name, re.IGNORECASE): - if re.match(r'(sentence|文|句|문장|frase|phrase|satz|frase)', name, re.IGNORECASE): return 'sentence_audio' - else: return 'word_audio' - - if re.match(r'(definition|定義|定义|정의|definición|definição|définition|definition|definição)', name, re.IGNORECASE): return 'definition' - if re.match(r'(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)', name, re.IGNORECASE): return 'notes' - return 'none' def toggle_migaku_mode(editor: Editor): migaku_fields = get("migakuFields", {}) @@ -115,7 +145,7 @@ def toggle_migaku_mode(editor: Editor): for field_name in field_names: if field_name not in data: data[field_name] = infer_migaku_type(field_name) - + for field_name in data.keys(): if field_name not in field_names: del data[field_name] diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py new file mode 100644 index 0000000..b2f5f20 --- /dev/null +++ b/src/editor/current_editor.py @@ -0,0 +1,34 @@ +from anki.hooks import wrap +from anki.notes import Note +from aqt.editor import Editor +import aqt + +current_editors = [] + + +def set_current_editor(editor: Editor): + global current_editors + remove_editor(editor) + current_editors.append(editor) + + +def remove_editor(editor: Editor): + global current_editors + current_editors = [e for e in current_editors if e != editor] + + +def get_current_editor() -> Editor: + if len(current_editors) > 0: + return current_editors[-1] + return None + + +def get_current_note_info() -> Note: + for editor in reversed(current_editors): + if editor.note: + return {"note": editor.note, "editor": editor} + if aqt.mw.reviewer and aqt.mw.reviewer.card: + note = aqt.mw.reviewer.card.note() + if note: + return {"note": note, "reviewer": aqt.mw.reviewer} + return None diff --git a/src/migaku_connection/__init__.py b/src/migaku_connection/__init__.py index b6d903c..9261979 100644 --- a/src/migaku_connection/__init__.py +++ b/src/migaku_connection/__init__.py @@ -10,6 +10,7 @@ from .hello import MigakuHello from .migaku_connector import MigakuConnector from .card_creator import CardCreator +from .card_receiver import CardReceiver from .audio_condenser import AudioCondenser from .learning_status_handler import LearningStatusHandler from .profile_data_provider import ProfileDataProvider @@ -141,8 +142,8 @@ def timeout(self, *args, **kwargs): ("/condense", AudioCondenser), ("/learning-statuses", LearningStatusHandler), ("/create", CardCreator), + ("/receive", CardReceiver), ("/profile-data", ProfileDataProvider), - ("/create", CardCreator), ("/info", InfoProvider), ("/sendcard", CardSender), ("/search", SearchHandler), diff --git a/src/migaku_connection/card_creator.py b/src/migaku_connection/card_creator.py index 510f8a5..e5f74a4 100644 --- a/src/migaku_connection/card_creator.py +++ b/src/migaku_connection/card_creator.py @@ -13,6 +13,7 @@ from .. import config from .. import util from ..inplace_editor import reviewer_reshow +from ..editor.current_editor import get_current_note_info class CardCreator(MigakuHTTPHandler): @@ -300,37 +301,3 @@ def template_find_field_name_and_syntax_for_data_type(template, data_type): syntax = field_data.get("syntax", False) return field_name, syntax return None, None - - -# this is dirty... - -from anki.hooks import wrap -from aqt.editor import Editor - -current_editors = [] - - -def set_current_editor(editor: aqt.editor.Editor): - global current_editors - remove_editor(editor) - current_editors.append(editor) - - -def remove_editor(editor: aqt.editor.Editor): - global current_editors - current_editors = [e for e in current_editors if e != editor] - - -aqt.gui_hooks.editor_did_init.append(set_current_editor) -Editor.cleanup = wrap(Editor.cleanup, remove_editor, "before") - - -def get_current_note_info() -> Note: - for editor in reversed(current_editors): - if editor.note: - return {"note": editor.note, "editor": editor} - if aqt.mw.reviewer and aqt.mw.reviewer.card: - note = aqt.mw.reviewer.card.note() - if note: - return {"note": note, "reviewer": aqt.mw.reviewer} - return None diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py new file mode 100644 index 0000000..b9e2d43 --- /dev/null +++ b/src/migaku_connection/card_receiver.py @@ -0,0 +1,72 @@ +from pydub import AudioSegment +from pydub import effects +import time +import subprocess +import json +import os +import re + +import aqt +from anki.notes import Note +from tornado.web import RequestHandler + +from .migaku_http_handler import MigakuHTTPHandler +from .. import config +from .. import util +from ..inplace_editor import reviewer_reshow + + +class CardReceiver(MigakuHTTPHandler): + image_formats = ["webp"] + audio_formats = ["m4a"] + + TO_MP3_RE = re.compile(r"\[sound:(.*?)\.(wav|ogg)\]") + BR_RE = re.compile(r"") + + def post(self: RequestHandler): + if not self.check_version(): + self.finish("Card could not be created: Version mismatch") + return + + # card = self.get_body_argument("card", default=None) + # if card: + # self.create_card('') + + # return + + self.finish("Invalid request.") + + def create_card(self, card_data_json): + # card_data = json.loads(card_data_json) + print('create_card') + + # note_type_id = card_data["noteTypeId"] + # note_type = aqt.mw.col.models.get(note_type_id) + # deck_id = card_data["deckId"] + + # note = Note(aqt.mw.col, note_type) + + # for field in card_data["fields"]: + # if "content" in field and field["content"]: + # content = field["content"] + # field_name = field["name"] + # content = self.post_process_text(content, field_name) + # note[field_name] = content + + # tags = card_data.get("tags") + # if tags: + # note.set_tags_from_str(tags) + + # note.model()["did"] = int(deck_id) + # aqt.mw.col.addNote(note) + # aqt.mw.col.save() + # aqt.mw.taskman.run_on_main(aqt.mw.reset) + + self.handle_files(self.request.files) + + self.finish(json.dumps({"id": 0})) + + # def move_file_to_media_dir(self, file_body, filename): + # file_path = util.col_media_path(filename) + # with open(file_path, "wb") as file_handle: + # file_handle.write(file_body) diff --git a/src/migaku_connection/migaku_http_handler.py b/src/migaku_connection/migaku_http_handler.py index 6d29629..c3d7c9e 100644 --- a/src/migaku_connection/migaku_http_handler.py +++ b/src/migaku_connection/migaku_http_handler.py @@ -14,7 +14,9 @@ def initialize(self): self.connection = self.application.settings["connection"] def check_version(self): - version = int(self.get_body_argument("version", default=False)) + version_string = self.get_body_argument("version", default=False) + version = int(version_string) + print('version', self.connection.PROTOCOL_VERSION, 'versus', version, version_string) version_match = self.connection.PROTOCOL_VERSION == version return version_match From 5f52d02f3f248db454739192839c58d02ade300f Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Mon, 11 Dec 2023 18:53:36 +0100 Subject: [PATCH 06/47] feat: fetch card in card receiver --- src/editor/current_editor.py | 11 +++++-- src/migaku_connection/card_receiver.py | 33 ++++++++++++++------ src/migaku_connection/migaku_http_handler.py | 1 - 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py index b2f5f20..5f6a202 100644 --- a/src/editor/current_editor.py +++ b/src/editor/current_editor.py @@ -26,9 +26,16 @@ def get_current_editor() -> Editor: def get_current_note_info() -> Note: for editor in reversed(current_editors): if editor.note: - return {"note": editor.note, "editor": editor} + return { + "note": editor.note, + "editor": editor, + } + if aqt.mw.reviewer and aqt.mw.reviewer.card: note = aqt.mw.reviewer.card.note() if note: - return {"note": note, "reviewer": aqt.mw.reviewer} + return { + "note": note, + "reviewer": aqt.mw.reviewer, + } return None diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py index b9e2d43..c6a306e 100644 --- a/src/migaku_connection/card_receiver.py +++ b/src/migaku_connection/card_receiver.py @@ -8,6 +8,7 @@ import aqt from anki.notes import Note +from ..editor.current_editor import get_current_note_info from tornado.web import RequestHandler from .migaku_http_handler import MigakuHTTPHandler @@ -28,21 +29,35 @@ def post(self: RequestHandler): self.finish("Card could not be created: Version mismatch") return - # card = self.get_body_argument("card", default=None) - # if card: - # self.create_card('') + card = self.get_body_argument("card", default=None) - # return + if not card: + self.finish("Invalid request.") - self.finish("Invalid request.") + self.create_card("") + return def create_card(self, card_data_json): # card_data = json.loads(card_data_json) - print('create_card') + print("create_card") - # note_type_id = card_data["noteTypeId"] - # note_type = aqt.mw.col.models.get(note_type_id) - # deck_id = card_data["deckId"] + info = get_current_note_info() + + if not info: + return "No current note." + + note = info["note"] + + note_type = note.note_type() + if not note_type: + return "Current note has no valid note_type." + + notetype_name = str(note_type.get("name", "")) + notetype_id = str(note_type.get("id", "")) + notetype_name + + note_tags = note.tags + + print("note_tags", note_tags, notetype_name, notetype_id) # note = Note(aqt.mw.col, note_type) diff --git a/src/migaku_connection/migaku_http_handler.py b/src/migaku_connection/migaku_http_handler.py index c3d7c9e..4e5fe69 100644 --- a/src/migaku_connection/migaku_http_handler.py +++ b/src/migaku_connection/migaku_http_handler.py @@ -16,7 +16,6 @@ def initialize(self): def check_version(self): version_string = self.get_body_argument("version", default=False) version = int(version_string) - print('version', self.connection.PROTOCOL_VERSION, 'versus', version, version_string) version_match = self.connection.PROTOCOL_VERSION == version return version_match From 886e603c4ba1c607277941b2ff6dbaa5f9d0e69b Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 12 Dec 2023 17:07:11 +0100 Subject: [PATCH 07/47] feat: parse from json instead of formdata --- src/__init__.py | 2 + src/card_types.py | 33 ++++--- src/editor/__init__.py | 66 +------------- src/editor/current_editor.py | 71 +++++++++++++++ src/editor/editor.js | 17 ++-- src/migaku_connection/__init__.py | 5 ++ src/migaku_connection/card_creator.py | 107 ++++------------------- src/migaku_connection/card_receiver.py | 85 +++++++----------- src/migaku_connection/handle_files.py | 90 +++++++++++++++++++ src/migaku_connection/program_manager.py | 1 + src/migaku_fields.py | 75 ++++++++++++++++ 11 files changed, 324 insertions(+), 228 deletions(-) create mode 100644 src/migaku_connection/handle_files.py create mode 100644 src/migaku_fields.py diff --git a/src/__init__.py b/src/__init__.py index 59ad2ea..0192bd9 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -59,6 +59,8 @@ def setup_hooks(): aqt.gui_hooks.editor_did_init.append(editor.editor_did_init) aqt.gui_hooks.editor_did_load_note.append(editor.editor_did_load_note) aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) + aqt.gui_hooks.add_cards_did_change_deck.append(editor.current_editor.on_addcards_did_change_deck) + aqt.gui_hooks.addcards_did_change_note_type.append(editor.current_editor.on_addcards_did_change_note_type) aqt.gui_hooks.editor_did_init.append(editor.current_editor.set_current_editor) aqt.editor.Editor.cleanup = anki.hooks.wrap( diff --git a/src/card_types.py b/src/card_types.py index a1b1105..2ce6c2f 100644 --- a/src/card_types.py +++ b/src/card_types.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional # From media.types @@ -23,12 +23,25 @@ class ImageAsset: @dataclass class CardFields: - targetWord: str - sentence: str - translation: str - definitions: str - sentenceAudio: list[AudioAsset] - wordAudio: list[AudioAsset] - images: list[ImageAsset] - exampleSentences: str - notes: str + targetWord: str = "" + sentence: str = "" + translation: str = "" + definitions: str = "" + sentenceAudio: list[AudioAsset] = field(default_factory=lambda: []) + wordAudio: list[AudioAsset] = field(default_factory=lambda: []) + images: list[ImageAsset] = field(default_factory=lambda: []) + exampleSentences: str = "" + notes: str = "" + +def card_fields_from_dict(d: dict[str, any]): + return CardFields( + targetWord=d.get("targetWord", ""), + sentence=d.get("sentence", ""), + translation=d.get("translation", ""), + definitions=d.get("definitions", ""), + sentenceAudio=[AudioAsset(**a) for a in d.get("sentenceAudio", [])], + wordAudio=[AudioAsset(**a) for a in d.get("wordAudio", [])], + images=[ImageAsset(**a) for a in d.get("images", [])], + exampleSentences=d.get("exampleSentences", ""), + notes=d.get("notes", ""), + ) diff --git a/src/editor/__init__.py b/src/editor/__init__.py index e2cbbfa..57c0995 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -11,6 +11,7 @@ from ..languages import Language from ..note_type_mgr import nt_get_lang from ..util import addon_path, show_critical +from ..migaku_fields import get_migaku_fields def editor_get_lang(editor: Editor): @@ -89,67 +90,8 @@ def do_edit(): editor.call_after_note_saved(callback=do_edit, keepFocus=True) -def infer_migaku_type(name: str) -> str: - if re.match( - r"(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)", name, re.IGNORECASE - ): - return "word" - if re.match( - r"(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE - ): - return "image" - if re.match(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): - return "sentence" - if re.match( - r"(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)", - name, - re.IGNORECASE, - ): - return "sentence_translation" - if re.match( - r"(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)", - name, - re.IGNORECASE, - ): - return "example_sentence" - - if re.match( - r"(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)", name, re.IGNORECASE - ): - if re.match(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): - return "sentence_audio" - else: - return "word_audio" - - if re.match( - r"(definition|定義|定义|정의|definición|definição|définition|definition|definição)", - name, - re.IGNORECASE, - ): - return "definition" - if re.match( - r"(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)", name, re.IGNORECASE - ): - return "notes" - return "none" - - def toggle_migaku_mode(editor: Editor): - migaku_fields = get("migakuFields", {}) - data = migaku_fields.get(editor.note.mid, {}) - - nt = editor.note.note_type() - - field_names = [field["name"] for field in nt["flds"]] - - for field_name in field_names: - if field_name not in data: - data[field_name] = infer_migaku_type(field_name) - - for field_name in data.keys(): - if field_name not in field_names: - del data[field_name] - + data = get_migaku_fields(editor.note.note_type()) editor.web.eval(f"MigakuEditor.toggleMode({json.dumps(data)});") @@ -206,7 +148,7 @@ def on_migaku_bridge_cmds(self: Editor, cmd: str, _old): if cmd.startswith("migakuSelectChange"): (_, migaku_type, field_name) = cmd.split(":", 2) migakuFields = get("migakuFields", {}) - print("migakuFields", migakuFields) + set( "migakuFields", { @@ -220,7 +162,7 @@ def on_migaku_bridge_cmds(self: Editor, cmd: str, _old): field_name: migaku_type, }, }, - True, + do_write=True, ) else: _old(self, cmd) diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py index 5f6a202..ae9cc0b 100644 --- a/src/editor/current_editor.py +++ b/src/editor/current_editor.py @@ -2,6 +2,7 @@ from anki.notes import Note from aqt.editor import Editor import aqt +from ..migaku_fields import get_migaku_fields current_editors = [] @@ -39,3 +40,73 @@ def get_current_note_info() -> Note: "reviewer": aqt.mw.reviewer, } return None + + +def get_add_cards() -> Note: + for editor in reversed(current_editors): + if editor.addMode: + return { + "note": editor.note, + "editor": editor, + } + + return None + +current_note_type_id = 0 +current_deck_id = 0 + +def get_add_cards_info(): + base = get_add_cards() + + if base: + note = base["note"] + tags = note.tags + notetype = note.note_type() + + fields = get_migaku_fields(notetype) + notetype_name = notetype["name"] + notetype_id = notetype["id"] + + deck_id = current_deck_id + deck = aqt.mw.col.decks.get(deck_id) + deck_name = deck["name"] + + else: + notetype_id = aqt.mw.col.get_config("curModel") + notetype = aqt.mw.col.models.get(notetype_id) + fields = get_migaku_fields(notetype) + notetype_name = notetype["name"] + + deck_id = aqt.mw.col.get_config("curDeck") + deck = aqt.mw.col.decks.get(deck_id) + deck_name = deck["name"] + tags = [] + + return { + "fields": fields, + "notetype": notetype, + "notetype_name": notetype_name, + "notetype_id": notetype_id, + "deck": deck, + "deck_name": deck_name, + "deck_id": deck_id, + "tags": tags, + } + + +def on_addcards_did_change_note_type(editor, old_id, new_id): + global current_note_type_id + current_note_type_id = new_id + + +def on_addcards_did_change_deck(new_id): + global current_deck_id + current_deck_id = new_id + + +def get_current_note_type_id(): + return current_note_type_id + + +def get_current_deck_id(): + return current_deck_id diff --git a/src/editor/editor.js b/src/editor/editor.js index 40b9ca5..097db5b 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -25,16 +25,17 @@ MigakuEditor.hideButtons = function () { } } +/** These are the values on CardFields */ const selectorOptions = [ { value: 'none', text: '(None)' }, { value: 'sentence', text: 'Sentence' }, - { value: 'word', text: 'Word' }, - { value: 'sentence_translation', text: 'Sentence Translation' }, - { value: 'sentence_audio', text: 'Sentence Audio' }, - { value: 'word_audio', text: 'Word Audio' }, - { value: 'image', text: 'Image' }, - { value: 'definition', text: 'Definitions' }, - { value: 'example_sentence', text: 'Example sentences' }, + { value: 'targetWord', text: 'Word' }, + { value: 'translation', text: 'Sentence Translation' }, + { value: 'sentenceAudio', text: 'Sentence Audio' }, + { value: 'wordAudio', text: 'Word Audio' }, + { value: 'images', text: 'Image' }, + { value: 'definitions', text: 'Definitions' }, + { value: 'exampleSentences', text: 'Example sentences' }, { value: 'notes', text: 'Notes' }, ] @@ -85,8 +86,6 @@ MigakuEditor.toggleMode = function (settings) { // New Migaku Editor function setupMigakuEditor(settings) { - console.log('data', settings) - document.querySelectorAll('.editing-area').forEach((field) => field.style.display = 'none'); document.querySelectorAll('.plain-text-badge').forEach((field) => field.style.display = 'none'); document.querySelectorAll('svg#mdi-pin-outline').forEach((field) => field.parentElement.parentElement.parentElement.style.display = 'none'); diff --git a/src/migaku_connection/__init__.py b/src/migaku_connection/__init__.py index 9261979..b5e0e2c 100644 --- a/src/migaku_connection/__init__.py +++ b/src/migaku_connection/__init__.py @@ -327,3 +327,8 @@ def set_connected(self): def set_disconnected(self): self.setText("● Browser Extension Not Connected") self.setStyleSheet("color: red") + +class ConnectionCallback: + def __init__(self, connect_callback, disconnect_callback): + aqt.mw.migaku_connection.connected.connect(self) + aqt.mw.migaku_connection.disconnected.connect(self) diff --git a/src/migaku_connection/card_creator.py b/src/migaku_connection/card_creator.py index e5f74a4..2874297 100644 --- a/src/migaku_connection/card_creator.py +++ b/src/migaku_connection/card_creator.py @@ -1,25 +1,20 @@ -from pydub import AudioSegment -from pydub import effects -import time -import subprocess import json -import os import re import aqt from anki.notes import Note -from .migaku_http_handler import MigakuHTTPHandler + from .. import config from .. import util from ..inplace_editor import reviewer_reshow from ..editor.current_editor import get_current_note_info +from .migaku_http_handler import MigakuHTTPHandler +from .handle_files import handle_files + class CardCreator(MigakuHTTPHandler): - image_formats = ["jpg", "gif", "png"] - audio_formats = ["mp3", "ogg", "wav"] - TO_MP3_RE = re.compile(r"\[sound:(.*?)\.(wav|ogg)\]") BR_RE = re.compile(r"") @@ -94,86 +89,18 @@ def create_card(self, card_data_json): aqt.mw.col.save() aqt.mw.taskman.run_on_main(aqt.mw.reset) - self.handle_files(self.request.files) + handle_files(self.request.files) self.finish(json.dumps({"id": note.id})) def handle_definitions(self, msg_id, definition_data): definitions = json.loads(definition_data) - self.handle_files(self.request.files) + handle_files(self.request.files) self.connection._recv_data( {"id": msg_id, "msg": "Migaku-Deliver-Definitions", "data": definitions} ) self.finish("Received defintions from card creator.") - def move_file_to_media_dir(self, file_body, filename): - file_path = util.col_media_path(filename) - with open(file_path, "wb") as file_handle: - file_handle.write(file_body) - - def move_file_to_tmp_dir(self, file_body, filename): - file_path = util.tmp_path(filename) - with open(file_path, "wb") as file_handle: - file_handle.write(file_body) - - def handle_files(self, file_dict): - if file_dict: - for name_internal, sub_file_dicts in file_dict.items(): - sub_file_dict = sub_file_dicts[0] - file_name = sub_file_dict["filename"] - file_body = sub_file_dict["body"] - - suffix = file_name[-3:] - - if suffix in self.image_formats: - self.move_file_to_media_dir(file_body, file_name) - - elif suffix in self.audio_formats: - self.handleAudioFile(file_body, file_name, suffix) - - def handleAudioFile(self, file, filename, suffix): - if config.get("normalize_audio", True) or ( - config.get("convert_audio_mp3", True) and suffix != "mp3" - ): - self.move_file_to_tmp_dir(file, filename) - audio_temp_path = util.tmp_path(filename) - if not self.checkFileExists(audio_temp_path): - alert(filename + " could not be converted to an mp3.") - return - filename = filename[0:-3] + "mp3" - if config.get("normalize_audio", True): - self.moveExtensionMp3NormalizeToMediaFolder(audio_temp_path, filename) - # self.moveExtensionMp3ToMediaFolder(audio_temp_path, filename) - else: - self.moveExtensionMp3ToMediaFolder(audio_temp_path, filename) - else: - print("moving audio file") - self.move_file_to_media_dir(file, filename) - - def checkFileExists(self, source): - now = time.time() - while True: - if os.path.exists(source): - return True - if time.time() - now > 15: - return False - - def moveExtensionMp3NormalizeToMediaFolder(self, source, filename): - path = util.col_media_path(filename) - - def match_target_amplitude(sound, target_dBFS): - change_in_dBFS = target_dBFS - sound.dBFS - return sound.apply_gain(change_in_dBFS) - - sound = AudioSegment.from_file(source) - normalized_sound = match_target_amplitude(sound, -25.0) - with open(path, "wb") as file: - normalized_sound.export(file, format="mp3") - - def moveExtensionMp3ToMediaFolder(self, source, filename): - path = util.col_media_path(filename) - self.connection.ffmpeg.call("-i", source, path) - def handle_data_from_card_creator(self, jsonData): r = self._handle_data_from_card_creator(jsonData) self.finish(r) @@ -189,12 +116,12 @@ def _handle_data_from_card_creator(self, jsonData): templates = data["templates"] default_templates = data["defaultTemplates"] - current_note_info = get_current_note_info() + info = get_current_note_info() - if not current_note_info: + if not info: return "No current note." - note = current_note_info["note"] + note = info["note"] note_type = note.note_type() @@ -226,7 +153,7 @@ def _handle_data_from_card_creator(self, jsonData): if not field_name: return "No field for data_type found for current note" - self.handle_files(self.request.files) + handle_files(self.request.files) field_contents = note[field_name].rstrip() if field_contents: @@ -237,23 +164,23 @@ def _handle_data_from_card_creator(self, jsonData): def update(): # runs GUI code, so it needs to run on main thread - if "editor" in current_note_info: - editor = current_note_info["editor"] + if "editor" in info: + editor = info["editor"] editor.loadNote() if not editor.addMode: editor._save_current_note() - if "reviewer" in current_note_info: + if "reviewer" in info: # NOTE: cannot use aqt.operations.update_note as it invalidates mw note.flush() aqt.mw.col.save() - reviewer_reshow(current_note_info["reviewer"], mute=True) + reviewer_reshow(info["reviewer"], mute=True) aqt.mw.taskman.run_on_main(update) return "Added data to note." def handle_audio_delivery(self, text): - self.handle_files(self.request.files) + handle_files(self.request.files) print("Audio was received by anki.") print(text) self.finish("Audio was received by anki.") @@ -289,10 +216,6 @@ def conv_mp3(match): return text -def alert(msg: str): - aqt.mw.taskman.run_on_main(util.show_info(msg, "Condensed Audio Export")) - - def template_find_field_name_and_syntax_for_data_type(template, data_type): for field_name, field_data in template.items(): if isinstance(field_data, dict): diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py index c6a306e..3568aed 100644 --- a/src/migaku_connection/card_receiver.py +++ b/src/migaku_connection/card_receiver.py @@ -8,13 +8,15 @@ import aqt from anki.notes import Note -from ..editor.current_editor import get_current_note_info +from ..card_types import CardFields, card_fields_from_dict +from .handle_files import handle_files +from ..editor.current_editor import get_add_cards_info from tornado.web import RequestHandler from .migaku_http_handler import MigakuHTTPHandler -from .. import config -from .. import util -from ..inplace_editor import reviewer_reshow +from ..migaku_fields import get_migaku_fields +from ..config import get + class CardReceiver(MigakuHTTPHandler): @@ -25,63 +27,36 @@ class CardReceiver(MigakuHTTPHandler): BR_RE = re.compile(r"") def post(self: RequestHandler): - if not self.check_version(): - self.finish("Card could not be created: Version mismatch") - return - - card = self.get_body_argument("card", default=None) - - if not card: - self.finish("Invalid request.") + try: + body = json.loads(self.request.body) + print('body', body) + card = card_fields_from_dict(body) + print('card', card) + self.create_card(card) + except Exception as e: + self.finish(f"Invalid request: {str(e)}") - self.create_card("") return - def create_card(self, card_data_json): - # card_data = json.loads(card_data_json) - print("create_card") - - info = get_current_note_info() - - if not info: - return "No current note." - - note = info["note"] - - note_type = note.note_type() - if not note_type: - return "Current note has no valid note_type." - - notetype_name = str(note_type.get("name", "")) - notetype_id = str(note_type.get("id", "")) + notetype_name - - note_tags = note.tags - - print("note_tags", note_tags, notetype_name, notetype_id) - - # note = Note(aqt.mw.col, note_type) + def create_card(self, card: CardFields): + info = get_add_cards_info() + note = Note(aqt.mw.col, info["notetype"]) + fields = info["fields"] - # for field in card_data["fields"]: - # if "content" in field and field["content"]: - # content = field["content"] - # field_name = field["name"] - # content = self.post_process_text(content, field_name) - # note[field_name] = content + for (fieldname, type) in fields.items(): + if type == "none": + continue - # tags = card_data.get("tags") - # if tags: - # note.set_tags_from_str(tags) + note[fieldname] = str(getattr(card, type)) - # note.model()["did"] = int(deck_id) - # aqt.mw.col.addNote(note) - # aqt.mw.col.save() - # aqt.mw.taskman.run_on_main(aqt.mw.reset) + note.tags = info["tags"] + note.model()["did"] = int(info["deck_id"]) - self.handle_files(self.request.files) + aqt.mw.col.addNote(note) + aqt.mw.col.save() + aqt.mw.taskman.run_on_main(aqt.mw.reset) - self.finish(json.dumps({"id": 0})) + # handle_files(self.request.files, only_move=True) + print('noteId', note.id) - # def move_file_to_media_dir(self, file_body, filename): - # file_path = util.col_media_path(filename) - # with open(file_path, "wb") as file_handle: - # file_handle.write(file_body) + self.finish(json.dumps({"id": note.id})) diff --git a/src/migaku_connection/handle_files.py b/src/migaku_connection/handle_files.py new file mode 100644 index 0000000..b7618fd --- /dev/null +++ b/src/migaku_connection/handle_files.py @@ -0,0 +1,90 @@ +from pydub import AudioSegment +import time +import os + +import aqt + +from .. import config +from .. import util + +image_formats = ["jpg", "gif", "png"] +audio_formats = ["mp3", "ogg", "wav"] + + +def move_file_to_media_dir(file_body, filename): + file_path = util.col_media_path(filename) + with open(file_path, "wb") as file_handle: + file_handle.write(file_body) + + +def move_file_to_tmp_dir(file_body, filename): + file_path = util.tmp_path(filename) + with open(file_path, "wb") as file_handle: + file_handle.write(file_body) + + +def check_file_exists(source): + now = time.time() + while True: + if os.path.exists(source): + return True + if time.time() - now > 15: + return False + + +def move_extension_mp3_to_media_folder(source, filename): + path = util.col_media_path(filename) + aqt.mw.migaku_connection.ffmpeg.call("-i", source, path) + + +def move_extension_mp3_normalize_to_media_folder(source, filename): + path = util.col_media_path(filename) + + def match_target_amplitude(sound, target_dBFS): + change_in_dBFS = target_dBFS - sound.dBFS + return sound.apply_gain(change_in_dBFS) + + sound = AudioSegment.from_file(source) + normalized_sound = match_target_amplitude(sound, -25.0) + with open(path, "wb") as file: + normalized_sound.export(file, format="mp3") + + +def alert(msg: str): + aqt.mw.taskman.run_on_main(util.show_info(msg, "Condensed Audio Export")) + + +def handle_audio_file(file, filename, suffix): + if config.get("normalize_audio", True) or ( + config.get("convert_audio_mp3", True) and suffix != "mp3" + ): + move_file_to_tmp_dir(file, filename) + audio_temp_path = util.tmp_path(filename) + if not check_file_exists(audio_temp_path): + alert(filename + " could not be converted to an mp3.") + return + filename = filename[0:-3] + "mp3" + if config.get("normalize_audio", True): + move_extension_mp3_normalize_to_media_folder(audio_temp_path, filename) + else: + move_extension_mp3_to_media_folder(audio_temp_path, filename) + else: + print("moving audio file") + move_file_to_media_dir(file, filename) + + +def handle_files(file_dict, only_move=False): + if not file_dict: + return + + for _, sub_file_dicts in file_dict.items(): + sub_file_dict = sub_file_dicts[0] + file_name = sub_file_dict["filename"] + file_body = sub_file_dict["body"] + + suffix = file_name[-3:] + + if suffix in image_formats or only_move: + move_file_to_media_dir(file_body, file_name) + elif suffix in audio_formats: + handle_audio_file(file_body, file_name, suffix) diff --git a/src/migaku_connection/program_manager.py b/src/migaku_connection/program_manager.py index e34d8f2..fdc5bb6 100644 --- a/src/migaku_connection/program_manager.py +++ b/src/migaku_connection/program_manager.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import subprocess import requests import zipfile diff --git a/src/migaku_fields.py b/src/migaku_fields.py new file mode 100644 index 0000000..8850082 --- /dev/null +++ b/src/migaku_fields.py @@ -0,0 +1,75 @@ +import re + +from .config import get +import anki + + +def infer_migaku_type(name: str) -> str: + if re.search( + r"(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)", name, re.IGNORECASE + ): + if re.search(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): + return "sentence_audio" + else: + return "word_audio" + + if re.search( + r"(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)", name, re.IGNORECASE + ): + return "word" + if re.search( + r"(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE + ) or re.search( + r"(picture|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE + ) or re.search( + r"(photo|写真|照片|사진|foto|foto|photo|foto|foto)", name, re.IGNORECASE + ) or re.search( + r"(drawing|絵|图画|그림|dibujo|desenho|dessin|zeichnung|desenho)", name, re.IGNORECASE, + ) or re.search( + r"(screenshots|スクリーンショット|截图|스크린샷|capturas de pantalla|capturas de tela|captures d'écran|bildschirmfotos|capturas de tela)", name, re.IGNORECASE, + ): + return "image" + if re.search(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): + return "sentence" + if re.search( + r"(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)", + name, + re.IGNORECASE, + ): + return "sentence_translation" + if re.search( + r"(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)", + name, + re.IGNORECASE, + ): + return "example_sentence" + + + if re.search( + r"(definition|定義|定义|정의|definición|definição|définition|definition|definição)", + name, + re.IGNORECASE, + ): + return "definition" + if re.search( + r"(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)", name, re.IGNORECASE + ): + return "notes" + return "none" + + +def get_migaku_fields(nt: anki.models.NoteType): + migaku_fields = get("migakuFields", {}) + data = migaku_fields.get(str(nt["id"]), {}) + + field_names = [field["name"] for field in nt["flds"]] + + for field_name in field_names: + if field_name not in data: + data[field_name] = infer_migaku_type(field_name) + + for field_name in data.keys(): + if field_name not in field_names: + del data[field_name] + + return data From b96ccfd76670f8a9735ef164b7fe01a96c82aba2 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 12 Dec 2023 18:49:58 +0100 Subject: [PATCH 08/47] fix: migaku fields infer --- src/__init__.py | 19 ++++++++- src/card_types.py | 8 +++- src/config.py | 1 + src/editor/current_editor.py | 2 + src/migaku_connection/__init__.py | 7 ++-- src/migaku_connection/card_receiver.py | 24 ++++++++--- src/migaku_fields.py | 57 +++++++++++++++----------- src/toolbar/__init__.py | 36 ++++++++++++++++ src/toolbar/toolbar.html | 33 +++++++++++++++ 9 files changed, 152 insertions(+), 35 deletions(-) create mode 100644 src/toolbar/__init__.py create mode 100644 src/toolbar/toolbar.html diff --git a/src/__init__.py b/src/__init__.py index 0192bd9..7f381a8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -29,6 +29,7 @@ retirement, reviewer, settings_window, + toolbar, webview_contextmenu, welcome_wizard, ) @@ -59,8 +60,12 @@ def setup_hooks(): aqt.gui_hooks.editor_did_init.append(editor.editor_did_init) aqt.gui_hooks.editor_did_load_note.append(editor.editor_did_load_note) aqt.gui_hooks.editor_did_init_buttons.append(editor.setup_editor_buttons) - aqt.gui_hooks.add_cards_did_change_deck.append(editor.current_editor.on_addcards_did_change_deck) - aqt.gui_hooks.addcards_did_change_note_type.append(editor.current_editor.on_addcards_did_change_note_type) + aqt.gui_hooks.add_cards_did_change_deck.append( + editor.current_editor.on_addcards_did_change_deck + ) + aqt.gui_hooks.addcards_did_change_note_type.append( + editor.current_editor.on_addcards_did_change_note_type + ) aqt.gui_hooks.editor_did_init.append(editor.current_editor.set_current_editor) aqt.editor.Editor.cleanup = anki.hooks.wrap( @@ -69,6 +74,16 @@ def setup_hooks(): aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) + aqt.gui_hooks.top_toolbar_will_set_right_tray_content.append( + toolbar.inject_migaku_toolbar + ) + aqt.gui_hooks.add_cards_did_change_deck.append( + lambda _: toolbar.refresh_migaku_toolbar() + ) + aqt.gui_hooks.addcards_did_change_note_type.append( + lambda _, _1, _2: toolbar.refresh_migaku_toolbar() + ) + setup_menu() setup_hooks() diff --git a/src/card_types.py b/src/card_types.py index 2ce6c2f..dfd1999 100644 --- a/src/card_types.py +++ b/src/card_types.py @@ -33,13 +33,19 @@ class CardFields: exampleSentences: str = "" notes: str = "" + def card_fields_from_dict(d: dict[str, any]): + sentenceAudios = [AudioAsset(**a) for a in d.get("sentenceAudio", [])] + + for audio in sentenceAudios: + print("audio", audio) + return CardFields( targetWord=d.get("targetWord", ""), sentence=d.get("sentence", ""), translation=d.get("translation", ""), definitions=d.get("definitions", ""), - sentenceAudio=[AudioAsset(**a) for a in d.get("sentenceAudio", [])], + sentenceAudio=[sentenceAudios], wordAudio=[AudioAsset(**a) for a in d.get("wordAudio", [])], images=[ImageAsset(**a) for a in d.get("images", [])], exampleSentences=d.get("exampleSentences", ""), diff --git a/src/config.py b/src/config.py index c80bbdd..8a14f69 100644 --- a/src/config.py +++ b/src/config.py @@ -2,6 +2,7 @@ _config = aqt.mw.addonManager.getConfig(__name__) +_config["migakuFields"] = {} def write(): diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py index ae9cc0b..d2a6275 100644 --- a/src/editor/current_editor.py +++ b/src/editor/current_editor.py @@ -52,9 +52,11 @@ def get_add_cards() -> Note: return None + current_note_type_id = 0 current_deck_id = 0 + def get_add_cards_info(): base = get_add_cards() diff --git a/src/migaku_connection/__init__.py b/src/migaku_connection/__init__.py index b5e0e2c..e62eef4 100644 --- a/src/migaku_connection/__init__.py +++ b/src/migaku_connection/__init__.py @@ -328,7 +328,8 @@ def set_disconnected(self): self.setText("● Browser Extension Not Connected") self.setStyleSheet("color: red") -class ConnectionCallback: + +class ConnectionListener: def __init__(self, connect_callback, disconnect_callback): - aqt.mw.migaku_connection.connected.connect(self) - aqt.mw.migaku_connection.disconnected.connect(self) + aqt.mw.migaku_connection.connected.connect(connect_callback) + aqt.mw.migaku_connection.disconnected.connect(disconnect_callback) diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py index 3568aed..e34e8e0 100644 --- a/src/migaku_connection/card_receiver.py +++ b/src/migaku_connection/card_receiver.py @@ -18,7 +18,6 @@ from ..config import get - class CardReceiver(MigakuHTTPHandler): image_formats = ["webp"] audio_formats = ["m4a"] @@ -29,9 +28,9 @@ class CardReceiver(MigakuHTTPHandler): def post(self: RequestHandler): try: body = json.loads(self.request.body) - print('body', body) + print("body", body) card = card_fields_from_dict(body) - print('card', card) + print("card", card) self.create_card(card) except Exception as e: self.finish(f"Invalid request: {str(e)}") @@ -43,11 +42,24 @@ def create_card(self, card: CardFields): note = Note(aqt.mw.col, info["notetype"]) fields = info["fields"] - for (fieldname, type) in fields.items(): + for fieldname, type in fields.items(): if type == "none": continue - note[fieldname] = str(getattr(card, type)) + if type == "sentence_audio": + # { value: 'none', text: '(None)' }, + # { value: 'sentence', text: 'Sentence' }, + # { value: 'targetWord', text: 'Word' }, + # { value: 'translation', text: 'Sentence Translation' }, + # { value: 'sentenceAudio', text: 'Sentence Audio' }, + # { value: 'wordAudio', text: 'Word Audio' }, + # { value: 'images', text: 'Image' }, + # { value: 'definitions', text: 'Definitions' }, + # { value: 'exampleSentences', text: 'Example sentences' }, + # { value: 'notes', text: 'Notes' }, + pass + else: + note[fieldname] = str(getattr(card, type)) note.tags = info["tags"] note.model()["did"] = int(info["deck_id"]) @@ -57,6 +69,6 @@ def create_card(self, card: CardFields): aqt.mw.taskman.run_on_main(aqt.mw.reset) # handle_files(self.request.files, only_move=True) - print('noteId', note.id) + print("noteId", note.id) self.finish(json.dumps({"id": note.id})) diff --git a/src/migaku_fields.py b/src/migaku_fields.py index 8850082..4bce4d0 100644 --- a/src/migaku_fields.py +++ b/src/migaku_fields.py @@ -1,56 +1,67 @@ import re +from typing import Literal from .config import get import anki -def infer_migaku_type(name: str) -> str: +def infer_migaku_type(name: str) -> Literal['none', 'sentence', 'targetWord', 'translation', 'sentenceAudio', 'wordAudio', 'images', 'definitions', 'exampleSentences', 'notes']: if re.search( r"(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)", name, re.IGNORECASE ): if re.search(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): - return "sentence_audio" + return "sentenceAudio" else: - return "word_audio" + return "wordAudio" if re.search( r"(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)", name, re.IGNORECASE ): - return "word" - if re.search( - r"(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE - ) or re.search( - r"(picture|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE - ) or re.search( - r"(photo|写真|照片|사진|foto|foto|photo|foto|foto)", name, re.IGNORECASE - ) or re.search( - r"(drawing|絵|图画|그림|dibujo|desenho|dessin|zeichnung|desenho)", name, re.IGNORECASE, - ) or re.search( - r"(screenshots|スクリーンショット|截图|스크린샷|capturas de pantalla|capturas de tela|captures d'écran|bildschirmfotos|capturas de tela)", name, re.IGNORECASE, + return "targetWord" + if ( + re.search( + r"(image|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE + ) + or re.search( + r"(picture|画像|图片|이미지|imagen|imagem|image|bild|imagem)", name, re.IGNORECASE + ) + or re.search(r"(photo|写真|照片|사진|foto|foto|photo|foto|foto)", name, re.IGNORECASE) + or re.search( + r"(drawing|絵|图画|그림|dibujo|desenho|dessin|zeichnung|desenho)", + name, + re.IGNORECASE, + ) + or re.search( + r"(screenshots|スクリーンショット|截图|스크린샷|capturas de pantalla|capturas de tela|captures d'écran|bildschirmfotos|capturas de tela)", + name, + re.IGNORECASE, + ) ): - return "image" - if re.search(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): - return "sentence" + return "images" + if re.search( - r"(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)", + r"(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)", name, re.IGNORECASE, ): - return "sentence_translation" + return "exampleSentences" + + if re.search(r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE): + return "sentence" + if re.search( - r"(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)", + r"(translation|訳|译|번역|traducción|traduction|übersetzung|tradução)", name, re.IGNORECASE, ): - return "example_sentence" - + return "translation" if re.search( r"(definition|定義|定义|정의|definición|definição|définition|definition|definição)", name, re.IGNORECASE, ): - return "definition" + return "definitions" if re.search( r"(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)", name, re.IGNORECASE ): diff --git a/src/toolbar/__init__.py b/src/toolbar/__init__.py new file mode 100644 index 0000000..358a417 --- /dev/null +++ b/src/toolbar/__init__.py @@ -0,0 +1,36 @@ +import json + +from ..editor.current_editor import get_add_cards_info +from ..migaku_connection import ConnectionListener +from ..util import addon_path + + +global_toolbar = None + + +def activate_migaku_toolbar(toolbar): + info = get_add_cards_info() + toolbar.web.eval(f"MigakuToolbar.activate({json.dumps(info)})") + + +def refresh_migaku_toolbar(): + info = get_add_cards_info() + print("hey", global_toolbar) + global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") + + +def deactivate_migaku_toolbar(toolbar): + toolbar.web.eval("MigakuToolbar.deactivate()") + + +def inject_migaku_toolbar(html: str, toolbar): + global global_toolbar + global_toolbar = toolbar + + ConnectionListener( + lambda: activate_migaku_toolbar(toolbar), + lambda: deactivate_migaku_toolbar(toolbar), + ) + + with open(addon_path("toolbar/toolbar.html"), "r", encoding="utf-8") as file: + html.append(file.read()) diff --git a/src/toolbar/toolbar.html b/src/toolbar/toolbar.html new file mode 100644 index 0000000..e313424 --- /dev/null +++ b/src/toolbar/toolbar.html @@ -0,0 +1,33 @@ + + + From 57f828c6399793fc9d5386d77f5bc3ec1309c022 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 12 Dec 2023 19:02:48 +0100 Subject: [PATCH 09/47] feat: update migaku nt + deck on open addcards --- src/__init__.py | 7 +++++++ src/migaku_fields.py | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 7f381a8..2229a05 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -84,6 +84,13 @@ def setup_hooks(): lambda _, _1, _2: toolbar.refresh_migaku_toolbar() ) + aqt.gui_hooks.add_cards_did_init.append(lambda _: toolbar.refresh_migaku_toolbar()) + aqt.addcards.AddCards._close = anki.hooks.wrap( + aqt.addcards.AddCards._close, + lambda _: toolbar.refresh_migaku_toolbar(), + "after", + ) + setup_menu() setup_hooks() diff --git a/src/migaku_fields.py b/src/migaku_fields.py index 4bce4d0..a206feb 100644 --- a/src/migaku_fields.py +++ b/src/migaku_fields.py @@ -5,7 +5,20 @@ import anki -def infer_migaku_type(name: str) -> Literal['none', 'sentence', 'targetWord', 'translation', 'sentenceAudio', 'wordAudio', 'images', 'definitions', 'exampleSentences', 'notes']: +def infer_migaku_type( + name: str, +) -> Literal[ + "none", + "sentence", + "targetWord", + "translation", + "sentenceAudio", + "wordAudio", + "images", + "definitions", + "exampleSentences", + "notes", +]: if re.search( r"(audio|音声|音频|오디오|audio|áudio|audio|audio|áudio)", name, re.IGNORECASE ): @@ -32,7 +45,7 @@ def infer_migaku_type(name: str) -> Literal['none', 'sentence', 'targetWord', 't re.IGNORECASE, ) or re.search( - r"(screenshots|スクリーンショット|截图|스크린샷|capturas de pantalla|capturas de tela|captures d'écran|bildschirmfotos|capturas de tela)", + r"(screenshot|スクリーンショット|截图|스크린샷|capturas de pantalla|capturas de tela|captures d'écran|bildschirmfotos|capturas de tela)", name, re.IGNORECASE, ) @@ -62,9 +75,7 @@ def infer_migaku_type(name: str) -> Literal['none', 'sentence', 'targetWord', 't re.IGNORECASE, ): return "definitions" - if re.search( - r"(notes|ノート|笔记|노트|notas|notas|notes|notizen|notas)", name, re.IGNORECASE - ): + if re.search(r"(note|ノート|笔记|노트|nota|nota|note|notiz|nota)", name, re.IGNORECASE): return "notes" return "none" From 5b167911f80eecae5f54f468776a665f16a08f10 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 12 Dec 2023 19:22:20 +0100 Subject: [PATCH 10/47] feat: click on top indicator to open editor --- src/__init__.py | 2 +- src/toolbar/__init__.py | 7 ++++++- src/toolbar/toolbar.html | 22 +++++++++++++++------- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 2229a05..f089a25 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -74,7 +74,7 @@ def setup_hooks(): aqt.gui_hooks.profile_did_open.append(note_type_mgr.update_all_installed) - aqt.gui_hooks.top_toolbar_will_set_right_tray_content.append( + aqt.gui_hooks.top_toolbar_will_set_left_tray_content.append( toolbar.inject_migaku_toolbar ) aqt.gui_hooks.add_cards_did_change_deck.append( diff --git a/src/toolbar/__init__.py b/src/toolbar/__init__.py index 358a417..6e10999 100644 --- a/src/toolbar/__init__.py +++ b/src/toolbar/__init__.py @@ -1,4 +1,5 @@ import json +import aqt from ..editor.current_editor import get_add_cards_info from ..migaku_connection import ConnectionListener @@ -8,14 +9,18 @@ global_toolbar = None +def open_add_cards(): + aqt.mw.onAddCard() + + def activate_migaku_toolbar(toolbar): info = get_add_cards_info() toolbar.web.eval(f"MigakuToolbar.activate({json.dumps(info)})") + toolbar.link_handlers["openAddCards"] = open_add_cards def refresh_migaku_toolbar(): info = get_add_cards_info() - print("hey", global_toolbar) global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") diff --git a/src/toolbar/toolbar.html b/src/toolbar/toolbar.html index e313424..3f349d0 100644 --- a/src/toolbar/toolbar.html +++ b/src/toolbar/toolbar.html @@ -1,8 +1,3 @@ - - + + From 01f8d5a7fdd41d2d51e5edb805ae47eb7d8021df Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 12 Dec 2023 19:45:35 +0100 Subject: [PATCH 11/47] feat: toggle away migaku mode before changing notetype --- src/__init__.py | 6 ++++++ src/editor/__init__.py | 3 +++ src/editor/editor.js | 18 +++++++++--------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index f089a25..d5b79d8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -91,6 +91,12 @@ def setup_hooks(): "after", ) + aqt.addcards.AddCards.on_notetype_change = anki.hooks.wrap( + aqt.addcards.AddCards.on_notetype_change, + lambda addcards, _1: editor.reset_migaku_mode(addcards.editor), + "before", + ) + setup_menu() setup_hooks() diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 57c0995..4eb878b 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -171,3 +171,6 @@ def on_migaku_bridge_cmds(self: Editor, cmd: str, _old): def editor_did_init(editor: Editor): with open(addon_path("editor/editor.js"), "r", encoding="utf-8") as editor_file: editor.web.eval(editor_file.read()) + +def reset_migaku_mode(editor: Editor): + editor.web.eval(f"MigakuEditor.resetMigakuEditor();") diff --git a/src/editor/editor.js b/src/editor/editor.js index 097db5b..5d867d6 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -76,14 +76,6 @@ const hiddenButtonCategories = [ 'image-occlusion-button', ] -MigakuEditor.toggleMode = function (settings) { - if (document.querySelector('.migaku-field-selector')) { - resetMigakuEditor(); - } else { - setupMigakuEditor(settings); - } -} - // New Migaku Editor function setupMigakuEditor(settings) { document.querySelectorAll('.editing-area').forEach((field) => field.style.display = 'none'); @@ -96,7 +88,7 @@ function setupMigakuEditor(settings) { } } -function resetMigakuEditor() { +MigakuEditor.resetMigakuEditor = function () { document.querySelectorAll('.editing-area').forEach((field) => field.style.display = ''); document.querySelectorAll('.plain-text-badge').forEach((field) => field.style.display = ''); document.querySelectorAll('svg#mdi-pin-outline').forEach((field) => field.parentElement.parentElement.parentElement.style.display = ''); @@ -104,3 +96,11 @@ function resetMigakuEditor() { document.querySelectorAll('.migaku-field-selector').forEach((selector) => selector.remove()); } + +MigakuEditor.toggleMode = function (settings) { + if (document.querySelector('.migaku-field-selector')) { + MigakuEditor.resetMigakuEditor(); + } else { + setupMigakuEditor(settings); + } +} From cc0a6f7dd565062244f9fb068d80b6cd60b1cf36 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Wed, 13 Dec 2023 04:26:35 +0100 Subject: [PATCH 12/47] feat: convert audio and image files --- src/card_types.py | 43 +++++++++++++++++++------- src/editor/__init__.py | 1 + src/migaku_connection/card_receiver.py | 8 ----- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/card_types.py b/src/card_types.py index dfd1999..3f1a5e9 100644 --- a/src/card_types.py +++ b/src/card_types.py @@ -1,7 +1,8 @@ +from base64 import b64decode from dataclasses import dataclass, field from typing import Optional -# From media.types +from .migaku_connection.handle_files import move_file_to_media_dir @dataclass @@ -27,27 +28,47 @@ class CardFields: sentence: str = "" translation: str = "" definitions: str = "" - sentenceAudio: list[AudioAsset] = field(default_factory=lambda: []) - wordAudio: list[AudioAsset] = field(default_factory=lambda: []) - images: list[ImageAsset] = field(default_factory=lambda: []) + sentenceAudio: str = "" + wordAudio: str = "" + images: str = "" exampleSentences: str = "" notes: str = "" -def card_fields_from_dict(d: dict[str, any]): - sentenceAudios = [AudioAsset(**a) for a in d.get("sentenceAudio", [])] +def process_image_asset(image: ImageAsset): + data = image.src.split(",", 1)[1] + name = f"{image.title}-{image.id}.webp" + move_file_to_media_dir(b64decode(data), name) + return f"{image.alt}" + + +def process_audio_asset(audio: AudioAsset): + data = audio.input.split(",", 1)[1] + name = f"{audio.title}-{audio.id}.m4a" + move_file_to_media_dir(b64decode(data), name) + return f"[sound:{name}]" + - for audio in sentenceAudios: - print("audio", audio) +def card_fields_from_dict(d: dict[str, any]): + br = "\n
\n" + sentenceAudios = br.join( + [process_audio_asset(AudioAsset(**a)) for a in d.get("sentenceAudio", [])] + ) + wordAudios = br.join( + [process_audio_asset(AudioAsset(**a)) for a in d.get("wordAudio", [])] + ) + imagess = br.join( + [process_image_asset(ImageAsset(**a)) for a in d.get("images", [])] + ) return CardFields( targetWord=d.get("targetWord", ""), sentence=d.get("sentence", ""), translation=d.get("translation", ""), definitions=d.get("definitions", ""), - sentenceAudio=[sentenceAudios], - wordAudio=[AudioAsset(**a) for a in d.get("wordAudio", [])], - images=[ImageAsset(**a) for a in d.get("images", [])], + sentenceAudio=sentenceAudios, + wordAudio=wordAudios, + images=imagess, exampleSentences=d.get("exampleSentences", ""), notes=d.get("notes", ""), ) diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 4eb878b..98ffff7 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -172,5 +172,6 @@ def editor_did_init(editor: Editor): with open(addon_path("editor/editor.js"), "r", encoding="utf-8") as editor_file: editor.web.eval(editor_file.read()) + def reset_migaku_mode(editor: Editor): editor.web.eval(f"MigakuEditor.resetMigakuEditor();") diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py index e34e8e0..cfa30ce 100644 --- a/src/migaku_connection/card_receiver.py +++ b/src/migaku_connection/card_receiver.py @@ -1,21 +1,13 @@ -from pydub import AudioSegment -from pydub import effects -import time -import subprocess import json -import os import re import aqt from anki.notes import Note from ..card_types import CardFields, card_fields_from_dict -from .handle_files import handle_files from ..editor.current_editor import get_add_cards_info from tornado.web import RequestHandler from .migaku_http_handler import MigakuHTTPHandler -from ..migaku_fields import get_migaku_fields -from ..config import get class CardReceiver(MigakuHTTPHandler): From 37c34650fa3197596593dff15d6eed88cebf2cd6 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 14 Dec 2023 02:30:37 +0100 Subject: [PATCH 13/47] fix: wrong deck being set --- src/__init__.py | 4 +++- src/editor/__init__.py | 2 +- src/editor/current_editor.py | 24 +++++++++++++++++++----- src/migaku_connection/__init__.py | 4 ++-- src/toolbar/__init__.py | 7 +++++++ 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index d5b79d8..f5c32bc 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -84,7 +84,9 @@ def setup_hooks(): lambda _, _1, _2: toolbar.refresh_migaku_toolbar() ) - aqt.gui_hooks.add_cards_did_init.append(lambda _: toolbar.refresh_migaku_toolbar()) + aqt.gui_hooks.add_cards_did_init.append( + lambda _: toolbar.refresh_migaku_toolbar_opened_addcards() + ) aqt.addcards.AddCards._close = anki.hooks.wrap( aqt.addcards.AddCards._close, lambda _: toolbar.refresh_migaku_toolbar(), diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 98ffff7..17d5a98 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -98,7 +98,7 @@ def toggle_migaku_mode(editor: Editor): def setup_editor_buttons(buttons: List[str], editor: Editor): added_buttons = [ editor.addButton( - label="Migaku Mode", + label="Field Maps", icon=None, id="migaku_btn_toggle_mode", cmd="migaku_toggle_mode", diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py index d2a6275..f7138c0 100644 --- a/src/editor/current_editor.py +++ b/src/editor/current_editor.py @@ -57,11 +57,25 @@ def get_add_cards() -> Note: current_deck_id = 0 -def get_add_cards_info(): - base = get_add_cards() +def get_add_cards_info(defaults=None): + addcards = get_add_cards() - if base: - note = base["note"] + if addcards and defaults: + note = addcards["note"] + tags = note.tags + + notetype_id = defaults.notetype_id + notetype = aqt.mw.col.models.get(notetype_id) + notetype_name = notetype["name"] + + deck_id = defaults.deck_id + deck = aqt.mw.col.decks.get(deck_id) + deck_name = deck["name"] + + fields = get_migaku_fields(notetype) + + elif addcards: + note = addcards["note"] tags = note.tags notetype = note.note_type() @@ -69,7 +83,7 @@ def get_add_cards_info(): notetype_name = notetype["name"] notetype_id = notetype["id"] - deck_id = current_deck_id + deck_id = get_current_deck_id() deck = aqt.mw.col.decks.get(deck_id) deck_name = deck["name"] diff --git a/src/migaku_connection/__init__.py b/src/migaku_connection/__init__.py index e62eef4..1282787 100644 --- a/src/migaku_connection/__init__.py +++ b/src/migaku_connection/__init__.py @@ -321,11 +321,11 @@ def set_status(self, status): self.set_disconnected() def set_connected(self): - self.setText("● Browser Extension Connected") + self.setText("● Legacy Browser Extension Connected") self.setStyleSheet("color: green") def set_disconnected(self): - self.setText("● Browser Extension Not Connected") + self.setText("● Legacy Browser Extension Not Connected") self.setStyleSheet("color: red") diff --git a/src/toolbar/__init__.py b/src/toolbar/__init__.py index 6e10999..aac43c1 100644 --- a/src/toolbar/__init__.py +++ b/src/toolbar/__init__.py @@ -24,6 +24,13 @@ def refresh_migaku_toolbar(): global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") +def refresh_migaku_toolbar_opened_addcards(): + defaults = aqt.mw.col.defaults_for_adding(current_review_card=aqt.mw.reviewer.card) + info = get_add_cards_info(defaults) + + global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") + + def deactivate_migaku_toolbar(toolbar): toolbar.web.eval("MigakuToolbar.deactivate()") From ef0d9e0c3c05ed019efec05331f43a8eedb85bcc Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 14 Dec 2023 02:33:22 +0100 Subject: [PATCH 14/47] fix: do not reset migakuFields --- src/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config.py b/src/config.py index 8a14f69..c80bbdd 100644 --- a/src/config.py +++ b/src/config.py @@ -2,7 +2,6 @@ _config = aqt.mw.addonManager.getConfig(__name__) -_config["migakuFields"] = {} def write(): From e1acf812c1277c2bf0f24f1a5401ba9896262559 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 14 Dec 2023 03:07:39 +0100 Subject: [PATCH 15/47] feat: add new migaku cards to history --- src/__init__.py | 2 +- src/editor/__init__.py | 62 +++++++++++++++----------- src/editor/current_editor.py | 12 ++++- src/migaku_connection/card_receiver.py | 22 ++------- src/toolbar/__init__.py | 9 +++- src/toolbar/toolbar.html | 11 +++-- 6 files changed, 67 insertions(+), 51 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index f5c32bc..8bbf230 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -64,7 +64,7 @@ def setup_hooks(): editor.current_editor.on_addcards_did_change_deck ) aqt.gui_hooks.addcards_did_change_note_type.append( - editor.current_editor.on_addcards_did_change_note_type + lambda _, _1, id: editor.current_editor.on_addcards_did_change_note_type(id) ) aqt.gui_hooks.editor_did_init.append(editor.current_editor.set_current_editor) diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 17d5a98..774c240 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -96,33 +96,41 @@ def toggle_migaku_mode(editor: Editor): def setup_editor_buttons(buttons: List[str], editor: Editor): - added_buttons = [ - editor.addButton( - label="Field Maps", - icon=None, - id="migaku_btn_toggle_mode", - cmd="migaku_toggle_mode", - toggleable=True, - disables=False, - func=toggle_migaku_mode, - ), - editor.addButton( - icon="tmp", - id="migaku_btn_syntax_generate", - cmd="migaku_syntax_generate", - func=editor_generate_syntax, - tip="Generate syntax (F2)", - keys="F2", - ), - editor.addButton( - icon="tmp", - id="migaku_btn_syntax_remove", - cmd="migaku_syntax_remove", - func=editor_remove_syntax, - tip="Remove syntax (F4)", - keys="F4", - ), - ] + added_buttons = [] + + if editor.addMode: + added_buttons.append( + editor.addButton( + label="Field Maps", + icon=None, + id="migaku_btn_toggle_mode", + cmd="migaku_toggle_mode", + toggleable=True, + disables=False, + func=toggle_migaku_mode, + ) + ) + + added_buttons.extend( + [ + editor.addButton( + icon="tmp", + id="migaku_btn_syntax_generate", + cmd="migaku_syntax_generate", + func=editor_generate_syntax, + tip="Generate syntax (F2)", + keys="F2", + ), + editor.addButton( + icon="tmp", + id="migaku_btn_syntax_remove", + cmd="migaku_syntax_remove", + func=editor_remove_syntax, + tip="Remove syntax (F4)", + keys="F4", + ), + ] + ) buttons[0:0] = added_buttons return buttons diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py index f7138c0..bf24a36 100644 --- a/src/editor/current_editor.py +++ b/src/editor/current_editor.py @@ -46,6 +46,7 @@ def get_add_cards() -> Note: for editor in reversed(current_editors): if editor.addMode: return { + "addcards": editor.parentWindow, "note": editor.note, "editor": editor, } @@ -57,6 +58,15 @@ def get_add_cards() -> Note: current_deck_id = 0 +def add_cards_add_to_history(note): + addcards = get_add_cards() + + if not addcards: + return + + addcards["addcards"].addHistory(note) + + def get_add_cards_info(defaults=None): addcards = get_add_cards() @@ -110,7 +120,7 @@ def get_add_cards_info(defaults=None): } -def on_addcards_did_change_note_type(editor, old_id, new_id): +def on_addcards_did_change_note_type(new_id): global current_note_type_id current_note_type_id = new_id diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py index cfa30ce..6c461bb 100644 --- a/src/migaku_connection/card_receiver.py +++ b/src/migaku_connection/card_receiver.py @@ -4,7 +4,7 @@ import aqt from anki.notes import Note from ..card_types import CardFields, card_fields_from_dict -from ..editor.current_editor import get_add_cards_info +from ..editor.current_editor import add_cards_add_to_history, get_add_cards_info from tornado.web import RequestHandler from .migaku_http_handler import MigakuHTTPHandler @@ -38,20 +38,7 @@ def create_card(self, card: CardFields): if type == "none": continue - if type == "sentence_audio": - # { value: 'none', text: '(None)' }, - # { value: 'sentence', text: 'Sentence' }, - # { value: 'targetWord', text: 'Word' }, - # { value: 'translation', text: 'Sentence Translation' }, - # { value: 'sentenceAudio', text: 'Sentence Audio' }, - # { value: 'wordAudio', text: 'Word Audio' }, - # { value: 'images', text: 'Image' }, - # { value: 'definitions', text: 'Definitions' }, - # { value: 'exampleSentences', text: 'Example sentences' }, - # { value: 'notes', text: 'Notes' }, - pass - else: - note[fieldname] = str(getattr(card, type)) + note[fieldname] = str(getattr(card, type)) note.tags = info["tags"] note.model()["did"] = int(info["deck_id"]) @@ -59,8 +46,7 @@ def create_card(self, card: CardFields): aqt.mw.col.addNote(note) aqt.mw.col.save() aqt.mw.taskman.run_on_main(aqt.mw.reset) - - # handle_files(self.request.files, only_move=True) - print("noteId", note.id) + aqt.mw.taskman.run_on_main(lambda: aqt.utils.tooltip("Migaku Card created")) + aqt.mw.taskman.run_on_main(lambda: add_cards_add_to_history(note)) self.finish(json.dumps({"id": note.id})) diff --git a/src/toolbar/__init__.py b/src/toolbar/__init__.py index aac43c1..bf7303c 100644 --- a/src/toolbar/__init__.py +++ b/src/toolbar/__init__.py @@ -1,7 +1,11 @@ import json import aqt -from ..editor.current_editor import get_add_cards_info +from ..editor.current_editor import ( + get_add_cards_info, + on_addcards_did_change_deck, + on_addcards_did_change_note_type, +) from ..migaku_connection import ConnectionListener from ..util import addon_path @@ -28,6 +32,9 @@ def refresh_migaku_toolbar_opened_addcards(): defaults = aqt.mw.col.defaults_for_adding(current_review_card=aqt.mw.reviewer.card) info = get_add_cards_info(defaults) + on_addcards_did_change_deck(defaults.deck_id) + on_addcards_did_change_note_type(defaults.notetype_id) + global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") diff --git a/src/toolbar/toolbar.html b/src/toolbar/toolbar.html index 3f349d0..302bc0b 100644 --- a/src/toolbar/toolbar.html +++ b/src/toolbar/toolbar.html @@ -20,7 +20,7 @@ MigakuToolbar.activate = (info) => { MigakuToolbar.refresh(info) const toolbar = document.getElementById('migakuToolbar') - toolbar.style.display = 'block' + toolbar.style.display = 'flex' toolbar.addEventListener('click', openAddCards) } @@ -36,6 +36,11 @@ From 79337e4cc8e9ae66b2b7a5af4db5d7431fcc665f Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 14 Dec 2023 03:10:19 +0100 Subject: [PATCH 16/47] feat: reposition Migaku fields to end --- src/editor/__init__.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 774c240..3ff670e 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -96,7 +96,24 @@ def toggle_migaku_mode(editor: Editor): def setup_editor_buttons(buttons: List[str], editor: Editor): - added_buttons = [] + added_buttons = [ + editor.addButton( + icon="tmp", + id="migaku_btn_syntax_generate", + cmd="migaku_syntax_generate", + func=editor_generate_syntax, + tip="Generate syntax (F2)", + keys="F2", + ), + editor.addButton( + icon="tmp", + id="migaku_btn_syntax_remove", + cmd="migaku_syntax_remove", + func=editor_remove_syntax, + tip="Remove syntax (F4)", + keys="F4", + ), + ] if editor.addMode: added_buttons.append( @@ -111,27 +128,6 @@ def setup_editor_buttons(buttons: List[str], editor: Editor): ) ) - added_buttons.extend( - [ - editor.addButton( - icon="tmp", - id="migaku_btn_syntax_generate", - cmd="migaku_syntax_generate", - func=editor_generate_syntax, - tip="Generate syntax (F2)", - keys="F2", - ), - editor.addButton( - icon="tmp", - id="migaku_btn_syntax_remove", - cmd="migaku_syntax_remove", - func=editor_remove_syntax, - tip="Remove syntax (F4)", - keys="F4", - ), - ] - ) - buttons[0:0] = added_buttons return buttons From a1a51f3463ebd3d0282cd237431ea44e6f4acd5a Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 16 Dec 2023 01:19:57 +0100 Subject: [PATCH 17/47] feat: intercept migaku cards --- src/__init__.py | 6 ++++++ src/editor/__init__.py | 6 +++++- src/editor/current_editor.py | 20 ++++++++++++++++++ src/editor/editor.js | 28 ++++++++++++++++++++++++++ src/migaku_connection/card_receiver.py | 18 +++++++++++++---- 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 8bbf230..d08c8d6 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -99,6 +99,12 @@ def setup_hooks(): "before", ) + aqt.deckbrowser.DeckBrowser.set_current_deck = anki.hooks.wrap( + aqt.deckbrowser.DeckBrowser.set_current_deck, + lambda self, deck_id: toolbar.refresh_migaku_toolbar(), + "after", + ) + setup_menu() setup_hooks() diff --git a/src/editor/__init__.py b/src/editor/__init__.py index 3ff670e..98b026c 100644 --- a/src/editor/__init__.py +++ b/src/editor/__init__.py @@ -149,7 +149,11 @@ def editor_did_load_note(editor: Editor): def on_migaku_bridge_cmds(self: Editor, cmd: str, _old): - if cmd.startswith("migakuSelectChange"): + if cmd.startswith("migakuIntercept"): + (_, value) = cmd.split(":", 1) + set("migakuIntercept", True if value == "true" else False, do_write=True) + + elif cmd.startswith("migakuSelectChange"): (_, migaku_type, field_name) = cmd.split(":", 2) migakuFields = get("migakuFields", {}) diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py index bf24a36..b611434 100644 --- a/src/editor/current_editor.py +++ b/src/editor/current_editor.py @@ -2,6 +2,7 @@ from anki.notes import Note from aqt.editor import Editor import aqt +from ..card_types import CardFields from ..migaku_fields import get_migaku_fields current_editors = [] @@ -120,6 +121,25 @@ def get_add_cards_info(defaults=None): } +def map_to_add_cards(card: CardFields): + addcards = get_add_cards() + + if not addcards: + return False + + info = get_add_cards_info() + note = addcards["note"] + fields = info["fields"] + + for fieldname, type in fields.items(): + if type == "none": + continue + + note[fieldname] = str(getattr(card, type)) + + return True + + def on_addcards_did_change_note_type(new_id): global current_note_type_id current_note_type_id = new_id diff --git a/src/editor/editor.js b/src/editor/editor.js index 5d867d6..dc1a0af 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -104,3 +104,31 @@ MigakuEditor.toggleMode = function (settings) { setupMigakuEditor(settings); } } + + +require('anki/ui') + .loaded + .then(() => new Promise((resolve) => setTimeout(resolve, 400))) + .then(() => { + const migakuMode = document.getElementById('migaku_btn_toggle_mode') + if (!migakuMode) return + + const input = document.createElement('input') + input.type = 'checkbox' + input.id = 'migaku_btn_intercept_fields' + input.style.margin = '0 3px' + + const label = document.createElement('label') + label.htmlFor = 'migaku_btn_intercept_fields' + label.textContent = 'Intercept Fields' + label.style.userSelect = 'none' + label.style.padding = '0 3px' + + input.addEventListener('change', (e) => { + const cmd = `migakuIntercept:${e.currentTarget.checked}` + bridgeCommand(cmd) + }) + + migakuMode.parentElement.append(input) + migakuMode.parentElement.append(label) + }) diff --git a/src/migaku_connection/card_receiver.py b/src/migaku_connection/card_receiver.py index 6c461bb..2e41d5b 100644 --- a/src/migaku_connection/card_receiver.py +++ b/src/migaku_connection/card_receiver.py @@ -3,8 +3,13 @@ import aqt from anki.notes import Note +from ..config import get from ..card_types import CardFields, card_fields_from_dict -from ..editor.current_editor import add_cards_add_to_history, get_add_cards_info +from ..editor.current_editor import ( + add_cards_add_to_history, + get_add_cards_info, + map_to_add_cards, +) from tornado.web import RequestHandler from .migaku_http_handler import MigakuHTTPHandler @@ -20,9 +25,7 @@ class CardReceiver(MigakuHTTPHandler): def post(self: RequestHandler): try: body = json.loads(self.request.body) - print("body", body) card = card_fields_from_dict(body) - print("card", card) self.create_card(card) except Exception as e: self.finish(f"Invalid request: {str(e)}") @@ -30,7 +33,14 @@ def post(self: RequestHandler): return def create_card(self, card: CardFields): + if get("migakuIntercept"): + success = map_to_add_cards(card) + if success: + self.finish(json.dumps({"id": 0, "created": False})) + return + info = get_add_cards_info() + note = Note(aqt.mw.col, info["notetype"]) fields = info["fields"] @@ -49,4 +59,4 @@ def create_card(self, card: CardFields): aqt.mw.taskman.run_on_main(lambda: aqt.utils.tooltip("Migaku Card created")) aqt.mw.taskman.run_on_main(lambda: add_cards_add_to_history(note)) - self.finish(json.dumps({"id": note.id})) + self.finish(json.dumps({"id": note.id, "created": True})) From 170b798f73976c42da4bc41a927763ea381a3be1 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 16 Dec 2023 02:26:34 +0100 Subject: [PATCH 18/47] refactor: put menu into its own directory --- src/__init__.py | 21 +- src/menu/__init__.py | 27 ++ src/{ => menu}/balance_scheduler.py | 2 +- .../dayoff_window.py} | 2 +- src/{ => menu}/ease_reset.py | 2 +- src/{ => menu}/retirement.html | 0 src/{ => menu}/retirement.js | 0 src/{ => menu}/retirement.py | 8 +- .../scheduler_func.py} | 0 src/{ => menu}/settings_window.py | 6 +- .../vacation_window.py} | 0 src/settings_widget.py | 71 ++++ src/settings_widgets.py | 386 +----------------- src/tutorial_widgets.py | 308 ++++++++++++++ src/welcome_wizard.py | 3 +- 15 files changed, 432 insertions(+), 404 deletions(-) create mode 100644 src/menu/__init__.py rename src/{ => menu}/balance_scheduler.py (98%) rename src/{balance_scheduler_dayoff_window.py => menu/dayoff_window.py} (99%) rename src/{ => menu}/ease_reset.py (95%) rename src/{ => menu}/retirement.html (100%) rename src/{ => menu}/retirement.js (100%) rename src/{ => menu}/retirement.py (98%) rename src/{balance_scheduler_func.py => menu/scheduler_func.py} (100%) rename src/{ => menu}/settings_window.py (94%) rename src/{balance_scheduler_vacation_window.py => menu/vacation_window.py} (100%) create mode 100644 src/settings_widget.py create mode 100644 src/tutorial_widgets.py diff --git a/src/__init__.py b/src/__init__.py index d08c8d6..ccce087 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -13,40 +13,23 @@ # Initialize sub modules from . import ( anki_version, - balance_scheduler, browser, - balance_scheduler_vacation_window, - balance_scheduler_dayoff_window, card_layout, card_type_selector, click_play_audio, - ease_reset, editor, inplace_editor, migaku_connection, note_type_dialogs, note_type_mgr, - retirement, reviewer, - settings_window, toolbar, webview_contextmenu, welcome_wizard, + menu, ) -def setup_menu(): - menu = QMenu("Migaku", aqt.mw) - menu.addAction(settings_window.action) - menu.addSeparator() - menu.addAction(ease_reset.action) - menu.addAction(retirement.action) - menu.addAction(balance_scheduler.action) - menu.addAction(balance_scheduler_vacation_window.action) - menu.addAction(balance_scheduler_dayoff_window.action) - aqt.mw.form.menubar.insertMenu(aqt.mw.form.menuHelp.menuAction(), menu) - - def setup_hooks(): # Allow webviews to access necessary resources aqt.mw.addonManager.setWebExports( @@ -106,6 +89,6 @@ def setup_hooks(): ) -setup_menu() +menu.setup_menu() setup_hooks() anki_version.check_anki_version_dialog() diff --git a/src/menu/__init__.py b/src/menu/__init__.py new file mode 100644 index 0000000..41e80b1 --- /dev/null +++ b/src/menu/__init__.py @@ -0,0 +1,27 @@ +import aqt +from aqt.qt import * + +from . import ( + settings_window, + ease_reset, + retirement, + balance_scheduler, + dayoff_window, + vacation_window, +) + + +def setup_menu(): + menu = QMenu("Migaku", aqt.mw) + menu.addAction(settings_window.action) + + menu.addSeparator() + menu.addAction(ease_reset.action) + menu.addAction(retirement.action) + menu.addAction(balance_scheduler.action) + menu.addAction(vacation_window.action) + menu.addAction(dayoff_window.action) + + menu.addSeparator() + + aqt.mw.form.menubar.insertMenu(aqt.mw.form.menuHelp.menuAction(), menu) diff --git a/src/balance_scheduler.py b/src/menu/balance_scheduler.py similarity index 98% rename from src/balance_scheduler.py rename to src/menu/balance_scheduler.py index 3133ded..8920c56 100644 --- a/src/balance_scheduler.py +++ b/src/menu/balance_scheduler.py @@ -6,7 +6,7 @@ from aqt.qt import QAction from anki.decks import DeckConfigId -from .balance_scheduler_func import balance, Card, Vacation +from .scheduler_func import balance, Card, Vacation SECOND_MS = 1000 diff --git a/src/balance_scheduler_dayoff_window.py b/src/menu/dayoff_window.py similarity index 99% rename from src/balance_scheduler_dayoff_window.py rename to src/menu/dayoff_window.py index 29e616b..9d10720 100644 --- a/src/balance_scheduler_dayoff_window.py +++ b/src/menu/dayoff_window.py @@ -2,7 +2,7 @@ from aqt.qt import * from .balance_scheduler import BalanceScheduler -from . import util +from .. import util class BalanceSchedulerDayOffWindow(QDialog): diff --git a/src/ease_reset.py b/src/menu/ease_reset.py similarity index 95% rename from src/ease_reset.py rename to src/menu/ease_reset.py index 8daa523..66339cf 100644 --- a/src/ease_reset.py +++ b/src/menu/ease_reset.py @@ -1,7 +1,7 @@ import aqt from aqt.qt import * -from . import config +from .. import config def reset_ease(sync=True, force_sync=True): diff --git a/src/retirement.html b/src/menu/retirement.html similarity index 100% rename from src/retirement.html rename to src/menu/retirement.html diff --git a/src/retirement.js b/src/menu/retirement.js similarity index 100% rename from src/retirement.js rename to src/menu/retirement.js diff --git a/src/retirement.py b/src/menu/retirement.py similarity index 98% rename from src/retirement.py rename to src/menu/retirement.py index 558bec0..3bea708 100644 --- a/src/retirement.py +++ b/src/menu/retirement.py @@ -8,8 +8,8 @@ from aqt.reviewer import Reviewer from aqt.operations import CollectionOp -from . import config -from . import util +from .. import config +from .. import util class RetirementHandler: @@ -285,10 +285,10 @@ def answer_hook(rev: Reviewer, card: Card, ease: Literal[1, 2, 3, 4]) -> None: aqt.gui_hooks.reviewer_did_answer_card.append(answer_hook) -with open(util.addon_path("retirement.html")) as f: +with open(util.addon_path("menu/retirement.html")) as f: retirement_html = f.read() -with open(util.addon_path("retirement.js")) as f: +with open(util.addon_path("menu/retirement.js")) as f: retirement_js = f.read() diff --git a/src/balance_scheduler_func.py b/src/menu/scheduler_func.py similarity index 100% rename from src/balance_scheduler_func.py rename to src/menu/scheduler_func.py diff --git a/src/settings_window.py b/src/menu/settings_window.py similarity index 94% rename from src/settings_window.py rename to src/menu/settings_window.py index a60018f..bae41ef 100644 --- a/src/settings_window.py +++ b/src/menu/settings_window.py @@ -2,9 +2,9 @@ import aqt from aqt.qt import * -from . import util -from . import config -from .settings_widgets import SETTINGS_WIDGETS +from .. import util +from .. import config +from ..settings_widgets import SETTINGS_WIDGETS class SettingsWindow(QDialog): diff --git a/src/balance_scheduler_vacation_window.py b/src/menu/vacation_window.py similarity index 100% rename from src/balance_scheduler_vacation_window.py rename to src/menu/vacation_window.py diff --git a/src/settings_widget.py b/src/settings_widget.py new file mode 100644 index 0000000..7cd71e1 --- /dev/null +++ b/src/settings_widget.py @@ -0,0 +1,71 @@ +import aqt +from aqt.qt import * + + +class SettingsWidget(QWidget): + class WizardPage(QWizardPage): + def __init__(self, widget_cls, parent=None, is_tutorial=True): + super().__init__(parent) + self.widget = widget_cls(is_tutorial=is_tutorial) + if self.widget.TITLE: + self.setTitle(self.widget.TITLE) + if self.widget.SUBTITLE: + self.setSubTitle(self.widget.SUBTITLE) + if self.widget.PIXMAP: + if hasattr(QWizard, "WizardPixmap"): + QWizard_WatermarkPixmap = QWizard.WizardPixmap.WatermarkPixmap + else: + QWizard_WatermarkPixmap = QWizard.WatermarkPixmap + self.setPixmap( + QWizard_WatermarkPixmap, util.make_pixmap(self.widget.PIXMAP) + ) + self.lyt = QVBoxLayout() + self.lyt.setContentsMargins(0, 0, 0, 0) + self.lyt.addWidget(self.widget) + self.setLayout(self.lyt) + + def save(self): + self.widget.save() + + TITLE = None + SUBTITLE = None + PIXMAP = None + BOTTOM_STRETCH = True + + def __init__(self, parent=None, is_tutorial=False): + super().__init__(parent) + + self.is_tutorial = is_tutorial + + self.lyt = QVBoxLayout() + self.setLayout(self.lyt) + self.init_ui() + self.toggle_advanced(False) + if self.BOTTOM_STRETCH: + self.lyt.addStretch() + + def init_ui(self) -> None: + pass + + def toggle_advanced(self, state: bool) -> None: + pass + + def save(self) -> None: + pass + + @classmethod + def wizard_page(cls, parent=None, is_tutorial=True) -> WizardPage: + return cls.WizardPage(cls, parent, is_tutorial) + + @classmethod + def make_label(cls, text: str) -> QLabel: + lbl = QLabel(text) + lbl.setWordWrap(True) + lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + lbl.linkActivated.connect(aqt.utils.openLink) + return lbl + + def add_label(self, text: str) -> QLabel: + lbl = self.make_label(text) + self.lyt.addWidget(lbl) + return lbl diff --git a/src/settings_widgets.py b/src/settings_widgets.py index 4b3f836..9389c00 100644 --- a/src/settings_widgets.py +++ b/src/settings_widgets.py @@ -1,83 +1,19 @@ import aqt from aqt.qt import * +from .settings_widget import SettingsWidget +from .tutorial_widgets import ( + ExtensionWidget, + GlobalHotkeysWidget, + InplaceEditorWidget, + LanguageWidget, + ReviewWidget, + SyntaxAddRemoveWidget, + SyntaxWidget, +) + from .version import VERSION_STRING -from . import config -from . import util -from .languages import Languages -from . import note_type_mgr -from .migaku_connection import ConnectionStatusLabel -from .global_hotkeys import HotkeyConfigWidget, hotkey_handler -from .balance_scheduler_vacation_window import BalanceSchedulerVacationWindow - - -class SettingsWidget(QWidget): - class WizardPage(QWizardPage): - def __init__(self, widget_cls, parent=None, is_tutorial=True): - super().__init__(parent) - self.widget = widget_cls(is_tutorial=is_tutorial) - if self.widget.TITLE: - self.setTitle(self.widget.TITLE) - if self.widget.SUBTITLE: - self.setSubTitle(self.widget.SUBTITLE) - if self.widget.PIXMAP: - if hasattr(QWizard, "WizardPixmap"): - QWizard_WatermarkPixmap = QWizard.WizardPixmap.WatermarkPixmap - else: - QWizard_WatermarkPixmap = QWizard.WatermarkPixmap - self.setPixmap( - QWizard_WatermarkPixmap, util.make_pixmap(self.widget.PIXMAP) - ) - self.lyt = QVBoxLayout() - self.lyt.setContentsMargins(0, 0, 0, 0) - self.lyt.addWidget(self.widget) - self.setLayout(self.lyt) - - def save(self): - self.widget.save() - - TITLE = None - SUBTITLE = None - PIXMAP = None - BOTTOM_STRETCH = True - - def __init__(self, parent=None, is_tutorial=False): - super().__init__(parent) - - self.is_tutorial = is_tutorial - - self.lyt = QVBoxLayout() - self.setLayout(self.lyt) - self.init_ui() - self.toggle_advanced(False) - if self.BOTTOM_STRETCH: - self.lyt.addStretch() - - def init_ui(self) -> None: - pass - - def toggle_advanced(self, state: bool) -> None: - pass - - def save(self) -> None: - pass - - @classmethod - def wizard_page(cls, parent=None, is_tutorial=True) -> WizardPage: - return cls.WizardPage(cls, parent, is_tutorial) - - @classmethod - def make_label(cls, text: str) -> QLabel: - lbl = QLabel(text) - lbl.setWordWrap(True) - lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) - lbl.linkActivated.connect(aqt.utils.openLink) - return lbl - - def add_label(self, text: str) -> QLabel: - lbl = self.make_label(text) - self.lyt.addWidget(lbl) - return lbl +from . import config, util class AboutWidget(SettingsWidget): @@ -109,246 +45,6 @@ def on_toggle_advanced(self) -> None: self.settings_window.toggle_advanced(state) -class WelcomeWidget(SettingsWidget): - TITLE = "Welcome!" - SUBTITLE = "This is the first time you are using the Migaku Anki add-on." - PIXMAP = "migaku_side.png" - - def init_ui(self): - welcome_lbl = QLabel( - "Migaku Anki provides all features you need to optimally learn languages with Anki and Migaku.

" - "This setup will give you a quick overview over the feature-set and explain how to use it.

" - "Let's go!

" - "You can always later see this guide and the settings by clicking Migaku > Settings/Help." - ) - welcome_lbl.setWordWrap(True) - self.lyt.addWidget(welcome_lbl) - - -class LanguageWidget(SettingsWidget): - TITLE = "Language Selection" - - def init_ui(self): - lbl1 = QLabel( - "Migaku allows you to learn all of the following languages. Please select all the ones you want to learn:" - ) - lbl1.setWordWrap(True) - self.lyt.addWidget(lbl1) - - self.lang_list = QListWidget() - self.lang_list.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.lyt.addWidget(self.lang_list) - - lbl2 = QLabel( - "Note: This will create a note type for every selected language. " - "If you want to uninstall a language, remove the correspending note type with Tools > Manage Note Types." - ) - lbl2.setWordWrap(True) - self.lyt.addWidget(lbl2) - - self.setup_langs() - - def setup_langs(self): - self.lang_list.clear() - - for lang in Languages: - is_installed = note_type_mgr.is_installed(lang) - text = lang.name_en - if lang.name_native and lang.name_native != lang.name_en: - text += f" ({lang.name_native})" - item = QListWidgetItem(text) - item.setData(Qt.ItemDataRole.UserRole, lang.code) - item.setCheckState( - Qt.CheckState.Checked if is_installed else Qt.CheckState.Unchecked - ) - if is_installed: - item.setFlags( - item.flags() - & ~(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable) - ) - self.lang_list.addItem(item) - - def save(self): - for i in range(self.lang_list.count()): - item = self.lang_list.item(i) - lang_code = item.data(Qt.ItemDataRole.UserRole) - if item.checkState() == Qt.CheckState.Checked: - lang = Languages[lang_code] - note_type_mgr.install(lang) - self.setup_langs() - - -class ExtensionWidget(SettingsWidget): - TITLE = "Browser Extension" - - EXTENSION_URL = "https://www.migaku.io/todo" - - BOTTOM_STRETCH = False - - def init_ui(self, parent=None): - lbl1 = QLabel( - "Migaku Anki uses the Migaku Browser Extension as a dictionary, to add syntax to your cards and several other features.

" - f'Make sure to install the extension for your browser to use this functionality. See here for instructions.

' - "If the browser extension is installed and running, the status below will reflect so." - ) - lbl1.setWordWrap(True) - lbl1.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) - lbl1.linkActivated.connect(aqt.utils.openLink) - self.lyt.addWidget(lbl1) - - # Information on changing port - self.custom_port_hr = self.add_label("
") - self.custom_port_info = self.add_label( - "In some cases, you might want to have Anki and the browser extension communicate on a custom port, because another application is already using the default port. You need to set the same port in the browser extension settings! You need to restart Anki after changing the port. " - ) - - self.custom_port = QLineEdit(config.get("port", "")) - self.custom_port.setPlaceholderText(str(util.DEFAULT_PORT)) - self.custom_port.textChanged.connect( - lambda text: config.set("port", text if len(text) else None) - ) - - self.lyt.addWidget(self.custom_port) - - self.lyt.addStretch() - self.lyt.addWidget(ConnectionStatusLabel()) - - def toggle_advanced(self, state: bool) -> None: - if not state: - self.custom_port_hr.hide() - self.custom_port_info.hide() - self.custom_port.hide() - - else: - self.custom_port_hr.show() - self.custom_port_info.show() - self.custom_port.show() - - -class GlobalHotkeysWidget(SettingsWidget): - TITLE = "Global Hotkeys" - - BOTTOM_STRETCH = False - - def init_ui(self, parent=None): - if not hotkey_handler.is_available(): - self.add_label( - "For Migaku global hotkeys to work, you must allow Anki to monitor keyboard inputs.\n\n" - 'To do this, go to System Preferences > Security & Privacy > Privacy. Then for both "Accessibility" and "Input Monitoring" check the box for "Anki".\n\n' - "These permissions are required to detect when the specified shortcuts are pressend and are required to copy the selected text. " - "Finally restart Anki." - ) - return - - self.add_label( - "You can use the following hotkeys to interact with the browser extension while it is connected:" - ) - - self.hotkey_config = HotkeyConfigWidget(hotkey_handler) - self.lyt.addWidget(self.hotkey_config) - - self.add_label( - "To set new key combinations click the buttons on the right and press a new key combination. " - "To disable a hotkey click the button again without pressing a new key combination." - ) - - self.lyt.addStretch() - self.lyt.addWidget(ConnectionStatusLabel()) - - def save(self): - config.write() - - -class SyntaxWidget(SettingsWidget): - TITLE = "Language Syntax" - - def init_ui(self, parent=None): - lbl1 = QLabel( - "Migaku Anki offers card syntax that can be used in fields to add information to your flashcard fields. " - "This information includes (depending on the language): Readings, tone/pitch/gender coloring, " - "part of speech and more.

" - "Generally the syntax is added after words, enclosed in square brackets like this:" - ) - lbl1.setWordWrap(True) - self.lyt.addWidget(lbl1) - - lbl2 = QLabel( - "This[this,pron,ðɪs;ðɪs] is[be,aux,ɪz;ɪz] a[a,x,ʌ;eɪ] test[test,noun,test]." - ) - lbl2.setStyleSheet("background-color:#202020; color:#F8F8F8;") - lbl2.setWordWrap(True) - self.lyt.addWidget(lbl2) - - lbl3 = QLabel( - "
On your cards the information in the brackets is displayed in a popup over the word, as ruby text or changes the color of the word, depending on the language:" - ) - lbl3.setWordWrap(True) - self.lyt.addWidget(lbl3) - - lbl4 = QLabel() - lbl4.setPixmap(util.make_pixmap("syntax_displayed_small_example.png")) - self.lyt.addWidget(lbl4) - - -class SyntaxAddRemoveWidget(SettingsWidget): - TITLE = "Add/Remove Language Syntax" - - def init_ui(self): - lbl1 = QLabel( - "To add or update syntax of your cards go to any editor window in Anki, select a field and press F2 or press this button (icons vary depending on language:" - ) - lbl1.setWordWrap(True) - self.lyt.addWidget(lbl1) - - lbl2 = QLabel() - lbl2.setPixmap(util.make_pixmap("syntax_buttons_example.png")) - self.lyt.addWidget(lbl2) - - lbl3 = QLabel( - "
Make sure the browser extension is connected when using these features.

" - "To remove syntax either press F4 or press the right button." - ) - lbl3.setWordWrap(True) - self.lyt.addWidget(lbl3) - - -class InplaceEditorWidget(SettingsWidget): - TITLE = "Inplace Editor" - - def init_ui(self): - self.add_label( - "With Migaku Anki you can edit your flash cards during your reviews.

" - "To do so simply double click any field on your card. A cursor will appear and you can freely edit the field and even paste images and audio files. " - "Simply click out of the field to finish editing.

" - 'If you want to add editing support to your own note types add the "editable" filter before the field names:' - ) - - lbl = QLabel("{{ExampleField}} -> {{editable:ExampleField}}") - lbl.setStyleSheet("background-color:#202020; color:#F8F8F8;") - lbl.setWordWrap(True) - self.lyt.addWidget(lbl) - - self.add_label("") - - self.add_label( - "By enabling the following option you can also edit empty fields on your cards:" - ) - - show_empty_fields = QCheckBox("Show empty fields") - show_empty_fields.setChecked( - config.get("inplace_editor_show_empty_fields", False) - ) - show_empty_fields.toggled.connect( - lambda checked: config.set("inplace_editor_show_empty_fields", checked) - ) - self.lyt.addWidget(show_empty_fields) - - def save(self): - from .inplace_editor import update_show_empty_fields - - update_show_empty_fields() - - class CardTypeWidget(SettingsWidget): TITLE = "Card Type Changing" @@ -378,53 +74,6 @@ def init_ui(self): self.lyt.addWidget(tag) -class ReviewWidget(SettingsWidget): - TITLE = "Review Settings" - - def init_ui(self): - self.add_label( - "By default Anki allows grading cards as Again, Hard, Good and Easy.
" - "The hard and easy buttons can lead to unnecessarily long grading decision times as well as permanently seeing cards too often/little.
" - "Enabling Pass/Fail will remove those buttons." - ) - - pass_fail = QCheckBox("Enable Pass/Fail (Recommended)") - pass_fail.setChecked(config.get("reviewer_pass_fail", True)) - pass_fail.toggled.connect( - lambda checked: config.set("reviewer_pass_fail", checked) - ) - self.lyt.addWidget(pass_fail) - - self.add_label("
") - - self.add_label( - "The negative effects of the ease factor can be prevented by fixing the ease factor (how difficult a card is considered) in place.
" - "This option will prevent those effects if you want to use the Easy/Hard buttons and fixes negative effects caused in the past." - ) - - pass_fail = QCheckBox("Maintain Ease Factor (Recommended)") - pass_fail.setChecked(config.get("maintain_ease", True)) - pass_fail.toggled.connect(lambda checked: config.set("maintain_ease", checked)) - self.lyt.addWidget(pass_fail) - - self.add_label( - "If you review cards on mobile devices your ease factor will not be maintained. " - 'To reset it press "Reset Ease Factor" from the Migaku menu. Note that this action will force a full sync.' - ) - - self.add_label("
") - - colored_buttons = QCheckBox("Colored Grading Buttons") - colored_buttons.setChecked(config.get("reviewer_button_coloring", True)) - colored_buttons.toggled.connect( - lambda checked: config.set("reviewer_button_coloring", checked) - ) - self.lyt.addWidget(colored_buttons) - - def save(self): - config.write() - - class SchedulingWidget(SettingsWidget): TITLE = "Review Scheduling (Beta)" @@ -1031,14 +680,3 @@ def change_dir(self): FieldSettingsWidget, CondensedAudioWidget, ] - -TUTORIAL_WIDGETS = [ - WelcomeWidget, - LanguageWidget, - ExtensionWidget, - GlobalHotkeysWidget, - SyntaxWidget, - SyntaxAddRemoveWidget, - InplaceEditorWidget, - ReviewWidget, -] diff --git a/src/tutorial_widgets.py b/src/tutorial_widgets.py new file mode 100644 index 0000000..6a719ca --- /dev/null +++ b/src/tutorial_widgets.py @@ -0,0 +1,308 @@ +import aqt +from aqt.qt import * + +from .settings_widget import SettingsWidget +from .languages import Languages +from .migaku_connection import ConnectionStatusLabel +from .global_hotkeys import HotkeyConfigWidget, hotkey_handler + +from . import config, util, note_type_mgr + + +class WelcomeWidget(SettingsWidget): + TITLE = "Welcome!" + SUBTITLE = "This is the first time you are using the Migaku Anki add-on." + PIXMAP = "migaku_side.png" + + def init_ui(self): + welcome_lbl = QLabel( + "Migaku Anki provides all features you need to optimally learn languages with Anki and Migaku.

" + "This setup will give you a quick overview over the feature-set and explain how to use it.

" + "Let's go!

" + "You can always later see this guide and the settings by clicking Migaku > Settings/Help." + ) + welcome_lbl.setWordWrap(True) + self.lyt.addWidget(welcome_lbl) + + +class LanguageWidget(SettingsWidget): + TITLE = "Language Selection" + + def init_ui(self): + lbl1 = QLabel( + "Migaku allows you to learn all of the following languages. Please select all the ones you want to learn:" + ) + lbl1.setWordWrap(True) + self.lyt.addWidget(lbl1) + + self.lang_list = QListWidget() + self.lang_list.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.lyt.addWidget(self.lang_list) + + lbl2 = QLabel( + "Note: This will create a note type for every selected language. " + "If you want to uninstall a language, remove the correspending note type with Tools > Manage Note Types." + ) + lbl2.setWordWrap(True) + self.lyt.addWidget(lbl2) + + self.setup_langs() + + def setup_langs(self): + self.lang_list.clear() + + for lang in Languages: + is_installed = note_type_mgr.is_installed(lang) + text = lang.name_en + if lang.name_native and lang.name_native != lang.name_en: + text += f" ({lang.name_native})" + item = QListWidgetItem(text) + item.setData(Qt.ItemDataRole.UserRole, lang.code) + item.setCheckState( + Qt.CheckState.Checked if is_installed else Qt.CheckState.Unchecked + ) + if is_installed: + item.setFlags( + item.flags() + & ~(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable) + ) + self.lang_list.addItem(item) + + def save(self): + for i in range(self.lang_list.count()): + item = self.lang_list.item(i) + lang_code = item.data(Qt.ItemDataRole.UserRole) + if item.checkState() == Qt.CheckState.Checked: + lang = Languages[lang_code] + note_type_mgr.install(lang) + self.setup_langs() + + +class ExtensionWidget(SettingsWidget): + TITLE = "Browser Extension" + + EXTENSION_URL = "https://www.migaku.io/todo" + + BOTTOM_STRETCH = False + + def init_ui(self, parent=None): + lbl1 = QLabel( + "Migaku Anki uses the Migaku Browser Extension as a dictionary, to add syntax to your cards and several other features.

" + f'Make sure to install the extension for your browser to use this functionality. See here for instructions.

' + "If the browser extension is installed and running, the status below will reflect so." + ) + lbl1.setWordWrap(True) + lbl1.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + lbl1.linkActivated.connect(aqt.utils.openLink) + self.lyt.addWidget(lbl1) + + # Information on changing port + self.custom_port_hr = self.add_label("
") + self.custom_port_info = self.add_label( + "In some cases, you might want to have Anki and the browser extension communicate on a custom port, because another application is already using the default port. You need to set the same port in the browser extension settings! You need to restart Anki after changing the port. " + ) + + self.custom_port = QLineEdit(config.get("port", "")) + self.custom_port.setPlaceholderText(str(util.DEFAULT_PORT)) + self.custom_port.textChanged.connect( + lambda text: config.set("port", text if len(text) else None) + ) + + self.lyt.addWidget(self.custom_port) + + self.lyt.addStretch() + self.lyt.addWidget(ConnectionStatusLabel()) + + def toggle_advanced(self, state: bool) -> None: + if not state: + self.custom_port_hr.hide() + self.custom_port_info.hide() + self.custom_port.hide() + + else: + self.custom_port_hr.show() + self.custom_port_info.show() + self.custom_port.show() + + +class GlobalHotkeysWidget(SettingsWidget): + TITLE = "Global Hotkeys" + + BOTTOM_STRETCH = False + + def init_ui(self, parent=None): + if not hotkey_handler.is_available(): + self.add_label( + "For Migaku global hotkeys to work, you must allow Anki to monitor keyboard inputs.\n\n" + 'To do this, go to System Preferences > Security & Privacy > Privacy. Then for both "Accessibility" and "Input Monitoring" check the box for "Anki".\n\n' + "These permissions are required to detect when the specified shortcuts are pressend and are required to copy the selected text. " + "Finally restart Anki." + ) + return + + self.add_label( + "You can use the following hotkeys to interact with the browser extension while it is connected:" + ) + + self.hotkey_config = HotkeyConfigWidget(hotkey_handler) + self.lyt.addWidget(self.hotkey_config) + + self.add_label( + "To set new key combinations click the buttons on the right and press a new key combination. " + "To disable a hotkey click the button again without pressing a new key combination." + ) + + self.lyt.addStretch() + self.lyt.addWidget(ConnectionStatusLabel()) + + def save(self): + config.write() + + +class SyntaxWidget(SettingsWidget): + TITLE = "Language Syntax" + + def init_ui(self, parent=None): + lbl1 = QLabel( + "Migaku Anki offers card syntax that can be used in fields to add information to your flashcard fields. " + "This information includes (depending on the language): Readings, tone/pitch/gender coloring, " + "part of speech and more.

" + "Generally the syntax is added after words, enclosed in square brackets like this:" + ) + lbl1.setWordWrap(True) + self.lyt.addWidget(lbl1) + + lbl2 = QLabel( + "This[this,pron,ðɪs;ðɪs] is[be,aux,ɪz;ɪz] a[a,x,ʌ;eɪ] test[test,noun,test]." + ) + lbl2.setStyleSheet("background-color:#202020; color:#F8F8F8;") + lbl2.setWordWrap(True) + self.lyt.addWidget(lbl2) + + lbl3 = QLabel( + "
On your cards the information in the brackets is displayed in a popup over the word, as ruby text or changes the color of the word, depending on the language:" + ) + lbl3.setWordWrap(True) + self.lyt.addWidget(lbl3) + + lbl4 = QLabel() + lbl4.setPixmap(util.make_pixmap("syntax_displayed_small_example.png")) + self.lyt.addWidget(lbl4) + + +class SyntaxAddRemoveWidget(SettingsWidget): + TITLE = "Add/Remove Language Syntax" + + def init_ui(self): + lbl1 = QLabel( + "To add or update syntax of your cards go to any editor window in Anki, select a field and press F2 or press this button (icons vary depending on language:" + ) + lbl1.setWordWrap(True) + self.lyt.addWidget(lbl1) + + lbl2 = QLabel() + lbl2.setPixmap(util.make_pixmap("syntax_buttons_example.png")) + self.lyt.addWidget(lbl2) + + lbl3 = QLabel( + "
Make sure the browser extension is connected when using these features.

" + "To remove syntax either press F4 or press the right button." + ) + lbl3.setWordWrap(True) + self.lyt.addWidget(lbl3) + + +class InplaceEditorWidget(SettingsWidget): + TITLE = "Inplace Editor" + + def init_ui(self): + self.add_label( + "With Migaku Anki you can edit your flash cards during your reviews.

" + "To do so simply double click any field on your card. A cursor will appear and you can freely edit the field and even paste images and audio files. " + "Simply click out of the field to finish editing.

" + 'If you want to add editing support to your own note types add the "editable" filter before the field names:' + ) + + lbl = QLabel("{{ExampleField}} -> {{editable:ExampleField}}") + lbl.setStyleSheet("background-color:#202020; color:#F8F8F8;") + lbl.setWordWrap(True) + self.lyt.addWidget(lbl) + + self.add_label("") + + self.add_label( + "By enabling the following option you can also edit empty fields on your cards:" + ) + + show_empty_fields = QCheckBox("Show empty fields") + show_empty_fields.setChecked( + config.get("inplace_editor_show_empty_fields", False) + ) + show_empty_fields.toggled.connect( + lambda checked: config.set("inplace_editor_show_empty_fields", checked) + ) + self.lyt.addWidget(show_empty_fields) + + def save(self): + from .inplace_editor import update_show_empty_fields + + update_show_empty_fields() + + +class ReviewWidget(SettingsWidget): + TITLE = "Review Settings" + + def init_ui(self): + self.add_label( + "By default Anki allows grading cards as Again, Hard, Good and Easy.
" + "The hard and easy buttons can lead to unnecessarily long grading decision times as well as permanently seeing cards too often/little.
" + "Enabling Pass/Fail will remove those buttons." + ) + + pass_fail = QCheckBox("Enable Pass/Fail (Recommended)") + pass_fail.setChecked(config.get("reviewer_pass_fail", True)) + pass_fail.toggled.connect( + lambda checked: config.set("reviewer_pass_fail", checked) + ) + self.lyt.addWidget(pass_fail) + + self.add_label("
") + + self.add_label( + "The negative effects of the ease factor can be prevented by fixing the ease factor (how difficult a card is considered) in place.
" + "This option will prevent those effects if you want to use the Easy/Hard buttons and fixes negative effects caused in the past." + ) + + pass_fail = QCheckBox("Maintain Ease Factor (Recommended)") + pass_fail.setChecked(config.get("maintain_ease", True)) + pass_fail.toggled.connect(lambda checked: config.set("maintain_ease", checked)) + self.lyt.addWidget(pass_fail) + + self.add_label( + "If you review cards on mobile devices your ease factor will not be maintained. " + 'To reset it press "Reset Ease Factor" from the Migaku menu. Note that this action will force a full sync.' + ) + + self.add_label("
") + + colored_buttons = QCheckBox("Colored Grading Buttons") + colored_buttons.setChecked(config.get("reviewer_button_coloring", True)) + colored_buttons.toggled.connect( + lambda checked: config.set("reviewer_button_coloring", checked) + ) + self.lyt.addWidget(colored_buttons) + + def save(self): + config.write() + + +TUTORIAL_WIDGETS = [ + WelcomeWidget, + LanguageWidget, + ExtensionWidget, + GlobalHotkeysWidget, + SyntaxWidget, + SyntaxAddRemoveWidget, + InplaceEditorWidget, + ReviewWidget, +] diff --git a/src/welcome_wizard.py b/src/welcome_wizard.py index 9e8e6bd..5219d26 100644 --- a/src/welcome_wizard.py +++ b/src/welcome_wizard.py @@ -3,7 +3,8 @@ from . import util from . import config -from .settings_widgets import TUTORIAL_WIDGETS + +from .tutorial_widgets import TUTORIAL_WIDGETS class WelcomeWizard(QWizard): From 4df94cc8ae16da50f3b1d4da94bca654000aaa70 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 16 Dec 2023 03:05:18 +0100 Subject: [PATCH 19/47] feat: activate deck/type in Migaku menu bar --- src/menu/__init__.py | 28 ++++++++++++++++++++++++++-- src/toolbar/__init__.py | 19 ++++++++++++++----- src/toolbar/toolbar.html | 5 +++-- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/menu/__init__.py b/src/menu/__init__.py index 41e80b1..bd7e83c 100644 --- a/src/menu/__init__.py +++ b/src/menu/__init__.py @@ -10,9 +10,16 @@ vacation_window, ) +menu = QMenu("Migaku", aqt.mw) + +deckItem = QAction("", aqt.mw) +deckItem.triggered.connect(lambda: aqt.mw.onAddCard()) + +typeItem = QAction("", aqt.mw) +typeItem.triggered.connect(lambda: aqt.mw.onAddCard()) + def setup_menu(): - menu = QMenu("Migaku", aqt.mw) menu.addAction(settings_window.action) menu.addSeparator() @@ -23,5 +30,22 @@ def setup_menu(): menu.addAction(dayoff_window.action) menu.addSeparator() - aqt.mw.form.menubar.insertMenu(aqt.mw.form.menuHelp.menuAction(), menu) + + +def deactivate_deck_type(): + menu.removeAction(deckItem) + menu.removeAction(typeItem) + + +def activate_deck_type(): + menu.addAction(deckItem) + menu.addAction(typeItem) + + +def set_deck_name(name): + deckItem.setText(f"Deck: {name}") + + +def set_type_name(name): + typeItem.setText(f"Type: {name}") diff --git a/src/toolbar/__init__.py b/src/toolbar/__init__.py index bf7303c..500d4ad 100644 --- a/src/toolbar/__init__.py +++ b/src/toolbar/__init__.py @@ -1,5 +1,6 @@ import json import aqt +from .. import menu from ..editor.current_editor import ( get_add_cards_info, @@ -19,13 +20,18 @@ def open_add_cards(): def activate_migaku_toolbar(toolbar): info = get_add_cards_info() - toolbar.web.eval(f"MigakuToolbar.activate({json.dumps(info)})") - toolbar.link_handlers["openAddCards"] = open_add_cards + # toolbar.web.eval(f"MigakuToolbar.activate({json.dumps(info)})") + # toolbar.link_handlers["openAddCards"] = open_add_cards + menu.set_deck_name(info["deck_name"]) + menu.set_type_name(info["notetype_name"]) + menu.activate_deck_type() def refresh_migaku_toolbar(): info = get_add_cards_info() - global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") + # global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") + menu.set_deck_name(info["deck_name"]) + menu.set_type_name(info["notetype_name"]) def refresh_migaku_toolbar_opened_addcards(): @@ -35,11 +41,14 @@ def refresh_migaku_toolbar_opened_addcards(): on_addcards_did_change_deck(defaults.deck_id) on_addcards_did_change_note_type(defaults.notetype_id) - global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") + # global_toolbar.web.eval(f"MigakuToolbar.refresh({json.dumps(info)})") + menu.set_deck_name(info["deck_name"]) + menu.set_type_name(info["notetype_name"]) def deactivate_migaku_toolbar(toolbar): - toolbar.web.eval("MigakuToolbar.deactivate()") + # toolbar.web.eval("MigakuToolbar.deactivate()") + menu.deactivate_deck_type() def inject_migaku_toolbar(html: str, toolbar): diff --git a/src/toolbar/toolbar.html b/src/toolbar/toolbar.html index 302bc0b..61228f9 100644 --- a/src/toolbar/toolbar.html +++ b/src/toolbar/toolbar.html @@ -36,11 +36,12 @@