mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 20:57:13 -05:00
Handle operation changes from other screens
This commit is contained in:
parent
fbfd2784d8
commit
4ec88a8351
9 changed files with 126 additions and 47 deletions
|
|
@ -923,6 +923,7 @@ exposed_backend_list = [
|
||||||
"get_custom_colours",
|
"get_custom_colours",
|
||||||
# DeckService
|
# DeckService
|
||||||
"get_deck_names",
|
"get_deck_names",
|
||||||
|
"get_deck",
|
||||||
# I18nService
|
# I18nService
|
||||||
"i18n_resources",
|
"i18n_resources",
|
||||||
# ImportExportService
|
# ImportExportService
|
||||||
|
|
@ -1003,11 +1004,7 @@ def raw_backend_request(endpoint: str) -> Callable[[], bytes]:
|
||||||
response.ParseFromString(output)
|
response.ParseFromString(output)
|
||||||
|
|
||||||
def handle_on_main() -> None:
|
def handle_on_main() -> None:
|
||||||
from aqt.editor import NewEditor
|
handler = aqt.mw.app.activeWindow()
|
||||||
|
|
||||||
handler = aqt.mw.app.activeModalWidget()
|
|
||||||
if handler and isinstance(getattr(handler, "editor", None), NewEditor):
|
|
||||||
handler = handler.editor # type: ignore
|
|
||||||
on_op_finished(aqt.mw, response, handler)
|
on_op_finished(aqt.mw, response, handler)
|
||||||
|
|
||||||
aqt.mw.taskman.run_on_main(handle_on_main)
|
aqt.mw.taskman.run_on_main(handle_on_main)
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,16 @@ from collections.abc import Callable, Sequence
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, Type, cast
|
from typing import TYPE_CHECKING, Any, Type, cast
|
||||||
|
|
||||||
|
from google.protobuf.json_format import MessageToDict
|
||||||
from typing_extensions import TypedDict, Unpack
|
from typing_extensions import TypedDict, Unpack
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki._legacy import deprecated
|
from anki._legacy import deprecated
|
||||||
from anki.lang import is_rtl
|
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 import colors, gui_hooks
|
||||||
|
from aqt.operations import OpChanges
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.qt import sip
|
from aqt.qt import sip
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
|
|
@ -382,6 +384,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
self._filterSet = False
|
self._filterSet = False
|
||||||
gui_hooks.theme_did_change.append(self.on_theme_did_change)
|
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.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)
|
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.theme_did_change.remove(self.on_theme_did_change)
|
||||||
gui_hooks.body_classes_need_update.remove(self.on_body_classes_need_update)
|
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
|
# 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
|
# 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)))
|
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())}); """
|
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")
|
@deprecated(info="use theme_manager.qcolor() instead")
|
||||||
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
|
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
|
||||||
return theme_manager.qcolor(colors.CANVAS)
|
return theme_manager.qcolor(colors.CANVAS)
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mdiBookOutline } from "./icons";
|
import { mdiBookOutline } from "./icons";
|
||||||
import { getDeckNames } from "@generated/backend";
|
import { getDeck, getDeckNames } from "@generated/backend";
|
||||||
import ItemChooser from "./ItemChooser.svelte";
|
import ItemChooser from "./ItemChooser.svelte";
|
||||||
import type { DeckNameId } from "@generated/anki/decks_pb";
|
import type { DeckNameId } from "@generated/anki/decks_pb";
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
|
import { registerOperationHandler } from "@tslib/operations";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedDeck: DeckNameId | null;
|
|
||||||
onChange?: (deck: DeckNameId) => void;
|
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 decks: DeckNameId[] = $state([]);
|
||||||
let itemChooser: ItemChooser<DeckNameId> | null = $state(null);
|
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) {
|
export function select(notetypeId: bigint) {
|
||||||
itemChooser?.select(notetypeId);
|
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(() => {
|
$effect(() => {
|
||||||
getDeckNames({ skipEmptyDefault: true, includeFiltered: false }).then(
|
fetchDecks();
|
||||||
(response) => {
|
|
||||||
decks = response.entries;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
export function select(itemId: bigint) {
|
export function select(itemId: bigint) {
|
||||||
|
if (selectedItem?.id === itemId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const item = items.find((item) => item.id === itemId);
|
const item = items.find((item) => item.id === itemId);
|
||||||
selectedItem = item ? item : null;
|
if (item) {
|
||||||
|
selectedItem = item;
|
||||||
|
onChange?.(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
|
||||||
|
|
@ -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 type { NotetypeNameId } from "@generated/anki/notetypes_pb";
|
||||||
|
|
||||||
import { mdiNewspaper } from "./icons";
|
import { mdiNewspaper } from "./icons";
|
||||||
import { getNotetypeNames } from "@generated/backend";
|
import { getNotetype, getNotetypeNames } from "@generated/backend";
|
||||||
import ItemChooser from "./ItemChooser.svelte";
|
import ItemChooser from "./ItemChooser.svelte";
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
|
import { registerOperationHandler } from "@tslib/operations";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedNotetype: NotetypeNameId | null;
|
|
||||||
onChange?: (notetype: NotetypeNameId) => void;
|
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 notetypes: NotetypeNameId[] = $state([]);
|
||||||
let itemChooser: ItemChooser<NotetypeNameId> | null = $state(null);
|
let itemChooser: ItemChooser<NotetypeNameId> | null = $state(null);
|
||||||
|
|
||||||
|
async function fetchNotetypes() {
|
||||||
|
notetypes = (await getNotetypeNames({})).entries;
|
||||||
|
}
|
||||||
|
|
||||||
export function select(notetypeId: bigint) {
|
export function select(notetypeId: bigint) {
|
||||||
itemChooser?.select(notetypeId);
|
itemChooser?.select(notetypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
export async function getSelected(): Promise<NotetypeNameId> {
|
||||||
getNotetypeNames({}).then((response) => {
|
await fetchNotetypes();
|
||||||
notetypes = response.entries;
|
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>
|
</script>
|
||||||
|
|
||||||
<ItemChooser
|
<ItemChooser
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ export function globalExport(globals: Record<string, unknown>): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// but also export as window.anki
|
// but also export as window.anki
|
||||||
window["anki"] = globals;
|
window["anki"] = window["anki"] || {};
|
||||||
|
window["anki"] = { ...window["anki"], ...globals };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
ts/lib/tslib/operations.ts
Normal file
20
ts/lib/tslib/operations.ts
Normal 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;
|
||||||
|
|
@ -15,8 +15,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import LabelName from "./LabelName.svelte";
|
import LabelName from "./LabelName.svelte";
|
||||||
import { EditorState, type EditorMode } from "./types";
|
import { EditorState, type EditorMode } from "./types";
|
||||||
import { ContextMenu, Item } from "$lib/context-menu";
|
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 {
|
export interface NoteEditorAPI {
|
||||||
fields: EditorFieldAPI[];
|
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 reviewerCard: Card | null = null;
|
||||||
|
|
||||||
let notetypeChooser: NotetypeChooser;
|
let notetypeChooser: NotetypeChooser;
|
||||||
let selectedNotetype: NotetypeNameId | null = null;
|
|
||||||
let deckChooser: DeckChooser;
|
let deckChooser: DeckChooser;
|
||||||
let selectedDeck: DeckNameId | null = null;
|
|
||||||
|
|
||||||
async function onNotetypeChange(notetype: NotetypeNameId) {
|
async function onNotetypeChange(notetypeId: bigint, updateDeck: boolean = true) {
|
||||||
loadNote({ notetypeId: notetype.id, copyFromNote: note });
|
loadNote({ notetypeId, copyFromNote: note });
|
||||||
if (
|
if (
|
||||||
|
updateDeck &&
|
||||||
!(
|
!(
|
||||||
await getConfigBool({
|
await getConfigBool({
|
||||||
key: ConfigKey_Bool.ADDING_DEFAULTS_TO_CURRENT_DECK,
|
key: ConfigKey_Bool.ADDING_DEFAULTS_TO_CURRENT_DECK,
|
||||||
})
|
})
|
||||||
).val
|
).val
|
||||||
) {
|
) {
|
||||||
const deckId = await defaultDeckForNotetype({ ntid: notetype.id });
|
const deckId = await defaultDeckForNotetype({ ntid: notetypeId });
|
||||||
deckChooser.select(deckId.did);
|
deckChooser.select(deckId.did);
|
||||||
}
|
}
|
||||||
lastAddedNote = null;
|
lastAddedNote = null;
|
||||||
|
|
@ -487,7 +484,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAdd() {
|
async function onAdd() {
|
||||||
await addCurrentNote(selectedDeck!.id);
|
await addCurrentNote((await deckChooser.getSelected()).id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let historyModal: Modal;
|
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 { wrapInternal } from "@tslib/wrap";
|
||||||
import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile";
|
import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile";
|
||||||
import Shortcut from "$lib/components/Shortcut.svelte";
|
import Shortcut from "$lib/components/Shortcut.svelte";
|
||||||
|
import { registerOperationHandler } from "@tslib/operations";
|
||||||
|
|
||||||
import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte";
|
import { mathjaxConfig } from "$lib/editable/mathjax-element.svelte";
|
||||||
import ImageOcclusionPage from "../image-occlusion/ImageOcclusionPage.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(() => {
|
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") {
|
if (mode === "add") {
|
||||||
deregisterSticky = registerShortcut(toggleStickyAll, "Shift+F9");
|
deregisterSticky = registerShortcut(toggleStickyAll, "Shift+F9");
|
||||||
}
|
}
|
||||||
|
|
@ -1371,9 +1382,7 @@ components and functionality for general note editing.
|
||||||
<EditorChoosers
|
<EditorChoosers
|
||||||
bind:notetypeChooser
|
bind:notetypeChooser
|
||||||
bind:deckChooser
|
bind:deckChooser
|
||||||
bind:selectedNotetype
|
onNotetypeChange={(notetype) => onNotetypeChange(notetype.id)}
|
||||||
bind:selectedDeck
|
|
||||||
{onNotetypeChange}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedNotetype: NotetypeNameId | null;
|
|
||||||
selectedDeck?: DeckNameId | null;
|
|
||||||
notetypeChooser?: NotetypeChooser;
|
notetypeChooser?: NotetypeChooser;
|
||||||
deckChooser?: DeckChooser;
|
deckChooser?: DeckChooser;
|
||||||
onNotetypeChange?: (notetype: NotetypeNameId) => void;
|
onNotetypeChange?: (notetype: NotetypeNameId) => void;
|
||||||
onDeckChange?: (deck: DeckNameId) => void;
|
onDeckChange?: (deck: DeckNameId) => void;
|
||||||
}
|
}
|
||||||
let {
|
let {
|
||||||
selectedNotetype = $bindable(null),
|
|
||||||
selectedDeck = $bindable(null),
|
|
||||||
notetypeChooser = $bindable(),
|
notetypeChooser = $bindable(),
|
||||||
deckChooser = $bindable(),
|
deckChooser = $bindable(),
|
||||||
onNotetypeChange,
|
onNotetypeChange,
|
||||||
|
|
@ -31,19 +27,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<p>{tr.notetypesType()}</p>
|
<p>{tr.notetypesType()}</p>
|
||||||
<div class="notetype-chooser">
|
<div class="notetype-chooser">
|
||||||
<NotetypeChooser
|
<NotetypeChooser bind:this={notetypeChooser} onChange={onNotetypeChange} />
|
||||||
bind:this={notetypeChooser}
|
|
||||||
bind:selectedNotetype
|
|
||||||
onChange={onNotetypeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p>{tr.decksDeck()}</p>
|
<p>{tr.decksDeck()}</p>
|
||||||
<div class="deck-chooser">
|
<div class="deck-chooser">
|
||||||
<DeckChooser
|
<DeckChooser bind:this={deckChooser} onChange={onDeckChange} />
|
||||||
bind:this={deckChooser}
|
|
||||||
bind:selectedDeck
|
|
||||||
onChange={onDeckChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue