From 4ec88a8351b57c1b16c32aa281bda0329b0b69b1 Mon Sep 17 00:00:00 2001 From: Abdo Date: Tue, 14 Oct 2025 23:24:25 +0300 Subject: [PATCH] Handle operation changes from other screens --- qt/aqt/mediasrv.py | 7 +--- qt/aqt/webview.py | 17 ++++++++- ts/lib/components/DeckChooser.svelte | 37 +++++++++++++++---- ts/lib/components/ItemChooser.svelte | 8 +++- ts/lib/components/NotetypeChooser.svelte | 34 ++++++++++++++--- ts/lib/tslib/globals.ts | 3 +- ts/lib/tslib/operations.ts | 20 ++++++++++ ts/routes/editor/NoteEditor.svelte | 31 ++++++++++------ .../editor-toolbar/EditorChoosers.svelte | 16 +------- 9 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 ts/lib/tslib/operations.ts diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 47470c0f3..b03925ce7 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -923,6 +923,7 @@ exposed_backend_list = [ "get_custom_colours", # DeckService "get_deck_names", + "get_deck", # I18nService "i18n_resources", # ImportExportService @@ -1003,11 +1004,7 @@ def raw_backend_request(endpoint: str) -> Callable[[], bytes]: response.ParseFromString(output) def handle_on_main() -> None: - from aqt.editor import NewEditor - - handler = aqt.mw.app.activeModalWidget() - if handler and isinstance(getattr(handler, "editor", None), NewEditor): - handler = handler.editor # type: ignore + handler = aqt.mw.app.activeWindow() on_op_finished(aqt.mw, response, handler) aqt.mw.taskman.run_on_main(handle_on_main) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 95d84c00e..d48743d16 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -12,14 +12,16 @@ from collections.abc import Callable, Sequence from enum import Enum from typing import TYPE_CHECKING, Any, Type, cast +from google.protobuf.json_format import MessageToDict from typing_extensions import TypedDict, Unpack import anki import anki.lang from anki._legacy import deprecated from anki.lang import is_rtl -from anki.utils import hmr_mode, is_lin, is_mac, is_win +from anki.utils import hmr_mode, is_lin, is_mac, is_win, to_json_bytes from aqt import colors, gui_hooks +from aqt.operations import OpChanges from aqt.qt import * from aqt.qt import sip from aqt.theme import theme_manager @@ -382,6 +384,7 @@ class AnkiWebView(QWebEngineView): self._filterSet = False gui_hooks.theme_did_change.append(self.on_theme_did_change) gui_hooks.body_classes_need_update.append(self.on_body_classes_need_update) + gui_hooks.operation_did_execute.append(self.on_operation_did_execute) qconnect(self.loadFinished, self._on_load_finished) @@ -911,6 +914,7 @@ html {{ {font} }} gui_hooks.theme_did_change.remove(self.on_theme_did_change) gui_hooks.body_classes_need_update.remove(self.on_body_classes_need_update) + gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) # defer page cleanup so that in-flight requests have a chance to complete first # https://forums.ankiweb.net/t/error-when-exiting-browsing-when-the-software-is-installed-in-the-path-c-program-files-anki/38363 mw.progress.single_shot(5000, lambda: mw.mediaServer.clear_page_html(id(self))) @@ -960,6 +964,17 @@ html {{ {font} }} f"""document.body.classList.toggle("reduce-motion", {json.dumps(mw.pm.reduce_motion())}); """ ) + def on_operation_did_execute( + self, changes: OpChanges, handler: object | None + ) -> None: + if handler is self.parentWidget(): + return + + changes_json = to_json_bytes(MessageToDict(changes)).decode() + self.eval( + f"if(globalThis.anki && globalThis.anki.onOperationDidExecute) globalThis.anki.onOperationDidExecute({changes_json})" + ) + @deprecated(info="use theme_manager.qcolor() instead") def get_window_bg_color(self, night_mode: bool | None = None) -> QColor: return theme_manager.qcolor(colors.CANVAS) diff --git a/ts/lib/components/DeckChooser.svelte b/ts/lib/components/DeckChooser.svelte index 59ca750a4..f9c6ba541 100644 --- a/ts/lib/components/DeckChooser.svelte +++ b/ts/lib/components/DeckChooser.svelte @@ -4,29 +4,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> diff --git a/ts/lib/components/ItemChooser.svelte b/ts/lib/components/ItemChooser.svelte index 233b1145b..4637a9be9 100644 --- a/ts/lib/components/ItemChooser.svelte +++ b/ts/lib/components/ItemChooser.svelte @@ -60,8 +60,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } export function select(itemId: bigint) { + if (selectedItem?.id === itemId) { + return; + } const item = items.find((item) => item.id === itemId); - selectedItem = item ? item : null; + if (item) { + selectedItem = item; + onChange?.(item); + } } $effect(() => { diff --git a/ts/lib/components/NotetypeChooser.svelte b/ts/lib/components/NotetypeChooser.svelte index 4820bdd92..0952c5029 100644 --- a/ts/lib/components/NotetypeChooser.svelte +++ b/ts/lib/components/NotetypeChooser.svelte @@ -6,27 +6,49 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { NotetypeNameId } from "@generated/anki/notetypes_pb"; import { mdiNewspaper } from "./icons"; - import { getNotetypeNames } from "@generated/backend"; + import { getNotetype, getNotetypeNames } from "@generated/backend"; import ItemChooser from "./ItemChooser.svelte"; import * as tr from "@generated/ftl"; + import { registerOperationHandler } from "@tslib/operations"; + import { onMount } from "svelte"; interface Props { - selectedNotetype: NotetypeNameId | null; onChange?: (notetype: NotetypeNameId) => void; } - let { selectedNotetype = $bindable(null), onChange }: Props = $props(); + let { onChange }: Props = $props(); + let selectedNotetype: NotetypeNameId | null = $state(null); let notetypes: NotetypeNameId[] = $state([]); let itemChooser: ItemChooser | null = $state(null); + async function fetchNotetypes() { + notetypes = (await getNotetypeNames({})).entries; + } + export function select(notetypeId: bigint) { itemChooser?.select(notetypeId); } - $effect(() => { - getNotetypeNames({}).then((response) => { - notetypes = response.entries; + export async function getSelected(): Promise { + await fetchNotetypes(); + try { + await getNotetype({ ntid: selectedNotetype!.id }, { alertOnError: false }); + } catch (error) { + select(notetypes[0].id); + } + return selectedNotetype!; + } + + onMount(() => { + registerOperationHandler((changes) => { + if (changes.notetype) { + getSelected(); + } }); }); + + $effect(() => { + fetchNotetypes(); + }); ): void { } // but also export as window.anki - window["anki"] = globals; + window["anki"] = window["anki"] || {}; + window["anki"] = { ...window["anki"], ...globals }; } diff --git a/ts/lib/tslib/operations.ts b/ts/lib/tslib/operations.ts new file mode 100644 index 000000000..f7244e8a9 --- /dev/null +++ b/ts/lib/tslib/operations.ts @@ -0,0 +1,20 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import type { OpChanges } from "@generated/anki/collection_pb"; + +type OperationHandler = (changes: Partial) => void; +const handlers: OperationHandler[] = []; + +export function registerOperationHandler(handler: (changes: Partial) => void): void { + handlers.push(handler); +} + +function onOperationDidExecute(changes: Partial): void { + for (const handler of handlers) { + handler(changes); + } +} + +globalThis.anki = globalThis.anki || {}; +globalThis.anki.onOperationDidExecute = onOperationDidExecute; diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index d7e9021ce..94c74a0b0 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -15,8 +15,6 @@ 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 { NotetypeNameId } from "@generated/anki/notetypes_pb"; - import type { DeckNameId } from "@generated/anki/decks_pb"; export interface NoteEditorAPI { fields: EditorFieldAPI[]; @@ -312,20 +310,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let reviewerCard: Card | null = null; let notetypeChooser: NotetypeChooser; - let selectedNotetype: NotetypeNameId | null = null; let deckChooser: DeckChooser; - let selectedDeck: DeckNameId | null = null; - async function onNotetypeChange(notetype: NotetypeNameId) { - loadNote({ notetypeId: notetype.id, copyFromNote: note }); + async function onNotetypeChange(notetypeId: bigint, updateDeck: boolean = true) { + loadNote({ notetypeId, copyFromNote: note }); if ( + updateDeck && !( await getConfigBool({ key: ConfigKey_Bool.ADDING_DEFAULTS_TO_CURRENT_DECK, }) ).val ) { - const deckId = await defaultDeckForNotetype({ ntid: notetype.id }); + const deckId = await defaultDeckForNotetype({ ntid: notetypeId }); deckChooser.select(deckId.did); } lastAddedNote = null; @@ -487,7 +484,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } async function onAdd() { - await addCurrentNote(selectedDeck!.id); + await addCurrentNote((await deckChooser.getSelected()).id); } let historyModal: Modal; @@ -742,6 +739,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { wrapInternal } from "@tslib/wrap"; import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile"; import Shortcut from "$lib/components/Shortcut.svelte"; + import { registerOperationHandler } from "@tslib/operations"; import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte"; import ImageOcclusionPage from "../image-occlusion/ImageOcclusionPage.svelte"; @@ -1236,6 +1234,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } onMount(() => { + registerOperationHandler(async (changes) => { + if (mode === "add" && (changes.notetype || changes.deck)) { + let homeDeckId = 0n; + if (reviewerCard) { + homeDeckId = reviewerCard.originalDeckId || reviewerCard.deckId; + } + const chooserDefaults = await defaultsForAdding({ + homeDeckOfCurrentReviewCard: homeDeckId, + }); + onNotetypeChange(chooserDefaults.notetypeId, false); + } + }); + if (mode === "add") { deregisterSticky = registerShortcut(toggleStickyAll, "Shift+F9"); } @@ -1371,9 +1382,7 @@ components and functionality for general note editing. onNotetypeChange(notetype.id)} /> {/if} diff --git a/ts/routes/editor/editor-toolbar/EditorChoosers.svelte b/ts/routes/editor/editor-toolbar/EditorChoosers.svelte index c706990a8..10267a9e9 100644 --- a/ts/routes/editor/editor-toolbar/EditorChoosers.svelte +++ b/ts/routes/editor/editor-toolbar/EditorChoosers.svelte @@ -11,16 +11,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import * as tr from "@generated/ftl"; interface Props { - selectedNotetype: NotetypeNameId | null; - selectedDeck?: DeckNameId | null; notetypeChooser?: NotetypeChooser; deckChooser?: DeckChooser; onNotetypeChange?: (notetype: NotetypeNameId) => void; onDeckChange?: (deck: DeckNameId) => void; } let { - selectedNotetype = $bindable(null), - selectedDeck = $bindable(null), notetypeChooser = $bindable(), deckChooser = $bindable(), onNotetypeChange, @@ -31,19 +27,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

{tr.notetypesType()}

- +

{tr.decksDeck()}

- +