From 6e22ce9f419d370998bc4639f3582eb8a0f1881a Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 28 Apr 2025 15:53:10 +0300 Subject: [PATCH 001/133] Add ts/routes/editor --- ts/routes/editor/[noteId]/+page.svelte | 12 ++++++++++++ ts/routes/editor/[noteId]/+page.ts | 10 ++++++++++ 2 files changed, 22 insertions(+) create mode 100644 ts/routes/editor/[noteId]/+page.svelte create mode 100644 ts/routes/editor/[noteId]/+page.ts diff --git a/ts/routes/editor/[noteId]/+page.svelte b/ts/routes/editor/[noteId]/+page.svelte new file mode 100644 index 000000000..995cede15 --- /dev/null +++ b/ts/routes/editor/[noteId]/+page.svelte @@ -0,0 +1,12 @@ + + + +
{data.noteId}
diff --git a/ts/routes/editor/[noteId]/+page.ts b/ts/routes/editor/[noteId]/+page.ts new file mode 100644 index 000000000..66e82b2cd --- /dev/null +++ b/ts/routes/editor/[noteId]/+page.ts @@ -0,0 +1,10 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import type { PageLoad } from "./$types"; + +export const load = (async ({ params }) => { + const noteId = BigInt(params.noteId); + + return { noteId }; +}) satisfies PageLoad; From 5cbb5242c8dce70065042cf1a921d1956c0b2bbb Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 22 May 2025 02:46:01 +0300 Subject: [PATCH 002/133] Move editor code to ts/routes --- build/configure/src/aqt.rs | 12 +++---- build/configure/src/web.rs | 33 ++--------------- proto/anki/frontend.proto | 2 ++ qt/aqt/editor.py | 36 +++++++++++-------- qt/aqt/mediasrv.py | 14 ++++++++ ts/editor/index.ts | 8 ----- ts/{ => lib}/editable/ContentEditable.svelte | 0 ts/{ => lib}/editable/Mathjax.svelte | 0 ts/{ => lib}/editable/change-timer.ts | 0 ts/{ => lib}/editable/content-editable.ts | 0 ts/{ => lib}/editable/decorated.ts | 0 ts/{ => lib}/editable/editable-base.scss | 2 +- ts/{ => lib}/editable/frame-element.ts | 0 ts/{ => lib}/editable/frame-handle.ts | 0 ts/{ => lib}/editable/index.ts | 0 .../editable/mathjax-element.svelte.ts | 0 ts/{ => lib}/editable/mathjax.ts | 0 ts/{ => lib}/html-filter/element.ts | 0 ts/{ => lib}/html-filter/helpers.ts | 0 ts/{ => lib}/html-filter/index.test.ts | 0 ts/{ => lib}/html-filter/index.ts | 0 ts/{ => lib}/html-filter/node.ts | 0 ts/{ => lib}/html-filter/styling.ts | 0 ts/routes/editor/+page.svelte | 23 ++++++++++++ ts/{ => routes}/editor/BrowserEditor.svelte | 0 ts/{ => routes}/editor/ClozeButtons.svelte | 0 ts/{ => routes}/editor/CodeMirror.svelte | 0 ts/{ => routes}/editor/CollapseBadge.svelte | 0 ts/{ => routes}/editor/CollapseLabel.svelte | 0 ts/{ => routes}/editor/DuplicateLink.svelte | 0 ts/{ => routes}/editor/EditingArea.svelte | 0 ts/{ => routes}/editor/EditorField.svelte | 0 .../editor/FieldDescription.svelte | 0 ts/{ => routes}/editor/FieldState.svelte | 0 ts/{ => routes}/editor/Fields.svelte | 0 .../editor/HandleBackground.svelte | 0 ts/{ => routes}/editor/HandleControl.svelte | 0 ts/{ => routes}/editor/HandleLabel.svelte | 0 ts/{ => routes}/editor/LabelContainer.svelte | 0 ts/{ => routes}/editor/LabelName.svelte | 0 ts/{ => routes}/editor/NoteCreator.svelte | 0 ts/{ => routes}/editor/NoteEditor.svelte | 16 ++++----- ts/{ => routes}/editor/Notification.svelte | 0 ts/{ => routes}/editor/PlainTextBadge.svelte | 0 ts/{ => routes}/editor/PreviewButton.svelte | 0 ts/{ => routes}/editor/ReviewerEditor.svelte | 0 ts/{ => routes}/editor/RichTextBadge.svelte | 0 ts/{ => routes}/editor/StickyBadge.svelte | 0 ts/routes/editor/[noteId]/+page.svelte | 12 ------- ts/routes/editor/[noteId]/+page.ts | 10 ------ ts/{ => routes}/editor/base.ts | 9 ++--- ts/{ => routes}/editor/code-mirror.ts | 0 ts/{ => routes}/editor/decorated-elements.ts | 8 ++--- ts/{ => routes}/editor/destroyable.ts | 0 ts/{ => routes}/editor/editor-base.scss | 0 .../editor/editor-toolbar/AddonButtons.svelte | 0 .../editor/editor-toolbar/BlockButtons.svelte | 0 .../editor/editor-toolbar/BoldButton.svelte | 0 .../editor/editor-toolbar/ColorPicker.svelte | 0 .../editor-toolbar/CommandIconButton.svelte | 0 .../editor-toolbar/EditorToolbar.svelte | 0 .../HighlightColorButton.svelte | 0 .../ImageOcclusionButton.svelte | 4 +-- .../editor-toolbar/InlineButtons.svelte | 0 .../editor/editor-toolbar/ItalicButton.svelte | 0 .../editor/editor-toolbar/LatexButton.svelte | 2 +- .../editor-toolbar/NotetypeButtons.svelte | 0 .../editor-toolbar/OptionsButton.svelte | 2 +- .../editor-toolbar/OptionsButtons.svelte | 0 .../editor-toolbar/RemoveFormatButton.svelte | 0 .../RichTextClozeButtons.svelte | 0 .../editor-toolbar/SubscriptButton.svelte | 0 .../editor-toolbar/SuperscriptButton.svelte | 0 .../editor-toolbar/TemplateButtons.svelte | 0 .../editor-toolbar/TextAttributeButton.svelte | 0 .../editor-toolbar/TextColorButton.svelte | 0 .../editor-toolbar/UnderlineButton.svelte | 0 .../editor-toolbar/WithColorHelper.svelte | 0 .../editor/editor-toolbar/index.ts | 0 ts/{ => routes}/editor/helpers.ts | 0 .../editor/image-overlay/FloatButtons.svelte | 0 .../editor/image-overlay/ImageOverlay.svelte | 0 .../editor/image-overlay/SizeSelect.svelte | 0 ts/{ => routes}/editor/image-overlay/index.ts | 0 ts/{ => routes}/editor/legacy.scss | 0 .../mathjax-overlay/MathjaxButtons.svelte | 0 .../mathjax-overlay/MathjaxEditor.svelte | 0 .../mathjax-overlay/MathjaxOverlay.svelte | 4 +-- .../editor/mathjax-overlay/index.ts | 0 ts/{ => routes}/editor/old-editor-adapter.ts | 2 +- .../plain-text-input/PlainTextInput.svelte | 0 .../editor/plain-text-input/index.ts | 0 .../plain-text-input/remove-prohibited.ts | 0 .../editor/plain-text-input/transform.ts | 0 .../rich-text-input/CustomStyles.svelte | 0 .../rich-text-input/RichTextInput.svelte | 4 +-- .../rich-text-input/RichTextStyles.svelte | 0 .../editor/rich-text-input/StyleLink.svelte | 0 .../editor/rich-text-input/StyleTag.svelte | 0 .../editor/rich-text-input/index.ts | 0 .../rich-text-input/normalizing-node-store.ts | 2 +- .../rich-text-input/rich-text-resolve.ts | 0 .../editor/rich-text-input/transform.ts | 0 ts/{ => routes}/editor/surround.ts | 0 ts/{ => routes}/editor/types.ts | 2 ++ ts/routes/tmp/_page.ts | 1 - 106 files changed, 96 insertions(+), 112 deletions(-) delete mode 100644 ts/editor/index.ts rename ts/{ => lib}/editable/ContentEditable.svelte (100%) rename ts/{ => lib}/editable/Mathjax.svelte (100%) rename ts/{ => lib}/editable/change-timer.ts (100%) rename ts/{ => lib}/editable/content-editable.ts (100%) rename ts/{ => lib}/editable/decorated.ts (100%) rename ts/{ => lib}/editable/editable-base.scss (96%) rename ts/{ => lib}/editable/frame-element.ts (100%) rename ts/{ => lib}/editable/frame-handle.ts (100%) rename ts/{ => lib}/editable/index.ts (100%) rename ts/{ => lib}/editable/mathjax-element.svelte.ts (100%) rename ts/{ => lib}/editable/mathjax.ts (100%) rename ts/{ => lib}/html-filter/element.ts (100%) rename ts/{ => lib}/html-filter/helpers.ts (100%) rename ts/{ => lib}/html-filter/index.test.ts (100%) rename ts/{ => lib}/html-filter/index.ts (100%) rename ts/{ => lib}/html-filter/node.ts (100%) rename ts/{ => lib}/html-filter/styling.ts (100%) create mode 100644 ts/routes/editor/+page.svelte rename ts/{ => routes}/editor/BrowserEditor.svelte (100%) rename ts/{ => routes}/editor/ClozeButtons.svelte (100%) rename ts/{ => routes}/editor/CodeMirror.svelte (100%) rename ts/{ => routes}/editor/CollapseBadge.svelte (100%) rename ts/{ => routes}/editor/CollapseLabel.svelte (100%) rename ts/{ => routes}/editor/DuplicateLink.svelte (100%) rename ts/{ => routes}/editor/EditingArea.svelte (100%) rename ts/{ => routes}/editor/EditorField.svelte (100%) rename ts/{ => routes}/editor/FieldDescription.svelte (100%) rename ts/{ => routes}/editor/FieldState.svelte (100%) rename ts/{ => routes}/editor/Fields.svelte (100%) rename ts/{ => routes}/editor/HandleBackground.svelte (100%) rename ts/{ => routes}/editor/HandleControl.svelte (100%) rename ts/{ => routes}/editor/HandleLabel.svelte (100%) rename ts/{ => routes}/editor/LabelContainer.svelte (100%) rename ts/{ => routes}/editor/LabelName.svelte (100%) rename ts/{ => routes}/editor/NoteCreator.svelte (100%) rename ts/{ => routes}/editor/NoteEditor.svelte (98%) rename ts/{ => routes}/editor/Notification.svelte (100%) rename ts/{ => routes}/editor/PlainTextBadge.svelte (100%) rename ts/{ => routes}/editor/PreviewButton.svelte (100%) rename ts/{ => routes}/editor/ReviewerEditor.svelte (100%) rename ts/{ => routes}/editor/RichTextBadge.svelte (100%) rename ts/{ => routes}/editor/StickyBadge.svelte (100%) delete mode 100644 ts/routes/editor/[noteId]/+page.svelte delete mode 100644 ts/routes/editor/[noteId]/+page.ts rename ts/{ => routes}/editor/base.ts (91%) rename ts/{ => routes}/editor/code-mirror.ts (100%) rename ts/{ => routes}/editor/decorated-elements.ts (78%) rename ts/{ => routes}/editor/destroyable.ts (100%) rename ts/{ => routes}/editor/editor-base.scss (100%) rename ts/{ => routes}/editor/editor-toolbar/AddonButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/BlockButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/BoldButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/ColorPicker.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/CommandIconButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/EditorToolbar.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/HighlightColorButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/ImageOcclusionButton.svelte (94%) rename ts/{ => routes}/editor/editor-toolbar/InlineButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/ItalicButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/LatexButton.svelte (98%) rename ts/{ => routes}/editor/editor-toolbar/NotetypeButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/OptionsButton.svelte (97%) rename ts/{ => routes}/editor/editor-toolbar/OptionsButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/RemoveFormatButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/RichTextClozeButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/SubscriptButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/SuperscriptButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/TemplateButtons.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/TextAttributeButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/TextColorButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/UnderlineButton.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/WithColorHelper.svelte (100%) rename ts/{ => routes}/editor/editor-toolbar/index.ts (100%) rename ts/{ => routes}/editor/helpers.ts (100%) rename ts/{ => routes}/editor/image-overlay/FloatButtons.svelte (100%) rename ts/{ => routes}/editor/image-overlay/ImageOverlay.svelte (100%) rename ts/{ => routes}/editor/image-overlay/SizeSelect.svelte (100%) rename ts/{ => routes}/editor/image-overlay/index.ts (100%) rename ts/{ => routes}/editor/legacy.scss (100%) rename ts/{ => routes}/editor/mathjax-overlay/MathjaxButtons.svelte (100%) rename ts/{ => routes}/editor/mathjax-overlay/MathjaxEditor.svelte (100%) rename ts/{ => routes}/editor/mathjax-overlay/MathjaxOverlay.svelte (98%) rename ts/{ => routes}/editor/mathjax-overlay/index.ts (100%) rename ts/{ => routes}/editor/old-editor-adapter.ts (93%) rename ts/{ => routes}/editor/plain-text-input/PlainTextInput.svelte (100%) rename ts/{ => routes}/editor/plain-text-input/index.ts (100%) rename ts/{ => routes}/editor/plain-text-input/remove-prohibited.ts (100%) rename ts/{ => routes}/editor/plain-text-input/transform.ts (100%) rename ts/{ => routes}/editor/rich-text-input/CustomStyles.svelte (100%) rename ts/{ => routes}/editor/rich-text-input/RichTextInput.svelte (98%) rename ts/{ => routes}/editor/rich-text-input/RichTextStyles.svelte (100%) rename ts/{ => routes}/editor/rich-text-input/StyleLink.svelte (100%) rename ts/{ => routes}/editor/rich-text-input/StyleTag.svelte (100%) rename ts/{ => routes}/editor/rich-text-input/index.ts (100%) rename ts/{ => routes}/editor/rich-text-input/normalizing-node-store.ts (92%) rename ts/{ => routes}/editor/rich-text-input/rich-text-resolve.ts (100%) rename ts/{ => routes}/editor/rich-text-input/transform.ts (100%) rename ts/{ => routes}/editor/surround.ts (100%) rename ts/{ => routes}/editor/types.ts (92%) diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 5b2b8ec49..80f7462f5 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -148,10 +148,8 @@ fn build_css(build: &mut Build) -> Result<()> { }, )?; } - let other_ts_css = build.inputs_with_suffix( - inputs![":ts:editor", ":ts:editable", ":ts:reviewer:reviewer.css"], - ".css", - ); + let other_ts_css = + build.inputs_with_suffix(inputs![":ts:editable", ":ts:reviewer:reviewer.css"], ".css"); build.add_action( "qt:aqt:data:web:css", CopyFiles { @@ -192,10 +190,8 @@ fn build_js(build: &mut Build) -> Result<()> { inputs: files, }, )?; - let files_from_ts = build.inputs_with_suffix( - inputs![":ts:editor", ":ts:reviewer:reviewer.js", ":ts:mathjax"], - ".js", - ); + let files_from_ts = + build.inputs_with_suffix(inputs![":ts:reviewer:reviewer.js", ":ts:mathjax"], ".js"); build.add_action( "qt:aqt:data:web:js", CopyFiles { diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index ef2d268bb..2e5411c41 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -29,7 +29,6 @@ pub fn build_and_check_web(build: &mut Build) -> Result<()> { build_sveltekit(build)?; declare_and_check_other_libraries(build)?; build_and_check_pages(build)?; - build_and_check_editor(build)?; build_and_check_reviewer(build)?; build_and_check_mathjax(build)?; check_web(build)?; @@ -170,7 +169,7 @@ fn declare_and_check_other_libraries(build: &mut Build) -> Result<()> { "components", inputs![":ts:lib", ":ts:sveltelib", glob!("ts/components/**")], ), - ("html-filter", inputs![glob!("ts/html-filter/**")]), + ("html-filter", inputs![glob!("ts/lib/html-filter/**")]), ] { let library_with_ts = format!("ts:{library}"); build.add_dependency(&library_with_ts, inputs.clone()); @@ -187,7 +186,7 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { let entrypoint = if html { format!("ts/routes/{name}/index.ts") } else { - format!("ts/{name}/index.ts") + format!("ts/lib/{name}/index.ts") }; build.add_action( &group, @@ -208,7 +207,6 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { "editable", false, inputs![ - // ":ts:lib", ":ts:components", ":ts:domlib", @@ -232,33 +230,6 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { Ok(()) } -fn build_and_check_editor(build: &mut Build) -> Result<()> { - let editor_deps = inputs![ - // - ":ts:lib", - ":ts:components", - ":ts:domlib", - ":ts:sveltelib", - ":ts:html-filter", - ":sass", - ":sveltekit", - glob!("ts/{editable,editor,routes/image-occlusion}/**") - ]; - - build.add_action( - "ts:editor", - EsbuildScript { - script: "ts/bundle_svelte.mjs".into(), - entrypoint: "ts/editor/index.ts".into(), - output_stem: "ts/editor/editor", - deps: editor_deps.clone(), - extra_exts: &["css"], - }, - )?; - - Ok(()) -} - fn build_and_check_reviewer(build: &mut Build) -> Result<()> { let reviewer_deps = inputs![ ":ts:lib", diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 95b929c5c..9bf50d8e2 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -27,6 +27,8 @@ service FrontendService { rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty); // Warns python that the deck option web view is ready to receive requests. rpc deckOptionsReady(generic.Empty) returns (generic.Empty); + + rpc editorReady(generic.Empty) returns (generic.Empty); } service BackendFrontendService {} diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index c1eb14b18..ead8e1df4 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -107,6 +107,17 @@ class EditorState(Enum): IO_FIELDS = 3 +def on_editor_ready(func: Callable) -> Callable: + @functools.wraps(func) + def decorated(self: Editor, *args: Any, **kwargs: Any) -> None: + if self._ready: + func(self, *args, **kwargs) + else: + self._ready_callbacks.append(lambda: func(self, *args, **kwargs)) + + return decorated + + class Editor: """The screen that embeds an editing widget should listen for changes via the `operation_did_execute` hook, and call set_note() when the editor needs @@ -146,12 +157,14 @@ class Editor: self.state: EditorState = EditorState.INITIAL # used for the io mask editor's context menu self.last_io_image_path: str | None = None + self._ready = False + self._ready_callbacks: list[Callable[[], None]] = [] self._init_links() self.setupOuter() self.add_webview() self.setupWeb() self.setupShortcuts() - gui_hooks.editor_did_init(self) + # gui_hooks.editor_did_init(self) # Initial setup ############################################################ @@ -175,21 +188,9 @@ class Editor: mode = "browse" else: mode = "review" + self.web.load_sveltekit_page(f"editor/?mode={mode}") - # then load page - self.web.stdHtml( - "", - css=["css/editor.css"], - js=[ - "js/mathjax.js", - "js/editor.js", - ], - context=self, - default_css=False, - ) - self.web.eval(f"setupEditor('{mode}')") - self.web.show() - + def _set_ready(self) -> None: lefttopbtns: list[str] = [] gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) @@ -218,6 +219,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too ) self.web.eval(f"{lefttopbtns_js} {righttopbtns_js}") + gui_hooks.editor_did_init(self) + self._ready = True + for cb in self._ready_callbacks: + cb() # Top buttons ###################################################################### @@ -543,6 +548,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too def loadNoteKeepingFocus(self) -> None: self.loadNote(self.currentField) + @on_editor_ready def loadNote(self, focusTo: int | None = None) -> None: if not self.note: return diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 203f23ef9..45caef008 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -337,6 +337,7 @@ def is_sveltekit_page(path: str) -> bool: "import-csv", "import-page", "image-occlusion", + "editor", ] @@ -602,6 +603,18 @@ def deck_options_ready() -> bytes: return b"" +def editor_ready() -> bytes: + from aqt.editor import Editor + + def handle_on_main() -> None: + window = aqt.mw.app.activeWindow() + if isinstance(getattr(window, "editor"), Editor): + window.editor._set_ready() # type: ignore + + aqt.mw.taskman.run_on_main(handle_on_main) + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -617,6 +630,7 @@ post_handler_list = [ search_in_browser, deck_options_require_close, deck_options_ready, + editor_ready, ] diff --git a/ts/editor/index.ts b/ts/editor/index.ts deleted file mode 100644 index bd8fc9029..000000000 --- a/ts/editor/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -import { globalExport } from "@tslib/globals"; - -import * as base from "./base"; - -globalExport(base); diff --git a/ts/editable/ContentEditable.svelte b/ts/lib/editable/ContentEditable.svelte similarity index 100% rename from ts/editable/ContentEditable.svelte rename to ts/lib/editable/ContentEditable.svelte diff --git a/ts/editable/Mathjax.svelte b/ts/lib/editable/Mathjax.svelte similarity index 100% rename from ts/editable/Mathjax.svelte rename to ts/lib/editable/Mathjax.svelte diff --git a/ts/editable/change-timer.ts b/ts/lib/editable/change-timer.ts similarity index 100% rename from ts/editable/change-timer.ts rename to ts/lib/editable/change-timer.ts diff --git a/ts/editable/content-editable.ts b/ts/lib/editable/content-editable.ts similarity index 100% rename from ts/editable/content-editable.ts rename to ts/lib/editable/content-editable.ts diff --git a/ts/editable/decorated.ts b/ts/lib/editable/decorated.ts similarity index 100% rename from ts/editable/decorated.ts rename to ts/lib/editable/decorated.ts diff --git a/ts/editable/editable-base.scss b/ts/lib/editable/editable-base.scss similarity index 96% rename from ts/editable/editable-base.scss rename to ts/lib/editable/editable-base.scss index 3cfaa63b4..e2cfb77d8 100644 --- a/ts/editable/editable-base.scss +++ b/ts/lib/editable/editable-base.scss @@ -1,4 +1,4 @@ -@use "../lib/sass/scrollbar"; +@use "../sass/scrollbar"; * { max-width: 100%; diff --git a/ts/editable/frame-element.ts b/ts/lib/editable/frame-element.ts similarity index 100% rename from ts/editable/frame-element.ts rename to ts/lib/editable/frame-element.ts diff --git a/ts/editable/frame-handle.ts b/ts/lib/editable/frame-handle.ts similarity index 100% rename from ts/editable/frame-handle.ts rename to ts/lib/editable/frame-handle.ts diff --git a/ts/editable/index.ts b/ts/lib/editable/index.ts similarity index 100% rename from ts/editable/index.ts rename to ts/lib/editable/index.ts diff --git a/ts/editable/mathjax-element.svelte.ts b/ts/lib/editable/mathjax-element.svelte.ts similarity index 100% rename from ts/editable/mathjax-element.svelte.ts rename to ts/lib/editable/mathjax-element.svelte.ts diff --git a/ts/editable/mathjax.ts b/ts/lib/editable/mathjax.ts similarity index 100% rename from ts/editable/mathjax.ts rename to ts/lib/editable/mathjax.ts diff --git a/ts/html-filter/element.ts b/ts/lib/html-filter/element.ts similarity index 100% rename from ts/html-filter/element.ts rename to ts/lib/html-filter/element.ts diff --git a/ts/html-filter/helpers.ts b/ts/lib/html-filter/helpers.ts similarity index 100% rename from ts/html-filter/helpers.ts rename to ts/lib/html-filter/helpers.ts diff --git a/ts/html-filter/index.test.ts b/ts/lib/html-filter/index.test.ts similarity index 100% rename from ts/html-filter/index.test.ts rename to ts/lib/html-filter/index.test.ts diff --git a/ts/html-filter/index.ts b/ts/lib/html-filter/index.ts similarity index 100% rename from ts/html-filter/index.ts rename to ts/lib/html-filter/index.ts diff --git a/ts/html-filter/node.ts b/ts/lib/html-filter/node.ts similarity index 100% rename from ts/html-filter/node.ts rename to ts/lib/html-filter/node.ts diff --git a/ts/html-filter/styling.ts b/ts/lib/html-filter/styling.ts similarity index 100% rename from ts/html-filter/styling.ts rename to ts/lib/html-filter/styling.ts diff --git a/ts/routes/editor/+page.svelte b/ts/routes/editor/+page.svelte new file mode 100644 index 000000000..2130ad26e --- /dev/null +++ b/ts/routes/editor/+page.svelte @@ -0,0 +1,23 @@ + + diff --git a/ts/editor/BrowserEditor.svelte b/ts/routes/editor/BrowserEditor.svelte similarity index 100% rename from ts/editor/BrowserEditor.svelte rename to ts/routes/editor/BrowserEditor.svelte diff --git a/ts/editor/ClozeButtons.svelte b/ts/routes/editor/ClozeButtons.svelte similarity index 100% rename from ts/editor/ClozeButtons.svelte rename to ts/routes/editor/ClozeButtons.svelte diff --git a/ts/editor/CodeMirror.svelte b/ts/routes/editor/CodeMirror.svelte similarity index 100% rename from ts/editor/CodeMirror.svelte rename to ts/routes/editor/CodeMirror.svelte diff --git a/ts/editor/CollapseBadge.svelte b/ts/routes/editor/CollapseBadge.svelte similarity index 100% rename from ts/editor/CollapseBadge.svelte rename to ts/routes/editor/CollapseBadge.svelte diff --git a/ts/editor/CollapseLabel.svelte b/ts/routes/editor/CollapseLabel.svelte similarity index 100% rename from ts/editor/CollapseLabel.svelte rename to ts/routes/editor/CollapseLabel.svelte diff --git a/ts/editor/DuplicateLink.svelte b/ts/routes/editor/DuplicateLink.svelte similarity index 100% rename from ts/editor/DuplicateLink.svelte rename to ts/routes/editor/DuplicateLink.svelte diff --git a/ts/editor/EditingArea.svelte b/ts/routes/editor/EditingArea.svelte similarity index 100% rename from ts/editor/EditingArea.svelte rename to ts/routes/editor/EditingArea.svelte diff --git a/ts/editor/EditorField.svelte b/ts/routes/editor/EditorField.svelte similarity index 100% rename from ts/editor/EditorField.svelte rename to ts/routes/editor/EditorField.svelte diff --git a/ts/editor/FieldDescription.svelte b/ts/routes/editor/FieldDescription.svelte similarity index 100% rename from ts/editor/FieldDescription.svelte rename to ts/routes/editor/FieldDescription.svelte diff --git a/ts/editor/FieldState.svelte b/ts/routes/editor/FieldState.svelte similarity index 100% rename from ts/editor/FieldState.svelte rename to ts/routes/editor/FieldState.svelte diff --git a/ts/editor/Fields.svelte b/ts/routes/editor/Fields.svelte similarity index 100% rename from ts/editor/Fields.svelte rename to ts/routes/editor/Fields.svelte diff --git a/ts/editor/HandleBackground.svelte b/ts/routes/editor/HandleBackground.svelte similarity index 100% rename from ts/editor/HandleBackground.svelte rename to ts/routes/editor/HandleBackground.svelte diff --git a/ts/editor/HandleControl.svelte b/ts/routes/editor/HandleControl.svelte similarity index 100% rename from ts/editor/HandleControl.svelte rename to ts/routes/editor/HandleControl.svelte diff --git a/ts/editor/HandleLabel.svelte b/ts/routes/editor/HandleLabel.svelte similarity index 100% rename from ts/editor/HandleLabel.svelte rename to ts/routes/editor/HandleLabel.svelte diff --git a/ts/editor/LabelContainer.svelte b/ts/routes/editor/LabelContainer.svelte similarity index 100% rename from ts/editor/LabelContainer.svelte rename to ts/routes/editor/LabelContainer.svelte diff --git a/ts/editor/LabelName.svelte b/ts/routes/editor/LabelName.svelte similarity index 100% rename from ts/editor/LabelName.svelte rename to ts/routes/editor/LabelName.svelte diff --git a/ts/editor/NoteCreator.svelte b/ts/routes/editor/NoteCreator.svelte similarity index 100% rename from ts/editor/NoteCreator.svelte rename to ts/routes/editor/NoteCreator.svelte diff --git a/ts/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte similarity index 98% rename from ts/editor/NoteEditor.svelte rename to ts/routes/editor/NoteEditor.svelte index 17ced575b..98bd7c989 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -57,8 +57,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { type ImageLoadedEvent, resetIOImage, - } from "../routes/image-occlusion/mask-editor"; - import { ChangeTimer } from "../editable/change-timer"; + } from "../image-occlusion/mask-editor"; + import { ChangeTimer } from "$lib/editable/change-timer"; import { clearableArray } from "./destroyable"; import DuplicateLink from "./DuplicateLink.svelte"; import EditorToolbar from "./editor-toolbar"; @@ -417,16 +417,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Shortcut from "$lib/components/Shortcut.svelte"; - import { mathjaxConfig } from "../editable/mathjax-element.svelte"; - import ImageOcclusionPage from "../routes/image-occlusion/ImageOcclusionPage.svelte"; - import ImageOcclusionPicker from "../routes/image-occlusion/ImageOcclusionPicker.svelte"; - import type { IOMode } from "../routes/image-occlusion/lib"; - import { exportShapesToClozeDeletions } from "../routes/image-occlusion/shapes/to-cloze"; + import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte"; + import ImageOcclusionPage from "../image-occlusion/ImageOcclusionPage.svelte"; + import ImageOcclusionPicker from "../image-occlusion/ImageOcclusionPicker.svelte"; + import type { IOMode } from "../image-occlusion/lib"; + import { exportShapesToClozeDeletions } from "../image-occlusion/shapes/to-cloze"; import { hideAllGuessOne, ioImageLoadedStore, ioMaskEditorVisible, - } from "../routes/image-occlusion/store"; + } from "../image-occlusion/store"; import CollapseLabel from "./CollapseLabel.svelte"; import * as oldEditorAdapter from "./old-editor-adapter"; diff --git a/ts/editor/Notification.svelte b/ts/routes/editor/Notification.svelte similarity index 100% rename from ts/editor/Notification.svelte rename to ts/routes/editor/Notification.svelte diff --git a/ts/editor/PlainTextBadge.svelte b/ts/routes/editor/PlainTextBadge.svelte similarity index 100% rename from ts/editor/PlainTextBadge.svelte rename to ts/routes/editor/PlainTextBadge.svelte diff --git a/ts/editor/PreviewButton.svelte b/ts/routes/editor/PreviewButton.svelte similarity index 100% rename from ts/editor/PreviewButton.svelte rename to ts/routes/editor/PreviewButton.svelte diff --git a/ts/editor/ReviewerEditor.svelte b/ts/routes/editor/ReviewerEditor.svelte similarity index 100% rename from ts/editor/ReviewerEditor.svelte rename to ts/routes/editor/ReviewerEditor.svelte diff --git a/ts/editor/RichTextBadge.svelte b/ts/routes/editor/RichTextBadge.svelte similarity index 100% rename from ts/editor/RichTextBadge.svelte rename to ts/routes/editor/RichTextBadge.svelte diff --git a/ts/editor/StickyBadge.svelte b/ts/routes/editor/StickyBadge.svelte similarity index 100% rename from ts/editor/StickyBadge.svelte rename to ts/routes/editor/StickyBadge.svelte diff --git a/ts/routes/editor/[noteId]/+page.svelte b/ts/routes/editor/[noteId]/+page.svelte deleted file mode 100644 index 995cede15..000000000 --- a/ts/routes/editor/[noteId]/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - -
{data.noteId}
diff --git a/ts/routes/editor/[noteId]/+page.ts b/ts/routes/editor/[noteId]/+page.ts deleted file mode 100644 index 66e82b2cd..000000000 --- a/ts/routes/editor/[noteId]/+page.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -import type { PageLoad } from "./$types"; - -export const load = (async ({ params }) => { - const noteId = BigInt(params.noteId); - - return { noteId }; -}) satisfies PageLoad; diff --git a/ts/editor/base.ts b/ts/routes/editor/base.ts similarity index 91% rename from ts/editor/base.ts rename to ts/routes/editor/base.ts index c21a64811..bfc4a9183 100644 --- a/ts/editor/base.ts +++ b/ts/routes/editor/base.ts @@ -34,6 +34,7 @@ declare global { import { ModuleName } from "@tslib/i18n"; import { mount } from "svelte"; +import type { EditorMode } from "./types"; export const editorModules = [ ModuleName.EDITING, @@ -70,16 +71,16 @@ async function setupReviewerEditor(): Promise { mount(ReviewerEditor, { target: document.body, props: { uiResolve } }); } -export function setupEditor(mode: "add" | "browse" | "review") { +export async function setupEditor(mode: EditorMode) { switch (mode) { case "add": - setupNoteCreator(); + await setupNoteCreator(); break; case "browse": - setupBrowserEditor(); + await setupBrowserEditor(); break; case "review": - setupReviewerEditor(); + await setupReviewerEditor(); break; default: alert("unexpected editor type"); diff --git a/ts/editor/code-mirror.ts b/ts/routes/editor/code-mirror.ts similarity index 100% rename from ts/editor/code-mirror.ts rename to ts/routes/editor/code-mirror.ts diff --git a/ts/editor/decorated-elements.ts b/ts/routes/editor/decorated-elements.ts similarity index 78% rename from ts/editor/decorated-elements.ts rename to ts/routes/editor/decorated-elements.ts index 6eb603897..e2257b3a8 100644 --- a/ts/editor/decorated-elements.ts +++ b/ts/routes/editor/decorated-elements.ts @@ -3,10 +3,10 @@ import { BLOCK_ELEMENTS } from "@tslib/dom"; -import { CustomElementArray } from "../editable/decorated"; -import { FrameElement } from "../editable/frame-element"; -import { FrameEnd, FrameStart } from "../editable/frame-handle"; -import { Mathjax } from "../editable/mathjax-element.svelte"; +import { CustomElementArray } from "$lib/editable/decorated"; +import { FrameElement } from "$lib/editable/frame-element"; +import { FrameEnd, FrameStart } from "$lib/editable/frame-handle"; +import { Mathjax } from "$lib/editable/mathjax-element.svelte"; import { parsingInstructions } from "./plain-text-input"; const decoratedElements = new CustomElementArray(); diff --git a/ts/editor/destroyable.ts b/ts/routes/editor/destroyable.ts similarity index 100% rename from ts/editor/destroyable.ts rename to ts/routes/editor/destroyable.ts diff --git a/ts/editor/editor-base.scss b/ts/routes/editor/editor-base.scss similarity index 100% rename from ts/editor/editor-base.scss rename to ts/routes/editor/editor-base.scss diff --git a/ts/editor/editor-toolbar/AddonButtons.svelte b/ts/routes/editor/editor-toolbar/AddonButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/AddonButtons.svelte rename to ts/routes/editor/editor-toolbar/AddonButtons.svelte diff --git a/ts/editor/editor-toolbar/BlockButtons.svelte b/ts/routes/editor/editor-toolbar/BlockButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/BlockButtons.svelte rename to ts/routes/editor/editor-toolbar/BlockButtons.svelte diff --git a/ts/editor/editor-toolbar/BoldButton.svelte b/ts/routes/editor/editor-toolbar/BoldButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/BoldButton.svelte rename to ts/routes/editor/editor-toolbar/BoldButton.svelte diff --git a/ts/editor/editor-toolbar/ColorPicker.svelte b/ts/routes/editor/editor-toolbar/ColorPicker.svelte similarity index 100% rename from ts/editor/editor-toolbar/ColorPicker.svelte rename to ts/routes/editor/editor-toolbar/ColorPicker.svelte diff --git a/ts/editor/editor-toolbar/CommandIconButton.svelte b/ts/routes/editor/editor-toolbar/CommandIconButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/CommandIconButton.svelte rename to ts/routes/editor/editor-toolbar/CommandIconButton.svelte diff --git a/ts/editor/editor-toolbar/EditorToolbar.svelte b/ts/routes/editor/editor-toolbar/EditorToolbar.svelte similarity index 100% rename from ts/editor/editor-toolbar/EditorToolbar.svelte rename to ts/routes/editor/editor-toolbar/EditorToolbar.svelte diff --git a/ts/editor/editor-toolbar/HighlightColorButton.svelte b/ts/routes/editor/editor-toolbar/HighlightColorButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/HighlightColorButton.svelte rename to ts/routes/editor/editor-toolbar/HighlightColorButton.svelte diff --git a/ts/editor/editor-toolbar/ImageOcclusionButton.svelte b/ts/routes/editor/editor-toolbar/ImageOcclusionButton.svelte similarity index 94% rename from ts/editor/editor-toolbar/ImageOcclusionButton.svelte rename to ts/routes/editor/editor-toolbar/ImageOcclusionButton.svelte index c9e8bc6fd..712ed9676 100644 --- a/ts/editor/editor-toolbar/ImageOcclusionButton.svelte +++ b/ts/routes/editor/editor-toolbar/ImageOcclusionButton.svelte @@ -20,8 +20,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { ioImageLoadedStore, ioMaskEditorVisible, - } from "../../routes/image-occlusion/store"; - import { toggleMaskEditorKeyCombination } from "../../routes/image-occlusion/tools/shortcuts"; + } from "../../image-occlusion/store"; + import { toggleMaskEditorKeyCombination } from "../../image-occlusion/tools/shortcuts"; export let api = {}; diff --git a/ts/editor/editor-toolbar/InlineButtons.svelte b/ts/routes/editor/editor-toolbar/InlineButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/InlineButtons.svelte rename to ts/routes/editor/editor-toolbar/InlineButtons.svelte diff --git a/ts/editor/editor-toolbar/ItalicButton.svelte b/ts/routes/editor/editor-toolbar/ItalicButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/ItalicButton.svelte rename to ts/routes/editor/editor-toolbar/ItalicButton.svelte diff --git a/ts/editor/editor-toolbar/LatexButton.svelte b/ts/routes/editor/editor-toolbar/LatexButton.svelte similarity index 98% rename from ts/editor/editor-toolbar/LatexButton.svelte rename to ts/routes/editor/editor-toolbar/LatexButton.svelte index 1d0df1f2e..78731fa81 100644 --- a/ts/editor/editor-toolbar/LatexButton.svelte +++ b/ts/routes/editor/editor-toolbar/LatexButton.svelte @@ -15,7 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Shortcut from "$lib/components/Shortcut.svelte"; import WithFloating from "$lib/components/WithFloating.svelte"; - import { mathjaxConfig } from "../../editable/mathjax-element.svelte"; + import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte"; import type { RichTextInputAPI } from "../rich-text-input"; import { editingInputIsRichText } from "../rich-text-input"; diff --git a/ts/editor/editor-toolbar/NotetypeButtons.svelte b/ts/routes/editor/editor-toolbar/NotetypeButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/NotetypeButtons.svelte rename to ts/routes/editor/editor-toolbar/NotetypeButtons.svelte diff --git a/ts/editor/editor-toolbar/OptionsButton.svelte b/ts/routes/editor/editor-toolbar/OptionsButton.svelte similarity index 97% rename from ts/editor/editor-toolbar/OptionsButton.svelte rename to ts/routes/editor/editor-toolbar/OptionsButton.svelte index 0f4e346ae..b3c54ed0f 100644 --- a/ts/editor/editor-toolbar/OptionsButton.svelte +++ b/ts/routes/editor/editor-toolbar/OptionsButton.svelte @@ -14,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Popover from "$lib/components/Popover.svelte"; import WithFloating from "$lib/components/WithFloating.svelte"; - import { mathjaxConfig } from "../../editable/mathjax-element.svelte"; + import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte"; import { shrinkImagesByDefault } from "../image-overlay/ImageOverlay.svelte"; import { closeHTMLTags } from "../plain-text-input/PlainTextInput.svelte"; diff --git a/ts/editor/editor-toolbar/OptionsButtons.svelte b/ts/routes/editor/editor-toolbar/OptionsButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/OptionsButtons.svelte rename to ts/routes/editor/editor-toolbar/OptionsButtons.svelte diff --git a/ts/editor/editor-toolbar/RemoveFormatButton.svelte b/ts/routes/editor/editor-toolbar/RemoveFormatButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/RemoveFormatButton.svelte rename to ts/routes/editor/editor-toolbar/RemoveFormatButton.svelte diff --git a/ts/editor/editor-toolbar/RichTextClozeButtons.svelte b/ts/routes/editor/editor-toolbar/RichTextClozeButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/RichTextClozeButtons.svelte rename to ts/routes/editor/editor-toolbar/RichTextClozeButtons.svelte diff --git a/ts/editor/editor-toolbar/SubscriptButton.svelte b/ts/routes/editor/editor-toolbar/SubscriptButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/SubscriptButton.svelte rename to ts/routes/editor/editor-toolbar/SubscriptButton.svelte diff --git a/ts/editor/editor-toolbar/SuperscriptButton.svelte b/ts/routes/editor/editor-toolbar/SuperscriptButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/SuperscriptButton.svelte rename to ts/routes/editor/editor-toolbar/SuperscriptButton.svelte diff --git a/ts/editor/editor-toolbar/TemplateButtons.svelte b/ts/routes/editor/editor-toolbar/TemplateButtons.svelte similarity index 100% rename from ts/editor/editor-toolbar/TemplateButtons.svelte rename to ts/routes/editor/editor-toolbar/TemplateButtons.svelte diff --git a/ts/editor/editor-toolbar/TextAttributeButton.svelte b/ts/routes/editor/editor-toolbar/TextAttributeButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/TextAttributeButton.svelte rename to ts/routes/editor/editor-toolbar/TextAttributeButton.svelte diff --git a/ts/editor/editor-toolbar/TextColorButton.svelte b/ts/routes/editor/editor-toolbar/TextColorButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/TextColorButton.svelte rename to ts/routes/editor/editor-toolbar/TextColorButton.svelte diff --git a/ts/editor/editor-toolbar/UnderlineButton.svelte b/ts/routes/editor/editor-toolbar/UnderlineButton.svelte similarity index 100% rename from ts/editor/editor-toolbar/UnderlineButton.svelte rename to ts/routes/editor/editor-toolbar/UnderlineButton.svelte diff --git a/ts/editor/editor-toolbar/WithColorHelper.svelte b/ts/routes/editor/editor-toolbar/WithColorHelper.svelte similarity index 100% rename from ts/editor/editor-toolbar/WithColorHelper.svelte rename to ts/routes/editor/editor-toolbar/WithColorHelper.svelte diff --git a/ts/editor/editor-toolbar/index.ts b/ts/routes/editor/editor-toolbar/index.ts similarity index 100% rename from ts/editor/editor-toolbar/index.ts rename to ts/routes/editor/editor-toolbar/index.ts diff --git a/ts/editor/helpers.ts b/ts/routes/editor/helpers.ts similarity index 100% rename from ts/editor/helpers.ts rename to ts/routes/editor/helpers.ts diff --git a/ts/editor/image-overlay/FloatButtons.svelte b/ts/routes/editor/image-overlay/FloatButtons.svelte similarity index 100% rename from ts/editor/image-overlay/FloatButtons.svelte rename to ts/routes/editor/image-overlay/FloatButtons.svelte diff --git a/ts/editor/image-overlay/ImageOverlay.svelte b/ts/routes/editor/image-overlay/ImageOverlay.svelte similarity index 100% rename from ts/editor/image-overlay/ImageOverlay.svelte rename to ts/routes/editor/image-overlay/ImageOverlay.svelte diff --git a/ts/editor/image-overlay/SizeSelect.svelte b/ts/routes/editor/image-overlay/SizeSelect.svelte similarity index 100% rename from ts/editor/image-overlay/SizeSelect.svelte rename to ts/routes/editor/image-overlay/SizeSelect.svelte diff --git a/ts/editor/image-overlay/index.ts b/ts/routes/editor/image-overlay/index.ts similarity index 100% rename from ts/editor/image-overlay/index.ts rename to ts/routes/editor/image-overlay/index.ts diff --git a/ts/editor/legacy.scss b/ts/routes/editor/legacy.scss similarity index 100% rename from ts/editor/legacy.scss rename to ts/routes/editor/legacy.scss diff --git a/ts/editor/mathjax-overlay/MathjaxButtons.svelte b/ts/routes/editor/mathjax-overlay/MathjaxButtons.svelte similarity index 100% rename from ts/editor/mathjax-overlay/MathjaxButtons.svelte rename to ts/routes/editor/mathjax-overlay/MathjaxButtons.svelte diff --git a/ts/editor/mathjax-overlay/MathjaxEditor.svelte b/ts/routes/editor/mathjax-overlay/MathjaxEditor.svelte similarity index 100% rename from ts/editor/mathjax-overlay/MathjaxEditor.svelte rename to ts/routes/editor/mathjax-overlay/MathjaxEditor.svelte diff --git a/ts/editor/mathjax-overlay/MathjaxOverlay.svelte b/ts/routes/editor/mathjax-overlay/MathjaxOverlay.svelte similarity index 98% rename from ts/editor/mathjax-overlay/MathjaxOverlay.svelte rename to ts/routes/editor/mathjax-overlay/MathjaxOverlay.svelte index 5442fd05c..09fe33a5e 100644 --- a/ts/editor/mathjax-overlay/MathjaxOverlay.svelte +++ b/ts/routes/editor/mathjax-overlay/MathjaxOverlay.svelte @@ -19,8 +19,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { placeCaretAfter } from "$lib/domlib/place-caret"; import { isComposing } from "$lib/sveltelib/composition"; - import { escapeSomeEntities, unescapeSomeEntities } from "../../editable/mathjax"; - import { Mathjax } from "../../editable/mathjax-element.svelte"; + import { escapeSomeEntities, unescapeSomeEntities } from "$lib/editable/mathjax"; + import { Mathjax } from "$lib/editable/mathjax-element.svelte"; import type { EditingInputAPI } from "../EditingArea.svelte"; import HandleBackground from "../HandleBackground.svelte"; import { context } from "../NoteEditor.svelte"; diff --git a/ts/editor/mathjax-overlay/index.ts b/ts/routes/editor/mathjax-overlay/index.ts similarity index 100% rename from ts/editor/mathjax-overlay/index.ts rename to ts/routes/editor/mathjax-overlay/index.ts diff --git a/ts/editor/old-editor-adapter.ts b/ts/routes/editor/old-editor-adapter.ts similarity index 93% rename from ts/editor/old-editor-adapter.ts rename to ts/routes/editor/old-editor-adapter.ts index 6b6dcad82..260164116 100644 --- a/ts/editor/old-editor-adapter.ts +++ b/ts/routes/editor/old-editor-adapter.ts @@ -4,7 +4,7 @@ import { updateAllState } from "$lib/components/WithState.svelte"; import { execCommand } from "$lib/domlib"; -import { filterHTML } from "../html-filter"; +import { filterHTML } from "$lib/html-filter"; export function pasteHTML( html: string, diff --git a/ts/editor/plain-text-input/PlainTextInput.svelte b/ts/routes/editor/plain-text-input/PlainTextInput.svelte similarity index 100% rename from ts/editor/plain-text-input/PlainTextInput.svelte rename to ts/routes/editor/plain-text-input/PlainTextInput.svelte diff --git a/ts/editor/plain-text-input/index.ts b/ts/routes/editor/plain-text-input/index.ts similarity index 100% rename from ts/editor/plain-text-input/index.ts rename to ts/routes/editor/plain-text-input/index.ts diff --git a/ts/editor/plain-text-input/remove-prohibited.ts b/ts/routes/editor/plain-text-input/remove-prohibited.ts similarity index 100% rename from ts/editor/plain-text-input/remove-prohibited.ts rename to ts/routes/editor/plain-text-input/remove-prohibited.ts diff --git a/ts/editor/plain-text-input/transform.ts b/ts/routes/editor/plain-text-input/transform.ts similarity index 100% rename from ts/editor/plain-text-input/transform.ts rename to ts/routes/editor/plain-text-input/transform.ts diff --git a/ts/editor/rich-text-input/CustomStyles.svelte b/ts/routes/editor/rich-text-input/CustomStyles.svelte similarity index 100% rename from ts/editor/rich-text-input/CustomStyles.svelte rename to ts/routes/editor/rich-text-input/CustomStyles.svelte diff --git a/ts/editor/rich-text-input/RichTextInput.svelte b/ts/routes/editor/rich-text-input/RichTextInput.svelte similarity index 98% rename from ts/editor/rich-text-input/RichTextInput.svelte rename to ts/routes/editor/rich-text-input/RichTextInput.svelte index 88549aefc..0d6d0993a 100644 --- a/ts/editor/rich-text-input/RichTextInput.svelte +++ b/ts/routes/editor/rich-text-input/RichTextInput.svelte @@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { InputHandlerAPI } from "$lib/sveltelib/input-handler"; - import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte"; + import type { ContentEditableAPI } from "$lib/editable/ContentEditable.svelte"; import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte"; import type { SurroundedAPI } from "../surround"; @@ -74,7 +74,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import useInputHandler from "$lib/sveltelib/input-handler"; import { pageTheme } from "$lib/sveltelib/theme"; - import ContentEditable from "../../editable/ContentEditable.svelte"; + import ContentEditable from "$lib/editable/ContentEditable.svelte"; import { context as editingAreaContext } from "../EditingArea.svelte"; import { Flag } from "../helpers"; import { context as noteEditorContext } from "../NoteEditor.svelte"; diff --git a/ts/editor/rich-text-input/RichTextStyles.svelte b/ts/routes/editor/rich-text-input/RichTextStyles.svelte similarity index 100% rename from ts/editor/rich-text-input/RichTextStyles.svelte rename to ts/routes/editor/rich-text-input/RichTextStyles.svelte diff --git a/ts/editor/rich-text-input/StyleLink.svelte b/ts/routes/editor/rich-text-input/StyleLink.svelte similarity index 100% rename from ts/editor/rich-text-input/StyleLink.svelte rename to ts/routes/editor/rich-text-input/StyleLink.svelte diff --git a/ts/editor/rich-text-input/StyleTag.svelte b/ts/routes/editor/rich-text-input/StyleTag.svelte similarity index 100% rename from ts/editor/rich-text-input/StyleTag.svelte rename to ts/routes/editor/rich-text-input/StyleTag.svelte diff --git a/ts/editor/rich-text-input/index.ts b/ts/routes/editor/rich-text-input/index.ts similarity index 100% rename from ts/editor/rich-text-input/index.ts rename to ts/routes/editor/rich-text-input/index.ts diff --git a/ts/editor/rich-text-input/normalizing-node-store.ts b/ts/routes/editor/rich-text-input/normalizing-node-store.ts similarity index 92% rename from ts/editor/rich-text-input/normalizing-node-store.ts rename to ts/routes/editor/rich-text-input/normalizing-node-store.ts index 197322faf..9b1cd2d0a 100644 --- a/ts/editor/rich-text-input/normalizing-node-store.ts +++ b/ts/routes/editor/rich-text-input/normalizing-node-store.ts @@ -4,7 +4,7 @@ import type { NodeStore } from "$lib/sveltelib/node-store"; import { nodeStore } from "$lib/sveltelib/node-store"; -import type { DecoratedElement } from "../../editable/decorated"; +import type { DecoratedElement } from "$lib/editable/decorated"; import { decoratedElements } from "../decorated-elements"; function normalizeFragment(fragment: DocumentFragment): void { diff --git a/ts/editor/rich-text-input/rich-text-resolve.ts b/ts/routes/editor/rich-text-input/rich-text-resolve.ts similarity index 100% rename from ts/editor/rich-text-input/rich-text-resolve.ts rename to ts/routes/editor/rich-text-input/rich-text-resolve.ts diff --git a/ts/editor/rich-text-input/transform.ts b/ts/routes/editor/rich-text-input/transform.ts similarity index 100% rename from ts/editor/rich-text-input/transform.ts rename to ts/routes/editor/rich-text-input/transform.ts diff --git a/ts/editor/surround.ts b/ts/routes/editor/surround.ts similarity index 100% rename from ts/editor/surround.ts rename to ts/routes/editor/surround.ts diff --git a/ts/editor/types.ts b/ts/routes/editor/types.ts similarity index 92% rename from ts/editor/types.ts rename to ts/routes/editor/types.ts index 14cece175..4ce9fd730 100644 --- a/ts/editor/types.ts +++ b/ts/routes/editor/types.ts @@ -27,3 +27,5 @@ export enum EditorState { ImageOcclusionMasks = 2, ImageOcclusionFields = 3, } + +export type EditorMode = "add" | "browse" | "review"; diff --git a/ts/routes/tmp/_page.ts b/ts/routes/tmp/_page.ts index 185bba104..f68486ecf 100644 --- a/ts/routes/tmp/_page.ts +++ b/ts/routes/tmp/_page.ts @@ -2,5 +2,4 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // this route pulls in code that's currently bundled separately, so that // errors in it get caught by svelte-check -import * as _editor from "$lib/../editor"; import * as _reviewer from "$lib/../reviewer"; From f453e3790c90b7b531cd9207fa0ec76d369d248e Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 22 May 2025 02:56:30 +0300 Subject: [PATCH 003/133] Fix scss paths --- ts/routes/editor/EditorField.svelte | 2 +- ts/routes/editor/editor-base.scss | 4 ++-- ts/routes/editor/legacy.scss | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/routes/editor/EditorField.svelte b/ts/routes/editor/EditorField.svelte index 4fc80e0c1..c7f6fb9d8 100644 --- a/ts/routes/editor/EditorField.svelte +++ b/ts/routes/editor/EditorField.svelte @@ -130,7 +130,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/lib/editable/Mathjax.svelte b/ts/lib/editable/Mathjax.svelte index bc8e5cf81..7be3c986d 100644 --- a/ts/lib/editable/Mathjax.svelte +++ b/ts/lib/editable/Mathjax.svelte @@ -127,24 +127,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html /> diff --git a/ts/lib/editable/content-editable.scss b/ts/lib/editable/content-editable.scss new file mode 100644 index 000000000..6648283d1 --- /dev/null +++ b/ts/lib/editable/content-editable.scss @@ -0,0 +1,15 @@ +anki-editable { + display: block; + position: relative; + + overflow: auto; + overflow-wrap: anywhere; + /* fallback for iOS */ + word-break: break-word; + + &:focus { + outline: none; + } + + min-height: 1.5em; +} diff --git a/ts/lib/editable/index.ts b/ts/lib/editable/index.ts index b44eb6435..00984a718 100644 --- a/ts/lib/editable/index.ts +++ b/ts/lib/editable/index.ts @@ -2,6 +2,5 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import "./editable-base.scss"; -/* only imported for the CSS */ -import "./ContentEditable.svelte"; -import "./Mathjax.svelte"; +import "./content-editable.scss"; +import "./mathjax.scss"; diff --git a/ts/lib/editable/mathjax.scss b/ts/lib/editable/mathjax.scss new file mode 100644 index 000000000..30f4c7541 --- /dev/null +++ b/ts/lib/editable/mathjax.scss @@ -0,0 +1,20 @@ +:global(anki-mathjax) { + white-space: pre; +} + +img { + vertical-align: var(--vertical-center); +} + +.block { + display: block; + margin: 1rem auto; + transform: scale(1.1); +} + +.empty { + vertical-align: text-bottom; + + width: var(--font-size); + height: var(--font-size); +} diff --git a/ts/routes/editor/rich-text-input/RichTextStyles.svelte b/ts/routes/editor/rich-text-input/RichTextStyles.svelte index b4f9cae0c..87629c347 100644 --- a/ts/routes/editor/rich-text-input/RichTextStyles.svelte +++ b/ts/routes/editor/rich-text-input/RichTextStyles.svelte @@ -7,6 +7,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { StyleLinkType, StyleObject } from "./CustomStyles.svelte"; import CustomStyles from "./CustomStyles.svelte"; + import editableBaseCSS from "$lib/editable/editable-base.scss?url"; + import contentEditableCSS from "$lib/editable/content-editable.scss?url"; + import mathjaxCSS from "$lib/editable/mathjax.scss?url"; + import { mount } from "svelte"; export let callback: (styles: Record) => void; @@ -45,9 +49,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const styles: StyleLinkType[] = [ { - id: "rootStyle", + id: "editableBaseStyle", type: "link", - href: "./_anki/css/editable.css", + href: editableBaseCSS, + }, + { + id: "contentEditableStyle", + type: "link", + href: contentEditableCSS, + }, + { + id: "mathjaxStyle", + type: "link", + href: mathjaxCSS, }, ]; From 35f0bc5af65ec542b7754f2f0def5062f2411d27 Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 26 May 2025 02:50:31 +0300 Subject: [PATCH 014/133] Reimplement editor_will_munge_html callbacks --- qt/aqt/editor.py | 20 -------------------- qt/aqt/mediasrv.py | 1 + ts/routes/editor/NoteEditor.svelte | 27 +++++++++++++++++++-------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 595059b57..568de8ec6 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -499,9 +499,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too else: print("uncaught cmd", cmd) - def mungeHTML(self, txt: str) -> str: - return gui_hooks.editor_will_munge_html(txt, self) - def signal_state_change( self, new_state: EditorState, old_state: EditorState ) -> None: @@ -1648,24 +1645,7 @@ def fontMungeHack(font: str) -> str: return re.sub(" L$", " Light", font) -def munge_html(txt: str, editor: Editor) -> str: - return "" if txt in ("
", "

") else txt - - -def remove_null_bytes(txt: str, editor: Editor) -> str: - # misbehaving apps may include a null byte in the text - return txt.replace("\x00", "") - - -def reverse_url_quoting(txt: str, editor: Editor) -> str: - # reverse the url quoting we added to get images to display - return editor.mw.col.media.escape_media_filenames(txt, unescape=True) - - gui_hooks.editor_will_use_font_for_field.append(fontMungeHack) -gui_hooks.editor_will_munge_html.append(munge_html) -gui_hooks.editor_will_munge_html.append(remove_null_bytes) -gui_hooks.editor_will_munge_html.append(reverse_url_quoting) def set_cloze_button(editor: Editor) -> None: diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index c08dd0abd..a9a6ccbb3 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -684,6 +684,7 @@ exposed_backend_list = [ "get_ignored_before_count", # CardRenderingService "encode_iri_paths", + "decode_iri_paths", ] diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index ff179e70c..755eae772 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -290,9 +290,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const fieldSave = new ChangeTimer(); - function transformContentBeforeSave(content: string): string { - return content.replace(/ data-editor-shrink="(true|false)"/g, ""); - // TODO: mungeHTML() + async function transformContentBeforeSave(content: string): Promise { + content = content.replace(/ data-editor-shrink="(true|false)"/g, ""); + // misbehaving apps may include a null byte in the text + content = content.replaceAll("\0", ""); + // reverse the url quoting we added to get images to display + content = (await decodeIriPaths({ val: content })).val; + + if (["
", "

"].includes(content)) { + return ""; + } + return content; } async function updateCurrentNote() { @@ -304,10 +312,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } - function updateField(index: number, content: string): void { - fieldSave.schedule(() => { + async function updateField(index: number, content: string): Promise { + fieldSave.schedule(async () => { bridgeCommand(`key:${index}`); - note!.fields[index] = transformContentBeforeSave(content); + note!.fields[index] = await transformContentBeforeSave(content); updateCurrentNote(); }, 600); } @@ -428,6 +436,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html encodeIriPaths, newNote, editorUpdateNote, + decodeIriPaths, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; @@ -790,11 +799,13 @@ components and functionality for general note editing. setAddonButtonsDisabled(false); bridgeCommand(`focus:${index}`); }} - on:focusout={() => { + on:focusout={async () => { $focusedField = null; setAddonButtonsDisabled(true); bridgeCommand(`blur:${index}`); - note!.fields[index] = transformContentBeforeSave(get(content)); + note!.fields[index] = await transformContentBeforeSave( + get(content), + ); updateCurrentNote(); }} on:mouseenter={() => { From 436d76a81e16bc8bd68141b038161e81ac0af26b Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 26 May 2025 02:53:53 +0300 Subject: [PATCH 015/133] Move setSticky() call --- qt/aqt/editor.py | 4 ---- ts/routes/editor/NoteEditor.svelte | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 568de8ec6..dd8b2828e 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -547,10 +547,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too assert self.mw.pm.profile is not None js = f"loadNote({self.note.id}, {self.note.mid});" - if self.addMode: - sticky = [field["sticky"] for field in self.note_type()["flds"]] - js += " setSticky(%s);" % json.dumps(sticky) - if self.current_notetype_is_image_occlusion(): io_field_indices = self.mw.backend.get_image_occlusion_fields(self.note.mid) image_field = self.note.fields[io_field_indices.image] diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 755eae772..821be1bc6 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -640,6 +640,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setShrinkImages(true); // TODO: closeHTMLTags col config setCloseHTMLTags(true); + if (mode === "add") { + setSticky(notetype.fields.map((field) => field.config?.sticky ?? false)); + } triggerChanges(); } From 423393107dcba2ec8c686b5e3bcd64c2d808b067 Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 28 May 2025 06:36:40 +0300 Subject: [PATCH 016/133] Move fontMungeHack() --- qt/aqt/editor.py | 16 ---------------- ts/routes/editor/NoteEditor.svelte | 10 ++++++++-- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index dd8b2828e..b3b647a8b 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -574,12 +574,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too initiator=self ) - def fonts(self) -> list[tuple[str, int, bool]]: - return [ - (gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"]) - for f in self.note_type()["flds"] - ] - def call_after_note_saved( self, callback: Callable, keepFocus: bool = False ) -> None: @@ -1634,16 +1628,6 @@ class EditorWebView(AnkiWebView): return clipboard -# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light" -# - there may be other cases like a trailing 'Bold' that need fixing, but will -# wait for further reports first. -def fontMungeHack(font: str) -> str: - return re.sub(" L$", " Light", font) - - -gui_hooks.editor_will_use_font_for_field.append(fontMungeHack) - - def set_cloze_button(editor: Editor) -> None: action = "show" if editor.note_type()["type"] == MODEL_CLOZE else "hide" editor.web.eval( diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 821be1bc6..4d937f824 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -185,6 +185,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const fields = clearableArray(); + // QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light" + // - there may be other cases like a trailing 'Bold' that need fixing, but will + // wait for further reports first. + function mungeFontName(fontName: string): string { + return fontName.replace(/ L$/g, " Light"); + } + export function setFonts(fs: [string, number, boolean][]): void { fonts = fs; } @@ -620,10 +627,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setDescriptions( notetype.fields.map((field) => field.config?.description ?? ""), ); - // TODO: gui_hooks.editor_will_use_font_for_field setFonts( notetype.fields.map((field) => [ - field.config?.fontName ?? "", + mungeFontName(field.config?.fontName ?? ""), field.config?.fontSize ?? 16, field.config?.rtl ?? false, ]), From 9395b89b9236492d0daa6e84f2a5954af11aa62e Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 28 May 2025 09:25:47 +0300 Subject: [PATCH 017/133] Move setColorButtons() --- proto/anki/frontend.proto | 9 ++++++++ qt/aqt/editor.py | 10 --------- qt/aqt/mediasrv.py | 22 ++++++++++++++++++- ts/lib/tslib/profile.ts | 16 ++++++++++++++ ts/routes/editor/NoteEditor.svelte | 9 +++++--- .../editor-toolbar/EditorToolbar.svelte | 6 ++--- .../HighlightColorButton.svelte | 4 ++-- .../editor-toolbar/InlineButtons.svelte | 18 +++++++++++---- .../editor-toolbar/TextColorButton.svelte | 4 ++-- 9 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 ts/lib/tslib/profile.ts diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 5385da051..1f3e7b6b3 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -31,6 +31,10 @@ service FrontendService { // Editor rpc editorUpdateNote(notes.UpdateNotesRequest) returns (generic.Empty); + + // Profile config + rpc GetProfileConfigJson(generic.String) returns (generic.Json); + rpc SetProfileConfigJson(SetProfileConfigJsonRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -44,3 +48,8 @@ message SetSchedulingStatesRequest { string key = 1; scheduler.SchedulingStates states = 2; } + +message SetProfileConfigJsonRequest { + string key = 1; + bytes value_json = 2; +} diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index b3b647a8b..b35d11a90 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -457,16 +457,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too return new_state - elif cmd.startswith("lastTextColor"): - (_, textColor) = cmd.split(":", 1) - assert self.mw.pm.profile is not None - self.mw.pm.profile["lastTextColor"] = textColor - - elif cmd.startswith("lastHighlightColor"): - (_, highlightColor) = cmd.split(":", 1) - assert self.mw.pm.profile is not None - self.mw.pm.profile["lastHighlightColor"] = highlightColor - elif cmd.startswith("saveTags"): gui_hooks.editor_did_update_tags(self.note) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index d507dfdd5..636d88545 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -4,6 +4,7 @@ from __future__ import annotations import enum +import json import logging import mimetypes import os @@ -28,7 +29,7 @@ from waitress.server import create_server import aqt import aqt.main import aqt.operations -from anki import hooks +from anki import frontend_pb2, generic_pb2, hooks from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest @@ -621,6 +622,23 @@ def editor_update_note() -> bytes: return output +def get_profile_config_json() -> bytes: + key = generic_pb2.String() + key.ParseFromString(request.data) + value = aqt.mw.pm.profile.get(key.val, None) + output = generic_pb2.Json(json=json.dumps(value).encode()).SerializeToString() + + return output + + +def set_profile_config_json() -> bytes: + req = frontend_pb2.SetProfileConfigJsonRequest() + req.ParseFromString(request.data) + aqt.mw.pm.profile[req.key] = json.loads(req.value_json) + + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -637,6 +655,8 @@ post_handler_list = [ deck_options_require_close, deck_options_ready, editor_update_note, + get_profile_config_json, + set_profile_config_json, ] diff --git a/ts/lib/tslib/profile.ts b/ts/lib/tslib/profile.ts new file mode 100644 index 000000000..3ec10eff7 --- /dev/null +++ b/ts/lib/tslib/profile.ts @@ -0,0 +1,16 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { getProfileConfigJson, setProfileConfigJson } from "@generated/backend"; + +export async function getProfileConfig(key: string): Promise { + const decoder = new TextDecoder(); + const json = decoder.decode((await getProfileConfigJson({ val: key })).json); + return JSON.parse(json); +} + +export async function setProfileConfig(key: string, value: any): Promise { + const encoder = new TextEncoder(); + const json = JSON.stringify(value); + await setProfileConfigJson({ key: key, valueJson: encoder.encode(json) }); +} diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 4d937f824..4e2159d5e 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -446,7 +446,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html decodeIriPaths, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; - + import { getProfileConfig } from "@tslib/profile"; import Shortcut from "$lib/components/Shortcut.svelte"; import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte"; @@ -465,6 +465,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import ButtonGroupItem from "$lib/components/ButtonGroupItem.svelte"; import PreviewButton from "./PreviewButton.svelte"; import type { Note } from "@generated/anki/notes_pb"; + import InlineButtons from "./editor-toolbar/InlineButtons.svelte"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded); @@ -613,6 +614,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ) ).map((field) => field.val); const tags = note!.tags; + const lastTextColor = (await getProfileConfig("lastTextColor")) ?? "#0000ff"; + const lastHighlightColor = + (await getProfileConfig("lastHighlightColor")) ?? "#0000ff"; saveSession(); setFields(fieldNames, fieldValues); @@ -635,8 +639,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ]), ); focusField(focusTo); - // TODO: lastTextColor/lastHighlightColor profile config - // setColorButtons(["#0000ff", "#0000ff"]); + toolbar.inlineButtons?.setColorButtons([lastTextColor, lastHighlightColor]); setTags(tags); // TODO: mw.pm.tags_collapsed() setTagsCollapsed(false); diff --git a/ts/routes/editor/editor-toolbar/EditorToolbar.svelte b/ts/routes/editor/editor-toolbar/EditorToolbar.svelte index 9266792b9..1ea545f8f 100644 --- a/ts/routes/editor/editor-toolbar/EditorToolbar.svelte +++ b/ts/routes/editor/editor-toolbar/EditorToolbar.svelte @@ -26,7 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export interface EditorToolbarAPI { toolbar: DefaultSlotInterface; notetypeButtons: DefaultSlotInterface; - inlineButtons: DefaultSlotInterface; + inlineButtons: InlineButtonsAPI; blockButtons: DefaultSlotInterface; templateButtons: DefaultSlotInterface; removeFormats: Writable; @@ -62,14 +62,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import OptionsButtons from "./OptionsButtons.svelte"; import RichTextClozeButtons from "./RichTextClozeButtons.svelte"; import TemplateButtons from "./TemplateButtons.svelte"; - + import type { InlineButtonsAPI } from "./InlineButtons.svelte"; export let size: number; export let wrap: boolean; const toolbar = {} as DefaultSlotInterface; const notetypeButtons = {} as DefaultSlotInterface; const optionsButtons = {} as DefaultSlotInterface; - const inlineButtons = {} as DefaultSlotInterface; + const inlineButtons = {} as InlineButtonsAPI; const blockButtons = {} as DefaultSlotInterface; const templateButtons = {} as DefaultSlotInterface; const removeFormats = writable([]); diff --git a/ts/routes/editor/editor-toolbar/HighlightColorButton.svelte b/ts/routes/editor/editor-toolbar/HighlightColorButton.svelte index 865ec5668..707f76872 100644 --- a/ts/routes/editor/editor-toolbar/HighlightColorButton.svelte +++ b/ts/routes/editor/editor-toolbar/HighlightColorButton.svelte @@ -4,7 +4,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> + diff --git a/ts/routes/editor/editor-toolbar/TextColorButton.svelte b/ts/routes/editor/editor-toolbar/TextColorButton.svelte index 165953180..ba27ddb27 100644 --- a/ts/routes/editor/editor-toolbar/TextColorButton.svelte +++ b/ts/routes/editor/editor-toolbar/TextColorButton.svelte @@ -4,7 +4,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> From 2ee708f9d9bfebd800d51fa70d65dfb74032db91 Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 28 May 2025 11:41:15 +0300 Subject: [PATCH 024/133] Formatting --- proto/anki/frontend.proto | 1 - 1 file changed, 1 deletion(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 461d10528..94c96351f 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -39,7 +39,6 @@ service FrontendService { // Metadata rpc GetMetaJson(generic.String) returns (generic.Json); rpc SetMetaJson(SetSettingJsonRequest) returns (generic.Empty); - } service BackendFrontendService {} From 7dbbbad914a70620f83ed3d6b72dd64f95988e53 Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 28 May 2025 11:43:48 +0300 Subject: [PATCH 025/133] Fix lint errors --- qt/aqt/mediasrv.py | 3 ++- ts/routes/editor/editor-toolbar/EditorToolbar.svelte | 1 + ts/routes/editor/editor-toolbar/InlineButtons.svelte | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 70b9673a4..bf7f4d94a 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -33,7 +33,6 @@ import aqt.operations from anki import frontend_pb2, generic_pb2, hooks from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs -from anki.errors import NotFoundError from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.utils import dev_mode from aqt.changenotetype import ChangeNotetypeDialog @@ -640,10 +639,12 @@ def set_setting_json(setter: Callable[[str, Any], Any]) -> bytes: def get_profile_config_json() -> bytes: + assert aqt.mw.pm.profile is not None return get_setting_json(aqt.mw.pm.profile.get) def set_profile_config_json() -> bytes: + assert aqt.mw.pm.profile is not None return set_setting_json(aqt.mw.pm.profile.__setitem__) diff --git a/ts/routes/editor/editor-toolbar/EditorToolbar.svelte b/ts/routes/editor/editor-toolbar/EditorToolbar.svelte index 1ea545f8f..10d11b28e 100644 --- a/ts/routes/editor/editor-toolbar/EditorToolbar.svelte +++ b/ts/routes/editor/editor-toolbar/EditorToolbar.svelte @@ -63,6 +63,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import RichTextClozeButtons from "./RichTextClozeButtons.svelte"; import TemplateButtons from "./TemplateButtons.svelte"; import type { InlineButtonsAPI } from "./InlineButtons.svelte"; + export let size: number; export let wrap: boolean; diff --git a/ts/routes/editor/editor-toolbar/InlineButtons.svelte b/ts/routes/editor/editor-toolbar/InlineButtons.svelte index 71da1e2f8..586137909 100644 --- a/ts/routes/editor/editor-toolbar/InlineButtons.svelte +++ b/ts/routes/editor/editor-toolbar/InlineButtons.svelte @@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - bridgeCommand("dupes")}> + {tr.editingShowDuplicates()} diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index e287dadf7..ec83fe969 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -585,7 +585,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }); } - async function loadNote(nid: bigint, notetypeId: bigint, focusTo: number, originalNoteId: bigint | null) { + async function loadNote( + nid: bigint, + notetypeId: bigint, + focusTo: number, + originalNoteId: bigint | null, + ) { const notetype = await getNotetype({ ntid: notetypeId, }); @@ -647,10 +652,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html if (mode === "add") { setSticky(notetype.fields.map((field) => field.config?.sticky ?? false)); } - if(isImageOcclusion) { + if (isImageOcclusion) { const imageField = note!.fields[ioFields.image]; // TODO: last_io_image_path - if(mode !== "add") { + if (mode !== "add") { setupMaskEditor({ html: imageField, mode: { @@ -658,8 +663,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html noteId: nid, }, }); - } - else if(originalNoteId) { + } else if (originalNoteId) { setupMaskEditor({ html: imageField, mode: { @@ -863,7 +867,7 @@ components and functionality for general note editing. {#if cols[index] === "dupe"} - + {/if} {#if mode === "add"} Date: Mon, 2 Jun 2025 03:59:29 +0300 Subject: [PATCH 029/133] Move fields_check() --- qt/aqt/editor.py | 45 ++---------------------------- qt/aqt/mediasrv.py | 1 + ts/routes/editor/NoteEditor.svelte | 27 ++++++++++++++++-- 3 files changed, 27 insertions(+), 46 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index a0c931970..e723cfc35 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -35,10 +35,9 @@ from anki.consts import MODEL_CLOZE from anki.hooks import runFilter from anki.httpclient import HttpClient from anki.models import NotetypeDict, NotetypeId, StockNotetype -from anki.notes import Note, NoteFieldsCheckResult, NoteId +from anki.notes import Note, NoteId from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp from aqt import AnkiQt, gui_hooks -from aqt.operations import QueryOp from aqt.operations.note import update_note from aqt.operations.notetype import update_notetype_legacy from aqt.qt import * @@ -407,11 +406,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self.mw.progress.timer( 100, self.loadNoteKeepingFocus, False, parent=self.widget ) - else: - self._check_and_update_duplicate_display_async() else: gui_hooks.editor_did_fire_typing_timer(self.note) - self._check_and_update_duplicate_display_async() # focused into field? elif cmd.startswith("focus"): @@ -511,7 +507,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too return self.widget.show() - note_fields_status = self.note.fields_check() + # note_fields_status = self.note.fields_check() def oncallback(arg: Any) -> None: if not self.note: @@ -519,7 +515,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self.setupForegroundButton() # we currently do this synchronously to ensure we load before the # sidebar on browser startup - self._update_duplicate_display(note_fields_status) if focusTo is not None: self.web.setFocus() gui_hooks.editor_did_load_note(self) @@ -552,42 +547,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too saveNow = call_after_note_saved - def _check_and_update_duplicate_display_async(self) -> None: - note = self.note - if not note: - return - - def on_done(result: NoteFieldsCheckResult.V) -> None: - if self.note != note: - return - self._update_duplicate_display(result) - - QueryOp( - parent=self.parentWindow, - op=lambda _: note.fields_check(), - success=on_done, - ).run_in_background() - - checkValid = _check_and_update_duplicate_display_async - - def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None: - assert self.note is not None - cols = [""] * len(self.note.fields) - cloze_hint = "" - if result == NoteFieldsCheckResult.DUPLICATE: - cols[0] = "dupe" - elif result == NoteFieldsCheckResult.NOTETYPE_NOT_CLOZE: - cloze_hint = tr.adding_cloze_outside_cloze_notetype() - elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE: - cloze_hint = tr.adding_cloze_outside_cloze_field() - - self.web.eval( - 'require("anki/ui").loaded.then(() => {' - f"setBackgrounds({json.dumps(cols)});\n" - f"setClozeHint({json.dumps(cloze_hint)});\n" - "}); " - ) - def fieldsAreBlank(self, previousNote: Note | None = None) -> bool: if not self.note: return True diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 3fd1996af..45eac407c 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -703,6 +703,7 @@ exposed_backend_list = [ "get_field_names", "get_note", "new_note", + "note_fields_check", # NotetypesService "get_notetype", "get_notetype_names", diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index ec83fe969..08fa4c2d5 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -325,7 +325,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html fieldSave.schedule(async () => { bridgeCommand(`key:${index}`); note!.fields[index] = await transformContentBeforeSave(content); - updateCurrentNote(); + await updateCurrentNote(); + await updateDuplicateDisplay(); }, 600); } @@ -347,6 +348,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + async function updateDuplicateDisplay(): Promise { + if (!note) { + return; + } + const result = await noteFieldsCheck(note); + const cols = new Array(note.fields.length).fill(""); + if (result.state === NoteFieldsCheckResponse_State.DUPLICATE) { + cols[0] = "dupe"; + } else if (result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) { + hint = tr.addingClozeOutsideClozeNotetype(); + } else if (result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) { + hint = tr.addingClozeOutsideClozeField(); + } + setBackgrounds(cols); + setClozeHint(hint); + } + export function focusIfField(x: number, y: number): boolean { const elements = document.elementsFromPoint(x, y); const first = elements[0].closest(".field-container"); @@ -446,6 +464,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html newNote, editorUpdateNote, decodeIriPaths, + noteFieldsCheck, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile"; @@ -466,7 +485,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import StickyBadge from "./StickyBadge.svelte"; import ButtonGroupItem from "$lib/components/ButtonGroupItem.svelte"; import PreviewButton from "./PreviewButton.svelte"; - import type { Note } from "@generated/anki/notes_pb"; + import { NoteFieldsCheckResponse_State, type Note } from "@generated/anki/notes_pb"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded); @@ -673,6 +692,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }); } } + await updateDuplicateDisplay(); triggerChanges(); } @@ -841,7 +861,8 @@ components and functionality for general note editing. note!.fields[index] = await transformContentBeforeSave( get(content), ); - updateCurrentNote(); + await updateCurrentNote(); + await updateDuplicateDisplay(); }} on:mouseenter={() => { $hoveredField = fields[index]; From b3aa8e93f340b8a272ce4a623ff5c06c630c0006 Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 2 Jun 2025 04:11:20 +0300 Subject: [PATCH 030/133] Remove legacy routines moved to JS --- qt/aqt/editor.py | 217 +---------------------------------------------- 1 file changed, 1 insertion(+), 216 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index e723cfc35..76e959a04 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -28,7 +28,6 @@ import aqt import aqt.forms import aqt.operations import aqt.sound -from anki._legacy import deprecated from anki.cards import Card from anki.collection import Config from anki.consts import MODEL_CLOZE @@ -36,27 +35,19 @@ from anki.hooks import runFilter from anki.httpclient import HttpClient from anki.models import NotetypeDict, NotetypeId, StockNotetype from anki.notes import Note, NoteId -from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp +from anki.utils import checksum, is_mac, is_win, namedtmp from aqt import AnkiQt, gui_hooks from aqt.operations.note import update_note from aqt.operations.notetype import update_notetype_legacy from aqt.qt import * from aqt.sound import av_player from aqt.utils import ( - HelpPage, KeyboardModifiersPressed, - disable_help_button, getFile, openFolder, - openHelp, - qtMenuShortcutWorkaround, - restoreGeom, - saveGeom, shortcut, show_in_folder, - showInfo, showWarning, - tooltip, tr, ) from aqt.webview import AnkiWebView, AnkiWebViewKind @@ -512,7 +503,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too def oncallback(arg: Any) -> None: if not self.note: return - self.setupForegroundButton() # we currently do this synchronously to ensure we load before the # sidebar on browser startup if focusTo is not None: @@ -1002,197 +992,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too def _create_edit_io_options(note_id: NoteId) -> dict: return {"mode": {"kind": "edit", "noteId": note_id}} - # Legacy editing routines - ###################################################################### - - _js_legacy = "this routine has been moved into JS, and will be removed soon" - - @deprecated(info=_js_legacy) - def onHtmlEdit(self) -> None: - field = self.currentField - self.call_after_note_saved(lambda: self._onHtmlEdit(field)) - - @deprecated(info=_js_legacy) - def _onHtmlEdit(self, field: int) -> None: - assert self.note is not None - d = QDialog(self.widget, Qt.WindowType.Window) - form = aqt.forms.edithtml.Ui_Dialog() - form.setupUi(d) - restoreGeom(d, "htmlEditor") - disable_help_button(d) - qconnect( - form.buttonBox.helpRequested, lambda: openHelp(HelpPage.EDITING_FEATURES) - ) - font = QFont("Courier") - font.setStyleHint(QFont.StyleHint.TypeWriter) - form.textEdit.setFont(font) - form.textEdit.setPlainText(self.note.fields[field]) - d.show() - form.textEdit.moveCursor(QTextCursor.MoveOperation.End) - d.exec() - html = form.textEdit.toPlainText() - if html.find(">") > -1: - # filter html through beautifulsoup so we can strip out things like a - # leading - html_escaped = self.mw.col.media.escape_media_filenames(html) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", UserWarning) - html_escaped = str(BeautifulSoup(html_escaped, "html.parser")) - html = self.mw.col.media.escape_media_filenames( - html_escaped, unescape=True - ) - self.note.fields[field] = html - if not self.addMode: - self._save_current_note() - self.loadNote(focusTo=field) - saveGeom(d, "htmlEditor") - - @deprecated(info=_js_legacy) - def toggleBold(self) -> None: - self.web.eval("setFormat('bold');") - - @deprecated(info=_js_legacy) - def toggleItalic(self) -> None: - self.web.eval("setFormat('italic');") - - @deprecated(info=_js_legacy) - def toggleUnderline(self) -> None: - self.web.eval("setFormat('underline');") - - @deprecated(info=_js_legacy) - def toggleSuper(self) -> None: - self.web.eval("setFormat('superscript');") - - @deprecated(info=_js_legacy) - def toggleSub(self) -> None: - self.web.eval("setFormat('subscript');") - - @deprecated(info=_js_legacy) - def removeFormat(self) -> None: - self.web.eval("setFormat('removeFormat');") - - @deprecated(info=_js_legacy) - def onCloze(self) -> None: - self.call_after_note_saved(self._onCloze, keepFocus=True) - - @deprecated(info=_js_legacy) - def _onCloze(self) -> None: - # check that the model is set up for cloze deletion - if self.note_type()["type"] != MODEL_CLOZE: - if self.addMode: - tooltip(tr.editing_warning_cloze_deletions_will_not_work()) - else: - showInfo(tr.editing_to_make_a_cloze_deletion_on()) - return - # find the highest existing cloze - highest = 0 - assert self.note is not None - for _, val in list(self.note.items()): - m = re.findall(r"\{\{c(\d+)::", val) - if m: - highest = max(highest, sorted(int(x) for x in m)[-1]) - # reuse last? - if not KeyboardModifiersPressed().alt: - highest += 1 - # must start at 1 - highest = max(1, highest) - self.web.eval("wrap('{{c%d::', '}}');" % highest) - - def setupForegroundButton(self) -> None: - assert self.mw.pm.profile is not None - self.fcolour = self.mw.pm.profile.get("lastColour", "#00f") - - # use last colour - @deprecated(info=_js_legacy) - def onForeground(self) -> None: - self._wrapWithColour(self.fcolour) - - # choose new colour - @deprecated(info=_js_legacy) - def onChangeCol(self) -> None: - if is_lin: - new = QColorDialog.getColor( - QColor(self.fcolour), - None, - None, - QColorDialog.ColorDialogOption.DontUseNativeDialog, - ) - else: - new = QColorDialog.getColor(QColor(self.fcolour), None) - # native dialog doesn't refocus us for some reason - self.parentWindow.activateWindow() - if new.isValid(): - self.fcolour = new.name() - self.onColourChanged() - self._wrapWithColour(self.fcolour) - - @deprecated(info=_js_legacy) - def _updateForegroundButton(self) -> None: - pass - - @deprecated(info=_js_legacy) - def onColourChanged(self) -> None: - self._updateForegroundButton() - assert self.mw.pm.profile is not None - self.mw.pm.profile["lastColour"] = self.fcolour - - @deprecated(info=_js_legacy) - def _wrapWithColour(self, colour: str) -> None: - self.web.eval(f"setFormat('forecolor', '{colour}')") - - @deprecated(info=_js_legacy) - def onAdvanced(self) -> None: - m = QMenu(self.mw) - - for text, handler, shortcut in ( - (tr.editing_mathjax_inline(), self.insertMathjaxInline, "Ctrl+M, M"), - (tr.editing_mathjax_block(), self.insertMathjaxBlock, "Ctrl+M, E"), - ( - tr.editing_mathjax_chemistry(), - self.insertMathjaxChemistry, - "Ctrl+M, C", - ), - (tr.editing_latex(), self.insertLatex, "Ctrl+T, T"), - (tr.editing_latex_equation(), self.insertLatexEqn, "Ctrl+T, E"), - (tr.editing_latex_math_env(), self.insertLatexMathEnv, "Ctrl+T, M"), - (tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"), - ): - a = m.addAction(text) - assert a is not None - qconnect(a.triggered, handler) - a.setShortcut(QKeySequence(shortcut)) - - qtMenuShortcutWorkaround(m) - - m.exec(QCursor.pos()) - - @deprecated(info=_js_legacy) - def insertLatex(self) -> None: - self.web.eval("wrap('[latex]', '[/latex]');") - - @deprecated(info=_js_legacy) - def insertLatexEqn(self) -> None: - self.web.eval("wrap('[$]', '[/$]');") - - @deprecated(info=_js_legacy) - def insertLatexMathEnv(self) -> None: - self.web.eval("wrap('[$$]', '[/$$]');") - - @deprecated(info=_js_legacy) - def insertMathjaxInline(self) -> None: - self.web.eval("wrap('\\\\(', '\\\\)');") - - @deprecated(info=_js_legacy) - def insertMathjaxBlock(self) -> None: - self.web.eval("wrap('\\\\[', '\\\\]');") - - @deprecated(info=_js_legacy) - def insertMathjaxChemistry(self) -> None: - self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');") - - def setTagsCollapsed(self, collapsed: bool) -> None: - aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed) - # Links from HTML ###################################################################### @@ -1200,24 +999,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self._links: dict[str, Callable] = dict( fields=Editor.onFields, cards=Editor.onCardLayout, - bold=Editor.toggleBold, - italic=Editor.toggleItalic, - underline=Editor.toggleUnderline, - super=Editor.toggleSuper, - sub=Editor.toggleSub, - clear=Editor.removeFormat, - colour=Editor.onForeground, - changeCol=Editor.onChangeCol, - cloze=Editor.onCloze, attach=Editor.onAddMedia, record=Editor.onRecSound, - more=Editor.onAdvanced, paste=Editor.onPaste, cutOrCopy=Editor.onCutOrCopy, - htmlEdit=Editor.onHtmlEdit, - mathjaxInline=Editor.insertMathjaxInline, - mathjaxBlock=Editor.insertMathjaxBlock, - mathjaxChemistry=Editor.insertMathjaxChemistry, addImageForOcclusion=Editor.select_image_and_occlude, addImageForOcclusionFromClipboard=Editor.select_image_from_clipboard_and_occlude, ) From 05360e2d195d6689203c4574b298178c5423cbe1 Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 2 Jun 2025 06:04:17 +0300 Subject: [PATCH 031/133] Add a temporary workaround for addcards --- qt/aqt/addcards.py | 67 +++--------------------------- qt/aqt/editor.py | 56 ++++++++++++++----------- qt/aqt/mediasrv.py | 1 + ts/routes/editor/NoteEditor.svelte | 63 ++++++++++++++++++++++++++-- 4 files changed, 97 insertions(+), 90 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 86e8a25b1..d3b8f343b 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -172,63 +172,13 @@ class AddCards(QMainWindow): if deck_id := self.col.default_deck_for_notetype(notetype_id): self.deck_chooser.selected_deck_id = deck_id - # only used for detecting changed sticky fields on close - self._last_added_note = None - - # copy fields into new note with the new notetype - old_note = self.editor.note - new_note = self._new_note() - if old_note: - old_field_names = list(old_note.keys()) - new_field_names = list(new_note.keys()) - copied_field_names = set() - for f in new_note.note_type()["flds"]: - field_name = f["name"] - # copy identical non-empty fields - if field_name in old_field_names and old_note[field_name]: - new_note[field_name] = old_note[field_name] - copied_field_names.add(field_name) - new_idx = 0 - for old_idx, old_field_value in enumerate(old_field_names): - # skip previously copied identical fields in new note - while ( - new_idx < len(new_field_names) - and new_field_names[new_idx] in copied_field_names - ): - new_idx += 1 - if new_idx >= len(new_field_names): - break - # copy non-empty old fields - if ( - old_field_value not in copied_field_names - and old_note.fields[old_idx] - ): - new_note.fields[new_idx] = old_note.fields[old_idx] - new_idx += 1 - - new_note.tags = old_note.tags - - # and update editor state - self.editor.note = new_note - self.editor.loadNote( - focusTo=min(self.editor.last_field_index or 0, len(new_note.fields) - 1) - ) - gui_hooks.addcards_did_change_note_type( - self, old_note.note_type(), new_note.note_type() - ) + if notetype_id: + self.editor.set_nid(None, mid=notetype_id, focus_to=0) def _load_new_note(self, sticky_fields_from: Note | None = None) -> None: - note = self._new_note() - if old_note := sticky_fields_from: - flds = note.note_type()["flds"] - # copy fields from old note - if old_note: - for n in range(min(len(note.fields), len(old_note.fields))): - if flds[n]["sticky"]: - note.fields[n] = old_note.fields[n] - # and tags - note.tags = old_note.tags - self.setAndFocusNote(note) + self.editor.set_nid( + None, mid=self.notetype_chooser.selected_notetype_id, focus_to=0 + ) def on_operation_did_execute( self, changes: OpChanges, handler: object | None @@ -279,12 +229,7 @@ class AddCards(QMainWindow): aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) def add_current_note(self) -> None: - if self.editor.current_notetype_is_image_occlusion(): - self.editor.update_occlusions_field() - self.editor.call_after_note_saved(self._add_current_note) - self.editor.reset_image_occlusion() - else: - self.editor.call_after_note_saved(self._add_current_note) + self.editor.web.eval(f"addCurrentNote({self.deck_chooser.selected_deck_id})") def _add_current_note(self) -> None: note = self.editor.note diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 76e959a04..582271b62 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -28,6 +28,7 @@ import aqt import aqt.forms import aqt.operations import aqt.sound +from anki._legacy import deprecated from anki.cards import Card from anki.collection import Config from anki.consts import MODEL_CLOZE @@ -37,7 +38,6 @@ from anki.models import NotetypeDict, NotetypeId, StockNotetype from anki.notes import Note, NoteId from anki.utils import checksum, is_mac, is_win, namedtmp from aqt import AnkiQt, gui_hooks -from aqt.operations.note import update_note from aqt.operations.notetype import update_notetype_legacy from aqt.qt import * from aqt.sound import av_player @@ -129,7 +129,7 @@ class Editor: self.mw = mw self.widget = widget self.parentWindow = parentWindow - self.note: Note | None = None + self.nid: NoteId | None = None # legacy argument provided? if addMode is not None: editor_mode = EditorMode.ADD_CARDS if addMode else EditorMode.EDIT_CURRENT @@ -380,10 +380,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too ###################################################################### def onBridgeCmd(self, cmd: str) -> Any: - if not self.note: - # shutdown - return - # focus lost or key/button pressed? if cmd.startswith("blur") or cmd.startswith("key"): (type, ord_str) = cmd.split(":", 1) @@ -475,6 +471,18 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too # Setting/unsetting the current note ###################################################################### + def set_nid( + self, + nid: NoteId | None, + mid: int, + focus_to: int | None = None, + ) -> None: + "Make note with ID `nid` the current note." + self.nid = nid + self.currentField = None + self.load_note(mid, focus_to=focus_to) + + @deprecated(replaced_by=set_nid) def set_note( self, note: Note | None, @@ -482,10 +490,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too focusTo: int | None = None, ) -> None: "Make NOTE the current note." - self.note = note self.currentField = None - if self.note: - self.loadNote(focusTo=focusTo) + if note: + self.nid = note.id + self.load_note(mid=note.mid, focus_to=focusTo) elif hide: self.widget.hide() @@ -493,43 +501,35 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self.loadNote(self.currentField) @on_editor_ready - def loadNote(self, focusTo: int | None = None) -> None: - if not self.note: - return + def load_note(self, mid: int, focus_to: int | None = None) -> None: self.widget.show() - # note_fields_status = self.note.fields_check() def oncallback(arg: Any) -> None: - if not self.note: + if not self.nid: return # we currently do this synchronously to ensure we load before the # sidebar on browser startup - if focusTo is not None: + if focus_to is not None: self.web.setFocus() gui_hooks.editor_did_load_note(self) assert self.mw.pm.profile is not None - js = f"loadNote({self.note.id}, {self.note.mid}, {json.dumps(focusTo)}, {json.dumps(self.orig_note_id)});" + js = f"loadNote({json.dumps(self.nid)}, {mid}, {json.dumps(focus_to)}, {json.dumps(self.orig_note_id)});" js = gui_hooks.editor_will_load_note(js, self.note, self) self.web.evalWithCallback( f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback ) - def _save_current_note(self) -> None: - "Call after note is updated with data from webview." - if not self.note: - return - - update_note(parent=self.widget, note=self.note).run_in_background( - initiator=self - ) + @deprecated(replaced_by=load_note) + def loadNote(self, focusTo: int | None = None) -> None: + self.load_note(self.note.mid, focus_to=focusTo) def call_after_note_saved( self, callback: Callable, keepFocus: bool = False ) -> None: "Save unsaved edits then call callback()." - if not self.note: + if not self.nid: # calling code may not expect the callback to fire immediately self.mw.progress.single_shot(10, callback) return @@ -1007,6 +1007,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too addImageForOcclusionFromClipboard=Editor.select_image_from_clipboard_and_occlude, ) + @property + def note(self) -> Note | None: + if self.nid is None: + return None + return self.mw.col.get_note(self.nid) + def note_type(self) -> NotetypeDict: assert self.note is not None note_type = self.note.note_type() diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 45eac407c..b263cd1b3 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -704,6 +704,7 @@ exposed_backend_list = [ "get_note", "new_note", "note_fields_check", + "add_note", # NotetypesService "get_notetype", "get_notetype_names", diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 08fa4c2d5..d7b2e214f 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -365,6 +365,57 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setClozeHint(hint); } + async function loadNewNote() { + await loadNote(0n, notetypeMeta.id, 0, null); + } + + async function noteCanBeAdded(): Promise { + let problem: string | null = null; + const result = await noteFieldsCheck(note!); + if(result.state === NoteFieldsCheckResponse_State.EMPTY) { + if(isImageOcclusion) { + problem = tr.notetypesNoOcclusionCreated2(); + } else { + problem = tr.addingTheFirstFieldIsEmpty(); + } + } + if(result.state === NoteFieldsCheckResponse_State.MISSING_CLOZE) { + // TODO: askUser(tr.addingYouHaveAClozeDeletionNote()) + return false; + } + if(result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) { + problem = tr.addingClozeOutsideClozeNotetype(); + } + if(result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) { + problem = tr.addingClozeOutsideClozeField(); + } + return problem ? false : true; + } + + async function addCurrentNoteInner(deckId: bigint) { + if(!await noteCanBeAdded()) { + return; + } + await addNote({ + note: note!, + deckId, + }); + await loadNewNote(); + } + + export async function addCurrentNote(deckId: bigint) { + if(mode !== "add") { + return; + } + if(isImageOcclusion) { + saveOcclusions(); + await addCurrentNoteInner(deckId); + resetIOImageLoaded(); + } else { + await addCurrentNoteInner(deckId); + } + } + export function focusIfField(x: number, y: number): boolean { const elements = document.elementsFromPoint(x, y); const first = elements[0].closest(".field-container"); @@ -465,6 +516,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html editorUpdateNote, decodeIriPaths, noteFieldsCheck, + addNote, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile"; @@ -605,11 +657,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } async function loadNote( - nid: bigint, + nid: bigint | null, notetypeId: bigint, focusTo: number, originalNoteId: bigint | null, - ) { + ): Promise { const notetype = await getNotetype({ ntid: notetypeId, }); @@ -627,7 +679,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } else { setNote( await getNote({ - nid, + nid!, }), ); } @@ -679,7 +731,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html html: imageField, mode: { kind: "edit", - noteId: nid, + noteId: nid!, }, }); } else if (originalNoteId) { @@ -694,6 +746,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } await updateDuplicateDisplay(); triggerChanges(); + + return note!.id; } $: signalEditorState(editorState); @@ -749,6 +803,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html resetIOImageLoaded, saveOcclusions, setSticky, + addCurrentNote, ...oldEditorAdapter, }); From 07d3b6d2b581c91d99d6e22c17c84b92e89685e9 Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 2 Jun 2025 06:14:15 +0300 Subject: [PATCH 032/133] Fix lint errors --- qt/aqt/editor.py | 17 ++++++++++++----- ts/routes/editor/NoteEditor.svelte | 18 +++++++++--------- .../[...imagePathOrNoteId]/+page.ts | 4 ++-- .../add-or-update-note.svelte.ts | 4 ++-- ts/routes/image-occlusion/index.ts | 4 ++-- ts/routes/image-occlusion/mask-editor.ts | 2 +- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 582271b62..ea18263c4 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -387,20 +387,24 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too if type == "blur": self.currentField = None # run any filters - if gui_hooks.editor_did_unfocus_field(False, self.note, ord): + if self.note and gui_hooks.editor_did_unfocus_field( + False, self.note, ord + ): # something updated the note; update it after a subsequent focus # event has had time to fire self.mw.progress.timer( 100, self.loadNoteKeepingFocus, False, parent=self.widget ) else: - gui_hooks.editor_did_fire_typing_timer(self.note) + if self.note: + gui_hooks.editor_did_fire_typing_timer(self.note) # focused into field? elif cmd.startswith("focus"): (type, num) = cmd.split(":", 1) self.last_field_index = self.currentField = int(num) - gui_hooks.editor_did_focus_field(self.note, self.currentField) + if self.note: + gui_hooks.editor_did_focus_field(self.note, self.currentField) elif cmd.startswith("toggleStickyAll"): model = self.note_type() @@ -436,7 +440,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too return new_state elif cmd.startswith("saveTags"): - gui_hooks.editor_did_update_tags(self.note) + if self.note: + gui_hooks.editor_did_update_tags(self.note) elif cmd.startswith("editorState"): (_, new_state_id, old_state_id) = cmd.split(":", 2) @@ -516,13 +521,15 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too assert self.mw.pm.profile is not None js = f"loadNote({json.dumps(self.nid)}, {mid}, {json.dumps(focus_to)}, {json.dumps(self.orig_note_id)});" - js = gui_hooks.editor_will_load_note(js, self.note, self) + if self.note: + js = gui_hooks.editor_will_load_note(js, self.note, self) self.web.evalWithCallback( f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback ) @deprecated(replaced_by=load_note) def loadNote(self, focusTo: int | None = None) -> None: + assert self.note is not None self.load_note(self.note.mid, focus_to=focusTo) def call_after_note_saved( diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index d7b2e214f..104258d2a 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -372,28 +372,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html async function noteCanBeAdded(): Promise { let problem: string | null = null; const result = await noteFieldsCheck(note!); - if(result.state === NoteFieldsCheckResponse_State.EMPTY) { - if(isImageOcclusion) { + if (result.state === NoteFieldsCheckResponse_State.EMPTY) { + if (isImageOcclusion) { problem = tr.notetypesNoOcclusionCreated2(); } else { problem = tr.addingTheFirstFieldIsEmpty(); } } - if(result.state === NoteFieldsCheckResponse_State.MISSING_CLOZE) { + if (result.state === NoteFieldsCheckResponse_State.MISSING_CLOZE) { // TODO: askUser(tr.addingYouHaveAClozeDeletionNote()) return false; } - if(result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) { + if (result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) { problem = tr.addingClozeOutsideClozeNotetype(); } - if(result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) { + if (result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) { problem = tr.addingClozeOutsideClozeField(); } return problem ? false : true; } async function addCurrentNoteInner(deckId: bigint) { - if(!await noteCanBeAdded()) { + if (!(await noteCanBeAdded())) { return; } await addNote({ @@ -404,10 +404,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } export async function addCurrentNote(deckId: bigint) { - if(mode !== "add") { + if (mode !== "add") { return; } - if(isImageOcclusion) { + if (isImageOcclusion) { saveOcclusions(); await addCurrentNoteInner(deckId); resetIOImageLoaded(); @@ -679,7 +679,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } else { setNote( await getNote({ - nid!, + nid: nid!, }), ); } diff --git a/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts b/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts index 6f83cc44c..beb0fde55 100644 --- a/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts +++ b/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts @@ -15,9 +15,9 @@ async function save(): Promise { export const load = (async ({ params }) => { let mode: IOMode; if (/^\d+/.test(params.imagePathOrNoteId)) { - mode = { kind: "edit", noteId: Number(params.imagePathOrNoteId) }; + mode = { kind: "edit", noteId: BigInt(params.imagePathOrNoteId) }; } else { - mode = { kind: "add", imagePath: params.imagePathOrNoteId, notetypeId: 0 }; + mode = { kind: "add", imagePath: params.imagePathOrNoteId, notetypeId: 0n }; } // for adding note from mobile devices diff --git a/ts/routes/image-occlusion/add-or-update-note.svelte.ts b/ts/routes/image-occlusion/add-or-update-note.svelte.ts index ce31eaaaf..98955f7a5 100644 --- a/ts/routes/image-occlusion/add-or-update-note.svelte.ts +++ b/ts/routes/image-occlusion/add-or-update-note.svelte.ts @@ -31,7 +31,7 @@ export const addOrUpdateNote = async function( if (mode.kind == "edit") { const result = await updateImageOcclusionNote({ - noteId: BigInt(mode.noteId), + noteId: mode.noteId, occlusions: occlusionCloze, header, backExtra, @@ -53,7 +53,7 @@ export const addOrUpdateNote = async function( }; // show toast message -const showResult = (noteId: number | null, result: OpChanges, count: number) => { +const showResult = (noteId: bigint | null, result: OpChanges, count: number) => { const props = $state({ message: "", type: "error" as "error" | "success", diff --git a/ts/routes/image-occlusion/index.ts b/ts/routes/image-occlusion/index.ts index 59720b1e0..84bf8cd87 100644 --- a/ts/routes/image-occlusion/index.ts +++ b/ts/routes/image-occlusion/index.ts @@ -50,10 +50,10 @@ export async function setupImageOcclusion(mode: IOMode, target = document.body): if (window.location.hash.startsWith("#test-")) { const imagePath = window.location.hash.replace("#test-", ""); - setupImageOcclusion({ kind: "add", imagePath, notetypeId: 0 }); + setupImageOcclusion({ kind: "add", imagePath, notetypeId: 0n }); } if (window.location.hash.startsWith("#testforedit-")) { - const noteId = parseInt(window.location.hash.replace("#testforedit-", "")); + const noteId = BigInt(window.location.hash.replace("#testforedit-", "")); setupImageOcclusion({ kind: "edit", noteId }); } diff --git a/ts/routes/image-occlusion/mask-editor.ts b/ts/routes/image-occlusion/mask-editor.ts index c5492d413..4602f5b19 100644 --- a/ts/routes/image-occlusion/mask-editor.ts +++ b/ts/routes/image-occlusion/mask-editor.ts @@ -44,7 +44,7 @@ export const setupMaskEditor = async ( }; export const setupMaskEditorForEdit = async ( - noteId: number, + noteId: bigint, onImageLoaded: (event: ImageLoadedEvent) => void, ): Promise => { const clozeNoteResponse = await getImageOcclusionNote({ noteId: BigInt(noteId) }); From 545d3dbfeda298b1822950ee5d9fc6f0b7df430a Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 5 Jun 2025 04:09:10 +0300 Subject: [PATCH 033/133] Move IO/cloze button toggles --- qt/aqt/editor.py | 22 ---------------------- ts/lib/sveltelib/dynamic-slotting.ts | 9 +++++++++ ts/routes/editor/NoteEditor.svelte | 14 +++++++++++++- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index ea18263c4..c0f6f1a61 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -1310,25 +1310,3 @@ class EditorWebView(AnkiWebView): clipboard = self.editor.mw.app.clipboard() assert clipboard is not None return clipboard - - -def set_cloze_button(editor: Editor) -> None: - action = "show" if editor.note_type()["type"] == MODEL_CLOZE else "hide" - editor.web.eval( - 'require("anki/ui").loaded.then(() =>' - f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")' - "); " - ) - - -def set_image_occlusion_button(editor: Editor) -> None: - action = "show" if editor.current_notetype_is_image_occlusion() else "hide" - editor.web.eval( - 'require("anki/ui").loaded.then(() =>' - f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("image-occlusion-button")' - "); " - ) - - -gui_hooks.editor_did_load_note.append(set_cloze_button) -gui_hooks.editor_did_load_note.append(set_image_occlusion_button) diff --git a/ts/lib/sveltelib/dynamic-slotting.ts b/ts/lib/sveltelib/dynamic-slotting.ts index db0bb12f9..8cc3b3293 100644 --- a/ts/lib/sveltelib/dynamic-slotting.ts +++ b/ts/lib/sveltelib/dynamic-slotting.ts @@ -240,6 +240,7 @@ export interface DefaultSlotInterface extends Record { show(position: Identifier): Promise; hide(position: Identifier): Promise; toggle(position: Identifier): Promise; + setShown(position: Identifier, shown: boolean): Promise; } export function defaultInterface({ @@ -287,12 +288,20 @@ export function defaultInterface({ }, id); } + function setShown(id: Identifier, shown: boolean): Promise { + return updateProps((props: T): T => { + props.detach.set(!shown); + return props; + }, id); + } + return { insert, append, show, hide, toggle, + setShown, }; } diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 104258d2a..00a0a13a2 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -503,7 +503,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { ImageOcclusionFieldIndexes } from "@generated/anki/image_occlusion_pb"; - import { StockNotetype_OriginalStockKind } from "@generated/anki/notetypes_pb"; + import { + Notetype_Config_Kind, + StockNotetype_OriginalStockKind, + } from "@generated/anki/notetypes_pb"; import type { Notetype } from "@generated/anki/notetypes_pb"; import { getFieldNames, @@ -715,6 +718,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ); focusField(focusTo); toolbar.inlineButtons?.setColorButtons([lastTextColor, lastHighlightColor]); + await toolbar.toolbar?.setShown( + "image-occlusion-button", + notetype.config?.originalStockKind === + StockNotetype_OriginalStockKind.IMAGE_OCCLUSION, + ); + await toolbar.toolbar?.setShown( + "cloze", + notetype.config?.kind === Notetype_Config_Kind.CLOZE, + ); setTags(tags); setTagsCollapsed(await getMeta(tagsCollapsedMetaKey)); setMathjaxEnabled((await getColConfig("renderMathjax")) ?? true); From 99396e5811685abdee68dfea8c265d6aa407da37 Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 9 Jun 2025 18:04:57 +0300 Subject: [PATCH 034/133] Start work on copy & paste handling --- proto/anki/frontend.proto | 17 + qt/aqt/editor.py | 52 +-- qt/aqt/mediasrv.py | 35 +- qt/aqt/utils.py | 46 +++ ts/lib/html-filter/element.ts | 8 +- ts/lib/html-filter/index.ts | 5 + ts/routes/editor/NoteEditor.svelte | 7 + .../editor/rich-text-input/data-transfer.ts | 346 ++++++++++++++++++ .../rich-text-input/rich-text-resolve.ts | 22 +- 9 files changed, 496 insertions(+), 42 deletions(-) create mode 100644 ts/routes/editor/rich-text-input/data-transfer.ts diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 94c96351f..1036e6b26 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -31,6 +31,9 @@ service FrontendService { // Editor rpc editorUpdateNote(notes.UpdateNotesRequest) returns (generic.Empty); + rpc convertPastedImage(ConvertPastedImageRequest) + returns (ConvertPastedImageResponse); + rpc retrieveUrl(generic.String) returns (RetrieveUrlResponse); // Profile config rpc GetProfileConfigJson(generic.String) returns (generic.Json); @@ -53,6 +56,20 @@ message SetSchedulingStatesRequest { scheduler.SchedulingStates states = 2; } +message ConvertPastedImageRequest { + bytes data = 1; + string ext = 2; +} + +message ConvertPastedImageResponse { + bytes data = 1; +} + +message RetrieveUrlResponse { + string filename = 1; + string error = 2; +} + message SetSettingJsonRequest { string key = 1; bytes value_json = 2; diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index c0f6f1a61..6b3cbc3bd 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -31,7 +31,6 @@ import aqt.sound from anki._legacy import deprecated from anki.cards import Card from anki.collection import Config -from anki.consts import MODEL_CLOZE from anki.hooks import runFilter from anki.httpclient import HttpClient from anki.models import NotetypeDict, NotetypeId, StockNotetype @@ -173,6 +172,7 @@ class Editor: def setupWeb(self) -> None: editor_key = self.mw.pm.editor_key(self.editorMode) self.web.load_sveltekit_page(f"editor/?mode={editor_key}") + self.web.allow_drops = True def _set_ready(self) -> None: lefttopbtns: list[str] = [] @@ -1124,37 +1124,37 @@ class EditorWebView(AnkiWebView): def onMiddleClickPaste(self) -> None: self._onPaste(QClipboard.Mode.Selection) - def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None: - assert evt is not None - evt.accept() + # def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None: + # assert evt is not None + # evt.accept() - def dropEvent(self, evt: QDropEvent | None) -> None: - assert evt is not None - extended = self._wantsExtendedPaste() - mime = evt.mimeData() - assert mime is not None + # def dropEvent(self, evt: QDropEvent | None) -> None: + # assert evt is not None + # extended = self._wantsExtendedPaste() + # mime = evt.mimeData() + # assert mime is not None - if ( - self.editor.state is EditorState.IO_PICKER - and (html := self._processUrls(mime, allowed_suffixes=pics)) - and (path := self.editor.extract_img_path_from_html(html)) - ): - self.editor.setup_mask_editor(path) - return + # if ( + # self.editor.state is EditorState.IO_PICKER + # and (html := self._processUrls(mime, allowed_suffixes=pics)) + # and (path := self.editor.extract_img_path_from_html(html)) + # ): + # self.editor.setup_mask_editor(path) + # return - evt_pos = evt.position() - cursor_pos = QPoint(int(evt_pos.x()), int(evt_pos.y())) + # evt_pos = evt.position() + # cursor_pos = QPoint(int(evt_pos.x()), int(evt_pos.y())) - if evt.source() and mime.hasHtml(): - # don't filter html from other fields - html, internal = mime.html(), True - else: - html, internal = self._processMime(mime, extended, drop_event=True) + # if evt.source() and mime.hasHtml(): + # # don't filter html from other fields + # html, internal = mime.html(), True + # else: + # html, internal = self._processMime(mime, extended, drop_event=True) - if not html: - return + # if not html: + # return - self.editor.doDrop(html, internal, extended, cursor_pos) + # self.editor.doDrop(html, internal, extended, cursor_pos) # returns (html, isInternal) def _processMime( diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index b263cd1b3..c32e55685 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -16,7 +16,7 @@ from collections.abc import Callable from dataclasses import dataclass from errno import EPROTOTYPE from http import HTTPStatus -from typing import Any +from typing import Any, cast import flask import flask_cors @@ -665,6 +665,34 @@ def set_config_json() -> bytes: return set_setting_json(aqt.mw.col.set_config) +def convert_pasted_image() -> bytes: + req = frontend_pb2.ConvertPastedImageRequest() + req.ParseFromString(request.data) + image = QImage.fromData(req.data) + buffer = QBuffer() + buffer.open(QBuffer.OpenModeFlag.ReadWrite) + if req.ext == "png": + quality = 50 + else: + quality = 80 + image.save(buffer, req.ext, quality) + buffer.reset() + data = bytes(cast(bytes, buffer.readAll())) + return frontend_pb2.ConvertPastedImageResponse(data=data).SerializeToString() + + +def retrieve_url() -> bytes: + from aqt.utils import retrieve_url + + req = generic_pb2.String() + req.ParseFromString(request.data) + url = req.val + filename, error = retrieve_url(url) + return frontend_pb2.RetrieveUrlResponse( + filename=filename, error=error + ).SerializeToString() + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -686,6 +714,8 @@ post_handler_list = [ get_meta_json, set_meta_json, get_config_json, + convert_pasted_image, + retrieve_url, ] @@ -739,6 +769,9 @@ exposed_backend_list = [ "decode_iri_paths", # ConfigService "set_config_json", + "get_config_bool", + # MediaService + "add_media_file", ] diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 6ae8bace8..44e2557c5 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -9,16 +9,19 @@ import re import shutil import subprocess import sys +import urllib from collections.abc import Callable, Sequence from functools import partial, wraps from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Union +import requests from send2trash import send2trash import aqt from anki._legacy import DeprecatedNamesMixinForModule from anki.collection import Collection, HelpPage +from anki.httpclient import HttpClient from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import from anki.utils import ( call, @@ -134,6 +137,49 @@ def openLink(link: str | QUrl) -> None: QDesktopServices.openUrl(QUrl(link)) +def retrieve_url(url: str) -> tuple[str, str]: + "Download file into media folder and return local filename or None." + + local = url.lower().startswith("file://") + content_type = None + error_msg: str | None = None + try: + if local: + # urllib doesn't understand percent-escaped utf8, but requires things like + # '#' to be escaped. + url = urllib.parse.unquote(url) + url = url.replace("%", "%25") + url = url.replace("#", "%23") + req = urllib.request.Request( + url, None, {"User-Agent": "Mozilla/5.0 (compatible; Anki)"} + ) + with urllib.request.urlopen(req) as response: + filecontents = response.read() + else: + with HttpClient() as client: + client.timeout = 30 + with client.get(url) as response: + if response.status_code != 200: + error_msg = tr.qt_misc_unexpected_response_code( + val=response.status_code, + ) + return "", error_msg + filecontents = response.content + content_type = response.headers.get("content-type") + except (urllib.error.URLError, requests.exceptions.RequestException) as e: + error_msg = tr.editing_an_error_occurred_while_opening(val=str(e)) + return "", error_msg + # strip off any query string + url = re.sub(r"\?.*?$", "", url) + fname = os.path.basename(urllib.parse.unquote(url)) + if not fname.strip(): + fname = "paste" + if content_type: + fname = aqt.mw.col.media.add_extension_based_on_mime(fname, content_type) + + return aqt.mw.col.media.write_data(fname, filecontents), "" + + class MessageBox(QMessageBox): def __init__( self, diff --git a/ts/lib/html-filter/element.ts b/ts/lib/html-filter/element.ts index b3ee90753..391fd22b8 100644 --- a/ts/lib/html-filter/element.ts +++ b/ts/lib/html-filter/element.ts @@ -34,6 +34,12 @@ const allow = (attrs: string[]): FilterMethod => (element: Element): void => element, ); +function convertToDiv(element: Element): void { + const div = document.createElement("div"); + div.innerHTML = element.innerHTML; + element.replaceWith(div); +} + function unwrapElement(element: Element): void { element.replaceWith(...element.childNodes); } @@ -50,7 +56,7 @@ const tagsAllowedBasic: TagsAllowed = { BR: allowNone, IMG: allow(["SRC", "ALT"]), DIV: allowNone, - P: allowNone, + P: convertToDiv, SUB: allowNone, SUP: allowNone, TITLE: removeElement, diff --git a/ts/lib/html-filter/index.ts b/ts/lib/html-filter/index.ts index fee6e607d..83812af0b 100644 --- a/ts/lib/html-filter/index.ts +++ b/ts/lib/html-filter/index.ts @@ -33,6 +33,11 @@ const outputHTMLProcessors: Record string> = }; export function filterHTML(html: string, internal: boolean, extended: boolean): string { + // https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx + if (html.indexOf(">") < 0) { + return html; + } + const template = document.createElement("template"); template.innerHTML = html; diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 00a0a13a2..309642dce 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -861,6 +861,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } + + + -
+ diff --git a/ts/lib/context-menu/Item.svelte b/ts/lib/context-menu/Item.svelte new file mode 100644 index 000000000..333cb3453 --- /dev/null +++ b/ts/lib/context-menu/Item.svelte @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/ts/lib/context-menu/index.ts b/ts/lib/context-menu/index.ts new file mode 100644 index 000000000..b6b85d6ce --- /dev/null +++ b/ts/lib/context-menu/index.ts @@ -0,0 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +export { default as ContextMenu } from "./ContextMenu.svelte"; +export { default as Item } from "./Item.svelte"; +export type { ContextMenuAPI, ContextMenuMouseEvent } from "./types"; diff --git a/ts/lib/context-menu/types.ts b/ts/lib/context-menu/types.ts new file mode 100644 index 000000000..d48405874 --- /dev/null +++ b/ts/lib/context-menu/types.ts @@ -0,0 +1,12 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +export interface ContextMenuMouseEvent { + clientX: number; + clientY: number; + preventDefault(): void; +} + +export interface ContextMenuAPI { + show(event: ContextMenuMouseEvent): void; +} diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 34fae2c46..14ae874ca 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -14,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import LabelContainer from "./LabelContainer.svelte"; import LabelName from "./LabelName.svelte"; import { EditorState, type EditorMode } from "./types"; - import ContextMenu, { Item } from "svelte-contextmenu"; + import { ContextMenu, Item } from "$lib/context-menu"; export interface NoteEditorAPI { fields: EditorFieldAPI[]; @@ -1226,7 +1226,7 @@ components and functionality for general note editing. {#each contextMenuItems as item} { + click={() => { item.action(); contextMenuInput?.focus(); }} diff --git a/ts/routes/editor/context-menu.svelte.ts b/ts/routes/editor/context-menu.svelte.ts index 8c2c17c8c..51cd7e35a 100644 --- a/ts/routes/editor/context-menu.svelte.ts +++ b/ts/routes/editor/context-menu.svelte.ts @@ -1,12 +1,12 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { ContextMenu, ContextMenuMouseEvent } from "$lib/context-menu"; + import { openMedia, showInMediaFolder } from "@generated/backend"; import * as tr from "@generated/ftl"; import { bridgeCommand } from "@tslib/bridgecommand"; import { getSelection } from "@tslib/cross-browser"; -import type ContextMenu from "svelte-contextmenu"; -import type { ContextMenuMouseEvent } from "svelte-contextmenu/ContextMenuMouseEvent"; import { get } from "svelte/store"; import type { EditingInputAPI } from "./EditingArea.svelte"; import type { NoteEditorAPI } from "./NoteEditor.svelte"; @@ -114,8 +114,6 @@ export function setupContextMenu(): [ if (contextMenuItems.length > 0) { contextMenu?.show(event); } - - event.preventDefault(); } return [onContextMenu, contextMenuItems]; diff --git a/yarn.lock b/yarn.lock index b19f7d2d0..d898d28c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1761,7 +1761,6 @@ __metadata: sass: "npm:<1.77" svelte: "npm:^5.34.9" svelte-check: "npm:^4.2.2" - svelte-contextmenu: "npm:^1.0.2" svelte-preprocess: "npm:^6.0.3" svelte-preprocess-esbuild: "npm:^3.0.1" svgo: "npm:^3.2.0" @@ -6229,13 +6228,6 @@ __metadata: languageName: node linkType: hard -"svelte-contextmenu@npm:^1.0.2": - version: 1.0.2 - resolution: "svelte-contextmenu@npm:1.0.2" - checksum: 10c0/33cf79540337862278927f4b732b5d97f2c029348666ebcd3105b5c08bed54498a1d66f61b0212c18c204942cf54c9f6ce5ed509981d9f19f99f9334d8961cdf - languageName: node - linkType: hard - "svelte-eslint-parser@npm:^0.43.0": version: 0.43.0 resolution: "svelte-eslint-parser@npm:0.43.0" From a8a2e4ee32613de3b646a9bf73fee9d7d17e7b93 Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 9 Jul 2025 05:32:46 +0300 Subject: [PATCH 071/133] Implement close button --- proto/anki/frontend.proto | 1 + qt/aqt/addcards.py | 2 +- qt/aqt/editcurrent.py | 6 ----- qt/aqt/forms/editcurrent.ui | 28 ----------------------- qt/aqt/mediasrv.py | 14 ++++++++++++ ts/routes/editor/ActionButtons.svelte | 23 +++++++++++++++++++ ts/routes/editor/CloseButton.svelte | 32 +++++++++++++++++++++++++++ ts/routes/editor/NoteEditor.svelte | 16 ++++++++++++++ 8 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 ts/routes/editor/ActionButtons.svelte create mode 100644 ts/routes/editor/CloseButton.svelte diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index f7557cd23..021322383 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -42,6 +42,7 @@ service FrontendService { rpc ShowInMediaFolder(generic.String) returns (generic.Empty); rpc RecordAudio(generic.Empty) returns (generic.String); rpc CloseAddCards(generic.Bool) returns (generic.Empty); + rpc CloseEditCurrent(generic.Empty) returns (generic.Empty); // Profile config rpc GetProfileConfigJson(generic.String) returns (generic.Json); diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 133bfb42b..3b0421b30 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -34,7 +34,7 @@ class AddCards(QMainWindow): def __init__(self, mw: AnkiQt) -> None: super().__init__(None, Qt.WindowType.Window) self._close_event_has_cleaned_up = False - self._close_callback: Callable[[], None] | None = None + self._close_callback: Callable[[], None] = self._close self.mw = mw self.col = mw.col form = aqt.forms.addcards.Ui_Dialog() diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index d8007c75c..4cd8dcb0c 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -32,13 +32,7 @@ class EditCurrent(QMainWindow): self.editor.card = self.mw.reviewer.card self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0) restoreGeom(self, "editcurrent") - close_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close) - assert close_button is not None - close_button.setShortcut(QKeySequence("Ctrl+Return")) add_close_shortcut(self) - # qt5.14+ doesn't handle numpad enter on Windows - self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self) - qconnect(self.compat_add_shorcut.activated, close_button.click) gui_hooks.operation_did_execute.append(self.on_operation_did_execute) self.show() diff --git a/qt/aqt/forms/editcurrent.ui b/qt/aqt/forms/editcurrent.ui index 14ecd21fa..d2102e306 100644 --- a/qt/aqt/forms/editcurrent.ui +++ b/qt/aqt/forms/editcurrent.ui @@ -28,16 +28,6 @@ - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - @@ -60,22 +50,4 @@ - - - buttonBox - rejected() - Dialog - close() - - - 316 - 260 - - - 286 - 274 - - - - diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 35ae517e0..1bc7cce8b 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -818,6 +818,18 @@ def close_add_cards() -> bytes: return b"" +def close_edit_current() -> bytes: + def handle_on_main() -> None: + from aqt.editcurrent import EditCurrent + + window = aqt.mw.app.activeWindow() + if isinstance(window, EditCurrent): + window.close() + + aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main)) + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -850,6 +862,7 @@ post_handler_list = [ read_clipboard, write_clipboard, close_add_cards, + close_edit_current, ] @@ -1048,3 +1061,4 @@ def _extract_dynamic_get_request(path: str) -> DynamicRequest | None: return legacy_page_data else: return None + return None diff --git a/ts/routes/editor/ActionButtons.svelte b/ts/routes/editor/ActionButtons.svelte new file mode 100644 index 000000000..9dfa8cbe9 --- /dev/null +++ b/ts/routes/editor/ActionButtons.svelte @@ -0,0 +1,23 @@ + + + +
+ {#if mode === "add" || mode === "current"} + + {/if} +
+ + diff --git a/ts/routes/editor/CloseButton.svelte b/ts/routes/editor/CloseButton.svelte new file mode 100644 index 000000000..824f1adcf --- /dev/null +++ b/ts/routes/editor/CloseButton.svelte @@ -0,0 +1,32 @@ + + + + +
{tr.actionsClose()}
+
+ + + diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 14ae874ca..0eb4de744 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -397,6 +397,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html await closeAddCardsBackend({ val: await shouldPromptBeforeClosing() }); } + async function closeEditCurrent() { + saveNow(); + await closeEditCurrentBackend({}); + } + + async function onClose() { + if (mode === "add") { + await closeAddCards(); + } else if (mode == "current") { + await closeEditCurrent(); + } + } + export function saveOnPageHide() { if (document.visibilityState === "hidden") { // will fire on session close and minimize @@ -588,6 +601,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html addMediaFromPath, updateEditorNotetype, closeAddCards as closeAddCardsBackend, + closeEditCurrent as closeEditCurrentBackend, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile"; @@ -611,6 +625,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { NoteFieldsCheckResponse_State, type Note } from "@generated/anki/notes_pb"; import { setupContextMenu } from "./context-menu.svelte"; import { registerShortcut } from "@tslib/shortcuts"; + import ActionButtons from "./ActionButtons.svelte"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded); @@ -1221,6 +1236,7 @@ components and functionality for general note editing. + {/if} From c2374dcc24fa23642e7f0b4d29890d6e3b0c47ac Mon Sep 17 00:00:00 2001 From: Abdo Date: Wed, 9 Jul 2025 06:26:58 +0300 Subject: [PATCH 072/133] Implement help button --- proto/anki/frontend.proto | 1 + qt/aqt/mediasrv.py | 13 ++++++++++- ts/lib/tslib/help-page.ts | 3 +++ ts/routes/editor/ActionButton.svelte | 32 +++++++++++++++++++++++++++ ts/routes/editor/ActionButtons.svelte | 4 ++++ ts/routes/editor/CloseButton.svelte | 24 +++++--------------- ts/routes/editor/HelpButton.svelte | 23 +++++++++++++++++++ 7 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 ts/routes/editor/ActionButton.svelte create mode 100644 ts/routes/editor/HelpButton.svelte diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 021322383..17090fe1e 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -43,6 +43,7 @@ service FrontendService { rpc RecordAudio(generic.Empty) returns (generic.String); rpc CloseAddCards(generic.Bool) returns (generic.Empty); rpc CloseEditCurrent(generic.Empty) returns (generic.Empty); + rpc OpenLink(generic.String) returns (generic.Empty); // Profile config rpc GetProfileConfigJson(generic.String) returns (generic.Json); diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 1bc7cce8b..f973394a0 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -40,7 +40,7 @@ from aqt.operations import on_op_finished from aqt.operations.deck import update_deck_configs as update_deck_configs_op from aqt.progress import ProgressUpdate from aqt.qt import * -from aqt.utils import aqt_data_path, show_warning, tr +from aqt.utils import aqt_data_path, openLink, show_warning, tr # https://forums.ankiweb.net/t/anki-crash-when-using-a-specific-deck/22266 waitress.wasyncore._DISCONNECTED = waitress.wasyncore._DISCONNECTED.union({EPROTOTYPE}) # type: ignore @@ -830,6 +830,14 @@ def close_edit_current() -> bytes: return b"" +def open_link() -> bytes: + req = generic_pb2.String() + req.ParseFromString(request.data) + url = req.val + aqt.mw.taskman.run_on_main(lambda: openLink(url)) + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -863,6 +871,7 @@ post_handler_list = [ write_clipboard, close_add_cards, close_edit_current, + open_link, ] @@ -1062,3 +1071,5 @@ def _extract_dynamic_get_request(path: str) -> DynamicRequest | None: else: return None return None + return None + return None diff --git a/ts/lib/tslib/help-page.ts b/ts/lib/tslib/help-page.ts index ff4e7e434..26363ba33 100644 --- a/ts/lib/tslib/help-page.ts +++ b/ts/lib/tslib/help-page.ts @@ -45,4 +45,7 @@ export const HelpPage = { updating: "https://docs.ankiweb.net/importing/text-files.html#duplicates-and-updating", html: "https://docs.ankiweb.net/importing/text-files.html#html", }, + Editing: { + adding: "https://docs.ankiweb.net/editing.html#adding-cards-and-notes", + }, }; diff --git a/ts/routes/editor/ActionButton.svelte b/ts/routes/editor/ActionButton.svelte new file mode 100644 index 000000000..1e7826132 --- /dev/null +++ b/ts/routes/editor/ActionButton.svelte @@ -0,0 +1,32 @@ + + + +
+ +
+ {@render children()} +
+
+
+ + diff --git a/ts/routes/editor/ActionButtons.svelte b/ts/routes/editor/ActionButtons.svelte index 9dfa8cbe9..dc7196862 100644 --- a/ts/routes/editor/ActionButtons.svelte +++ b/ts/routes/editor/ActionButtons.svelte @@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
+ {#if mode === "add"} + + {/if} {#if mode === "add" || mode === "current"} {/if} diff --git a/ts/routes/editor/CloseButton.svelte b/ts/routes/editor/CloseButton.svelte index 824f1adcf..51e14f43e 100644 --- a/ts/routes/editor/CloseButton.svelte +++ b/ts/routes/editor/CloseButton.svelte @@ -5,28 +5,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -
{tr.actionsClose()}
-
- - - + + {tr.actionsClose()} + + diff --git a/ts/routes/editor/HelpButton.svelte b/ts/routes/editor/HelpButton.svelte new file mode 100644 index 000000000..32053489f --- /dev/null +++ b/ts/routes/editor/HelpButton.svelte @@ -0,0 +1,23 @@ + + + + + {tr.actionsHelp()} + + From 8e9cc5c382bf95a19d8f85ec313efdab1f77be8c Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 10 Jul 2025 00:14:34 +0300 Subject: [PATCH 073/133] Implement add button --- ts/routes/editor/ActionButtons.svelte | 7 ++++++- ts/routes/editor/AddButton.svelte | 18 ++++++++++++++++++ ts/routes/editor/CloseButton.svelte | 8 ++++++-- ts/routes/editor/NoteEditor.svelte | 7 ++++++- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 ts/routes/editor/AddButton.svelte diff --git a/ts/routes/editor/ActionButtons.svelte b/ts/routes/editor/ActionButtons.svelte index dc7196862..30b273ff4 100644 --- a/ts/routes/editor/ActionButtons.svelte +++ b/ts/routes/editor/ActionButtons.svelte @@ -3,12 +3,14 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
@@ -16,7 +18,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} {#if mode === "add" || mode === "current"} - + + {/if} + {#if mode === "add"} + {/if}
diff --git a/ts/routes/editor/AddButton.svelte b/ts/routes/editor/AddButton.svelte new file mode 100644 index 000000000..145e40c75 --- /dev/null +++ b/ts/routes/editor/AddButton.svelte @@ -0,0 +1,18 @@ + + + + + {tr.actionsAdd()} + + diff --git a/ts/routes/editor/CloseButton.svelte b/ts/routes/editor/CloseButton.svelte index 51e14f43e..ab0038531 100644 --- a/ts/routes/editor/CloseButton.svelte +++ b/ts/routes/editor/CloseButton.svelte @@ -9,10 +9,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import ActionButton from "./ActionButton.svelte"; export let onClose: () => void; - const closeKeyCombination = "Control+Enter"; + export let enableShortcut: boolean; + + const closeKeyCombination = enableShortcut ? "Control+Enter" : ""; {tr.actionsClose()} - + {#if enableShortcut} + + {/if} diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 0eb4de744..a25bc6d9f 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -410,6 +410,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + async function onAdd() { + // TODO get selected deck + await addCurrentNote(1n); + } + export function saveOnPageHide() { if (document.visibilityState === "hidden") { // will fire on session close and minimize @@ -1236,7 +1241,7 @@ components and functionality for general note editing. - + {/if} From 6cf81965131cc81441fa59685ca21b55cf38d6c9 Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 10 Jul 2025 14:05:10 +0300 Subject: [PATCH 074/133] Remove unintended change in mediasrv.py --- qt/aqt/mediasrv.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index f973394a0..052dac291 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -1070,6 +1070,3 @@ def _extract_dynamic_get_request(path: str) -> DynamicRequest | None: return legacy_page_data else: return None - return None - return None - return None From 3a83958b296856ab19e048c9dfc3214269424b9a Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 10 Jul 2025 14:06:27 +0300 Subject: [PATCH 075/133] Remove old requirements.txt files --- python/requirements.bundle.txt | 494 ----------------------- python/requirements.dev.txt | 715 --------------------------------- 2 files changed, 1209 deletions(-) delete mode 100644 python/requirements.bundle.txt delete mode 100644 python/requirements.dev.txt diff --git a/python/requirements.bundle.txt b/python/requirements.bundle.txt deleted file mode 100644 index 8aab16467..000000000 --- a/python/requirements.bundle.txt +++ /dev/null @@ -1,494 +0,0 @@ -attrs==24.2.0 \ - --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ - --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 - # via jsonschema -beautifulsoup4==4.12.3 \ - --hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \ - --hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed - # via - # -r requirements.anki.in - # -r requirements.aqt.in -blinker==1.8.2 \ - --hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \ - --hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83 - # via flask -build==1.2.1 \ - --hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \ - --hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4 - # via pip-tools -certifi==2024.8.30 \ - --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ - --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 - # via requests -charset-normalizer==3.3.2 \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 - # via requests -click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via - # flask - # pip-tools -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via -r requirements.base.in -decorator==5.1.1 \ - --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ - --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 - # via -r requirements.anki.in -distro==1.9.0 ; sys_platform != "darwin" and sys_platform != "win32" \ - --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ - --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 - # via -r requirements.anki.in -flask[async]==3.0.3 \ - --hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \ - --hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842 - # via - # -r requirements.aqt.in - # flask-cors -flask-cors==6.0.0 \ - --hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \ - --hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657 - # via -r requirements.aqt.in -idna==3.8 \ - --hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \ - --hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603 - # via requests -importlib-metadata==8.4.0 \ - --hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \ - --hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5 - # via - # build - # flask - # markdown -itsdangerous==2.2.0 \ - --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ - --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 - # via flask -jinja2==3.1.5 \ - --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ - --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb - # via flask -jsonschema==4.1.2 \ - --hash=sha256:166870c8ab27bd712a8627e0598de4685bd8d199c4d7bd7cacc3d941ba0c6ca0 \ - --hash=sha256:5c1a282ee6b74235057421fd0f766ac5f2972f77440927f6471c9e8493632fac - # via - # -r requirements.aqt.in - # -r requirements.bundle.in -markdown==3.7 \ - --hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \ - --hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803 - # via -r requirements.anki.in -markupsafe==2.1.5 \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 - # via - # jinja2 - # werkzeug -orjson==3.10.7 \ - --hash=sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23 \ - --hash=sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9 \ - --hash=sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5 \ - --hash=sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad \ - --hash=sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98 \ - --hash=sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412 \ - --hash=sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1 \ - --hash=sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864 \ - --hash=sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6 \ - --hash=sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91 \ - --hash=sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac \ - --hash=sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c \ - --hash=sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1 \ - --hash=sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f \ - --hash=sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250 \ - --hash=sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09 \ - --hash=sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0 \ - --hash=sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225 \ - --hash=sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354 \ - --hash=sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f \ - --hash=sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e \ - --hash=sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469 \ - --hash=sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c \ - --hash=sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12 \ - --hash=sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3 \ - --hash=sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3 \ - --hash=sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149 \ - --hash=sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb \ - --hash=sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2 \ - --hash=sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2 \ - --hash=sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f \ - --hash=sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0 \ - --hash=sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a \ - --hash=sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58 \ - --hash=sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe \ - --hash=sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09 \ - --hash=sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e \ - --hash=sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2 \ - --hash=sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c \ - --hash=sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313 \ - --hash=sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6 \ - --hash=sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93 \ - --hash=sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7 \ - --hash=sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866 \ - --hash=sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c \ - --hash=sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b \ - --hash=sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5 \ - --hash=sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175 \ - --hash=sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9 \ - --hash=sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0 \ - --hash=sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff \ - --hash=sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20 \ - --hash=sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5 \ - --hash=sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960 \ - --hash=sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024 \ - --hash=sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd \ - --hash=sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84 - # via -r requirements.anki.in -packaging==24.1 \ - --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ - --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 - # via build -pip-system-certs==4.0 \ - --hash=sha256:47202b9403a6f40783a9674bbc8873f5fc86544ec01a49348fa913e99e2ff68b \ - --hash=sha256:db8e6a31388d9795ec9139957df1a89fa5274fb66164456fd091a5d3e94c350c - # via -r requirements.aqt.in -pip-tools==7.4.1 \ - --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ - --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 - # via -r requirements.base.in -protobuf==5.28.2 \ - --hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \ - --hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \ - --hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \ - --hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \ - --hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \ - --hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \ - --hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \ - --hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \ - --hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \ - --hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \ - --hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d - # via -r requirements.anki.in -pyproject-hooks==1.1.0 \ - --hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \ - --hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2 - # via - # build - # pip-tools -pyrsistent==0.20.0 \ - --hash=sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f \ - --hash=sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e \ - --hash=sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958 \ - --hash=sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34 \ - --hash=sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca \ - --hash=sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d \ - --hash=sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d \ - --hash=sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4 \ - --hash=sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714 \ - --hash=sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf \ - --hash=sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee \ - --hash=sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8 \ - --hash=sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224 \ - --hash=sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d \ - --hash=sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054 \ - --hash=sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656 \ - --hash=sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7 \ - --hash=sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423 \ - --hash=sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce \ - --hash=sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e \ - --hash=sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3 \ - --hash=sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0 \ - --hash=sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f \ - --hash=sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b \ - --hash=sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce \ - --hash=sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a \ - --hash=sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174 \ - --hash=sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86 \ - --hash=sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f \ - --hash=sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b \ - --hash=sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98 \ - --hash=sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022 - # via jsonschema -pysocks==1.7.1 \ - --hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \ - --hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \ - --hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 - # via requests -requests==2.32.3 \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via - # -r requirements.anki.in - # -r requirements.aqt.in -send2trash==1.8.3 \ - --hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \ - --hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf - # via -r requirements.aqt.in -soupsieve==2.6 \ - --hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \ - --hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9 - # via beautifulsoup4 -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via - # build - # pip-tools -typing-extensions==4.13.2 \ - --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ - --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef - # via -r requirements.anki.in -urllib3==2.2.2 \ - --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ - --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 - # via requests -waitress==3.0.1 \ - --hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \ - --hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3 - # via -r requirements.aqt.in -werkzeug==3.0.6 \ - --hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \ - --hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d - # via - # flask - # flask-cors -wheel==0.44.0 \ - --hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \ - --hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49 - # via pip-tools -wrapt==1.16.0 \ - --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \ - --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \ - --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \ - --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \ - --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \ - --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \ - --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \ - --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \ - --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \ - --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \ - --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \ - --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \ - --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \ - --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \ - --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \ - --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \ - --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \ - --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \ - --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \ - --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \ - --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \ - --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \ - --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \ - --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \ - --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \ - --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \ - --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \ - --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \ - --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \ - --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \ - --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \ - --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \ - --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \ - --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \ - --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \ - --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \ - --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \ - --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \ - --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \ - --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \ - --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \ - --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \ - --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \ - --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \ - --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \ - --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \ - --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \ - --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \ - --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \ - --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \ - --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \ - --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \ - --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \ - --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \ - --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \ - --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \ - --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \ - --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \ - --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \ - --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \ - --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \ - --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \ - --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \ - --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \ - --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \ - --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \ - --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \ - --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \ - --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \ - --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4 - # via pip-system-certs -zipp==3.20.1 \ - --hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \ - --hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -pip==24.2 \ - --hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \ - --hash=sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8 - # via pip-tools -setuptools==69.5.1 \ - --hash=sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987 \ - --hash=sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32 - # via - # -r requirements.bundle.in - # pip-tools diff --git a/python/requirements.dev.txt b/python/requirements.dev.txt deleted file mode 100644 index 293ee343d..000000000 --- a/python/requirements.dev.txt +++ /dev/null @@ -1,715 +0,0 @@ -astroid==3.2.4 \ - --hash=sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a \ - --hash=sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25 - # via pylint -atomicwrites==1.4.1 \ - --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 - # via -r requirements.dev.in -attrs==24.2.0 \ - --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ - --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 - # via jsonschema -beautifulsoup4==4.12.3 \ - --hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \ - --hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed - # via - # -r requirements.anki.in - # -r requirements.aqt.in -black==24.8.0 \ - --hash=sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6 \ - --hash=sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e \ - --hash=sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f \ - --hash=sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018 \ - --hash=sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e \ - --hash=sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd \ - --hash=sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4 \ - --hash=sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed \ - --hash=sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2 \ - --hash=sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42 \ - --hash=sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af \ - --hash=sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb \ - --hash=sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368 \ - --hash=sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb \ - --hash=sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af \ - --hash=sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed \ - --hash=sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47 \ - --hash=sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2 \ - --hash=sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a \ - --hash=sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c \ - --hash=sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920 \ - --hash=sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1 - # via -r requirements.dev.in -blinker==1.8.2 \ - --hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \ - --hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83 - # via flask -build==1.2.1 \ - --hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \ - --hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4 - # via pip-tools -certifi==2024.8.30 \ - --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ - --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 - # via requests -charset-normalizer==3.3.2 \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 - # via requests -click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via - # black - # flask - # pip-tools -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via - # -r requirements.base.in - # -r requirements.dev.in -compare-locales==9.0.4 \ - --hash=sha256:73d0d384aefa0bc96f5fd8521c08c8bb89b16a37316701323a77960accabd551 \ - --hash=sha256:933d2b6e20f460d3ac2d3176295684505a42085b25e6c31944fcafbaf52f1cc0 - # via -r requirements.dev.in -decorator==5.1.1 \ - --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ - --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 - # via -r requirements.anki.in -dill==0.3.8 \ - --hash=sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca \ - --hash=sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7 - # via pylint -distro==1.9.0 ; sys_platform != "darwin" and sys_platform != "win32" \ - --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ - --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 - # via -r requirements.anki.in -exceptiongroup==1.2.2 \ - --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \ - --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc - # via pytest -flask[async]==3.0.3 \ - --hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \ - --hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842 - # via - # -r requirements.aqt.in - # flask-cors - # types-flask-cors -flask-cors==6.0.0 \ - --hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \ - --hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657 - # via -r requirements.aqt.in -fluent-syntax==0.19.0 \ - --hash=sha256:920326d7f46864b9758f0044e9968e3112198bc826acee16ddd8f11d359004fd \ - --hash=sha256:b352b3475fac6c6ed5f06527921f432aac073d764445508ee5218aeccc7cc5c4 - # via - # -r requirements.dev.in - # compare-locales -idna==3.8 \ - --hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \ - --hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603 - # via requests -importlib-metadata==8.4.0 \ - --hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \ - --hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5 - # via - # build - # flask - # markdown -iniconfig==2.0.0 \ - --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ - --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 - # via pytest -isort==5.13.2 \ - --hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \ - --hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6 - # via - # -r requirements.dev.in - # pylint -itsdangerous==2.2.0 \ - --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ - --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 - # via flask -jinja2==3.1.5 \ - --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ - --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb - # via flask -jsonschema==4.1.2 \ - --hash=sha256:166870c8ab27bd712a8627e0598de4685bd8d199c4d7bd7cacc3d941ba0c6ca0 \ - --hash=sha256:5c1a282ee6b74235057421fd0f766ac5f2972f77440927f6471c9e8493632fac - # via -r requirements.aqt.in -markdown==3.7 \ - --hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \ - --hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803 - # via -r requirements.anki.in -markupsafe==2.1.5 \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 - # via - # jinja2 - # werkzeug -mccabe==0.7.0 \ - --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ - --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e - # via pylint -mock==5.1.0 \ - --hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \ - --hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d - # via -r requirements.dev.in -mypy==1.11.2 \ - --hash=sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36 \ - --hash=sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce \ - --hash=sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6 \ - --hash=sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b \ - --hash=sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca \ - --hash=sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24 \ - --hash=sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383 \ - --hash=sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7 \ - --hash=sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86 \ - --hash=sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d \ - --hash=sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4 \ - --hash=sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8 \ - --hash=sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987 \ - --hash=sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385 \ - --hash=sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79 \ - --hash=sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef \ - --hash=sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6 \ - --hash=sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70 \ - --hash=sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca \ - --hash=sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70 \ - --hash=sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12 \ - --hash=sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104 \ - --hash=sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a \ - --hash=sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318 \ - --hash=sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1 \ - --hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \ - --hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d - # via -r requirements.dev.in -mypy-extensions==1.0.0 \ - --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ - --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 - # via - # black - # mypy -mypy-protobuf==3.6.0 \ - --hash=sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c \ - --hash=sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c - # via -r requirements.dev.in -orjson==3.10.7 \ - --hash=sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23 \ - --hash=sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9 \ - --hash=sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5 \ - --hash=sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad \ - --hash=sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98 \ - --hash=sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412 \ - --hash=sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1 \ - --hash=sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864 \ - --hash=sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6 \ - --hash=sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91 \ - --hash=sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac \ - --hash=sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c \ - --hash=sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1 \ - --hash=sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f \ - --hash=sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250 \ - --hash=sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09 \ - --hash=sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0 \ - --hash=sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225 \ - --hash=sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354 \ - --hash=sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f \ - --hash=sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e \ - --hash=sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469 \ - --hash=sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c \ - --hash=sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12 \ - --hash=sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3 \ - --hash=sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3 \ - --hash=sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149 \ - --hash=sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb \ - --hash=sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2 \ - --hash=sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2 \ - --hash=sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f \ - --hash=sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0 \ - --hash=sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a \ - --hash=sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58 \ - --hash=sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe \ - --hash=sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09 \ - --hash=sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e \ - --hash=sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2 \ - --hash=sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c \ - --hash=sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313 \ - --hash=sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6 \ - --hash=sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93 \ - --hash=sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7 \ - --hash=sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866 \ - --hash=sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c \ - --hash=sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b \ - --hash=sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5 \ - --hash=sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175 \ - --hash=sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9 \ - --hash=sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0 \ - --hash=sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff \ - --hash=sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20 \ - --hash=sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5 \ - --hash=sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960 \ - --hash=sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024 \ - --hash=sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd \ - --hash=sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84 - # via -r requirements.anki.in -packaging==24.1 \ - --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ - --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 - # via - # black - # build - # pytest -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 - # via black -pip-system-certs==4.0 \ - --hash=sha256:47202b9403a6f40783a9674bbc8873f5fc86544ec01a49348fa913e99e2ff68b \ - --hash=sha256:db8e6a31388d9795ec9139957df1a89fa5274fb66164456fd091a5d3e94c350c - # via -r requirements.aqt.in -pip-tools==7.4.1 \ - --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ - --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 - # via - # -r requirements.base.in - # -r requirements.dev.in -platformdirs==4.2.2 \ - --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ - --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 - # via - # black - # pylint -pluggy==1.5.0 \ - --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ - --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 - # via pytest -protobuf==5.28.2 \ - --hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \ - --hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \ - --hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \ - --hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \ - --hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \ - --hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \ - --hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \ - --hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \ - --hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \ - --hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \ - --hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d - # via - # -r requirements.anki.in - # mypy-protobuf -pychromedevtools==1.0.3 \ - --hash=sha256:a429968bb18d34322da4ed1b727980d35fbd8104d4e764f6d1850b4ffc6e563b - # via -r requirements.dev.in -pylint==3.2.7 \ - --hash=sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b \ - --hash=sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e - # via -r requirements.dev.in -pyproject-hooks==1.1.0 \ - --hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \ - --hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2 - # via - # build - # pip-tools -pyrsistent==0.20.0 \ - --hash=sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f \ - --hash=sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e \ - --hash=sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958 \ - --hash=sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34 \ - --hash=sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca \ - --hash=sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d \ - --hash=sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d \ - --hash=sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4 \ - --hash=sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714 \ - --hash=sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf \ - --hash=sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee \ - --hash=sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8 \ - --hash=sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224 \ - --hash=sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d \ - --hash=sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054 \ - --hash=sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656 \ - --hash=sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7 \ - --hash=sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423 \ - --hash=sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce \ - --hash=sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e \ - --hash=sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3 \ - --hash=sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0 \ - --hash=sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f \ - --hash=sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b \ - --hash=sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce \ - --hash=sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a \ - --hash=sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174 \ - --hash=sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86 \ - --hash=sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f \ - --hash=sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b \ - --hash=sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98 \ - --hash=sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022 - # via jsonschema -pysocks==1.7.1 \ - --hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \ - --hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \ - --hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 - # via requests -pytest==8.3.2 \ - --hash=sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 \ - --hash=sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce - # via -r requirements.dev.in -requests==2.32.3 \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via - # -r requirements.anki.in - # -r requirements.aqt.in - # pychromedevtools -send2trash==1.8.3 \ - --hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \ - --hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf - # via -r requirements.aqt.in -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via compare-locales -soupsieve==2.6 \ - --hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \ - --hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9 - # via beautifulsoup4 -toml==0.10.2 \ - --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ - --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via compare-locales -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via - # black - # build - # mypy - # pip-tools - # pylint - # pytest -tomlkit==0.13.2 \ - --hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \ - --hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79 - # via pylint -types-click==7.1.8 \ - --hash=sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81 \ - --hash=sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092 - # via types-flask -types-decorator==5.1.8.20240310 \ - --hash=sha256:3af75dc38f5baf65b9b53ea6661ce2056c5ca7d70d620d0b1f620285c1242757 \ - --hash=sha256:52e316b03783886a8a2abdc228f7071680ba65894545cd2085ebe3cf88684a0e - # via -r requirements.dev.in -types-flask==1.1.6 \ - --hash=sha256:6ab8a9a5e258b76539d652f6341408867298550b19b81f0e41e916825fc39087 \ - --hash=sha256:aac777b3abfff9436e6b01f6d08171cf23ea6e5be71cbf773aaabb1c5763e9cf - # via -r requirements.dev.in -types-flask-cors==5.0.0.20240902 \ - --hash=sha256:595e5f36056cd128ab905832e055f2e5d116fbdc685356eea4490bc77df82137 \ - --hash=sha256:8921b273bf7cd9636df136b66408efcfa6338a935e5c8f53f5eff1cee03f3394 - # via -r requirements.dev.in -types-jinja2==2.11.9 \ - --hash=sha256:60a1e21e8296979db32f9374d8a239af4cb541ff66447bb915d8ad398f9c63b2 \ - --hash=sha256:dbdc74a40aba7aed520b7e4d89e8f0fe4286518494208b35123bcf084d4b8c81 - # via types-flask -types-markdown==3.7.0.20240822 \ - --hash=sha256:183557c9f4f865bdefd8f5f96a38145c31819271cde111d35557c3bd2069e78d \ - --hash=sha256:bec91c410aaf2470ffdb103e38438fbcc53689b00133f19e64869eb138432ad7 - # via -r requirements.dev.in -types-markupsafe==1.1.10 \ - --hash=sha256:85b3a872683d02aea3a5ac2a8ef590193c344092032f58457287fbf8e06711b1 \ - --hash=sha256:ca2bee0f4faafc45250602567ef38d533e877d2ddca13003b319c551ff5b3cc5 - # via types-jinja2 -types-orjson==3.6.2 \ - --hash=sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4 \ - --hash=sha256:cf9afcc79a86325c7aff251790338109ed6f6b1bab09d2d4262dd18c85a3c638 - # via -r requirements.dev.in -types-protobuf==5.27.0.20240626 \ - --hash=sha256:683ba14043bade6785e3f937a7498f243b37881a91ac8d81b9202ecf8b191e9c \ - --hash=sha256:688e8f7e8d9295db26bc560df01fb731b27a25b77cbe4c1ce945647f7024f5c1 - # via - # -r requirements.dev.in - # mypy-protobuf -types-pywin32==306.0.0.20240822 \ - --hash=sha256:31a16f7eaf711166e8aec50ee1ddf0f16b4512e19ecc92a019ae7a0860b64bad \ - --hash=sha256:34d22b58aaa2cc86fe585b6e2e1eda88a60b010badea0e0e4a410ebe28744645 - # via -r requirements.dev.in -types-requests==2.32.0.20240712 \ - --hash=sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358 \ - --hash=sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3 - # via -r requirements.dev.in -types-waitress==3.0.0.20240423 \ - --hash=sha256:7e9f77a3bc3c20436b9b7ef93da88c8fe0d1e2205d5891ae7526cbd93554f5a4 \ - --hash=sha256:ec3af592b5868ccf151645afc74d2e606cd5dec3ed326c9fd0259691b39430fe - # via -r requirements.dev.in -types-werkzeug==1.0.9 \ - --hash=sha256:194bd5715a13c598f05c63e8a739328657590943bce941e8a3619a6b5d4a54ec \ - --hash=sha256:5cc269604c400133d452a40cee6397655f878fc460e03fde291b9e3a5eaa518c - # via types-flask -typing-extensions==4.12.2 \ - --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ - --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 - # via - # -r requirements.anki.in - # astroid - # black - # fluent-syntax - # mypy - # pylint -urllib3==2.2.2 \ - --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ - --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 - # via - # requests - # types-requests -waitress==3.0.1 \ - --hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \ - --hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3 - # via -r requirements.aqt.in -websocket-client==1.8.0 \ - --hash=sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 \ - --hash=sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da - # via pychromedevtools -werkzeug==3.0.6 \ - --hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \ - --hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d - # via - # flask - # flask-cors -wheel==0.44.0 \ - --hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \ - --hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49 - # via pip-tools -wrapt==1.16.0 \ - --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \ - --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \ - --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \ - --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \ - --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \ - --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \ - --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \ - --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \ - --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \ - --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \ - --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \ - --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \ - --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \ - --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \ - --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \ - --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \ - --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \ - --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \ - --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \ - --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \ - --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \ - --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \ - --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \ - --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \ - --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \ - --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \ - --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \ - --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \ - --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \ - --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \ - --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \ - --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \ - --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \ - --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \ - --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \ - --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \ - --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \ - --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \ - --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \ - --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \ - --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \ - --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \ - --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \ - --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \ - --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \ - --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \ - --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \ - --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \ - --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \ - --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \ - --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \ - --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \ - --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \ - --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \ - --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \ - --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \ - --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \ - --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \ - --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \ - --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \ - --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \ - --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \ - --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \ - --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \ - --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \ - --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \ - --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \ - --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \ - --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \ - --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4 - # via pip-system-certs -zipp==3.20.1 \ - --hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \ - --hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -pip==24.2 \ - --hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \ - --hash=sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8 - # via pip-tools -setuptools==75.1.0 \ - --hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \ - --hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538 - # via pip-tools From 7dac2fc4ff3147088022a6ef97bc5f52a2f7ae0a Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 21 Jul 2025 22:30:25 +0300 Subject: [PATCH 076/133] Add back legacy code as separate screens --- qt/aqt/__init__.py | 2 + qt/aqt/addcards.py | 7 +- qt/aqt/addcards_legacy.py | 414 ++++++++ qt/aqt/browser/browser.py | 18 +- qt/aqt/editcurrent.py | 7 +- qt/aqt/editcurrent_legacy.py | 94 ++ qt/aqt/editor.py | 70 +- qt/aqt/editor_legacy.py | 1790 ++++++++++++++++++++++++++++++++++ qt/aqt/main.py | 16 +- qt/aqt/mediasrv.py | 14 +- qt/tools/genhooks_gui.py | 50 +- 11 files changed, 2383 insertions(+), 99 deletions(-) create mode 100644 qt/aqt/addcards_legacy.py create mode 100644 qt/aqt/editcurrent_legacy.py create mode 100644 qt/aqt/editor_legacy.py diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 53bdc3c92..74c4919a7 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -125,9 +125,11 @@ from aqt import stats, about, preferences, mediasync # isort:skip class DialogManager: _dialogs: dict[str, list] = { "AddCards": [addcards.AddCards, None], + "NewAddCards": [addcards.NewAddCards, None], "AddonsDialog": [addons.AddonsDialog, None], "Browser": [browser.Browser, None], "EditCurrent": [editcurrent.EditCurrent, None], + "NewEditCurrent": [editcurrent.NewEditCurrent, None], "FilteredDeckConfigDialog": [filtered_deck.FilteredDeckConfigDialog, None], "DeckStats": [stats.DeckStats, None], "NewDeckStats": [stats.NewDeckStats, None], diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 3b0421b30..8ea8b808e 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -14,6 +14,7 @@ from anki.models import NotetypeId from anki.notes import Note, NoteId from anki.utils import html_to_text_line, is_mac from aqt import AnkiQt, gui_hooks +from aqt.addcards_legacy import * from aqt.deckchooser import DeckChooser from aqt.notetypechooser import NotetypeChooser from aqt.qt import * @@ -30,7 +31,7 @@ from aqt.utils import ( ) -class AddCards(QMainWindow): +class NewAddCards(QMainWindow): def __init__(self, mw: AnkiQt) -> None: super().__init__(None, Qt.WindowType.Window) self._close_event_has_cleaned_up = False @@ -79,7 +80,7 @@ class AddCards(QMainWindow): self.setAndFocusNote(new_note) def setupEditor(self) -> None: - self.editor = aqt.editor.Editor( + self.editor = aqt.editor.NewEditor( self.mw, self.form.fieldsArea, self, @@ -244,7 +245,7 @@ class AddCards(QMainWindow): gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) self.mw.maybeReset() saveGeom(self, "add") - aqt.dialogs.markClosed("AddCards") + aqt.dialogs.markClosed("NewAddCards") self._close_event_has_cleaned_up = True self.mw.deferred_delete_and_garbage_collect(self) self.close() diff --git a/qt/aqt/addcards_legacy.py b/qt/aqt/addcards_legacy.py new file mode 100644 index 000000000..86e8a25b1 --- /dev/null +++ b/qt/aqt/addcards_legacy.py @@ -0,0 +1,414 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +from collections.abc import Callable + +import aqt.editor +import aqt.forms +from anki._legacy import deprecated +from anki.collection import OpChanges, SearchNode +from anki.decks import DeckId +from anki.models import NotetypeId +from anki.notes import Note, NoteFieldsCheckResult, NoteId +from anki.utils import html_to_text_line, is_mac +from aqt import AnkiQt, gui_hooks +from aqt.deckchooser import DeckChooser +from aqt.notetypechooser import NotetypeChooser +from aqt.operations.note import add_note +from aqt.qt import * +from aqt.sound import av_player +from aqt.utils import ( + HelpPage, + add_close_shortcut, + ask_user_dialog, + askUser, + downArrow, + openHelp, + restoreGeom, + saveGeom, + shortcut, + showWarning, + tooltip, + tr, +) + + +class AddCards(QMainWindow): + def __init__(self, mw: AnkiQt) -> None: + super().__init__(None, Qt.WindowType.Window) + self._close_event_has_cleaned_up = False + self.mw = mw + self.col = mw.col + form = aqt.forms.addcards.Ui_Dialog() + form.setupUi(self) + self.form = form + self.setWindowTitle(tr.actions_add()) + self.setMinimumHeight(300) + self.setMinimumWidth(400) + self.setup_choosers() + self.setupEditor() + add_close_shortcut(self) + self._load_new_note() + self.setupButtons() + self.history: list[NoteId] = [] + self._last_added_note: Note | None = None + gui_hooks.operation_did_execute.append(self.on_operation_did_execute) + restoreGeom(self, "add") + gui_hooks.add_cards_did_init(self) + if not is_mac: + self.setMenuBar(None) + self.show() + + def set_deck(self, deck_id: DeckId) -> None: + self.deck_chooser.selected_deck_id = deck_id + + def set_note_type(self, note_type_id: NotetypeId) -> None: + self.notetype_chooser.selected_notetype_id = note_type_id + + def set_note(self, note: Note, deck_id: DeckId | None = None) -> None: + """Set tags, field contents and notetype according to `note`. Deck is set + to `deck_id` or the deck last used with the notetype. + """ + self.notetype_chooser.selected_notetype_id = note.mid + if deck_id or (deck_id := self.col.default_deck_for_notetype(note.mid)): + self.deck_chooser.selected_deck_id = deck_id + + new_note = self._new_note() + new_note.fields = note.fields[:] + new_note.tags = note.tags[:] + + self.editor.orig_note_id = note.id + self.setAndFocusNote(new_note) + + def setupEditor(self) -> None: + self.editor = aqt.editor.Editor( + self.mw, + self.form.fieldsArea, + self, + editor_mode=aqt.editor.EditorMode.ADD_CARDS, + ) + + def setup_choosers(self) -> None: + defaults = self.col.defaults_for_adding( + current_review_card=self.mw.reviewer.card + ) + + self.notetype_chooser = NotetypeChooser( + mw=self.mw, + widget=self.form.modelArea, + starting_notetype_id=NotetypeId(defaults.notetype_id), + on_button_activated=self.show_notetype_selector, + on_notetype_changed=self.on_notetype_change, + ) + self.deck_chooser = DeckChooser( + self.mw, + self.form.deckArea, + starting_deck_id=DeckId(defaults.deck_id), + on_deck_changed=self.on_deck_changed, + ) + + def reopen(self, mw: AnkiQt) -> None: + if not self.editor.fieldsAreBlank(): + return + + defaults = self.col.defaults_for_adding( + current_review_card=self.mw.reviewer.card + ) + self.set_note_type(NotetypeId(defaults.notetype_id)) + self.set_deck(DeckId(defaults.deck_id)) + + def helpRequested(self) -> None: + openHelp(HelpPage.ADDING_CARD_AND_NOTE) + + def setupButtons(self) -> None: + bb = self.form.buttonBox + ar = QDialogButtonBox.ButtonRole.ActionRole + # add + self.addButton = bb.addButton(tr.actions_add(), ar) + qconnect(self.addButton.clicked, self.add_current_note) + self.addButton.setShortcut(QKeySequence("Ctrl+Return")) + # qt5.14+ doesn't handle numpad enter on Windows + self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self) + qconnect(self.compat_add_shorcut.activated, self.addButton.click) + self.addButton.setToolTip(shortcut(tr.adding_add_shortcut_ctrlandenter())) + + # close + self.closeButton = QPushButton(tr.actions_close()) + self.closeButton.setAutoDefault(False) + bb.addButton(self.closeButton, QDialogButtonBox.ButtonRole.RejectRole) + qconnect(self.closeButton.clicked, self.close) + # help + self.helpButton = QPushButton(tr.actions_help(), clicked=self.helpRequested) # type: ignore + self.helpButton.setAutoDefault(False) + bb.addButton(self.helpButton, QDialogButtonBox.ButtonRole.HelpRole) + # history + b = bb.addButton(f"{tr.adding_history()} {downArrow()}", ar) + if is_mac: + sc = "Ctrl+Shift+H" + else: + sc = "Ctrl+H" + b.setShortcut(QKeySequence(sc)) + b.setToolTip(tr.adding_shortcut(val=shortcut(sc))) + qconnect(b.clicked, self.onHistory) + b.setEnabled(False) + self.historyButton = b + + def setAndFocusNote(self, note: Note) -> None: + self.editor.set_note(note, focusTo=0) + + def show_notetype_selector(self) -> None: + self.editor.call_after_note_saved(self.notetype_chooser.choose_notetype) + + def on_deck_changed(self, deck_id: int) -> None: + gui_hooks.add_cards_did_change_deck(deck_id) + + def on_notetype_change( + self, notetype_id: NotetypeId, update_deck: bool = True + ) -> None: + # need to adjust current deck? + if update_deck: + if deck_id := self.col.default_deck_for_notetype(notetype_id): + self.deck_chooser.selected_deck_id = deck_id + + # only used for detecting changed sticky fields on close + self._last_added_note = None + + # copy fields into new note with the new notetype + old_note = self.editor.note + new_note = self._new_note() + if old_note: + old_field_names = list(old_note.keys()) + new_field_names = list(new_note.keys()) + copied_field_names = set() + for f in new_note.note_type()["flds"]: + field_name = f["name"] + # copy identical non-empty fields + if field_name in old_field_names and old_note[field_name]: + new_note[field_name] = old_note[field_name] + copied_field_names.add(field_name) + new_idx = 0 + for old_idx, old_field_value in enumerate(old_field_names): + # skip previously copied identical fields in new note + while ( + new_idx < len(new_field_names) + and new_field_names[new_idx] in copied_field_names + ): + new_idx += 1 + if new_idx >= len(new_field_names): + break + # copy non-empty old fields + if ( + old_field_value not in copied_field_names + and old_note.fields[old_idx] + ): + new_note.fields[new_idx] = old_note.fields[old_idx] + new_idx += 1 + + new_note.tags = old_note.tags + + # and update editor state + self.editor.note = new_note + self.editor.loadNote( + focusTo=min(self.editor.last_field_index or 0, len(new_note.fields) - 1) + ) + gui_hooks.addcards_did_change_note_type( + self, old_note.note_type(), new_note.note_type() + ) + + def _load_new_note(self, sticky_fields_from: Note | None = None) -> None: + note = self._new_note() + if old_note := sticky_fields_from: + flds = note.note_type()["flds"] + # copy fields from old note + if old_note: + for n in range(min(len(note.fields), len(old_note.fields))): + if flds[n]["sticky"]: + note.fields[n] = old_note.fields[n] + # and tags + note.tags = old_note.tags + self.setAndFocusNote(note) + + def on_operation_did_execute( + self, changes: OpChanges, handler: object | None + ) -> None: + if (changes.notetype or changes.deck) and handler is not self.editor: + self.on_notetype_change( + NotetypeId( + self.col.defaults_for_adding( + current_review_card=self.mw.reviewer.card + ).notetype_id + ), + update_deck=False, + ) + + def _new_note(self) -> Note: + return self.col.new_note( + self.col.models.get(self.notetype_chooser.selected_notetype_id) + ) + + def addHistory(self, note: Note) -> None: + self.history.insert(0, note.id) + self.history = self.history[:15] + self.historyButton.setEnabled(True) + + def onHistory(self) -> None: + m = QMenu(self) + for nid in self.history: + if self.col.find_notes(self.col.build_search_string(SearchNode(nid=nid))): + note = self.col.get_note(nid) + fields = note.fields + txt = html_to_text_line(", ".join(fields)) + if len(txt) > 30: + txt = f"{txt[:30]}..." + line = tr.adding_edit(val=txt) + line = gui_hooks.addcards_will_add_history_entry(line, note) + line = line.replace("&", "&&") + # In qt action "&i" means "underline i, trigger this line when i is pressed". + # except for "&&" which is replaced by a single "&" + a = m.addAction(line) + qconnect(a.triggered, lambda b, nid=nid: self.editHistory(nid)) + else: + a = m.addAction(tr.adding_note_deleted()) + a.setEnabled(False) + gui_hooks.add_cards_will_show_history_menu(self, m) + m.exec(self.historyButton.mapToGlobal(QPoint(0, 0))) + + def editHistory(self, nid: NoteId) -> None: + aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) + + def add_current_note(self) -> None: + if self.editor.current_notetype_is_image_occlusion(): + self.editor.update_occlusions_field() + self.editor.call_after_note_saved(self._add_current_note) + self.editor.reset_image_occlusion() + else: + self.editor.call_after_note_saved(self._add_current_note) + + def _add_current_note(self) -> None: + note = self.editor.note + + if not self._note_can_be_added(note): + return + + target_deck_id = self.deck_chooser.selected_deck_id + + def on_success(changes: OpChanges) -> None: + # only used for detecting changed sticky fields on close + self._last_added_note = note + + self.addHistory(note) + + tooltip(tr.adding_added(), period=500) + av_player.stop_and_clear_queue() + self._load_new_note(sticky_fields_from=note) + gui_hooks.add_cards_did_add_note(note) + + add_note(parent=self, note=note, target_deck_id=target_deck_id).success( + on_success + ).run_in_background() + + def _note_can_be_added(self, note: Note) -> bool: + result = note.fields_check() + # no problem, duplicate, and confirmed cloze cases + problem = None + if result == NoteFieldsCheckResult.EMPTY: + if self.editor.current_notetype_is_image_occlusion(): + problem = tr.notetypes_no_occlusion_created2() + else: + problem = tr.adding_the_first_field_is_empty() + elif result == NoteFieldsCheckResult.MISSING_CLOZE: + if not askUser(tr.adding_you_have_a_cloze_deletion_note()): + return False + elif result == NoteFieldsCheckResult.NOTETYPE_NOT_CLOZE: + problem = tr.adding_cloze_outside_cloze_notetype() + elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE: + problem = tr.adding_cloze_outside_cloze_field() + + # filter problem through add-ons + problem = gui_hooks.add_cards_will_add_note(problem, note) + if problem is not None: + showWarning(problem, help=HelpPage.ADDING_CARD_AND_NOTE) + return False + + optional_problems: list[str] = [] + gui_hooks.add_cards_might_add_note(optional_problems, note) + if not all(askUser(op) for op in optional_problems): + return False + + return True + + def keyPressEvent(self, evt: QKeyEvent) -> None: + if evt.key() == Qt.Key.Key_Escape: + self.close() + else: + super().keyPressEvent(evt) + + def closeEvent(self, evt: QCloseEvent) -> None: + if self._close_event_has_cleaned_up: + evt.accept() + return + self.ifCanClose(self._close) + evt.ignore() + + def _close(self) -> None: + self.editor.cleanup() + self.notetype_chooser.cleanup() + self.deck_chooser.cleanup() + gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) + self.mw.maybeReset() + saveGeom(self, "add") + aqt.dialogs.markClosed("AddCards") + self._close_event_has_cleaned_up = True + self.mw.deferred_delete_and_garbage_collect(self) + self.close() + + def ifCanClose(self, onOk: Callable) -> None: + def callback(choice: int) -> None: + if choice == 0: + onOk() + + def afterSave() -> None: + if self.editor.fieldsAreBlank(self._last_added_note): + return onOk() + + ask_user_dialog( + tr.adding_discard_current_input(), + callback=callback, + buttons=[ + QMessageBox.StandardButton.Discard, + (tr.adding_keep_editing(), QMessageBox.ButtonRole.RejectRole), + ], + ) + + self.editor.call_after_note_saved(afterSave) + + def closeWithCallback(self, cb: Callable[[], None]) -> None: + def doClose() -> None: + self._close() + cb() + + self.ifCanClose(doClose) + + # legacy aliases + + @property + def deckChooser(self) -> DeckChooser: + if getattr(self, "form", None): + # show this warning only after Qt form has been initialized, + # or PyQt's introspection triggers it + print("deckChooser is deprecated; use deck_chooser instead") + return self.deck_chooser + + addCards = add_current_note + _addCards = _add_current_note + onModelChange = on_notetype_change + + @deprecated(info="obsolete") + def addNote(self, note: Note) -> None: + pass + + @deprecated(info="does nothing; will go away") + def removeTempNote(self, note: Note) -> None: + pass diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 6e7af72cd..6be4d33de 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -27,7 +27,6 @@ from anki.scheduler.base import ScheduleCardsAsNew from anki.tags import MARKED_TAG from anki.utils import is_mac from aqt import AnkiQt, gui_hooks -from aqt.editor import Editor, EditorWebView from aqt.errors import show_exception from aqt.exporting import ExportDialog as LegacyExportDialog from aqt.import_export.exporting import ExportDialog @@ -77,7 +76,7 @@ from aqt.utils import ( tr, ) -from ..addcards import AddCards +from ..addcards import NewAddCards as AddCards from ..changenotetype import change_notetype_dialog from .card_info import BrowserCardInfo from .find_and_replace import FindAndReplaceDialog @@ -111,7 +110,7 @@ class MockModel: class Browser(QMainWindow): mw: AnkiQt col: Collection - editor: Editor | None + editor: aqt.editor.NewEditor | None table: Table def __init__( @@ -267,7 +266,7 @@ class Browser(QMainWindow): return None def add_card(self, deck_id: DeckId): - add_cards = cast(AddCards, aqt.dialogs.open("AddCards", self.mw)) + add_cards = cast(AddCards, aqt.dialogs.open("NewAddCards", self.mw)) add_cards.set_deck(deck_id) if note_type_id := self.get_active_note_type_id(): @@ -392,7 +391,7 @@ class Browser(QMainWindow): add_ellipsis_to_action_label(f.action_forget) add_ellipsis_to_action_label(f.action_grade_now) - def _editor_web_view(self) -> EditorWebView: + def _editor_web_view(self) -> aqt.editor.NewEditorWebView: assert self.editor is not None editor_web_view = self.editor.web assert editor_web_view is not None @@ -592,12 +591,14 @@ class Browser(QMainWindow): def setupEditor(self) -> None: QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.onTogglePreview) - def add_preview_button(editor: Editor) -> None: + def add_preview_button( + editor: aqt.editor.Editor | aqt.editor.NewEditor, + ) -> None: editor._links["preview"] = lambda _editor: self.onTogglePreview() gui_hooks.editor_did_init.remove(add_preview_button) gui_hooks.editor_did_init.append(add_preview_button) - self.editor = aqt.editor.Editor( + self.editor = aqt.editor.NewEditor( self.mw, self.form.fieldsArea, self, @@ -806,7 +807,7 @@ class Browser(QMainWindow): assert current_card is not None deck_id = current_card.current_deck_id() - aqt.dialogs.open("AddCards", self.mw).set_note(note, deck_id) + aqt.dialogs.open("NewAddCards", self.mw).set_note(note, deck_id) @no_arg_trigger @skip_if_selection_is_empty @@ -1264,3 +1265,4 @@ class Browser(QMainWindow): line_edit = self.form.searchEdit.lineEdit() assert line_edit is not None return line_edit + return line_edit diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index 4cd8dcb0c..1b42c5d83 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -7,11 +7,12 @@ from collections.abc import Callable import aqt.editor from anki.collection import OpChanges from aqt import gui_hooks +from aqt.editcurrent_legacy import * from aqt.qt import * from aqt.utils import add_close_shortcut, restoreGeom, saveGeom, tr -class EditCurrent(QMainWindow): +class NewEditCurrent(QMainWindow): def __init__(self, mw: aqt.AnkiQt) -> None: super().__init__(None, Qt.WindowType.Window) self.mw = mw @@ -22,7 +23,7 @@ class EditCurrent(QMainWindow): self.setMinimumWidth(250) if not is_mac: self.setMenuBar(None) - self.editor = aqt.editor.Editor( + self.editor = aqt.editor.NewEditor( self.mw, self.form.fieldsArea, self, @@ -46,7 +47,7 @@ class EditCurrent(QMainWindow): gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) self.editor.cleanup() saveGeom(self, "editcurrent") - aqt.dialogs.markClosed("EditCurrent") + aqt.dialogs.markClosed("NewEditCurrent") def reopen(self, mw: aqt.AnkiQt) -> None: if card := self.mw.reviewer.card: diff --git a/qt/aqt/editcurrent_legacy.py b/qt/aqt/editcurrent_legacy.py new file mode 100644 index 000000000..d4e969c21 --- /dev/null +++ b/qt/aqt/editcurrent_legacy.py @@ -0,0 +1,94 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + +from collections.abc import Callable + +import aqt.editor +from anki.collection import OpChanges +from anki.errors import NotFoundError +from aqt import gui_hooks +from aqt.qt import * +from aqt.utils import add_close_shortcut, restoreGeom, saveGeom, tr + + +class EditCurrent(QMainWindow): + def __init__(self, mw: aqt.AnkiQt) -> None: + super().__init__(None, Qt.WindowType.Window) + self.mw = mw + self.form = aqt.forms.editcurrent.Ui_Dialog() + self.form.setupUi(self) + self.setWindowTitle(tr.editing_edit_current()) + self.setMinimumHeight(400) + self.setMinimumWidth(250) + if not is_mac: + self.setMenuBar(None) + self.editor = aqt.editor.Editor( + self.mw, + self.form.fieldsArea, + self, + editor_mode=aqt.editor.EditorMode.EDIT_CURRENT, + ) + assert self.mw.reviewer.card is not None + self.editor.card = self.mw.reviewer.card + self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0) + restoreGeom(self, "editcurrent") + self.buttonbox = QDialogButtonBox(Qt.Orientation.Horizontal) + self.form.verticalLayout.insertWidget(1, self.buttonbox) + self.buttonbox.addButton(QDialogButtonBox.StandardButton.Close) + qconnect(self.buttonbox.rejected, self.close) + close_button = self.buttonbox.button(QDialogButtonBox.StandardButton.Close) + assert close_button is not None + close_button.setShortcut(QKeySequence("Ctrl+Return")) + add_close_shortcut(self) + # qt5.14+ doesn't handle numpad enter on Windows + self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self) + qconnect(self.compat_add_shorcut.activated, close_button.click) + gui_hooks.operation_did_execute.append(self.on_operation_did_execute) + self.show() + + def on_operation_did_execute( + self, changes: OpChanges, handler: object | None + ) -> None: + if changes.note_text and handler is not self.editor: + # reload note + note = self.editor.note + try: + assert note is not None + note.load() + except NotFoundError: + # note's been deleted + self.cleanup() + self.close() + return + + self.editor.set_note(note) + + def cleanup(self) -> None: + gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) + self.editor.cleanup() + saveGeom(self, "editcurrent") + aqt.dialogs.markClosed("EditCurrent") + + def reopen(self, mw: aqt.AnkiQt) -> None: + if card := self.mw.reviewer.card: + self.editor.card = card + self.editor.set_note(card.note()) + + def closeEvent(self, evt: QCloseEvent | None) -> None: + self.editor.call_after_note_saved(self.cleanup) + + def _saveAndClose(self) -> None: + self.cleanup() + self.mw.deferred_delete_and_garbage_collect(self) + self.close() + + def closeWithCallback(self, onsuccess: Callable[[], None]) -> None: + def callback() -> None: + self._saveAndClose() + onsuccess() + + self.editor.call_after_note_saved(callback) + + onReset = on_operation_did_execute + onReset = on_operation_did_execute diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 5610d64ed..394751065 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -10,7 +10,6 @@ import mimetypes import os from collections.abc import Callable from dataclasses import dataclass -from enum import Enum from random import randrange from typing import Any @@ -21,58 +20,16 @@ from anki.models import NotetypeId from anki.notes import Note, NoteId from anki.utils import is_win from aqt import AnkiQt, gui_hooks +from aqt.editor_legacy import * from aqt.qt import * from aqt.sound import av_player from aqt.utils import shortcut, showWarning from aqt.webview import AnkiWebView, AnkiWebViewKind -pics = ("jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "avif") -audio = ( - "3gp", - "aac", - "avi", - "flac", - "flv", - "m4a", - "mkv", - "mov", - "mp3", - "mp4", - "mpeg", - "mpg", - "oga", - "ogg", - "ogv", - "ogx", - "opus", - "spx", - "swf", - "wav", - "webm", -) - - -class EditorMode(Enum): - ADD_CARDS = 0 - EDIT_CURRENT = 1 - BROWSER = 2 - - -class EditorState(Enum): - """ - Current input state of the editing UI. - """ - - INITIAL = -1 - FIELDS = 0 - IO_PICKER = 1 - IO_MASKS = 2 - IO_FIELDS = 3 - def on_editor_ready(func: Callable) -> Callable: @functools.wraps(func) - def decorated(self: Editor, *args: Any, **kwargs: Any) -> None: + def decorated(self: NewEditor, *args: Any, **kwargs: Any) -> None: if self._ready: func(self, *args, **kwargs) else: @@ -96,7 +53,7 @@ class NoteInfo: self.mid = NotetypeId(int(self.mid)) -class Editor: +class NewEditor: """The screen that embeds an editing widget should listen for changes via the `operation_did_execute` hook, and call set_note() when the editor needs redrawing. @@ -152,7 +109,7 @@ class Editor: self.outerLayout = l def add_webview(self) -> None: - self.web = EditorWebView(self.widget, self) + self.web = NewEditorWebView(self.widget, self) self.web.set_bridge_command(self.onBridgeCmd, self) self.web.hide_while_preserving_layout() self.outerLayout.addWidget(self.web, 1) @@ -213,7 +170,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self, icon: str | None, cmd: str, - func: Callable[[Editor], None], + func: Callable[[NewEditor], None], tip: str = "", label: str = "", id: str | None = None, @@ -224,7 +181,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too ) -> str: """Assign func to bridge cmd, register shortcut, return button""" - def wrapped_func(editor: Editor) -> None: + def wrapped_func(editor: NewEditor) -> None: self.call_after_note_saved(functools.partial(func, editor), keepFocus=True) self._links[cmd] = wrapped_func @@ -553,11 +510,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too def _init_links(self) -> None: self._links: dict[str, Callable] = dict( - fields=Editor.onFields, - cards=Editor.onCardLayout, - paste=Editor.onPaste, - cut=Editor.onCut, - copy=Editor.onCopy, + fields=NewEditor.onFields, + cards=NewEditor.onCardLayout, + paste=NewEditor.onPaste, + cut=NewEditor.onCut, + copy=NewEditor.onCopy, ) def get_note_info(self, on_done: Callable[[NoteInfo], None]) -> None: @@ -571,8 +528,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too ###################################################################### -class EditorWebView(AnkiWebView): - def __init__(self, parent: QWidget, editor: Editor) -> None: +class NewEditorWebView(AnkiWebView): + def __init__(self, parent: QWidget, editor: NewEditor) -> None: AnkiWebView.__init__(self, kind=AnkiWebViewKind.EDITOR) self.editor = editor self.setAcceptDrops(True) @@ -592,3 +549,4 @@ class EditorWebView(AnkiWebView): def onPaste(self) -> None: self.triggerPageAction(QWebEnginePage.WebAction.Paste) + self.triggerPageAction(QWebEnginePage.WebAction.Paste) diff --git a/qt/aqt/editor_legacy.py b/qt/aqt/editor_legacy.py new file mode 100644 index 000000000..138deed7a --- /dev/null +++ b/qt/aqt/editor_legacy.py @@ -0,0 +1,1790 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import base64 +import functools +import html +import itertools +import json +import mimetypes +import os +import re +import urllib.error +import urllib.parse +import urllib.request +import warnings +from collections.abc import Callable +from enum import Enum +from random import randrange +from typing import Any, Iterable, Match, cast + +import bs4 +import requests +from bs4 import BeautifulSoup + +import aqt +import aqt.forms +import aqt.operations +import aqt.sound +from anki._legacy import deprecated +from anki.cards import Card +from anki.collection import Config, SearchNode +from anki.consts import MODEL_CLOZE +from anki.hooks import runFilter +from anki.httpclient import HttpClient +from anki.models import NotetypeDict, NotetypeId, StockNotetype +from anki.notes import Note, NoteFieldsCheckResult, NoteId +from anki.utils import checksum, is_lin, is_win, namedtmp +from aqt import AnkiQt, colors, gui_hooks +from aqt.operations import QueryOp +from aqt.operations.note import update_note +from aqt.operations.notetype import update_notetype_legacy +from aqt.qt import * +from aqt.sound import av_player +from aqt.theme import theme_manager +from aqt.utils import ( + HelpPage, + KeyboardModifiersPressed, + disable_help_button, + getFile, + openFolder, + openHelp, + qtMenuShortcutWorkaround, + restoreGeom, + saveGeom, + shortcut, + show_in_folder, + showInfo, + showWarning, + tooltip, + tr, +) +from aqt.webview import AnkiWebView, AnkiWebViewKind + +pics = ("jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "avif") +audio = ( + "3gp", + "aac", + "avi", + "flac", + "flv", + "m4a", + "mkv", + "mov", + "mp3", + "mp4", + "mpeg", + "mpg", + "oga", + "ogg", + "ogv", + "ogx", + "opus", + "spx", + "swf", + "wav", + "webm", +) + + +class EditorMode(Enum): + ADD_CARDS = 0 + EDIT_CURRENT = 1 + BROWSER = 2 + + +class EditorState(Enum): + """ + Current input state of the editing UI. + """ + + INITIAL = -1 + FIELDS = 0 + IO_PICKER = 1 + IO_MASKS = 2 + IO_FIELDS = 3 + + +class Editor: + """The screen that embeds an editing widget should listen for changes via + the `operation_did_execute` hook, and call set_note() when the editor needs + redrawing. + + The editor will cause that hook to be fired when it saves changes. To avoid + an unwanted refresh, the parent widget should check if handler + corresponds to this editor instance, and ignore the change if it does. + """ + + def __init__( + self, + mw: AnkiQt, + widget: QWidget, + parentWindow: QWidget, + addMode: bool | None = None, + *, + editor_mode: EditorMode = EditorMode.EDIT_CURRENT, + ) -> None: + self.mw = mw + self.widget = widget + self.parentWindow = parentWindow + self.note: Note | None = None + # legacy argument provided? + if addMode is not None: + editor_mode = EditorMode.ADD_CARDS if addMode else EditorMode.EDIT_CURRENT + self.addMode = editor_mode is EditorMode.ADD_CARDS + self.editorMode = editor_mode + self.currentField: int | None = None + # Similar to currentField, but not set to None on a blur. May be + # outside the bounds of the current notetype. + self.last_field_index: int | None = None + # used when creating a copy of an existing note + self.orig_note_id: NoteId | None = None + # current card, for card layout + self.card: Card | None = None + self.state: EditorState = EditorState.INITIAL + # used for the io mask editor's context menu + self.last_io_image_path: str | None = None + self._init_links() + self.setupOuter() + self.add_webview() + self.setupWeb() + self.setupShortcuts() + gui_hooks.editor_did_init(self) + + # Initial setup + ############################################################ + + def setupOuter(self) -> None: + l = QVBoxLayout() + l.setContentsMargins(0, 0, 0, 0) + l.setSpacing(0) + self.widget.setLayout(l) + self.outerLayout = l + + def add_webview(self) -> None: + self.web = EditorWebView(self.widget, self) + self.web.set_bridge_command(self.onBridgeCmd, self) + self.outerLayout.addWidget(self.web, 1) + + def setupWeb(self) -> None: + if self.editorMode == EditorMode.ADD_CARDS: + mode = "add" + elif self.editorMode == EditorMode.BROWSER: + mode = "browse" + else: + mode = "review" + + # then load page + self.web.stdHtml( + "", + css=["css/editor.css"], + js=[ + "js/mathjax.js", + "js/editor.js", + ], + context=self, + default_css=False, + ) + self.web.eval(f"setupEditor('{mode}')") + self.web.show() + + lefttopbtns: list[str] = [] + gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) + + lefttopbtns_defs = [ + f"uiPromise.then((noteEditor) => noteEditor.toolbar.notetypeButtons.appendButton({{ component: editorToolbar.Raw, props: {{ html: {json.dumps(button)} }} }}, -1));" + for button in lefttopbtns + ] + lefttopbtns_js = "\n".join(lefttopbtns_defs) + + righttopbtns: list[str] = [] + gui_hooks.editor_did_init_buttons(righttopbtns, self) + # legacy filter + righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) + + righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns]) + righttopbtns_js = ( + f""" +require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].toolbar.toolbar.append({{ + component: editorToolbar.AddonButtons, + id: "addons", + props: {{ buttons: [ {righttopbtns_defs} ] }}, +}})); +""" + if len(righttopbtns) > 0 + else "" + ) + + self.web.eval(f"{lefttopbtns_js} {righttopbtns_js}") + + # Top buttons + ###################################################################### + + def resourceToData(self, path: str) -> str: + """Convert a file (specified by a path) into a data URI.""" + if not os.path.exists(path): + raise FileNotFoundError + mime, _ = mimetypes.guess_type(path) + with open(path, "rb") as fp: + data = fp.read() + data64 = b"".join(base64.encodebytes(data).splitlines()) + return f"data:{mime};base64,{data64.decode('ascii')}" + + def addButton( + self, + icon: str | None, + cmd: str, + func: Callable[[Editor], None], + tip: str = "", + label: str = "", + id: str | None = None, + toggleable: bool = False, + keys: str | None = None, + disables: bool = True, + rightside: bool = True, + ) -> str: + """Assign func to bridge cmd, register shortcut, return button""" + + def wrapped_func(editor: Editor) -> None: + self.call_after_note_saved(functools.partial(func, editor), keepFocus=True) + + self._links[cmd] = wrapped_func + + if keys: + + def on_activated() -> None: + wrapped_func(self) + + if toggleable: + # generate a random id for triggering toggle + id = id or str(randrange(1_000_000)) + + def on_hotkey() -> None: + on_activated() + self.web.eval( + f'toggleEditorButton(document.getElementById("{id}"));' + ) + + else: + on_hotkey = on_activated + + QShortcut( # type: ignore + QKeySequence(keys), + self.widget, + activated=on_hotkey, + ) + + btn = self._addButton( + icon, + cmd, + tip=tip, + label=label, + id=id, + toggleable=toggleable, + disables=disables, + rightside=rightside, + ) + return btn + + def _addButton( + self, + icon: str | None, + cmd: str, + tip: str = "", + label: str = "", + id: str | None = None, + toggleable: bool = False, + disables: bool = True, + rightside: bool = True, + ) -> str: + title_attribute = tip + + if icon: + if icon.startswith("qrc:/"): + iconstr = icon + elif os.path.isabs(icon): + iconstr = self.resourceToData(icon) + else: + iconstr = f"/_anki/imgs/{icon}.png" + image_element = f'' + else: + image_element = "" + + if not label and icon: + label_element = "" + elif label: + label_element = label + else: + label_element = cmd + + title_attribute = shortcut(title_attribute) + id_attribute_assignment = f"id={id}" if id else "" + class_attribute = "linkb" if rightside else "rounded" + if not disables: + class_attribute += " perm" + + return f"""""" + + def setupShortcuts(self) -> None: + # if a third element is provided, enable shortcut even when no field selected + cuts: list[tuple] = [] + gui_hooks.editor_did_init_shortcuts(cuts, self) + for row in cuts: + if len(row) == 2: + keys, fn = row + fn = self._addFocusCheck(fn) + else: + keys, fn, _ = row + QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore + + def _addFocusCheck(self, fn: Callable) -> Callable: + def checkFocus() -> None: + if self.currentField is None: + return + fn() + + return checkFocus + + def onFields(self) -> None: + self.call_after_note_saved(self._onFields) + + def _onFields(self) -> None: + from aqt.fields import FieldDialog + + FieldDialog(self.mw, self.note_type(), parent=self.parentWindow) + + def onCardLayout(self) -> None: + self.call_after_note_saved(self._onCardLayout) + + def _onCardLayout(self) -> None: + from aqt.clayout import CardLayout + + if self.card: + ord = self.card.ord + else: + ord = 0 + + assert self.note is not None + CardLayout( + self.mw, + self.note, + ord=ord, + parent=self.parentWindow, + fill_empty=False, + ) + if is_win: + self.parentWindow.activateWindow() + + # JS->Python bridge + ###################################################################### + + def onBridgeCmd(self, cmd: str) -> Any: + if not self.note: + # shutdown + return + + # focus lost or key/button pressed? + if cmd.startswith("blur") or cmd.startswith("key"): + (type, ord_str, nid_str, txt) = cmd.split(":", 3) + ord = int(ord_str) + try: + nid = int(nid_str) + except ValueError: + nid = 0 + if nid != self.note.id: + print("ignored late blur") + return + + try: + self.note.fields[ord] = self.mungeHTML(txt) + except IndexError: + print("ignored late blur after notetype change") + return + + if not self.addMode: + self._save_current_note() + if type == "blur": + self.currentField = None + # run any filters + if gui_hooks.editor_did_unfocus_field(False, self.note, ord): + # something updated the note; update it after a subsequent focus + # event has had time to fire + self.mw.progress.timer( + 100, self.loadNoteKeepingFocus, False, parent=self.widget + ) + else: + self._check_and_update_duplicate_display_async() + else: + gui_hooks.editor_did_fire_typing_timer(self.note) + self._check_and_update_duplicate_display_async() + + # focused into field? + elif cmd.startswith("focus"): + (type, num) = cmd.split(":", 1) + self.last_field_index = self.currentField = int(num) + gui_hooks.editor_did_focus_field(self.note, self.currentField) + + elif cmd.startswith("toggleStickyAll"): + model = self.note_type() + flds = model["flds"] + + any_sticky = any([fld["sticky"] for fld in flds]) + result = [] + for fld in flds: + if not any_sticky or fld["sticky"]: + fld["sticky"] = not fld["sticky"] + + result.append(fld["sticky"]) + + update_notetype_legacy(parent=self.mw, notetype=model).run_in_background( + initiator=self + ) + + return result + + elif cmd.startswith("toggleSticky"): + (type, num) = cmd.split(":", 1) + ord = int(num) + + model = self.note_type() + fld = model["flds"][ord] + new_state = not fld["sticky"] + fld["sticky"] = new_state + + update_notetype_legacy(parent=self.mw, notetype=model).run_in_background( + initiator=self + ) + + return new_state + + elif cmd.startswith("lastTextColor"): + (_, textColor) = cmd.split(":", 1) + assert self.mw.pm.profile is not None + self.mw.pm.profile["lastTextColor"] = textColor + + elif cmd.startswith("lastHighlightColor"): + (_, highlightColor) = cmd.split(":", 1) + assert self.mw.pm.profile is not None + self.mw.pm.profile["lastHighlightColor"] = highlightColor + + elif cmd.startswith("saveTags"): + (type, tagsJson) = cmd.split(":", 1) + self.note.tags = json.loads(tagsJson) + + gui_hooks.editor_did_update_tags(self.note) + if not self.addMode: + self._save_current_note() + + elif cmd.startswith("setTagsCollapsed"): + (type, collapsed_string) = cmd.split(":", 1) + collapsed = collapsed_string == "true" + self.setTagsCollapsed(collapsed) + + elif cmd.startswith("editorState"): + (_, new_state_id, old_state_id) = cmd.split(":", 2) + self.signal_state_change( + EditorState(int(new_state_id)), EditorState(int(old_state_id)) + ) + + elif cmd.startswith("ioImageLoaded"): + (_, path_or_nid_data) = cmd.split(":", 1) + path_or_nid = json.loads(path_or_nid_data) + if self.addMode: + gui_hooks.editor_mask_editor_did_load_image(self, path_or_nid) + else: + gui_hooks.editor_mask_editor_did_load_image( + self, NoteId(int(path_or_nid)) + ) + + elif cmd in self._links: + return self._links[cmd](self) + + else: + print("uncaught cmd", cmd) + + def mungeHTML(self, txt: str) -> str: + return gui_hooks.editor_will_munge_html(txt, self) + + def signal_state_change( + self, new_state: EditorState, old_state: EditorState + ) -> None: + self.state = new_state + gui_hooks.editor_state_did_change(self, new_state, old_state) + + # Setting/unsetting the current note + ###################################################################### + + def set_note( + self, + note: Note | None, + hide: bool = True, + focusTo: int | None = None, + ) -> None: + "Make NOTE the current note." + self.note = note + self.currentField = None + if self.note: + self.loadNote(focusTo=focusTo) + elif hide: + self.widget.hide() + + def loadNoteKeepingFocus(self) -> None: + self.loadNote(self.currentField) + + def set_cloze_button(self) -> None: + action = "show" if self.note_type()["type"] == MODEL_CLOZE else "hide" + self.web.eval( + 'require("anki/ui").loaded.then(() =>' + f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")' + "); " + ) + + def set_image_occlusion_button(self) -> None: + action = "show" if self.current_notetype_is_image_occlusion() else "hide" + self.web.eval( + 'require("anki/ui").loaded.then(() =>' + f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("image-occlusion-button")' + "); " + ) + + def loadNote(self, focusTo: int | None = None) -> None: + if not self.note: + return + + data = [ + (fld, self.mw.col.media.escape_media_filenames(val)) + for fld, val in self.note.items() + ] + + note_type = self.note_type() + flds = note_type["flds"] + collapsed = [fld["collapsed"] for fld in flds] + cloze_fields_ords = self.mw.col.models.cloze_fields(self.note.mid) + cloze_fields = [ord in cloze_fields_ords for ord in range(len(flds))] + plain_texts = [fld.get("plainText", False) for fld in flds] + descriptions = [fld.get("description", "") for fld in flds] + notetype_meta = {"id": self.note.mid, "modTime": note_type["mod"]} + + self.widget.show() + + note_fields_status = self.note.fields_check() + + def oncallback(arg: Any) -> None: + if not self.note: + return + self.setupForegroundButton() + # we currently do this synchronously to ensure we load before the + # sidebar on browser startup + self._update_duplicate_display(note_fields_status) + if focusTo is not None: + self.web.setFocus() + self.set_cloze_button() + self.set_image_occlusion_button() + gui_hooks.editor_did_load_note(self) + + assert self.mw.pm.profile is not None + text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff") + highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#0000ff") + + js = f""" + saveSession(); + setFields({json.dumps(data)}); + setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())}); + setNotetypeMeta({json.dumps(notetype_meta)}); + setCollapsed({json.dumps(collapsed)}); + setClozeFields({json.dumps(cloze_fields)}); + setPlainTexts({json.dumps(plain_texts)}); + setDescriptions({json.dumps(descriptions)}); + setFonts({json.dumps(self.fonts())}); + focusField({json.dumps(focusTo)}); + setNoteId({json.dumps(self.note.id)}); + setColorButtons({json.dumps([text_color, highlight_color])}); + setTags({json.dumps(self.note.tags)}); + setTagsCollapsed({json.dumps(self.mw.pm.tags_collapsed(self.editorMode))}); + setMathjaxEnabled({json.dumps(self.mw.col.get_config("renderMathjax", True))}); + setShrinkImages({json.dumps(self.mw.col.get_config("shrinkEditorImages", True))}); + setCloseHTMLTags({json.dumps(self.mw.col.get_config("closeHTMLTags", True))}); + triggerChanges(); + """ + + if self.addMode: + sticky = [field["sticky"] for field in self.note_type()["flds"]] + js += " setSticky(%s);" % json.dumps(sticky) + + if self.current_notetype_is_image_occlusion(): + io_field_indices = self.mw.backend.get_image_occlusion_fields(self.note.mid) + image_field = self.note.fields[io_field_indices.image] + self.last_io_image_path = self.extract_img_path_from_html(image_field) + + if self.editorMode is not EditorMode.ADD_CARDS: + io_options = self._create_edit_io_options(note_id=self.note.id) + js += " setupMaskEditor(%s);" % json.dumps(io_options) + elif orig_note_id := self.orig_note_id: + self.orig_note_id = None + io_options = self._create_clone_io_options(orig_note_id) + js += " setupMaskEditor(%s);" % json.dumps(io_options) + + js = gui_hooks.editor_will_load_note(js, self.note, self) + self.web.evalWithCallback( + f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback + ) + + def _save_current_note(self) -> None: + "Call after note is updated with data from webview." + if not self.note: + return + + update_note(parent=self.widget, note=self.note).run_in_background( + initiator=self + ) + + def fonts(self) -> list[tuple[str, int, bool]]: + return [ + (gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"]) + for f in self.note_type()["flds"] + ] + + def call_after_note_saved( + self, callback: Callable, keepFocus: bool = False + ) -> None: + "Save unsaved edits then call callback()." + if not self.note: + # calling code may not expect the callback to fire immediately + self.mw.progress.single_shot(10, callback) + return + self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback()) + + saveNow = call_after_note_saved + + def _check_and_update_duplicate_display_async(self) -> None: + note = self.note + if not note: + return + + def on_done(result: NoteFieldsCheckResult.V) -> None: + if self.note != note: + return + self._update_duplicate_display(result) + + QueryOp( + parent=self.parentWindow, + op=lambda _: note.fields_check(), + success=on_done, + ).run_in_background() + + checkValid = _check_and_update_duplicate_display_async + + def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None: + assert self.note is not None + cols = [""] * len(self.note.fields) + cloze_hint = "" + if result == NoteFieldsCheckResult.DUPLICATE: + cols[0] = "dupe" + elif result == NoteFieldsCheckResult.NOTETYPE_NOT_CLOZE: + cloze_hint = tr.adding_cloze_outside_cloze_notetype() + elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE: + cloze_hint = tr.adding_cloze_outside_cloze_field() + + self.web.eval( + 'require("anki/ui").loaded.then(() => {' + f"setBackgrounds({json.dumps(cols)});\n" + f"setClozeHint({json.dumps(cloze_hint)});\n" + "}); " + ) + + def showDupes(self) -> None: + assert self.note is not None + aqt.dialogs.open( + "Browser", + self.mw, + search=( + SearchNode( + dupe=SearchNode.Dupe( + notetype_id=self.note_type()["id"], + first_field=self.note.fields[0], + ) + ), + ), + ) + + def fieldsAreBlank(self, previousNote: Note | None = None) -> bool: + if not self.note: + return True + m = self.note_type() + for c, f in enumerate(self.note.fields): + f = f.replace("
", "").strip() + notChangedvalues = {"", "
"} + if previousNote and m["flds"][c]["sticky"]: + notChangedvalues.add(previousNote.fields[c].replace("
", "").strip()) + if f not in notChangedvalues: + return False + return True + + def cleanup(self) -> None: + av_player.stop_and_clear_queue_if_caller(self.editorMode) + self.set_note(None) + # prevent any remaining evalWithCallback() events from firing after C++ object deleted + if self.web: + self.web.cleanup() + self.web = None # type: ignore + + # legacy + + setNote = set_note + + # Tag handling + ###################################################################### + + def setupTags(self) -> None: + import aqt.tagedit + + g = QGroupBox(self.widget) + g.setStyleSheet("border: 0") + tb = QGridLayout() + tb.setSpacing(12) + tb.setContentsMargins(2, 6, 2, 6) + # tags + l = QLabel(tr.editing_tags()) + tb.addWidget(l, 1, 0) + self.tags = aqt.tagedit.TagEdit(self.widget) + qconnect(self.tags.lostFocus, self.on_tag_focus_lost) + self.tags.setToolTip(shortcut(tr.editing_jump_to_tags_with_ctrlandshiftandt())) + border = theme_manager.var(colors.BORDER) + self.tags.setStyleSheet(f"border: 1px solid {border}") + tb.addWidget(self.tags, 1, 1) + g.setLayout(tb) + self.outerLayout.addWidget(g) + + def updateTags(self) -> None: + if self.tags.col != self.mw.col: + self.tags.setCol(self.mw.col) + if not self.tags.text() or not self.addMode: + assert self.note is not None + self.tags.setText(self.note.string_tags().strip()) + + def on_tag_focus_lost(self) -> None: + assert self.note is not None + self.note.tags = self.mw.col.tags.split(self.tags.text()) + gui_hooks.editor_did_update_tags(self.note) + if not self.addMode: + self._save_current_note() + + def blur_tags_if_focused(self) -> None: + if not self.note: + return + if self.tags.hasFocus(): + self.widget.setFocus() + + def hideCompleters(self) -> None: + self.tags.hideCompleter() + + def onFocusTags(self) -> None: + self.tags.setFocus() + + # legacy + + def saveAddModeVars(self) -> None: + pass + + saveTags = blur_tags_if_focused + + # Audio/video/images + ###################################################################### + + def onAddMedia(self) -> None: + """Show a file selection screen, then add the selected media. + This expects initial setup to have been done by TemplateButtons.svelte.""" + extension_filter = " ".join( + f"*.{extension}" for extension in sorted(itertools.chain(pics, audio)) + ) + filter = f"{tr.editing_media()} ({extension_filter})" + + def accept(file: str) -> None: + self.resolve_media(file) + + getFile( + parent=self.widget, + title=tr.editing_add_media(), + cb=cast(Callable[[Any], None], accept), + filter=filter, + key="media", + ) + + self.parentWindow.activateWindow() + + def addMedia(self, path: str, canDelete: bool = False) -> None: + """Legacy routine used by add-ons to add a media file and update the current field. + canDelete is ignored.""" + + try: + html = self._addMedia(path) + except Exception as e: + showWarning(str(e)) + return + + self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});") + + def resolve_media(self, path: str) -> None: + """Finish inserting media into a field. + This expects initial setup to have been done by TemplateButtons.svelte.""" + try: + html = self._addMedia(path) + except Exception as e: + showWarning(str(e)) + return + + self.web.eval( + f'require("anki/TemplateButtons").resolveMedia({json.dumps(html)})' + ) + + def _addMedia(self, path: str, canDelete: bool = False) -> str: + """Add to media folder and return local img or sound tag.""" + # copy to media folder + fname = self.mw.col.media.add_file(path) + # return a local html link + return self.fnameToLink(fname) + + def _addMediaFromData(self, fname: str, data: bytes) -> str: + return self.mw.col.media._legacy_write_data(fname, data) + + def onRecSound(self) -> None: + aqt.sound.record_audio( + self.parentWindow, + self.mw, + True, + self.resolve_media, + ) + + # Media downloads + ###################################################################### + + def urlToLink(self, url: str, allowed_suffixes: Iterable[str] = ()) -> str: + fname = ( + self.urlToFile(url, allowed_suffixes) + if allowed_suffixes + else self.urlToFile(url) + ) + if not fname: + return '{}'.format( + url, html.escape(urllib.parse.unquote(url)) + ) + return self.fnameToLink(fname) + + def fnameToLink(self, fname: str) -> str: + ext = fname.split(".")[-1].lower() + if ext in pics: + name = urllib.parse.quote(fname.encode("utf8")) + return f'' + else: + av_player.play_file_with_caller(fname, self.editorMode) + return f"[sound:{html.escape(fname, quote=False)}]" + + def urlToFile( + self, url: str, allowed_suffixes: Iterable[str] = pics + audio + ) -> str | None: + l = url.lower() + for suffix in allowed_suffixes: + if l.endswith(f".{suffix}"): + return self._retrieveURL(url) + # not a supported type + return None + + def isURL(self, s: str) -> bool: + s = s.lower() + return ( + s.startswith("http://") + or s.startswith("https://") + or s.startswith("ftp://") + or s.startswith("file://") + ) + + def inlinedImageToFilename(self, txt: str) -> str: + prefix = "data:image/" + suffix = ";base64," + for ext in ("jpg", "jpeg", "png", "gif"): + fullPrefix = prefix + ext + suffix + if txt.startswith(fullPrefix): + b64data = txt[len(fullPrefix) :].strip() + data = base64.b64decode(b64data, validate=True) + if ext == "jpeg": + ext = "jpg" + return self._addPastedImage(data, ext) + + return "" + + def inlinedImageToLink(self, src: str) -> str: + fname = self.inlinedImageToFilename(src) + if fname: + return self.fnameToLink(fname) + + return "" + + def _pasted_image_filename(self, data: bytes, ext: str) -> str: + csum = checksum(data) + return f"paste-{csum}.{ext}" + + def _read_pasted_image(self, mime: QMimeData) -> str: + image = QImage(mime.imageData()) + buffer = QBuffer() + buffer.open(QBuffer.OpenModeFlag.ReadWrite) + if self.mw.col.get_config_bool(Config.Bool.PASTE_IMAGES_AS_PNG): + ext = "png" + quality = 50 + else: + ext = "jpg" + quality = 80 + image.save(buffer, ext, quality) + buffer.reset() + data = bytes(buffer.readAll()) # type: ignore + fname = self._pasted_image_filename(data, ext) + path = namedtmp(fname) + with open(path, "wb") as file: + file.write(data) + + return path + + def _addPastedImage(self, data: bytes, ext: str) -> str: + # hash and write + fname = self._pasted_image_filename(data, ext) + return self._addMediaFromData(fname, data) + + def _retrieveURL(self, url: str) -> str | None: + "Download file into media folder and return local filename or None." + local = url.lower().startswith("file://") + # fetch it into a temporary folder + self.mw.progress.start(immediate=not local, parent=self.parentWindow) + content_type = None + error_msg: str | None = None + try: + if local: + # urllib doesn't understand percent-escaped utf8, but requires things like + # '#' to be escaped. + url = urllib.parse.unquote(url) + url = url.replace("%", "%25") + url = url.replace("#", "%23") + req = urllib.request.Request( + url, None, {"User-Agent": "Mozilla/5.0 (compatible; Anki)"} + ) + with urllib.request.urlopen(req) as response: + filecontents = response.read() + else: + with HttpClient() as client: + client.timeout = 30 + with client.get(url) as response: + if response.status_code != 200: + error_msg = tr.qt_misc_unexpected_response_code( + val=response.status_code, + ) + return None + filecontents = response.content + content_type = response.headers.get("content-type") + except (urllib.error.URLError, requests.exceptions.RequestException) as e: + error_msg = tr.editing_an_error_occurred_while_opening(val=str(e)) + return None + finally: + self.mw.progress.finish() + if error_msg: + showWarning(error_msg) + # strip off any query string + url = re.sub(r"\?.*?$", "", url) + fname = os.path.basename(urllib.parse.unquote(url)) + if not fname.strip(): + fname = "paste" + if content_type: + fname = self.mw.col.media.add_extension_based_on_mime(fname, content_type) + + return self.mw.col.media.write_data(fname, filecontents) + + # Paste/drag&drop + ###################################################################### + + removeTags = ["script", "iframe", "object", "style"] + + def _pastePreFilter(self, html: str, internal: bool) -> str: + # https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx + if html.find(">") < 0: + return html + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + doc = BeautifulSoup(html, "html.parser") + + if not internal: + for tag_name in self.removeTags: + for node in doc(tag_name): + node.decompose() + + # convert p tags to divs + for node in doc("p"): + if hasattr(node, "name"): + node.name = "div" + + for element in doc("img"): + if not isinstance(element, bs4.Tag): + continue + tag = element + try: + src = tag["src"] + except KeyError: + # for some bizarre reason, mnemosyne removes src elements + # from missing media + continue + + # in internal pastes, rewrite mediasrv references to relative + if internal: + m = re.match(r"http://127.0.0.1:\d+/(.*)$", str(src)) + if m: + tag["src"] = m.group(1) + # in external pastes, download remote media + elif isinstance(src, str) and self.isURL(src): + fname = self._retrieveURL(src) + if fname: + tag["src"] = fname + elif isinstance(src, str) and src.startswith("data:image/"): + # and convert inlined data + tag["src"] = self.inlinedImageToFilename(str(src)) + + html = str(doc) + return html + + def doPaste(self, html: str, internal: bool, extended: bool = False) -> None: + html = self._pastePreFilter(html, internal) + if extended: + ext = "true" + else: + ext = "false" + self.web.eval(f"pasteHTML({json.dumps(html)}, {json.dumps(internal)}, {ext});") + gui_hooks.editor_did_paste(self, html, internal, extended) + + def doDrop( + self, html: str, internal: bool, extended: bool, cursor_pos: QPoint + ) -> None: + def pasteIfField(ret: bool) -> None: + if ret: + self.doPaste(html, internal, extended) + + zoom = self.web.zoomFactor() + x, y = int(cursor_pos.x() / zoom), int(cursor_pos.y() / zoom) + + self.web.evalWithCallback(f"focusIfField({x}, {y});", pasteIfField) + + def onPaste(self) -> None: + self.web.onPaste() + + def onCutOrCopy(self) -> None: + self.web.user_cut_or_copied() + + # Image occlusion + ###################################################################### + + def current_notetype_is_image_occlusion(self) -> bool: + if not self.note: + return False + + return ( + self.note_type().get("originalStockKind", None) + == StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION + ) + + def setup_mask_editor(self, image_path: str) -> None: + try: + if self.editorMode == EditorMode.ADD_CARDS: + self.setup_mask_editor_for_new_note( + image_path=image_path, notetype_id=0 + ) + else: + assert self.note is not None + self.setup_mask_editor_for_existing_note( + note_id=self.note.id, image_path=image_path + ) + except Exception as e: + showWarning(str(e)) + + def select_image_and_occlude(self) -> None: + """Show a file selection screen, then get selected image path.""" + extension_filter = " ".join( + f"*.{extension}" for extension in sorted(itertools.chain(pics)) + ) + filter = f"{tr.editing_media()} ({extension_filter})" + + getFile( + parent=self.widget, + title=tr.editing_add_media(), + cb=cast(Callable[[Any], None], self.setup_mask_editor), + filter=filter, + key="media", + ) + + self.parentWindow.activateWindow() + + def extract_img_path_from_html(self, html: str) -> str | None: + assert self.note is not None + # with allowed_suffixes=pics, all non-pics will be rendered as s and won't be included here + if not (images := self.mw.col.media.files_in_str(self.note.mid, html)): + return None + image_path = urllib.parse.unquote(images[0]) + return os.path.join(self.mw.col.media.dir(), image_path) + + def select_image_from_clipboard_and_occlude(self) -> None: + """Set up the mask editor for the image in the clipboard.""" + + clipboard = self.mw.app.clipboard() + assert clipboard is not None + mime = clipboard.mimeData() + assert mime is not None + # try checking for urls first, fallback to image data + if ( + (html := self.web._processUrls(mime, allowed_suffixes=pics)) + and (path := self.extract_img_path_from_html(html)) + ) or (mime.hasImage() and (path := self._read_pasted_image(mime))): + self.setup_mask_editor(path) + self.parentWindow.activateWindow() + else: + showWarning(tr.editing_no_image_found_on_clipboard()) + return + + def setup_mask_editor_for_new_note( + self, + image_path: str, + notetype_id: NotetypeId | int = 0, + ): + """Set-up IO mask editor for adding new notes + Presupposes that active editor notetype is an image occlusion notetype + Args: + image_path: Absolute path to image. + notetype_id: ID of note type to use. Provided ID must belong to an + image occlusion notetype. Set this to 0 to auto-select the first + found image occlusion notetype in the user's collection. + """ + image_field_html = self._addMedia(image_path) + self.last_io_image_path = self.extract_img_path_from_html(image_field_html) + io_options = self._create_add_io_options( + image_path=image_path, + image_field_html=image_field_html, + notetype_id=notetype_id, + ) + self._setup_mask_editor(io_options) + + def setup_mask_editor_for_existing_note( + self, note_id: NoteId, image_path: str | None = None + ): + """Set-up IO mask editor for editing existing notes + Presupposes that active editor notetype is an image occlusion notetype + Args: + note_id: ID of note to edit. + image_path: (Optional) Absolute path to image that should replace current + image + """ + io_options = self._create_edit_io_options(note_id) + if image_path: + image_field_html = self._addMedia(image_path) + self.last_io_image_path = self.extract_img_path_from_html(image_field_html) + self.web.eval(f"resetIOImage({json.dumps(image_path)})") + self.web.eval(f"setImageField({json.dumps(image_field_html)})") + self._setup_mask_editor(io_options) + + def reset_image_occlusion(self) -> None: + self.web.eval("resetIOImageLoaded()") + + def update_occlusions_field(self) -> None: + self.web.eval("saveOcclusions()") + + def _setup_mask_editor(self, io_options: dict): + self.web.eval( + 'require("anki/ui").loaded.then(() =>' + f"setupMaskEditor({json.dumps(io_options)})" + "); " + ) + + @staticmethod + def _create_add_io_options( + image_path: str, image_field_html: str, notetype_id: NotetypeId | int = 0 + ) -> dict: + return { + "mode": {"kind": "add", "imagePath": image_path, "notetypeId": notetype_id}, + "html": image_field_html, + } + + @staticmethod + def _create_clone_io_options(orig_note_id: NoteId) -> dict: + return { + "mode": {"kind": "add", "clonedNoteId": orig_note_id}, + } + + @staticmethod + def _create_edit_io_options(note_id: NoteId) -> dict: + return {"mode": {"kind": "edit", "noteId": note_id}} + + # Legacy editing routines + ###################################################################### + + _js_legacy = "this routine has been moved into JS, and will be removed soon" + + @deprecated(info=_js_legacy) + def onHtmlEdit(self) -> None: + field = self.currentField + self.call_after_note_saved(lambda: self._onHtmlEdit(field)) + + @deprecated(info=_js_legacy) + def _onHtmlEdit(self, field: int) -> None: + assert self.note is not None + d = QDialog(self.widget, Qt.WindowType.Window) + form = aqt.forms.edithtml.Ui_Dialog() + form.setupUi(d) + restoreGeom(d, "htmlEditor") + disable_help_button(d) + qconnect( + form.buttonBox.helpRequested, lambda: openHelp(HelpPage.EDITING_FEATURES) + ) + font = QFont("Courier") + font.setStyleHint(QFont.StyleHint.TypeWriter) + form.textEdit.setFont(font) + form.textEdit.setPlainText(self.note.fields[field]) + d.show() + form.textEdit.moveCursor(QTextCursor.MoveOperation.End) + d.exec() + html = form.textEdit.toPlainText() + if html.find(">") > -1: + # filter html through beautifulsoup so we can strip out things like a + # leading
+ html_escaped = self.mw.col.media.escape_media_filenames(html) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + html_escaped = str(BeautifulSoup(html_escaped, "html.parser")) + html = self.mw.col.media.escape_media_filenames( + html_escaped, unescape=True + ) + self.note.fields[field] = html + if not self.addMode: + self._save_current_note() + self.loadNote(focusTo=field) + saveGeom(d, "htmlEditor") + + @deprecated(info=_js_legacy) + def toggleBold(self) -> None: + self.web.eval("setFormat('bold');") + + @deprecated(info=_js_legacy) + def toggleItalic(self) -> None: + self.web.eval("setFormat('italic');") + + @deprecated(info=_js_legacy) + def toggleUnderline(self) -> None: + self.web.eval("setFormat('underline');") + + @deprecated(info=_js_legacy) + def toggleSuper(self) -> None: + self.web.eval("setFormat('superscript');") + + @deprecated(info=_js_legacy) + def toggleSub(self) -> None: + self.web.eval("setFormat('subscript');") + + @deprecated(info=_js_legacy) + def removeFormat(self) -> None: + self.web.eval("setFormat('removeFormat');") + + @deprecated(info=_js_legacy) + def onCloze(self) -> None: + self.call_after_note_saved(self._onCloze, keepFocus=True) + + @deprecated(info=_js_legacy) + def _onCloze(self) -> None: + # check that the model is set up for cloze deletion + if self.note_type()["type"] != MODEL_CLOZE: + if self.addMode: + tooltip(tr.editing_warning_cloze_deletions_will_not_work()) + else: + showInfo(tr.editing_to_make_a_cloze_deletion_on()) + return + # find the highest existing cloze + highest = 0 + assert self.note is not None + for _, val in list(self.note.items()): + m = re.findall(r"\{\{c(\d+)::", val) + if m: + highest = max(highest, sorted(int(x) for x in m)[-1]) + # reuse last? + if not KeyboardModifiersPressed().alt: + highest += 1 + # must start at 1 + highest = max(1, highest) + self.web.eval("wrap('{{c%d::', '}}');" % highest) + + def setupForegroundButton(self) -> None: + assert self.mw.pm.profile is not None + self.fcolour = self.mw.pm.profile.get("lastColour", "#00f") + + # use last colour + @deprecated(info=_js_legacy) + def onForeground(self) -> None: + self._wrapWithColour(self.fcolour) + + # choose new colour + @deprecated(info=_js_legacy) + def onChangeCol(self) -> None: + if is_lin: + new = QColorDialog.getColor( + QColor(self.fcolour), + None, + None, + QColorDialog.ColorDialogOption.DontUseNativeDialog, + ) + else: + new = QColorDialog.getColor(QColor(self.fcolour), None) + # native dialog doesn't refocus us for some reason + self.parentWindow.activateWindow() + if new.isValid(): + self.fcolour = new.name() + self.onColourChanged() + self._wrapWithColour(self.fcolour) + + @deprecated(info=_js_legacy) + def _updateForegroundButton(self) -> None: + pass + + @deprecated(info=_js_legacy) + def onColourChanged(self) -> None: + self._updateForegroundButton() + assert self.mw.pm.profile is not None + self.mw.pm.profile["lastColour"] = self.fcolour + + @deprecated(info=_js_legacy) + def _wrapWithColour(self, colour: str) -> None: + self.web.eval(f"setFormat('forecolor', '{colour}')") + + @deprecated(info=_js_legacy) + def onAdvanced(self) -> None: + m = QMenu(self.mw) + + for text, handler, shortcut in ( + (tr.editing_mathjax_inline(), self.insertMathjaxInline, "Ctrl+M, M"), + (tr.editing_mathjax_block(), self.insertMathjaxBlock, "Ctrl+M, E"), + ( + tr.editing_mathjax_chemistry(), + self.insertMathjaxChemistry, + "Ctrl+M, C", + ), + (tr.editing_latex(), self.insertLatex, "Ctrl+T, T"), + (tr.editing_latex_equation(), self.insertLatexEqn, "Ctrl+T, E"), + (tr.editing_latex_math_env(), self.insertLatexMathEnv, "Ctrl+T, M"), + (tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"), + ): + a = m.addAction(text) + assert a is not None + qconnect(a.triggered, handler) + a.setShortcut(QKeySequence(shortcut)) + + qtMenuShortcutWorkaround(m) + + m.exec(QCursor.pos()) + + @deprecated(info=_js_legacy) + def insertLatex(self) -> None: + self.web.eval("wrap('[latex]', '[/latex]');") + + @deprecated(info=_js_legacy) + def insertLatexEqn(self) -> None: + self.web.eval("wrap('[$]', '[/$]');") + + @deprecated(info=_js_legacy) + def insertLatexMathEnv(self) -> None: + self.web.eval("wrap('[$$]', '[/$$]');") + + @deprecated(info=_js_legacy) + def insertMathjaxInline(self) -> None: + self.web.eval("wrap('\\\\(', '\\\\)');") + + @deprecated(info=_js_legacy) + def insertMathjaxBlock(self) -> None: + self.web.eval("wrap('\\\\[', '\\\\]');") + + @deprecated(info=_js_legacy) + def insertMathjaxChemistry(self) -> None: + self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');") + + def toggleMathjax(self) -> None: + self.mw.col.set_config( + "renderMathjax", not self.mw.col.get_config("renderMathjax", False) + ) + # hackily redraw the page + self.setupWeb() + self.loadNoteKeepingFocus() + + def toggleShrinkImages(self) -> None: + self.mw.col.set_config( + "shrinkEditorImages", + not self.mw.col.get_config("shrinkEditorImages", True), + ) + + def toggleCloseHTMLTags(self) -> None: + self.mw.col.set_config( + "closeHTMLTags", + not self.mw.col.get_config("closeHTMLTags", True), + ) + + def setTagsCollapsed(self, collapsed: bool) -> None: + aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed) + + # Links from HTML + ###################################################################### + + def _init_links(self) -> None: + self._links: dict[str, Callable] = dict( + fields=Editor.onFields, + cards=Editor.onCardLayout, + bold=Editor.toggleBold, + italic=Editor.toggleItalic, + underline=Editor.toggleUnderline, + super=Editor.toggleSuper, + sub=Editor.toggleSub, + clear=Editor.removeFormat, + colour=Editor.onForeground, + changeCol=Editor.onChangeCol, + cloze=Editor.onCloze, + attach=Editor.onAddMedia, + record=Editor.onRecSound, + more=Editor.onAdvanced, + dupes=Editor.showDupes, + paste=Editor.onPaste, + cutOrCopy=Editor.onCutOrCopy, + htmlEdit=Editor.onHtmlEdit, + mathjaxInline=Editor.insertMathjaxInline, + mathjaxBlock=Editor.insertMathjaxBlock, + mathjaxChemistry=Editor.insertMathjaxChemistry, + toggleMathjax=Editor.toggleMathjax, + toggleShrinkImages=Editor.toggleShrinkImages, + toggleCloseHTMLTags=Editor.toggleCloseHTMLTags, + addImageForOcclusion=Editor.select_image_and_occlude, + addImageForOcclusionFromClipboard=Editor.select_image_from_clipboard_and_occlude, + ) + + def note_type(self) -> NotetypeDict: + assert self.note is not None + note_type = self.note.note_type() + assert note_type is not None + return note_type + + +# Pasting, drag & drop, and keyboard layouts +###################################################################### + + +class EditorWebView(AnkiWebView): + def __init__(self, parent: QWidget, editor: Editor) -> None: + AnkiWebView.__init__(self, kind=AnkiWebViewKind.EDITOR) + self.editor = editor + self.setAcceptDrops(True) + self._store_field_content_on_next_clipboard_change = False + # when we detect the user copying from a field, we store the content + # here, and use it when they paste, so we avoid filtering field content + self._internal_field_text_for_paste: str | None = None + self._last_known_clipboard_mime: QMimeData | None = None + clip = self.editor.mw.app.clipboard() + assert clip is not None + clip.dataChanged.connect(self._on_clipboard_change) + gui_hooks.editor_web_view_did_init(self) + + def user_cut_or_copied(self) -> None: + self._store_field_content_on_next_clipboard_change = True + self._internal_field_text_for_paste = None + + def _on_clipboard_change( + self, mode: QClipboard.Mode = QClipboard.Mode.Clipboard + ) -> None: + self._last_known_clipboard_mime = self._clipboard().mimeData(mode) + if self._store_field_content_on_next_clipboard_change: + # if the flag was set, save the field data + self._internal_field_text_for_paste = self._get_clipboard_html_for_field( + mode + ) + self._store_field_content_on_next_clipboard_change = False + elif self._internal_field_text_for_paste != self._get_clipboard_html_for_field( + mode + ): + # if we've previously saved the field, blank it out if the clipboard state has changed + self._internal_field_text_for_paste = None + + def _get_clipboard_html_for_field(self, mode: QClipboard.Mode) -> str | None: + clip = self._clipboard() + if not (mime := clip.mimeData(mode)): + return None + if not mime.hasHtml(): + return None + return mime.html() + + def onCut(self) -> None: + self.triggerPageAction(QWebEnginePage.WebAction.Cut) + + def onCopy(self) -> None: + self.triggerPageAction(QWebEnginePage.WebAction.Copy) + + def on_copy_image(self) -> None: + self.triggerPageAction(QWebEnginePage.WebAction.CopyImageToClipboard) + + def _opened_context_menu_on_image(self) -> bool: + if not hasattr(self, "lastContextMenuRequest"): + return False + context_menu_request = self.lastContextMenuRequest() + assert context_menu_request is not None + return ( + context_menu_request.mediaType() + == context_menu_request.MediaType.MediaTypeImage + ) + + def _wantsExtendedPaste(self) -> bool: + strip_html = self.editor.mw.col.get_config_bool( + Config.Bool.PASTE_STRIPS_FORMATTING + ) + if KeyboardModifiersPressed().shift: + strip_html = not strip_html + return not strip_html + + def _onPaste(self, mode: QClipboard.Mode) -> None: + # Since _on_clipboard_change doesn't always trigger properly on macOS, we do a double check if any changes were made before pasting + clipboard = self._clipboard() + if self._last_known_clipboard_mime != clipboard.mimeData(mode): + self._on_clipboard_change(mode) + extended = self._wantsExtendedPaste() + if html := self._internal_field_text_for_paste: + print("reuse internal") + self.editor.doPaste(html, True, extended) + else: + if not (mime := clipboard.mimeData(mode=mode)): + return + print("use clipboard") + html, internal = self._processMime(mime, extended) + if html: + self.editor.doPaste(html, internal, extended) + + def onPaste(self) -> None: + self._onPaste(QClipboard.Mode.Clipboard) + + def onMiddleClickPaste(self) -> None: + self._onPaste(QClipboard.Mode.Selection) + + def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None: + assert evt is not None + evt.accept() + + def dropEvent(self, evt: QDropEvent | None) -> None: + assert evt is not None + extended = self._wantsExtendedPaste() + mime = evt.mimeData() + assert mime is not None + + if ( + self.editor.state is EditorState.IO_PICKER + and (html := self._processUrls(mime, allowed_suffixes=pics)) + and (path := self.editor.extract_img_path_from_html(html)) + ): + self.editor.setup_mask_editor(path) + return + + evt_pos = evt.position() + cursor_pos = QPoint(int(evt_pos.x()), int(evt_pos.y())) + + if evt.source() and mime.hasHtml(): + # don't filter html from other fields + html, internal = mime.html(), True + else: + html, internal = self._processMime(mime, extended, drop_event=True) + + if not html: + return + + self.editor.doDrop(html, internal, extended, cursor_pos) + + # returns (html, isInternal) + def _processMime( + self, mime: QMimeData, extended: bool = False, drop_event: bool = False + ) -> tuple[str, bool]: + # print("html=%s image=%s urls=%s txt=%s" % ( + # mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText())) + # print("html", mime.html()) + # print("urls", mime.urls()) + # print("text", mime.text()) + + internal = False + + mime = gui_hooks.editor_will_process_mime( + mime, self, internal, extended, drop_event + ) + + # try various content types in turn + if mime.hasHtml(): + html_content = mime.html()[11:] if internal else mime.html() + return html_content, internal + + # given _processUrls' extra allowed_suffixes kwarg, placate the typechecker + def process_url(mime: QMimeData, extended: bool = False) -> str | None: + return self._processUrls(mime, extended) + + # favour url if it's a local link + if ( + mime.hasUrls() + and (urls := mime.urls()) + and urls[0].toString().startswith("file://") + ): + types = (process_url, self._processImage, self._processText) + else: + types = (self._processImage, process_url, self._processText) + + for fn in types: + html = fn(mime, extended) + if html: + return html, True + return "", False + + def _processUrls( + self, + mime: QMimeData, + extended: bool = False, + allowed_suffixes: Iterable[str] = (), + ) -> str | None: + if not mime.hasUrls(): + return None + + buf = "" + for qurl in mime.urls(): + url = qurl.toString() + # chrome likes to give us the URL twice with a \n + if lines := url.splitlines(): + url = lines[0] + buf += self.editor.urlToLink(url, allowed_suffixes=allowed_suffixes) + + return buf + + def _processText(self, mime: QMimeData, extended: bool = False) -> str | None: + if not mime.hasText(): + return None + + txt = mime.text() + processed = [] + lines = txt.split("\n") + + for line in lines: + for token in re.split(r"(\S+)", line): + # inlined data in base64? + if extended and token.startswith("data:image/"): + processed.append(self.editor.inlinedImageToLink(token)) + elif extended and self.editor.isURL(token): + # if the user is pasting an image or sound link, convert it to local, otherwise paste as a hyperlink + link = self.editor.urlToLink(token) + processed.append(link) + else: + token = html.escape(token).replace("\t", " " * 4) + + # if there's more than one consecutive space, + # use non-breaking spaces for the second one on + def repl(match: Match) -> str: + return f"{match.group(1).replace(' ', ' ')} " + + token = re.sub(" ( +)", repl, token) + processed.append(token) + + processed.append("
") + # remove last
+ processed.pop() + return "".join(processed) + + def _processImage(self, mime: QMimeData, extended: bool = False) -> str | None: + if not mime.hasImage(): + return None + path = self.editor._read_pasted_image(mime) + fname = self.editor._addMedia(path) + + return fname + + def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None: + m = QMenu(self) + if self.hasSelection(): + self._add_cut_action(m) + self._add_copy_action(m) + a = m.addAction(tr.editing_paste()) + assert a is not None + qconnect(a.triggered, self.onPaste) + if self.editor.state is EditorState.IO_MASKS and ( + path := self.editor.last_io_image_path + ): + self._add_image_menu_with_path(m, path) + elif self._opened_context_menu_on_image(): + self._add_image_menu(m) + gui_hooks.editor_will_show_context_menu(self, m) + m.popup(QCursor.pos()) + + def _add_cut_action(self, menu: QMenu) -> None: + a = menu.addAction(tr.editing_cut()) + assert a is not None + qconnect(a.triggered, self.onCut) + + def _add_copy_action(self, menu: QMenu) -> None: + a = menu.addAction(tr.actions_copy()) + assert a is not None + qconnect(a.triggered, self.onCopy) + + def _add_image_menu(self, menu: QMenu) -> None: + a = menu.addAction(tr.editing_copy_image()) + assert a is not None + qconnect(a.triggered, self.on_copy_image) + + context_menu_request = self.lastContextMenuRequest() + assert context_menu_request is not None + url = context_menu_request.mediaUrl() + file_name = url.fileName() + path = os.path.join(self.editor.mw.col.media.dir(), file_name) + self._add_image_menu_with_path(menu, path) + + def _add_image_menu_with_path(self, menu: QMenu, path: str) -> None: + a = menu.addAction(tr.editing_open_image()) + assert a is not None + qconnect(a.triggered, lambda: openFolder(path)) + + a = menu.addAction(tr.editing_show_in_folder()) + assert a is not None + qconnect(a.triggered, lambda: show_in_folder(path)) + + def _clipboard(self) -> QClipboard: + clipboard = self.editor.mw.app.clipboard() + assert clipboard is not None + return clipboard + + +# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light" +# - there may be other cases like a trailing 'Bold' that need fixing, but will +# wait for further reports first. +def fontMungeHack(font: str) -> str: + return re.sub(" L$", " Light", font) + + +def munge_html(txt: str, editor: Editor) -> str: + return "" if txt in ("
", "

") else txt + + +def remove_null_bytes(txt: str, editor: Editor) -> str: + # misbehaving apps may include a null byte in the text + return txt.replace("\x00", "") + + +def reverse_url_quoting(txt: str, editor: Editor) -> str: + # reverse the url quoting we added to get images to display + return editor.mw.col.media.escape_media_filenames(txt, unescape=True) + + +gui_hooks.editor_will_use_font_for_field.append(fontMungeHack) +gui_hooks.editor_will_munge_html.append(munge_html) # type: ignore +gui_hooks.editor_will_munge_html.append(remove_null_bytes) # type: ignore +gui_hooks.editor_will_munge_html.append(reverse_url_quoting) # type: ignore +gui_hooks.editor_will_munge_html.append(reverse_url_quoting) # type: ignore diff --git a/qt/aqt/main.py b/qt/aqt/main.py index c707d1b2a..0ec2aa75c 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1280,14 +1280,20 @@ title="{}" {}>{}""".format( # Other menu operations ########################################################################## + def _open_new_or_legacy_dialog(self, name: str, *args: Any, **kwargs: Any) -> None: + want_old = KeyboardModifiersPressed().shift + if not want_old: + name = f"New{name}" + aqt.dialogs.open(name, self, *args, **kwargs) + def onAddCard(self) -> None: - aqt.dialogs.open("AddCards", self) + self._open_new_or_legacy_dialog("AddCards") def onBrowse(self) -> None: aqt.dialogs.open("Browser", self, card=self.reviewer.card) def onEditCurrent(self) -> None: - aqt.dialogs.open("EditCurrent", self) + self._open_new_or_legacy_dialog("EditCurrent") def onOverview(self) -> None: self.moveToState("overview") @@ -1296,11 +1302,7 @@ title="{}" {}>{}""".format( deck = self._selectedDeck() if not deck: return - want_old = KeyboardModifiersPressed().shift - if want_old: - aqt.dialogs.open("DeckStats", self) - else: - aqt.dialogs.open("NewDeckStats", self) + self._open_new_or_legacy_dialog("DeckStats", self) def onPrefs(self) -> None: aqt.dialogs.open("Preferences", self) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 052dac291..4328b1189 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -608,10 +608,10 @@ def editor_op_changes_request(endpoint: str) -> bytes: response.ParseFromString(output) def handle_on_main() -> None: - from aqt.editor import Editor + from aqt.editor import NewEditor handler = aqt.mw.app.activeWindow() - if handler and isinstance(getattr(handler, "editor", None), Editor): + if handler and isinstance(getattr(handler, "editor", None), NewEditor): handler = handler.editor # type: ignore on_op_finished(aqt.mw, response, handler) @@ -808,10 +808,10 @@ def close_add_cards() -> bytes: req.ParseFromString(request.data) def handle_on_main() -> None: - from aqt.addcards import AddCards + from aqt.addcards import NewAddCards window = aqt.mw.app.activeWindow() - if isinstance(window, AddCards): + if isinstance(window, NewAddCards): window._close_if_user_wants_to_discard_changes(req.val) aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main)) @@ -820,10 +820,10 @@ def close_add_cards() -> bytes: def close_edit_current() -> bytes: def handle_on_main() -> None: - from aqt.editcurrent import EditCurrent + from aqt.editcurrent import NewEditCurrent window = aqt.mw.app.activeWindow() - if isinstance(window, EditCurrent): + if isinstance(window, NewEditCurrent): window.close() aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main)) @@ -1070,3 +1070,5 @@ def _extract_dynamic_get_request(path: str) -> DynamicRequest | None: return legacy_page_data else: return None + return None + return None diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 33838c46b..8c3a7f994 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -1008,12 +1008,15 @@ hooks = [ ################### Hook( name="add_cards_will_show_history_menu", - args=["addcards: aqt.addcards.AddCards", "menu: QMenu"], + args=[ + "addcards: aqt.addcards.AddCards | aqt.addcards.NewAddCards", + "menu: QMenu", + ], legacy_hook="AddCards.onHistory", ), Hook( name="add_cards_did_init", - args=["addcards: aqt.addcards.AddCards"], + args=["addcards: aqt.addcards.AddCards | aqt.addcards.NewAddCards"], ), Hook( name="add_cards_did_add_note", @@ -1068,7 +1071,7 @@ hooks = [ Hook( name="addcards_did_change_note_type", args=[ - "addcards: aqt.addcards.AddCards", + "addcards: aqt.addcards.AddCards | aqt.addcards.NewAddCards", "old: anki.models.NoteType", "new: anki.models.NoteType", ], @@ -1087,20 +1090,26 @@ hooks = [ ################### Hook( name="editor_did_init_left_buttons", - args=["buttons: list[str]", "editor: aqt.editor.Editor"], + args=["buttons: list[str]", "editor: aqt.editor.Editor | aqt.editor.NewEditor"], ), Hook( name="editor_did_init_buttons", - args=["buttons: list[str]", "editor: aqt.editor.Editor"], + args=["buttons: list[str]", "editor: aqt.editor.Editor | aqt.editor.NewEditor"], ), Hook( name="editor_did_init_shortcuts", - args=["shortcuts: list[tuple]", "editor: aqt.editor.Editor"], + args=[ + "shortcuts: list[tuple]", + "editor: aqt.editor.Editor | aqt.editor.NewEditor", + ], legacy_hook="setupEditorShortcuts", ), Hook( name="editor_will_show_context_menu", - args=["editor_webview: aqt.editor.EditorWebView", "menu: QMenu"], + args=[ + "editor_webview: aqt.editor.EditorWebView | aqt.editor.NewEditorWebView", + "menu: QMenu", + ], legacy_hook="EditorWebView.contextMenuEvent", ), Hook( @@ -1121,7 +1130,7 @@ hooks = [ ), Hook( name="editor_did_load_note", - args=["editor: aqt.editor.Editor"], + args=["editor: aqt.editor.Editor | aqt.editor.NewEditor"], legacy_hook="loadNote", ), Hook( @@ -1131,7 +1140,7 @@ hooks = [ ), Hook( name="editor_will_munge_html", - args=["txt: str", "editor: aqt.editor.Editor"], + args=["txt: str", "editor: aqt.editor.Editor | aqt.editor.NewEditor"], return_type="str", doc="""Allows manipulating the text that will be saved by the editor""", ), @@ -1143,15 +1152,21 @@ hooks = [ ), Hook( name="editor_web_view_did_init", - args=["editor_web_view: aqt.editor.EditorWebView"], + args=[ + "editor_web_view: aqt.editor.EditorWebView | aqt.editor.NewEditorWebView" + ], ), Hook( name="editor_did_init", - args=["editor: aqt.editor.Editor"], + args=["editor: aqt.editor.Editor | aqt.editor.NewEditor"], ), Hook( name="editor_will_load_note", - args=["js: str", "note: anki.notes.Note", "editor: aqt.editor.Editor"], + args=[ + "js: str", + "note: anki.notes.Note", + "editor: aqt.editor.Editor | aqt.editor.NewEditor", + ], return_type="str", doc="""Allows changing the javascript commands to load note before executing it and do change in the QT editor.""", @@ -1159,7 +1174,7 @@ hooks = [ Hook( name="editor_did_paste", args=[ - "editor: aqt.editor.Editor", + "editor: aqt.editor.Editor | aqt.editor.NewEditor", "html: str", "internal: bool", "extended: bool", @@ -1170,7 +1185,7 @@ hooks = [ name="editor_will_process_mime", args=[ "mime: QMimeData", - "editor_web_view: aqt.editor.EditorWebView", + "editor_web_view: aqt.editor.EditorWebView | aqt.editor.NewEditorWebView", "internal: bool", "extended: bool", "drop_event: bool", @@ -1194,7 +1209,7 @@ hooks = [ Hook( name="editor_state_did_change", args=[ - "editor: aqt.editor.Editor", + "editor: aqt.editor.Editor | aqt.editor.NewEditor", "new_state: aqt.editor.EditorState", "old_state: aqt.editor.EditorState", ], @@ -1203,7 +1218,10 @@ hooks = [ ), Hook( name="editor_mask_editor_did_load_image", - args=["editor: aqt.editor.Editor", "path_or_nid: str | anki.notes.NoteId"], + args=[ + "editor: aqt.editor.Editor | aqt.editor.NewEditor", + "path_or_nid: str | anki.notes.NoteId", + ], doc="""Called when the image occlusion mask editor has completed loading an image. From e71f789958f298f1f556d095a40b452e3b277e1b Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 24 Jul 2025 11:31:31 +0300 Subject: [PATCH 077/133] Remove duplicate lines in mediasrv.py again Turned out to be a race condition when the ruff/black/isort extensions are installed. https://github.com/astral-sh/ruff-vscode/issues/651 --- qt/aqt/mediasrv.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 4328b1189..132160b61 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -1070,5 +1070,3 @@ def _extract_dynamic_get_request(path: str) -> DynamicRequest | None: return legacy_page_data else: return None - return None - return None From a05f3dd38ac16b736dbe7b08c17d69b391aadb24 Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 24 Jul 2025 11:49:10 +0300 Subject: [PATCH 078/133] Restore old editor entrypoint --- build/configure/src/aqt.rs | 4 ++-- build/configure/src/web.rs | 28 ++++++++++++++++++++++++++++ ts/routes/editor/index.ts | 10 ++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 ts/routes/editor/index.ts diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index cad07ae4d..242c81f57 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -146,7 +146,7 @@ fn build_css(build: &mut Build) -> Result<()> { }, )?; } - let other_ts_css = build.inputs_with_suffix(inputs![":ts:reviewer:reviewer.css"], ".css"); + let other_ts_css = build.inputs_with_suffix(inputs![":ts:editor", ":ts:reviewer:reviewer.css"], ".css"); build.add_action( "qt:aqt:data:web:css", CopyFiles { @@ -188,7 +188,7 @@ fn build_js(build: &mut Build) -> Result<()> { }, )?; let files_from_ts = - build.inputs_with_suffix(inputs![":ts:reviewer:reviewer.js", ":ts:mathjax"], ".js"); + build.inputs_with_suffix(inputs![":ts:editor", ":ts:reviewer:reviewer.js", ":ts:mathjax"], ".js"); build.add_action( "qt:aqt:data:web:js", CopyFiles { diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 152a1a4d0..9e027fec3 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -29,6 +29,7 @@ pub fn build_and_check_web(build: &mut Build) -> Result<()> { build_sveltekit(build)?; declare_and_check_other_libraries(build)?; build_and_check_pages(build)?; + build_and_check_editor(build)?; build_and_check_reviewer(build)?; build_and_check_mathjax(build)?; check_web(build)?; @@ -217,6 +218,33 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { Ok(()) } +/// Only used for the legacy editor page. +fn build_and_check_editor(build: &mut Build) -> Result<()> { + let editor_deps = inputs![ + ":ts:lib", + ":ts:components", + ":ts:domlib", + ":ts:sveltelib", + ":ts:html-filter", + ":sass", + ":sveltekit", + glob!("ts/lib/editable,routes/{editor,image-occlusion}/**") + ]; + + build.add_action( + "ts:editor", + EsbuildScript { + script: "ts/bundle_svelte.mjs".into(), + entrypoint: "ts/routes/editor/index.ts".into(), + output_stem: "ts/editor/editor", + deps: editor_deps.clone(), + extra_exts: &["css"], + }, + )?; + + Ok(()) +} + fn build_and_check_reviewer(build: &mut Build) -> Result<()> { let reviewer_deps = inputs![ ":ts:lib", diff --git a/ts/routes/editor/index.ts b/ts/routes/editor/index.ts new file mode 100644 index 000000000..d3de21cc3 --- /dev/null +++ b/ts/routes/editor/index.ts @@ -0,0 +1,10 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// Entry point for the legacy editor page. + +import { globalExport } from "@tslib/globals"; + +import * as base from "./base"; + +globalExport(base); From 63052e7fb9ea9b4fd3445ece96faffb1d3ff74d0 Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 24 Jul 2025 13:14:27 +0300 Subject: [PATCH 079/133] Add a legacy switch to NoteEditor --- build/configure/src/aqt.rs | 16 +- build/configure/src/web.rs | 21 +- qt/aqt/editcurrent_legacy.py | 1 - qt/aqt/editor_legacy.py | 11 +- ts/lib/editable/index.ts | 3 + ts/routes/editor/NoteEditor.svelte | 179 ++++++++++++------ ts/routes/editor/StickyBadge.svelte | 16 +- ts/routes/editor/base.ts | 4 +- .../editor-toolbar/InlineButtons.svelte | 2 + .../rich-text-input/RichTextInput.svelte | 2 + .../rich-text-input/RichTextStyles.svelte | 47 +++-- 11 files changed, 208 insertions(+), 94 deletions(-) diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 242c81f57..912a1360c 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -146,7 +146,10 @@ fn build_css(build: &mut Build) -> Result<()> { }, )?; } - let other_ts_css = build.inputs_with_suffix(inputs![":ts:editor", ":ts:reviewer:reviewer.css"], ".css"); + let other_ts_css = build.inputs_with_suffix( + inputs![":ts:editor", ":ts:editable", ":ts:reviewer:reviewer.css"], + ".css", + ); build.add_action( "qt:aqt:data:web:css", CopyFiles { @@ -187,8 +190,15 @@ fn build_js(build: &mut Build) -> Result<()> { inputs: files, }, )?; - let files_from_ts = - build.inputs_with_suffix(inputs![":ts:editor", ":ts:reviewer:reviewer.js", ":ts:mathjax"], ".js"); + let files_from_ts = build.inputs_with_suffix( + inputs![ + ":ts:editor", + ":ts:editable", + ":ts:reviewer:reviewer.js", + ":ts:mathjax" + ], + ".js", + ); build.add_action( "qt:aqt:data:web:js", CopyFiles { diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 9e027fec3..0d952dcc6 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -203,16 +203,23 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { Ok(()) }; + // we use the generated .css file separately in the legacy editor + build_page( + "editable", + false, + inputs![ + ":ts:lib", + ":ts:components", + ":ts:domlib", + ":ts:sveltelib", + ":sass", + ":sveltekit", + ], + )?; build_page( "congrats", true, - inputs![ - // - ":ts:lib", - ":ts:components", - ":sass", - ":sveltekit" - ], + inputs![":ts:lib", ":ts:components", ":sass", ":sveltekit"], )?; Ok(()) diff --git a/qt/aqt/editcurrent_legacy.py b/qt/aqt/editcurrent_legacy.py index d4e969c21..b74c0801a 100644 --- a/qt/aqt/editcurrent_legacy.py +++ b/qt/aqt/editcurrent_legacy.py @@ -91,4 +91,3 @@ class EditCurrent(QMainWindow): self.editor.call_after_note_saved(callback) onReset = on_operation_did_execute - onReset = on_operation_did_execute diff --git a/qt/aqt/editor_legacy.py b/qt/aqt/editor_legacy.py index 138deed7a..3f196584e 100644 --- a/qt/aqt/editor_legacy.py +++ b/qt/aqt/editor_legacy.py @@ -187,7 +187,7 @@ class Editor: context=self, default_css=False, ) - self.web.eval(f"setupEditor('{mode}')") + self.web.eval(f"setupEditor('{mode}', true)") self.web.show() lefttopbtns: list[str] = [] @@ -563,9 +563,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too if not self.note: return - data = [ - (fld, self.mw.col.media.escape_media_filenames(val)) - for fld, val in self.note.items() + field_names = self.note.keys() + field_values = [ + self.mw.col.media.escape_media_filenames(val) for val in self.note.values() ] note_type = self.note_type() @@ -600,7 +600,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too js = f""" saveSession(); - setFields({json.dumps(data)}); + setFields({json.dumps(field_names)}, {json.dumps(field_values)}); setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())}); setNotetypeMeta({json.dumps(notetype_meta)}); setCollapsed({json.dumps(collapsed)}); @@ -1787,4 +1787,3 @@ gui_hooks.editor_will_use_font_for_field.append(fontMungeHack) gui_hooks.editor_will_munge_html.append(munge_html) # type: ignore gui_hooks.editor_will_munge_html.append(remove_null_bytes) # type: ignore gui_hooks.editor_will_munge_html.append(reverse_url_quoting) # type: ignore -gui_hooks.editor_will_munge_html.append(reverse_url_quoting) # type: ignore diff --git a/ts/lib/editable/index.ts b/ts/lib/editable/index.ts index 00984a718..277588b5a 100644 --- a/ts/lib/editable/index.ts +++ b/ts/lib/editable/index.ts @@ -4,3 +4,6 @@ import "./editable-base.scss"; import "./content-editable.scss"; import "./mathjax.scss"; +/* only imported for the CSS in the legacy editor */ +import "./ContentEditable.svelte"; +import "./Mathjax.svelte"; diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index a25bc6d9f..2b7b5d5a8 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -215,18 +215,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } async function toggleStickyAll() { - const values: boolean[] = []; - const notetype = await getNotetype({ ntid: notetypeMeta.id }); - const anySticky = notetype.fields.some((f) => f.config!.sticky); - for (const field of notetype.fields) { - const sticky = field.config!.sticky; - if (!anySticky || sticky) { - field.config!.sticky = !sticky; + if (isLegacy) { + bridgeCommand( + "toggleStickyAll", + (values: boolean[]) => (stickies = values), + ); + } else { + const values: boolean[] = []; + const notetype = await getNotetype({ ntid: notetypeMeta.id }); + const anySticky = notetype.fields.some((f) => f.config!.sticky); + for (const field of notetype.fields) { + const sticky = field.config!.sticky; + if (!anySticky || sticky) { + field.config!.sticky = !sticky; + } + values.push(field.config!.sticky); } - values.push(field.config!.sticky); + await updateEditorNotetype(notetype); + setSticky(values); } - await updateEditorNotetype(notetype); - setSticky(values); } let deregisterSticky: () => void; @@ -262,9 +269,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html await setMeta(tagsCollapsedMetaKey, collapsed); } - let note: Note; - export function setNote(n: Note): void { - note = n; + function clearCodeMirrorHistory() { // TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput. // It should be refactored once we work on our own Undo stack for (const pi of plainTextInputs) { @@ -272,6 +277,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + let noteId: number | null = null; + export function setNoteId(ntid: number): void { + clearCodeMirrorHistory(); + noteId = ntid; + } + + function getNoteId(): number | null { + return noteId; + } + + let note: Note; + export function setNote(n: Note): void { + note = n; + clearCodeMirrorHistory(); + } + let notetypeMeta: NotetypeIdAndModTime; function setNotetypeMeta(notetype: Notetype): void { notetypeMeta = { id: notetype.id, modTime: notetype.mtimeSecs }; @@ -330,13 +351,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html async function transformContentBeforeSave(content: string): Promise { content = content.replace(/ data-editor-shrink="(true|false)"/g, ""); - // misbehaving apps may include a null byte in the text - content = content.replaceAll("\0", ""); - // reverse the url quoting we added to get images to display - content = (await decodeIriPaths({ val: content })).val; + if (!isLegacy) { + // misbehaving apps may include a null byte in the text + content = content.replaceAll("\0", ""); + // reverse the url quoting we added to get images to display + content = (await decodeIriPaths({ val: content })).val; - if (["
", "

"].includes(content)) { - return ""; + if (["
", "

"].includes(content)) { + return ""; + } } return content; } @@ -352,10 +375,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html async function updateField(index: number, content: string): Promise { fieldSave.schedule(async () => { - bridgeCommand(`key:${index}`); - note!.fields[index] = await transformContentBeforeSave(content); - await updateCurrentNote(); - await updateDuplicateDisplay(); + if (isLegacy) { + bridgeCommand( + `key:${index}:${getNoteId()}:${transformContentBeforeSave( + content, + )}`, + ); + } else { + bridgeCommand(`key:${index}`); + note!.fields[index] = await transformContentBeforeSave(content); + await updateCurrentNote(); + await updateDuplicateDisplay(); + } }, 600); } @@ -640,16 +671,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html async function pickIOImage() { imageOcclusionMode = undefined; - const filename = await openFilePickerForImageOcclusion(); - if (!filename) { - return; + if (isLegacy) { + bridgeCommand("addImageForOcclusion"); + } else { + const filename = await openFilePickerForImageOcclusion(); + if (!filename) { + return; + } + setupMaskEditor(filename); } - setupMaskEditor(filename); } async function pickIOImageFromClipboard() { imageOcclusionMode = undefined; - await setupMaskEditorFromClipboard(); + if (isLegacy) { + bridgeCommand("addImageForOcclusionFromClipboard"); + } else { + await setupMaskEditorFromClipboard(); + } } async function handlePickerDrop(event: DragEvent) { @@ -928,6 +967,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html await loadNote(note!.id, notetypeMeta.id, 0, null); } + function checkNonLegacy(value: any): any | undefined { + if (isLegacy) { + return value; + } + return undefined; + } + + function preventDefaultIfNonLegacy(event: Event) { + if (!isLegacy) { + event.preventDefault(); + } + } + $: signalEditorState($editorState); $: $editorState = getEditorState( @@ -976,6 +1028,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html saveNow, closeAddCards, focusIfField, + getNoteId, + setNoteId, setNotetypeMeta, wrap, setMathjaxEnabled, @@ -1036,6 +1090,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let uiResolve: (api: NoteEditorAPI) => void; export let mode: EditorMode; + export let isLegacy: boolean; $: if (noteEditor) { uiResolve(api as NoteEditorAPI); @@ -1044,9 +1099,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + + + {tr.addingHistory()} + + + diff --git a/ts/routes/editor/HistoryModal.svelte b/ts/routes/editor/HistoryModal.svelte new file mode 100644 index 000000000..29f5b71ae --- /dev/null +++ b/ts/routes/editor/HistoryModal.svelte @@ -0,0 +1,208 @@ + + + + + + diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 1416fc115..fa5a55fb7 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -15,6 +15,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import LabelName from "./LabelName.svelte"; import { EditorState, type EditorMode } from "./types"; import { ContextMenu, Item } from "$lib/context-menu"; + import type Modal from "bootstrap/js/dist/modal"; + import { getContext } from "svelte"; + import { modalsKey } from "$lib/components/context-keys"; export interface NoteEditorAPI { fields: EditorFieldAPI[]; @@ -85,7 +88,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import PlainTextBadge from "./PlainTextBadge.svelte"; import RichTextInput, { editingInputIsRichText } from "./rich-text-input"; import RichTextBadge from "./RichTextBadge.svelte"; - import type { NotetypeIdAndModTime, SessionOptions } from "./types"; + import type { HistoryEntry, NotetypeIdAndModTime, SessionOptions } from "./types"; let contextMenu: ContextMenu; const [onContextMenu, contextMenuItems] = setupContextMenu(); @@ -446,6 +449,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html await addCurrentNote(1n); } + const modals = getContext>(modalsKey); + let modalKey: string; + + let history: HistoryEntry[] = []; + + export async function addNoteToHistory(note: Note) { + let text = ( + await htmlToTextLine({ + text: note.fields.join(", "), + preserveMediaFilenames: true, + }) + ).val; + if (text.length > 30) { + text = `${text.slice(0, 30)}...`; + } + history = [ + ...history, + { + text, + noteId: note.id, + }, + ]; + } + + export function onHistory() { + modals.get(modalKey)!.show(); + } + export function saveOnPageHide() { if (document.visibilityState === "hidden") { // will fire on session close and minimize @@ -501,10 +532,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html if (!(await noteCanBeAdded())) { return; } - await addEditorNote({ - note: note!, - deckId, - }); + const noteId = ( + await addEditorNote({ + note: note!, + deckId, + }) + ).noteId; + note.id = noteId; + addNoteToHistory(note!); lastAddedNote = note; await loadNewNote(); } @@ -638,6 +673,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html updateEditorNotetype, closeAddCards as closeAddCardsBackend, closeEditCurrent as closeEditCurrentBackend, + htmlToTextLine, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile"; @@ -662,6 +698,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { setupContextMenu } from "./context-menu.svelte"; import { registerShortcut } from "@tslib/shortcuts"; import ActionButtons from "./ActionButtons.svelte"; + import HistoryModal from "./HistoryModal.svelte"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded); @@ -1309,7 +1346,8 @@ components and functionality for general note editing. {#if !isLegacy} - + + {/if} {/if} diff --git a/ts/routes/editor/base.ts b/ts/routes/editor/base.ts index 365dc4eaa..1c85a1d8f 100644 --- a/ts/routes/editor/base.ts +++ b/ts/routes/editor/base.ts @@ -29,6 +29,7 @@ declare global { } } +import { modalsKey } from "$lib/components/context-keys"; import { ModuleName } from "@tslib/i18n"; import { mount } from "svelte"; import type { EditorMode } from "./types"; @@ -41,6 +42,7 @@ export const editorModules = [ ModuleName.NOTETYPES, ModuleName.IMPORTING, ModuleName.UNDO, + ModuleName.ADDING, ]; export const components = { @@ -58,6 +60,8 @@ export async function setupEditor(mode: EditorMode, isLegacy = false) { alert("unexpected editor type"); return; } + const context = new Map(); + context.set(modalsKey, new Map()); await setupI18n({ modules: editorModules }); - mount(NoteEditor, { target: document.body, props: { uiResolve, mode, isLegacy } }); + mount(NoteEditor, { target: document.body, props: { uiResolve, mode, isLegacy }, context }); } diff --git a/ts/routes/editor/types.ts b/ts/routes/editor/types.ts index 01aa42855..7c24b0375 100644 --- a/ts/routes/editor/types.ts +++ b/ts/routes/editor/types.ts @@ -29,3 +29,8 @@ export enum EditorState { } export type EditorMode = "add" | "browser" | "current"; + +export type HistoryEntry = { + text: string; + noteId: bigint; +}; From 1662ff440bb3572954fbe2cc190d4916243960fe Mon Sep 17 00:00:00 2001 From: Abdo Date: Fri, 25 Jul 2025 14:43:33 +0300 Subject: [PATCH 083/133] Set a different shortcut for Mac --- ts/routes/editor/HistoryButton.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/routes/editor/HistoryButton.svelte b/ts/routes/editor/HistoryButton.svelte index 5a07cc8d6..2e2a47086 100644 --- a/ts/routes/editor/HistoryButton.svelte +++ b/ts/routes/editor/HistoryButton.svelte @@ -10,10 +10,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { HistoryEntry } from "./types"; import Icon from "$lib/components/Icon.svelte"; import { caretDownFill } from "$lib/components/icons"; + import { isApplePlatform } from "@tslib/platform"; export let onHistory: () => void; export let history: HistoryEntry[] = []; - const historyKeyCombination = "Control+H"; + const historyKeyCombination = isApplePlatform() ? "Control+Shift+H" : "Control+H"; Date: Fri, 25 Jul 2025 14:46:25 +0300 Subject: [PATCH 084/133] Remove Qt buttons from new Add screen --- qt/aqt/addcards.py | 76 ++-------------------------------------------- 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 8ea8b808e..a748443f9 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -5,14 +5,12 @@ from __future__ import annotations from collections.abc import Callable -import aqt.editor -import aqt.forms from anki._legacy import deprecated -from anki.collection import OpChanges, SearchNode +from anki.collection import OpChanges from anki.decks import DeckId from anki.models import NotetypeId -from anki.notes import Note, NoteId -from anki.utils import html_to_text_line, is_mac +from anki.notes import Note +from anki.utils import is_mac from aqt import AnkiQt, gui_hooks from aqt.addcards_legacy import * from aqt.deckchooser import DeckChooser @@ -22,11 +20,9 @@ from aqt.utils import ( HelpPage, add_close_shortcut, ask_user_dialog, - downArrow, openHelp, restoreGeom, saveGeom, - shortcut, tr, ) @@ -48,9 +44,6 @@ class NewAddCards(QMainWindow): self.setupEditor() add_close_shortcut(self) self._load_new_note() - self.setupButtons() - self.history: list[NoteId] = [] - self._last_added_note: Note | None = None gui_hooks.operation_did_execute.append(self.on_operation_did_execute) restoreGeom(self, "add") gui_hooks.add_cards_did_init(self) @@ -116,39 +109,6 @@ class NewAddCards(QMainWindow): def helpRequested(self) -> None: openHelp(HelpPage.ADDING_CARD_AND_NOTE) - def setupButtons(self) -> None: - bb = self.form.buttonBox - ar = QDialogButtonBox.ButtonRole.ActionRole - # add - self.addButton = bb.addButton(tr.actions_add(), ar) - qconnect(self.addButton.clicked, self.add_current_note) - self.addButton.setShortcut(QKeySequence("Ctrl+Return")) - # qt5.14+ doesn't handle numpad enter on Windows - self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self) - qconnect(self.compat_add_shorcut.activated, self.addButton.click) - self.addButton.setToolTip(shortcut(tr.adding_add_shortcut_ctrlandenter())) - - # close - self.closeButton = QPushButton(tr.actions_close()) - self.closeButton.setAutoDefault(False) - bb.addButton(self.closeButton, QDialogButtonBox.ButtonRole.RejectRole) - qconnect(self.closeButton.clicked, self.close) - # help - self.helpButton = QPushButton(tr.actions_help(), clicked=self.helpRequested) # type: ignore - self.helpButton.setAutoDefault(False) - bb.addButton(self.helpButton, QDialogButtonBox.ButtonRole.HelpRole) - # history - b = bb.addButton(f"{tr.adding_history()} {downArrow()}", ar) - if is_mac: - sc = "Ctrl+Shift+H" - else: - sc = "Ctrl+H" - b.setShortcut(QKeySequence(sc)) - b.setToolTip(tr.adding_shortcut(val=shortcut(sc))) - qconnect(b.clicked, self.onHistory) - b.setEnabled(False) - self.historyButton = b - def setAndFocusNote(self, note: Note) -> None: self.editor.set_note(note, focusTo=0) @@ -192,36 +152,6 @@ class NewAddCards(QMainWindow): self.col.models.get(self.notetype_chooser.selected_notetype_id) ) - def addHistory(self, note: Note) -> None: - self.history.insert(0, note.id) - self.history = self.history[:15] - self.historyButton.setEnabled(True) - - def onHistory(self) -> None: - m = QMenu(self) - for nid in self.history: - if self.col.find_notes(self.col.build_search_string(SearchNode(nid=nid))): - note = self.col.get_note(nid) - fields = note.fields - txt = html_to_text_line(", ".join(fields)) - if len(txt) > 30: - txt = f"{txt[:30]}..." - line = tr.adding_edit(val=txt) - line = gui_hooks.addcards_will_add_history_entry(line, note) - line = line.replace("&", "&&") - # In qt action "&i" means "underline i, trigger this line when i is pressed". - # except for "&&" which is replaced by a single "&" - a = m.addAction(line) - qconnect(a.triggered, lambda b, nid=nid: self.editHistory(nid)) - else: - a = m.addAction(tr.adding_note_deleted()) - a.setEnabled(False) - gui_hooks.add_cards_will_show_history_menu(self, m) - m.exec(self.historyButton.mapToGlobal(QPoint(0, 0))) - - def editHistory(self, nid: NoteId) -> None: - aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) - def add_current_note(self) -> None: self.editor.web.eval(f"addCurrentNote({self.deck_chooser.selected_deck_id})") From ae42f5685bf6487c7576f314a48ff098a5c9a554 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 26 Jul 2025 22:29:19 +0300 Subject: [PATCH 085/133] Fix Create Copy --- qt/aqt/addcards.py | 19 +++++-------------- qt/aqt/editor.py | 13 +++++++------ qt/aqt/reviewer.py | 2 +- ts/routes/editor/NoteEditor.svelte | 7 +++++++ 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index a748443f9..4bf44b5dd 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -65,12 +65,11 @@ class NewAddCards(QMainWindow): if deck_id or (deck_id := self.col.default_deck_for_notetype(note.mid)): self.deck_chooser.selected_deck_id = deck_id - new_note = self._new_note() - new_note.fields = note.fields[:] - new_note.tags = note.tags[:] - - self.editor.orig_note_id = note.id - self.setAndFocusNote(new_note) + self.editor.load_note( + mid=note.mid, + original_note_id=note.id, + focus_to=0, + ) def setupEditor(self) -> None: self.editor = aqt.editor.NewEditor( @@ -109,9 +108,6 @@ class NewAddCards(QMainWindow): def helpRequested(self) -> None: openHelp(HelpPage.ADDING_CARD_AND_NOTE) - def setAndFocusNote(self, note: Note) -> None: - self.editor.set_note(note, focusTo=0) - def show_notetype_selector(self) -> None: self.editor.call_after_note_saved(self.notetype_chooser.choose_notetype) @@ -147,11 +143,6 @@ class NewAddCards(QMainWindow): update_deck=False, ) - def _new_note(self) -> Note: - return self.col.new_note( - self.col.models.get(self.notetype_chooser.selected_notetype_id) - ) - def add_current_note(self) -> None: self.editor.web.eval(f"addCurrentNote({self.deck_chooser.selected_deck_id})") diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 394751065..fd11d1b45 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -13,7 +13,6 @@ from dataclasses import dataclass from random import randrange from typing import Any -from anki._legacy import deprecated from anki.cards import Card from anki.hooks import runFilter from anki.models import NotetypeId @@ -85,8 +84,6 @@ class NewEditor: # Similar to currentField, but not set to None on a blur. May be # outside the bounds of the current notetype. self.last_field_index: int | None = None - # used when creating a copy of an existing note - self.orig_note_id: NoteId | None = None # current card, for card layout self.card: Card | None = None self.state: EditorState = EditorState.INITIAL @@ -397,7 +394,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self.currentField = None self.load_note(mid, focus_to=focus_to) - @deprecated(replaced_by=set_nid) def set_note( self, note: Note | None, @@ -413,7 +409,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too self.widget.hide() @on_editor_ready - def load_note(self, mid: int, focus_to: int | None = None) -> None: + def load_note( + self, + mid: int, + original_note_id: NoteId | None = None, + focus_to: int | None = None, + ) -> None: self.widget.show() def oncallback(arg: Any) -> None: @@ -426,7 +427,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too gui_hooks.editor_did_load_note(self) assert self.mw.pm.profile is not None - js = f"loadNote({json.dumps(self.nid)}, {mid}, {json.dumps(focus_to)}, {json.dumps(self.orig_note_id)});" + js = f"loadNote({json.dumps(self.nid)}, {mid}, {json.dumps(focus_to)}, {json.dumps(original_note_id)});" self.web.evalWithCallback( f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback ) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a8839c598..efa113729 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1167,7 +1167,7 @@ timerStopped = false; def on_create_copy(self) -> None: if self.card: - aqt.dialogs.open("AddCards", self.mw).set_note( + aqt.dialogs.open("NewAddCards", self.mw).set_note( self.card.note(), self.card.current_deck_id() ) diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index fa5a55fb7..979c8e4b4 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -924,6 +924,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }), ); } + if (originalNoteId) { + const originalNote = await getNote({ + nid: originalNoteId, + }); + note!.fields = originalNote.fields; + note!.tags = originalNote.tags; + } const fieldValues = ( await Promise.all( note!.fields.map((field) => encodeIriPaths({ val: field })), From 26473e6f6cf513ce317bf71653ff898c80c189bd Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 26 Jul 2025 22:31:20 +0300 Subject: [PATCH 086/133] Remove unused add_current_note() --- qt/aqt/addcards.py | 3 --- ts/routes/editor/NoteEditor.svelte | 1 - 2 files changed, 4 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 4bf44b5dd..339a7e844 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -143,9 +143,6 @@ class NewAddCards(QMainWindow): update_deck=False, ) - def add_current_note(self) -> None: - self.editor.web.eval(f"addCurrentNote({self.deck_chooser.selected_deck_id})") - def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() == Qt.Key.Key_Escape: self.close() diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 979c8e4b4..7ea550695 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -1088,7 +1088,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html resetIOImageLoaded, saveOcclusions, setSticky, - addCurrentNote, ...oldEditorAdapter, }); From 1ce564adf0d93cec07cf72bcd7800bd6f662068a Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 26 Jul 2025 23:23:13 +0300 Subject: [PATCH 087/133] Fix bottom editor buttons not shown in IO editor Positioning needs to be fixed. --- ts/routes/editor/NoteEditor.svelte | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 7ea550695..8896cc0f8 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -1351,10 +1351,11 @@ components and functionality for general note editing. - {#if !isLegacy} - - - {/if} + {/if} + + {#if !isLegacy} + + {/if} {#if !isLegacy} From 0f062563a6df8d928838cd52ce471289c6556832 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sun, 27 Jul 2025 01:34:21 +0300 Subject: [PATCH 088/133] Add hack for buttons position in IO mode --- ts/routes/editor/NoteEditor.svelte | 4 ---- ts/routes/image-occlusion/MaskEditor.svelte | 6 ++---- ts/routes/image-occlusion/tools/tool-zoom.ts | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 8896cc0f8..7aa26eff4 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -1381,10 +1381,6 @@ components and functionality for general note editing. height: 100%; } - :global(.image-occlusion) { - position: fixed; - } - :global(.image-occlusion .tab-buttons) { display: none !important; } diff --git a/ts/routes/image-occlusion/MaskEditor.svelte b/ts/routes/image-occlusion/MaskEditor.svelte index 36de4db84..e90239755 100644 --- a/ts/routes/image-occlusion/MaskEditor.svelte +++ b/ts/routes/image-occlusion/MaskEditor.svelte @@ -93,14 +93,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/routes/deck-options/TextInputModal.svelte b/ts/routes/deck-options/TextInputModal.svelte index 3f43815d6..e40682330 100644 --- a/ts/routes/deck-options/TextInputModal.svelte +++ b/ts/routes/deck-options/TextInputModal.svelte @@ -3,11 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> -