Complete noteCanBeAdded()

This commit is contained in:
Abdo 2025-07-28 10:17:12 +03:00
parent 0c98a68528
commit 9a054d6924
3 changed files with 159 additions and 22 deletions

View file

@ -12,6 +12,7 @@ import "anki/generic.proto";
import "anki/search.proto"; import "anki/search.proto";
import "anki/notes.proto"; import "anki/notes.proto";
import "anki/notetypes.proto"; import "anki/notetypes.proto";
import "anki/links.proto";
service FrontendService { service FrontendService {
// Returns values from the reviewer // Returns values from the reviewer
@ -44,6 +45,8 @@ service FrontendService {
rpc CloseAddCards(generic.Bool) returns (generic.Empty); rpc CloseAddCards(generic.Bool) returns (generic.Empty);
rpc CloseEditCurrent(generic.Empty) returns (generic.Empty); rpc CloseEditCurrent(generic.Empty) returns (generic.Empty);
rpc OpenLink(generic.String) returns (generic.Empty); rpc OpenLink(generic.String) returns (generic.Empty);
rpc AskUser(AskUserRequest) returns (generic.Bool);
rpc ShowMessageBox(ShowMessageBoxRequest) returns (generic.Empty);
// Profile config // Profile config
rpc GetProfileConfigJson(generic.String) returns (generic.Json); rpc GetProfileConfigJson(generic.String) returns (generic.Json);
@ -107,3 +110,31 @@ message ReadClipboardResponse {
message WriteClipboardRequest { message WriteClipboardRequest {
map<string, bytes> data = 1; map<string, bytes> data = 1;
} }
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;
}

View file

@ -17,7 +17,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from errno import EPROTOTYPE from errno import EPROTOTYPE
from http import HTTPStatus from http import HTTPStatus
from typing import Any, cast from typing import Any, Generic, cast
import flask import flask
import flask_cors 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.operations.deck import update_deck_configs as update_deck_configs_op
from aqt.progress import ProgressUpdate from aqt.progress import ProgressUpdate
from aqt.qt import * 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 # https://forums.ankiweb.net/t/anki-crash-when-using-a-specific-deck/22266
waitress.wasyncore._DISCONNECTED = waitress.wasyncore._DISCONNECTED.union({EPROTOTYPE}) # type: ignore waitress.wasyncore._DISCONNECTED = waitress.wasyncore._DISCONNECTED.union({EPROTOTYPE}) # type: ignore
@ -704,18 +711,34 @@ def retrieve_url() -> bytes:
).SerializeToString() ).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: async def open_file_picker() -> bytes:
req = frontend_pb2.openFilePickerRequest() req = frontend_pb2.openFilePickerRequest()
req.ParseFromString(request.data) req.ParseFromString(request.data)
loop = asyncio.get_event_loop() def callback(request_handler: AsyncRequestHandler) -> None:
future = loop.create_future()
def on_main() -> None:
from aqt.utils import getFile from aqt.utils import getFile
def cb(filename: str | None) -> None: def cb(filename: str | None) -> None:
loop.call_soon_threadsafe(future.set_result, filename) request_handler.set_result(filename)
window = aqt.mw.app.activeWindow() window = aqt.mw.app.activeWindow()
assert window is not None assert window is not None
@ -727,9 +750,9 @@ async def open_file_picker() -> bytes:
key=req.key, key=req.key,
) )
aqt.mw.taskman.run_on_main(on_main) request_handler: AsyncRequestHandler[str | None] = AsyncRequestHandler(callback)
request_handler.run()
filename = await future filename = await request_handler.get_result()
return generic_pb2.String(val=filename if filename else "").SerializeToString() 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: async def record_audio() -> bytes:
loop = asyncio.get_event_loop() def callback(request_handler: AsyncRequestHandler) -> None:
future = loop.create_future()
def on_main() -> None:
from aqt.sound import record_audio from aqt.sound import record_audio
def cb(path: str | None) -> None: def cb(path: str | None) -> None:
loop.call_soon_threadsafe(future.set_result, path) request_handler.set_result(path)
window = aqt.mw.app.activeWindow() window = aqt.mw.app.activeWindow()
assert window is not None assert window is not None
record_audio(window, aqt.mw, True, cb) record_audio(window, aqt.mw, True, cb)
aqt.mw.taskman.run_on_main(on_main) request_handler: AsyncRequestHandler[str | None] = AsyncRequestHandler(callback)
request_handler.run()
path = await future path = await request_handler.get_result()
return generic_pb2.String(val=path if path else "").SerializeToString() return generic_pb2.String(val=path if path else "").SerializeToString()
@ -838,6 +858,67 @@ def open_link() -> bytes:
return b"" 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 = [ post_handler_list = [
congrats_info, congrats_info,
get_deck_configs_for_update, get_deck_configs_for_update,
@ -872,6 +953,8 @@ post_handler_list = [
close_add_cards, close_add_cards,
close_edit_current, close_edit_current,
open_link, open_link,
ask_user,
show_message_box,
] ]

View file

@ -518,16 +518,35 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
} }
if (result.state === NoteFieldsCheckResponse_State.MISSING_CLOZE) { if (result.state === NoteFieldsCheckResponse_State.MISSING_CLOZE) {
// TODO: askUser(tr.addingYouHaveAClozeDeletionNote()) const answer = (
await askUser({
text: tr.addingYouHaveAClozeDeletionNote(),
})
).val;
if (!answer) {
return false; return false;
} }
}
if (result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) { if (result.state === NoteFieldsCheckResponse_State.NOTETYPE_NOT_CLOZE) {
problem = tr.addingClozeOutsideClozeNotetype(); problem = tr.addingClozeOutsideClozeNotetype();
} }
if (result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) { if (result.state === NoteFieldsCheckResponse_State.FIELD_NOT_CLOZE) {
problem = tr.addingClozeOutsideClozeField(); 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) { 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, closeAddCards as closeAddCardsBackend,
closeEditCurrent as closeEditCurrentBackend, closeEditCurrent as closeEditCurrentBackend,
htmlToTextLine, htmlToTextLine,
askUser,
showMessageBox,
} from "@generated/backend"; } from "@generated/backend";
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";
@ -701,6 +722,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { registerShortcut } from "@tslib/shortcuts"; import { registerShortcut } from "@tslib/shortcuts";
import ActionButtons from "./ActionButtons.svelte"; import ActionButtons from "./ActionButtons.svelte";
import HistoryModal from "./HistoryModal.svelte"; import HistoryModal from "./HistoryModal.svelte";
import { HelpPageLinkRequest_HelpPage } from "@generated/anki/links_pb";
import { MessageBoxType } from "@generated/anki/frontend_pb";
$: isIOImageLoaded = false; $: isIOImageLoaded = false;
$: ioImageLoadedStore.set(isIOImageLoaded); $: ioImageLoadedStore.set(isIOImageLoaded);