Handle operation changes from other screens

This commit is contained in:
Abdo 2025-10-14 23:24:25 +03:00
parent fbfd2784d8
commit 4ec88a8351
9 changed files with 126 additions and 47 deletions

View file

@ -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)

View file

@ -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)

View file

@ -4,29 +4,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { mdiBookOutline } from "./icons";
import { getDeckNames } from "@generated/backend";
import { getDeck, getDeckNames } from "@generated/backend";
import ItemChooser from "./ItemChooser.svelte";
import type { DeckNameId } from "@generated/anki/decks_pb";
import * as tr from "@generated/ftl";
import { registerOperationHandler } from "@tslib/operations";
import { onMount } from "svelte";
interface Props {
selectedDeck: DeckNameId | null;
onChange?: (deck: DeckNameId) => void;
}
let { selectedDeck = $bindable(null), onChange }: Props = $props();
let { onChange }: Props = $props();
let selectedDeck: DeckNameId | null = $state(null);
let decks: DeckNameId[] = $state([]);
let itemChooser: ItemChooser<DeckNameId> | null = $state(null);
async function fetchDecks(skipEmptyDefault: boolean = true) {
decks = (await getDeckNames({ skipEmptyDefault, includeFiltered: false }))
.entries;
}
export function select(notetypeId: bigint) {
itemChooser?.select(notetypeId);
}
export async function getSelected(): Promise<DeckNameId> {
await fetchDecks(false);
try {
await getDeck({ did: selectedDeck!.id }, { alertOnError: false });
} catch (error) {
select(1n);
}
return selectedDeck!;
}
onMount(() => {
registerOperationHandler((changes) => {
if (changes.deck) {
getSelected();
}
});
});
$effect(() => {
getDeckNames({ skipEmptyDefault: true, includeFiltered: false }).then(
(response) => {
decks = response.entries;
},
);
fetchDecks();
});
</script>

View file

@ -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(() => {

View file

@ -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<NotetypeNameId> | 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<NotetypeNameId> {
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();
});
</script>
<ItemChooser

View file

@ -7,5 +7,6 @@ export function globalExport(globals: Record<string, unknown>): void {
}
// but also export as window.anki
window["anki"] = globals;
window["anki"] = window["anki"] || {};
window["anki"] = { ...window["anki"], ...globals };
}

View file

@ -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<OpChanges>) => void;
const handlers: OperationHandler[] = [];
export function registerOperationHandler(handler: (changes: Partial<OpChanges>) => void): void {
handlers.push(handler);
}
function onOperationDidExecute(changes: Partial<OpChanges>): void {
for (const handler of handlers) {
handler(changes);
}
}
globalThis.anki = globalThis.anki || {};
globalThis.anki.onOperationDidExecute = onOperationDidExecute;

View file

@ -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.
<EditorChoosers
bind:notetypeChooser
bind:deckChooser
bind:selectedNotetype
bind:selectedDeck
{onNotetypeChange}
onNotetypeChange={(notetype) => onNotetypeChange(notetype.id)}
/>
{/if}

View file

@ -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
<div class="top-bar">
<p>{tr.notetypesType()}</p>
<div class="notetype-chooser">
<NotetypeChooser
bind:this={notetypeChooser}
bind:selectedNotetype
onChange={onNotetypeChange}
/>
<NotetypeChooser bind:this={notetypeChooser} onChange={onNotetypeChange} />
</div>
<p>{tr.decksDeck()}</p>
<div class="deck-chooser">
<DeckChooser
bind:this={deckChooser}
bind:selectedDeck
onChange={onDeckChange}
/>
<DeckChooser bind:this={deckChooser} onChange={onDeckChange} />
</div>
</div>