diff --git a/src/__init__.py b/src/__init__.py
index e4a1192..9688b87 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -1,75 +1,128 @@
+from dataclasses import dataclass
from platform import platform
import sys
import os
-import platform
import aqt
from aqt.qt import *
import anki
+from .config import get
+from .sys_libraries import init_sys_libs
-# 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)")
+init_sys_libs()
# Initialize sub modules
from . import (
- note_type_dialogs,
+ anki_version,
+ browser,
+ card_layout,
card_type_selector,
- note_type_mgr,
- reviewer,
+ click_play_audio,
editor,
inplace_editor,
- click_play_audio,
- browser,
- card_layout,
- welcome_wizard,
+ migaku_connection,
+ note_type_dialogs,
+ note_type_mgr,
+ reviewer,
+ toolbar,
webview_contextmenu,
- settings_window,
- ease_reset,
- retirement,
- balance_scheduler,
- balance_scheduler_vacation_window,
- balance_scheduler_dayoff_window,
- anki_version,
+ welcome_wizard,
+ menu,
)
-from . import migaku_connection
+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.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)
+
+ # DECK CHANGE
+ def deck_change(id):
+ editor.current_editor.on_addcards_did_change_deck(id)
+ toolbar.refresh_migaku_toolbar()
+
+ if getattr(aqt.gui_hooks, "add_cards_did_change_deck", None):
+ aqt.gui_hooks.add_cards_did_change_deck.append(deck_change)
+ else:
+ aqt.addcards.AddCards.on_deck_changed = anki.hooks.wrap(
+ aqt.addcards.AddCards.on_deck_changed,
+ lambda _, id: deck_change(id),
+ "before",
+ )
+
+ ### MODEL CHANGE
+ def notetype_change(id):
+ editor.current_editor.on_addcards_did_change_note_type(id)
+ toolbar.refresh_migaku_toolbar()
+
+ if getattr(aqt.gui_hooks, "add_cards_did_change_note_type", None):
+ aqt.gui_hooks.add_cards_did_change_note_type.append(
+ lambda _, id: notetype_change(id)
+ )
+ elif getattr(aqt.addcards.AddCards, "on_notetype_change", None):
+ aqt.addcards.AddCards.on_notetype_change = anki.hooks.wrap(
+ aqt.addcards.AddCards.on_notetype_change,
+ lambda _, id: notetype_change(id),
+ )
+ else:
+ aqt.addcards.AddCards.onModelChange = anki.hooks.wrap(
+ aqt.addcards.AddCards.onModelChange,
+ notetype_change,
+ )
+
+ 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)
+
+ aqt.gui_hooks.collection_did_load.append(toolbar.inject_migaku_toolbar)
+
+ @dataclass
+ class Defaults:
+ notetype_id: int
+ deck_id: int
+
+ def set_current_deck_model_to_migaku(current_review_card):
+ toolbar.set_deck_type_to_migaku()
+ return Defaults(
+ get("migakuNotetypeId"),
+ get("migakuDeckId"),
+ )
+
+ def overwrite_defaults_for_adding(col):
+ col.defaults_for_adding = set_current_deck_model_to_migaku
+ aqt.gui_hooks.collection_did_load.append(overwrite_defaults_for_adding)
+
+ 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",
+ )
+
+ aqt.gui_hooks.add_cards_did_init.append(
+ lambda _: toolbar.refresh_migaku_toolbar_opened_addcards()
+ )
-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)
+ # 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()
+menu.setup_menu()
+setup_hooks()
anki_version.check_anki_version_dialog()
diff --git a/src/card_types.py b/src/card_types.py
new file mode 100644
index 0000000..427edf4
--- /dev/null
+++ b/src/card_types.py
@@ -0,0 +1,94 @@
+from base64 import b64decode
+from dataclasses import dataclass, field
+from typing import Optional
+import requests
+
+from .migaku_connection.handle_files import move_file_to_media_dir
+
+
+@dataclass
+class AudioAsset:
+ id: str
+ title: str
+ input: str
+ r2Url: Optional[str] = None
+
+
+@dataclass
+class ImageAsset:
+ id: str
+ title: str
+ src: str
+ alt: str
+ r2Url: Optional[str] = None
+
+
+@dataclass
+class CardFields:
+ targetWord: str = ""
+ sentence: str = ""
+ translation: str = ""
+ definitions: str = ""
+ sentenceAudio: str = ""
+ wordAudio: str = ""
+ images: str = ""
+ exampleSentences: str = ""
+ notes: str = ""
+
+
+def process_image_asset(image: ImageAsset):
+ name = f"{image.id}.webp"
+
+ if image.src.startswith("data:image/webp;base64,"):
+ data = image.src.split(",", 1)[1]
+ move_file_to_media_dir(b64decode(data), name)
+ elif image.input.startswith("http"):
+ data = requests.get(image.src, allow_redirects=True)
+ move_file_to_media_dir(data.content, name)
+
+ return f""
+
+
+def process_audio_asset(audio: AudioAsset):
+ name = f"{audio.id}.m4a"
+
+ if audio.input.startswith("data:audio/mp4;base64,"):
+ data = audio.input.split(",", 1)[1]
+ move_file_to_media_dir(b64decode(data), name)
+ elif audio.input.startswith("http"):
+ data = requests.get(audio.input, allow_redirects=True)
+ move_file_to_media_dir(data.content, name)
+
+ return f"[sound:{name}]"
+
+
+def card_fields_from_dict(data: dict[str, any]):
+ br = "\n
\n"
+
+ sentenceAudios = br.join(
+ [
+ process_audio_asset(AudioAsset(**audio))
+ for audio in data.get("sentenceAudio", [])
+ ]
+ )
+ wordAudios = br.join(
+ [
+ process_audio_asset(AudioAsset(**audio))
+ for audio in data.get("wordAudio", [])
+ ]
+ )
+ imagess = br.join(
+ [process_image_asset(ImageAsset(**img)) for img in data.get("images", [])]
+ )
+
+ return CardFields(
+ targetWord=data.get("targetWord", ""),
+ sentence=data.get("sentence", ""),
+ translation=data.get("translation", ""),
+ definitions=data.get("definitions", ""),
+ sentenceAudio=sentenceAudios,
+ wordAudio=wordAudios,
+ images=imagess,
+ exampleSentences=data.get("exampleSentences", ""),
+ notes=data.get("notes", ""),
+ )
diff --git a/src/editor.py b/src/editor/__init__.py
similarity index 50%
rename from src/editor.py
rename to src/editor/__init__.py
index 1a4a5b5..98b026c 100644
--- a/src/editor.py
+++ b/src/editor/__init__.py
@@ -1,20 +1,24 @@
import os
-from typing import List
+import re
import json
+from typing import List
import aqt
+import anki
from aqt.editor import Editor
-from . import note_type_mgr
-from . import util
-from .util import addon_path
+from ..config import get, set
+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):
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 +31,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 +58,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,
)
@@ -86,6 +90,11 @@ def do_edit():
editor.call_after_note_saved(callback=do_edit, keepFocus=True)
+def toggle_migaku_mode(editor: Editor):
+ data = get_migaku_fields(editor.note.note_type())
+ editor.web.eval(f"MigakuEditor.toggleMode({json.dumps(data)});")
+
+
def setup_editor_buttons(buttons: List[str], editor: Editor):
added_buttons = [
editor.addButton(
@@ -106,35 +115,71 @@ def setup_editor_buttons(buttons: List[str], editor: Editor):
),
]
+ 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,
+ )
+ )
+
buttons[0:0] = added_buttons
return buttons
-aqt.gui_hooks.editor_did_init_buttons.append(setup_editor_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):
+
+def editor_did_load_note(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)
-aqt.gui_hooks.editor_did_load_note.append(editor_note_changed)
+def on_migaku_bridge_cmds(self: Editor, cmd: str, _old):
+ 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", {})
+
+ set(
+ "migakuFields",
+ {
+ **migakuFields,
+ self.note.mid: {
+ **(
+ migakuFields[self.note.mid]
+ if self.note.mid in migakuFields
+ else {}
+ ),
+ field_name: migaku_type,
+ },
+ },
+ do_write=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:
+ editor.web.eval(editor_file.read())
+
+
+def reset_migaku_mode(editor: Editor):
+ editor.web.eval(f"MigakuEditor.resetMigakuEditor();")
diff --git a/src/editor/current_editor.py b/src/editor/current_editor.py
new file mode 100644
index 0000000..4d84a80
--- /dev/null
+++ b/src/editor/current_editor.py
@@ -0,0 +1,161 @@
+from anki.hooks import wrap
+from anki.notes import Note
+from aqt.editor import Editor
+import aqt
+from ..config import get
+from ..card_types import CardFields
+from ..migaku_fields import get_migaku_fields
+
+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
+
+
+def get_add_cards() -> Note:
+ for editor in reversed(current_editors):
+ if editor.addMode:
+ return {
+ "addcards": editor.parentWindow,
+ "note": editor.note,
+ "editor": editor,
+ }
+
+ return None
+
+
+current_note_type_id = 0
+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()
+
+ 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()
+
+ fields = get_migaku_fields(notetype)
+ notetype_name = notetype["name"]
+ notetype_id = notetype["id"]
+
+ deck_id = get_current_deck_id()
+ deck = aqt.mw.col.decks.get(deck_id)
+ deck_name = deck["name"]
+
+ else:
+ notetype_id = int(get("migakuNotetypeId", 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 = int(get("migakuDeckId", 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 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))
+
+ aqt.mw.taskman.run_on_main(addcards["editor"].loadNoteKeepingFocus)
+
+ return True
+
+
+def on_addcards_did_change_note_type(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
new file mode 100644
index 0000000..9ce2073
--- /dev/null
+++ b/src/editor/editor.js
@@ -0,0 +1,141 @@
+function 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 () {
+ 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';
+ }
+}
+
+/** These are the values on CardFields */
+const selectorOptions = [
+ { 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' },
+]
+
+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',
+]
+
+// New Migaku Editor
+function setupMigakuEditor(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) => {
+ const button = document.querySelector(`.item#${category}`)
+ if (button) button.style.display = 'none'
+ });
+
+ for (const field of document.querySelectorAll('.editor-field')) {
+ field.append(getSelectorField(field, settings))
+ }
+}
+
+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 = '');
+
+ hiddenButtonCategories.forEach((category) => {
+ const button = document.querySelector(`.item#${category}`)
+ if (button) button.style.display = ''
+ });
+
+ document.querySelectorAll('.migaku-field-selector').forEach((selector) => selector.remove());
+}
+
+MigakuEditor.toggleMode = function (settings) {
+ if (document.querySelector('.migaku-field-selector')) {
+ MigakuEditor.resetMigakuEditor();
+ } else {
+ 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/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/languages/ja/card/support.html b/src/languages/ja/card/support.html
index 240628f..08dec47 100644
--- a/src/languages/ja/card/support.html
+++ b/src/languages/ja/card/support.html
@@ -1,697 +1,780 @@
diff --git a/src/menu/__init__.py b/src/menu/__init__.py
new file mode 100644
index 0000000..f3163fb
--- /dev/null
+++ b/src/menu/__init__.py
@@ -0,0 +1,80 @@
+import aqt
+from aqt.qt import *
+from ..editor.current_editor import get_add_cards
+from ..config import set
+
+from . import (
+ settings_window,
+ ease_reset,
+ retirement,
+ balance_scheduler,
+ dayoff_window,
+ vacation_window,
+)
+
+menu = QMenu("Migaku", aqt.mw)
+
+titleItem = QAction("Map Fields…", aqt.mw)
+titleItem.triggered.connect(lambda: aqt.mw.onAddCard())
+
+typeItem = QAction("", aqt.mw)
+typeItem.setEnabled(False)
+
+
+def notetypeTrigger():
+ aqt.mw.onAddCard()
+ addcards = get_add_cards()["addcards"]
+ addcards.show_notetype_selector()
+
+
+typeItem.triggered.connect(notetypeTrigger)
+
+
+deckItem = QAction("", aqt.mw)
+deckItem.setEnabled(False)
+
+
+def deckTrigger():
+ aqt.mw.onAddCard()
+ addcards = get_add_cards()["addcards"]
+ addcards.deck_chooser.choose_deck()
+
+
+deckItem.triggered.connect(deckTrigger)
+
+
+def setup_menu():
+ 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()
+ menu.addAction(titleItem)
+ menu.addAction(typeItem)
+ menu.addAction(deckItem)
+
+ aqt.mw.form.menubar.insertMenu(aqt.mw.form.menuHelp.menuAction(), menu)
+
+
+def activate_deck_type():
+ pass
+
+
+def deactivate_deck_type():
+ pass
+
+
+def set_type_name(name, id):
+ set("migakuNotetypeId", id, do_write=True)
+ typeItem.setText(f"Type: {name}")
+ print("foo type", name, id)
+
+
+def set_deck_name(name, id):
+ set("migakuDeckId", id, do_write=True)
+ deckItem.setText(f"Deck: {name}")
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 97%
rename from src/balance_scheduler_dayoff_window.py
rename to src/menu/dayoff_window.py
index 29e616b..04500b8 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):
@@ -81,5 +81,5 @@ def accept(self):
super().accept()
-action = QAction("Day Off", aqt.mw)
+action = QAction("Day Off…", aqt.mw)
action.triggered.connect(lambda: BalanceSchedulerDayOffWindow().exec())
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 99%
rename from src/balance_scheduler_vacation_window.py
rename to src/menu/vacation_window.py
index 6a88c6b..5bb143f 100644
--- a/src/balance_scheduler_vacation_window.py
+++ b/src/menu/vacation_window.py
@@ -222,5 +222,5 @@ def date_to_col_day(self, date):
return max(0, date.toJulianDay() - self.col_date.toJulianDay())
-action = QAction("Manage Vacations", aqt.mw)
+action = QAction("Manage Vacations…", aqt.mw)
action.triggered.connect(lambda: BalanceSchedulerVacationWindow().exec())
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..2874297 100644
--- a/src/migaku_connection/card_creator.py
+++ b/src/migaku_connection/card_creator.py
@@ -1,24 +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"]
+class CardCreator(MigakuHTTPHandler):
TO_MP3_RE = re.compile(r"\[sound:(.*?)\.(wav|ogg)\]")
BR_RE = re.compile(r"
")
@@ -93,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)
@@ -188,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()
@@ -225,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:
@@ -236,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.")
@@ -288,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):
@@ -300,37 +224,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..f135ca2
--- /dev/null
+++ b/src/migaku_connection/card_receiver.py
@@ -0,0 +1,89 @@
+import json
+import re
+
+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,
+ map_to_add_cards,
+)
+from tornado.web import RequestHandler
+
+from .migaku_http_handler import MigakuHTTPHandler
+
+
+class CardReceiver(MigakuHTTPHandler):
+ def post(self: RequestHandler):
+ try:
+ body = json.loads(self.request.body)
+ card = card_fields_from_dict(body)
+ self.create_card(card)
+ except Exception as e:
+ self.finish({"success": False, "error": f"Invalid request: {str(e)}."})
+
+ return
+
+ def create_card(self, card: CardFields):
+ if get("migakuIntercept", False) and map_to_add_cards(card):
+ print("Tryied to map to add cards.")
+ aqt.mw.taskman.run_on_main(
+ lambda: aqt.utils.tooltip("Mapped Migaku fields to Add cards window.")
+ )
+ self.finish(
+ json.dumps(
+ {
+ "success": True,
+ "created": False,
+ }
+ )
+ )
+ return
+
+ info = get_add_cards_info()
+
+ note = Note(aqt.mw.col, info["notetype"])
+ fields = info["fields"]
+
+ if not any([type != "none" for (fieldname, type) in fields.items()]):
+ print("No fields to map to.")
+ aqt.mw.taskman.run_on_main(
+ lambda: aqt.utils.tooltip(
+ "Could not create Migaku Card: No fields to map to."
+ )
+ )
+ self.finish(
+ {
+ "success": False,
+ "error": "No fields to map to.",
+ }
+ )
+ return
+
+ for fieldname, type in fields.items():
+ if type == "none":
+ continue
+
+ note[fieldname] = str(getattr(card, type))
+
+ note.tags = info["tags"]
+ note.model()["did"] = int(info["deck_id"])
+
+ aqt.mw.col.addNote(note)
+ aqt.mw.col.save()
+ aqt.mw.taskman.run_on_main(aqt.mw.reset)
+ 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))
+ print(f"Card created. ID: {note.id}.")
+
+ self.finish(
+ json.dumps(
+ {
+ "success": True,
+ "created": True,
+ "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/migaku_http_handler.py b/src/migaku_connection/migaku_http_handler.py
index 6d29629..4e5fe69 100644
--- a/src/migaku_connection/migaku_http_handler.py
+++ b/src/migaku_connection/migaku_http_handler.py
@@ -14,7 +14,8 @@ 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)
version_match = self.connection.PROTOCOL_VERSION == version
return version_match
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_connection/srs_import.py b/src/migaku_connection/srs_import.py
index f9db403..f08db8a 100644
--- a/src/migaku_connection/srs_import.py
+++ b/src/migaku_connection/srs_import.py
@@ -19,7 +19,7 @@ def get(self):
self.write(
{
"ok": aqt.mw.col is not None and self.connection.is_connected(),
- "version": "2",
+ "version": "3",
}
)
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/migaku_fields.py b/src/migaku_fields.py
new file mode 100644
index 0000000..0df9966
--- /dev/null
+++ b/src/migaku_fields.py
@@ -0,0 +1,102 @@
+import re
+from typing import Literal
+
+from .config import get
+import anki
+
+
+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"(Is Audio Card|音声カード|音频卡|오디오 카드|tarjeta de audio|cartão de áudio|carte audio|audio karte|cartão de áudio)",
+ name,
+ re.IGNORECASE,
+ ):
+ return "none"
+ elif re.search(
+ r"(sentence|文|句|문장|frase|phrase|satz|frase)", name, re.IGNORECASE
+ ):
+ return "sentenceAudio"
+ else:
+ return "wordAudio"
+
+ if re.search(
+ r"(word|単語|单词|단어|palabra|palavra|mot|wort|palavra)", name, re.IGNORECASE
+ ):
+ return "targetWord"
+ if (
+ 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"(screenshot|スクリーンショット|截图|스크린샷|capturas de pantalla|capturas de tela|captures d'écran|bildschirmfotos|capturas de tela)",
+ name,
+ re.IGNORECASE,
+ )
+ ):
+ return "images"
+
+ if re.search(
+ r"(example|例|例句|例子|예|ejemplo|exemplo|exemple|beispiel|exemplo)",
+ name,
+ re.IGNORECASE,
+ ):
+ return "exampleSentences"
+
+ 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 "translation"
+
+ if re.search(
+ r"(definition|定義|定义|정의|definición|definição|définition|definition|definição)",
+ name,
+ re.IGNORECASE,
+ ):
+ return "definitions"
+ if re.search(r"(note|ノート|笔记|노트|nota|nota|note|notiz|nota)", 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
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)
diff --git a/src/note_type_mgr.py b/src/note_type_mgr.py
index a2b81a5..012d99a 100644
--- a/src/note_type_mgr.py
+++ b/src/note_type_mgr.py
@@ -67,58 +67,67 @@ def install(lang: Language) -> None:
def nt_update(nt: NotetypeDict, lang: Language, commit=True) -> None:
+ # It is the reserved, read-only note type, e.g. "Migaku Japanese".
+ # Custom notetypes are cloned from this one.
+ is_base_tmpl = nt["name"] == NOTE_TYPE_PREFIX + lang.name_en
nt_mgr = aqt.mw.col.models
- nt["name"] = NOTE_TYPE_PREFIX + lang.name_en
-
# Assure required fields exist
def field_exists(name):
return any([fld["name"] == name for fld in nt["flds"]])
- for field_name in lang.fields:
- if not field_exists(field_name):
+ if is_base_tmpl:
+ # Assure fields
+ for field_name in lang.fields:
+ if field_exists(field_name):
+ continue
+
field = nt_mgr.new_field(field_name)
nt_mgr.add_field(nt, field)
- css_path = lang.file_path("card", "styles.css")
- with open(css_path, "r", encoding="utf-8") as file:
- css_data = file.read()
+ # Set CSS
+ css_path = lang.file_path("card", "styles.css")
+ with open(css_path, "r", encoding="utf-8") as file:
+ css_data = file.read()
- # Set CSS
- nt["css"] = NOTE_TYPE_MARK_CSS + "\n\n" + css_data
+ nt["css"] = NOTE_TYPE_MARK_CSS + "\n\n" + css_data
- # Get or create template
+ # Assure standard template
template_name = "Standard"
template = None
template_idx = -1
- for i, t in enumerate(nt["tmpls"]):
- if t["name"] == template_name:
- template = t
- template_idx = i
+ for idx, tmpl in enumerate(nt["tmpls"]):
+ if tmpl["name"] == template_name:
+ template = tmpl
+ template_idx = idx
break
- if template is None:
+
+ if is_base_tmpl and not template:
template = nt_mgr.new_template(template_name)
nt["tmpls"].append(template)
template_idx = len(nt["tmpls"]) - 1
# Set template html
- for fmt, html_name in [("qfmt", "front.html"), ("afmt", "back.html")]:
- html_path = lang.file_path("card", html_name)
- with open(html_path, "r", encoding="utf-8") as file:
- html = file.read()
-
- fields_settings = nt_get_tmpl_fields_settings(nt, template_idx, fmt)
- nt["tmpls"][template_idx][fmt] = html
- nt_set_tmpl_lang(
- nt,
- lang,
- template_idx,
- fmt,
- fields_settings,
- settings_mismatch_ignore=True,
- commit=False,
- )
+ if template:
+ for fmt, html_name in [("qfmt", "front.html"), ("afmt", "back.html")]:
+ if nt["name"] == NOTE_TYPE_PREFIX + lang.name_en:
+ html_path = lang.file_path("card", html_name)
+ with open(html_path, "r", encoding="utf-8") as file:
+ html = file.read()
+ nt["tmpls"][template_idx][fmt] = html
+
+ fields_settings = nt_get_tmpl_fields_settings(nt, template_idx, fmt)
+
+ nt_set_tmpl_lang(
+ nt,
+ lang,
+ template_idx,
+ fmt,
+ fields_settings,
+ settings_mismatch_ignore=True,
+ commit=False,
+ )
# Set template css
nt_set_css_lang(nt, lang, commit=False)
@@ -148,7 +157,7 @@ def nt_get_lang(nt: NotetypeDict) -> Optional[Language]:
def nt_set_css_lang(nt: NotetypeDict, lang: Optional[Language], commit=True) -> None:
- # Remove CSS
+ # User CSS
css_data = STYLE_RE.sub("", nt["css"]).rstrip()
if lang:
@@ -283,11 +292,12 @@ def nt_was_installed(nt: NotetypeDict) -> bool:
def update_all_installed() -> None:
- nt_mgr = aqt.mw.col.models
- for lang in Languages:
- if is_installed(lang):
- nt = nt_mgr.by_name(NOTE_TYPE_PREFIX + lang.name_en)
- nt_update(nt, lang)
+ notetypes = aqt.mw.col.models.all()
+
+ for nt in notetypes:
+ lang = nt_get_lang(nt)
+ if not lang or not is_installed(lang):
+ continue
-aqt.gui_hooks.profile_did_open.append(update_all_installed)
+ nt_update(nt, lang)
diff --git a/src/settings_widget.py b/src/settings_widget.py
new file mode 100644
index 0000000..e519a7f
--- /dev/null
+++ b/src/settings_widget.py
@@ -0,0 +1,72 @@
+import aqt
+from aqt.qt import *
+from . import util
+
+
+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("