From 9a054d69242ba3c99fdeb03f773d2dca35130d6c Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 28 Jul 2025 10:17:12 +0300 Subject: [PATCH] Complete noteCanBeAdded() --- proto/anki/frontend.proto | 33 +++++++- qt/aqt/mediasrv.py | 119 ++++++++++++++++++++++++----- ts/routes/editor/NoteEditor.svelte | 29 ++++++- 3 files changed, 159 insertions(+), 22 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index ac439764a..925e41c1c 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -12,6 +12,7 @@ import "anki/generic.proto"; import "anki/search.proto"; import "anki/notes.proto"; import "anki/notetypes.proto"; +import "anki/links.proto"; service FrontendService { // Returns values from the reviewer @@ -44,6 +45,8 @@ service FrontendService { rpc CloseAddCards(generic.Bool) returns (generic.Empty); rpc CloseEditCurrent(generic.Empty) returns (generic.Empty); rpc OpenLink(generic.String) returns (generic.Empty); + rpc AskUser(AskUserRequest) returns (generic.Bool); + rpc ShowMessageBox(ShowMessageBoxRequest) returns (generic.Empty); // Profile config rpc GetProfileConfigJson(generic.String) returns (generic.Json); @@ -106,4 +109,32 @@ message ReadClipboardResponse { message WriteClipboardRequest { map data = 1; -} \ No newline at end of file +} + +message Help { + oneof value { + links.HelpPageLinkRequest.HelpPage help_page = 1; + string help_link = 2; + } +} + +message AskUserRequest { + string text = 1; + optional Help help = 2; + optional string title = 4; + optional bool default_no = 5; +} + +enum MessageBoxType { + INFO = 0; + WARNING = 1; + CRITICAL = 2; +} + +message ShowMessageBoxRequest { + string text = 1; + MessageBoxType type = 2; + optional Help help = 3; + optional string title = 4; + optional string text_format = 5; +} diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 8149ca560..2a75751dd 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -17,7 +17,7 @@ from collections.abc import Callable from dataclasses import dataclass from errno import EPROTOTYPE from http import HTTPStatus -from typing import Any, cast +from typing import Any, Generic, cast import flask import flask_cors @@ -40,7 +40,14 @@ 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, openLink, show_warning, tr +from aqt.utils import ( + aqt_data_path, + askUser, + openLink, + show_info, + 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 @@ -704,18 +711,34 @@ def retrieve_url() -> bytes: ).SerializeToString() +AsyncRequestReturnType = TypeVar("AsyncRequestReturnType") + + +class AsyncRequestHandler(Generic[AsyncRequestReturnType]): + def __init__(self, callback: Callable[[AsyncRequestHandler], None]) -> None: + self.callback = callback + self.loop = asyncio.get_event_loop() + self.future = self.loop.create_future() + + def run(self) -> None: + aqt.mw.taskman.run_on_main(lambda: self.callback(self)) + + def set_result(self, result: AsyncRequestReturnType) -> None: + self.loop.call_soon_threadsafe(self.future.set_result, result) + + async def get_result(self) -> AsyncRequestReturnType: + return await self.future + + async def open_file_picker() -> bytes: req = frontend_pb2.openFilePickerRequest() req.ParseFromString(request.data) - loop = asyncio.get_event_loop() - future = loop.create_future() - - def on_main() -> None: + def callback(request_handler: AsyncRequestHandler) -> None: from aqt.utils import getFile def cb(filename: str | None) -> None: - loop.call_soon_threadsafe(future.set_result, filename) + request_handler.set_result(filename) window = aqt.mw.app.activeWindow() assert window is not None @@ -727,9 +750,9 @@ async def open_file_picker() -> bytes: key=req.key, ) - aqt.mw.taskman.run_on_main(on_main) - - filename = await future + request_handler: AsyncRequestHandler[str | None] = AsyncRequestHandler(callback) + request_handler.run() + filename = await request_handler.get_result() return generic_pb2.String(val=filename if filename else "").SerializeToString() @@ -757,22 +780,19 @@ def show_in_media_folder() -> bytes: async def record_audio() -> bytes: - loop = asyncio.get_event_loop() - future = loop.create_future() - - def on_main() -> None: + def callback(request_handler: AsyncRequestHandler) -> None: from aqt.sound import record_audio def cb(path: str | None) -> None: - loop.call_soon_threadsafe(future.set_result, path) + request_handler.set_result(path) window = aqt.mw.app.activeWindow() assert window is not None record_audio(window, aqt.mw, True, cb) - aqt.mw.taskman.run_on_main(on_main) - - path = await future + request_handler: AsyncRequestHandler[str | None] = AsyncRequestHandler(callback) + request_handler.run() + path = await request_handler.get_result() return generic_pb2.String(val=path if path else "").SerializeToString() @@ -838,6 +858,67 @@ def open_link() -> bytes: return b"" +async def ask_user() -> bytes: + req = frontend_pb2.AskUserRequest() + req.ParseFromString(request.data) + + def callback(request_handler: AsyncRequestHandler) -> None: + kwargs: dict[str, Any] = dict(text=req.text) + if req.HasField("help"): + help_arg: Any + if req.help.WhichOneof("value") == "help_page": + help_arg = req.help.help_page + else: + help_arg = req.help.help_link + kwargs["help"] = help_arg + if req.HasField("title"): + kwargs["title"] = req.title + if req.HasField("default_no"): + kwargs["defaultno"] = req.default_no + answer = askUser(**kwargs) + request_handler.set_result(answer) + + request_handler: AsyncRequestHandler[bool] = AsyncRequestHandler(callback) + request_handler.run() + answer = await request_handler.get_result() + + return generic_pb2.Bool(val=answer).SerializeToString() + + +async def show_message_box() -> bytes: + req = frontend_pb2.ShowMessageBoxRequest() + req.ParseFromString(request.data) + + def callback(request_handler: AsyncRequestHandler) -> None: + kwargs: dict[str, Any] = dict(text=req.text) + if req.type == frontend_pb2.MessageBoxType.INFO: + icon = QMessageBox.Icon.Information + elif req.type == frontend_pb2.MessageBoxType.WARNING: + icon = QMessageBox.Icon.Warning + elif req.type == frontend_pb2.MessageBoxType.CRITICAL: + icon = QMessageBox.Icon.Critical + kwargs["icon"] = icon + if req.HasField("help"): + help_arg: Any + if req.help.WhichOneof("value") == "help_page": + help_arg = req.help.help_page + else: + help_arg = req.help.help_link + kwargs["help"] = help_arg + if req.HasField("title"): + kwargs["title"] = req.title + if req.HasField("text_format"): + kwargs["text_format"] = req.text_format + show_info(**kwargs) + request_handler.set_result(True) + + request_handler: AsyncRequestHandler[bool] = AsyncRequestHandler(callback) + request_handler.run() + answer = await request_handler.get_result() + + return generic_pb2.Bool(val=answer).SerializeToString() + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -872,6 +953,8 @@ post_handler_list = [ close_add_cards, close_edit_current, open_link, + ask_user, + show_message_box, ] diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index b75ef4f0c..6b8831085 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -518,8 +518,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } if (result.state === NoteFieldsCheckResponse_State.MISSING_CLOZE) { - // TODO: askUser(tr.addingYouHaveAClozeDeletionNote()) - return false; + const answer = ( + await askUser({ + text: tr.addingYouHaveAClozeDeletionNote(), + }) + ).val; + if (!answer) { + return false; + } } if (result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) { problem = tr.addingClozeOutsideClozeNotetype(); @@ -527,7 +533,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html if (result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) { problem = tr.addingClozeOutsideClozeField(); } - return problem ? false : true; + if (problem) { + showMessageBox({ + text: problem, + type: MessageBoxType.WARNING, + help: { + value: { + case: "helpPage", + value: HelpPageLinkRequest_HelpPage.ADDING_CARD_AND_NOTE, + }, + }, + }); + return false; + } + return true; } async function addCurrentNoteInner(deckId: bigint) { @@ -676,6 +695,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html closeAddCards as closeAddCardsBackend, closeEditCurrent as closeEditCurrentBackend, htmlToTextLine, + askUser, + showMessageBox, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; import { getProfileConfig, getMeta, setMeta, getColConfig } from "@tslib/profile"; @@ -701,6 +722,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { registerShortcut } from "@tslib/shortcuts"; import ActionButtons from "./ActionButtons.svelte"; import HistoryModal from "./HistoryModal.svelte"; + import { HelpPageLinkRequest_HelpPage } from "@generated/anki/links_pb"; + import { MessageBoxType } from "@generated/anki/frontend_pb"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded);