Skip to content

Commit e6e2999

Browse files
committed
backend for generating and adding occlusion notes
* implemented proto - getting image data with path - returning bytes, file name and current deck id to frontend - frontend return generated occlusion and notes to backend * Created image occlusion dialog to serve image-occlusion * Added image occlusion in editor toolbar to open image occlusion page *
1 parent 08d78b5 commit e6e2999

File tree

22 files changed

+482
-0
lines changed

22 files changed

+482
-0
lines changed

ftl/core/editing.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,5 @@ editing-close-html-tags = Auto-close HTML tags
6868
## You don't need to translate these strings, as they will be replaced with different ones soon.
6969

7070
editing-html-editor = HTML Editor
71+
72+
editing-image-occlusion = Image Occlusion

ftl/core/notetypes.ftl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ notetypes-note-types = Note Types
3535
notetypes-options = Options
3636
notetypes-please-add-another-note-type-first = Please add another note type first.
3737
notetypes-type = Type
38+
39+
## Image Occlusion
40+
41+
notetypes-occlusions-field = Occlusions
42+
notetypes-image-field = Image
43+
notetypes-notes-field = Notes

proto/anki/backend.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ enum ServiceIndex {
3131
SERVICE_INDEX_LINKS = 15;
3232
SERVICE_INDEX_IMPORT_EXPORT = 16;
3333
SERVICE_INDEX_ANKIDROID = 17;
34+
SERVICE_INDEX_IMAGE_OCCLUSION = 18;
3435
}
3536

3637
message BackendInit {

proto/anki/image_occlusion.proto

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright: Ankitects Pty Ltd and contributors
2+
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3+
4+
syntax = "proto3";
5+
6+
option java_multiple_files = true;
7+
8+
package anki.image_occlusion;
9+
10+
import "anki/cards.proto";
11+
import "anki/collection.proto";
12+
import "anki/notes.proto";
13+
import "anki/generic.proto";
14+
15+
service ImageOcclusionService {
16+
rpc GetImageClozeMetadata(ImageClozeMetadataRequest) returns (ImageClozeMetadata);
17+
rpc AddImageOcclusionNotes(AddImageOcclusionNotesRequest) returns (collection.OpChanges);
18+
}
19+
20+
message ImageClozeMetadataRequest {
21+
string path = 1;
22+
}
23+
24+
message ImageClozeMetadata {
25+
bytes data = 1;
26+
string name = 2;
27+
int64 deck_id = 3;
28+
}
29+
30+
message AddImageOcclusionNotesRequest {
31+
string image_path = 1;
32+
int64 deck_id = 2;
33+
string occlusions = 3;
34+
string header = 4;
35+
string notes = 5;
36+
repeated string tags = 6;
37+
}

pylib/anki/collection.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
config_pb2,
1212
generic_pb2,
1313
import_export_pb2,
14+
image_occlusion_pb2,
1415
links_pb2,
1516
search_pb2,
1617
stats_pb2,
@@ -38,6 +39,8 @@
3839
CsvMetadata = import_export_pb2.CsvMetadata
3940
DupeResolution = CsvMetadata.DupeResolution
4041
Delimiter = import_export_pb2.CsvMetadata.Delimiter
42+
ImageClozeMetadata = image_occlusion_pb2.ImageClozeMetadata
43+
AddImageOcclusionNotesRequest = image_occlusion_pb2.AddImageOcclusionNotesRequest
4144

4245
import copy
4346
import os
@@ -454,6 +457,32 @@ def import_json_file(self, path: str) -> ImportLogWithChanges:
454457
def import_json_string(self, json: str) -> ImportLogWithChanges:
455458
return self._backend.import_json_string(json)
456459

460+
# Image Occlusion
461+
def get_image_cloze_metadata(self, path: str | None) -> ImageClozeMetadata:
462+
request = image_occlusion_pb2.ImageClozeMetadataRequest(path=path)
463+
return self._backend.get_image_cloze_metadata(request)
464+
465+
def add_image_occlusion_notes(
466+
self,
467+
image_path: str | None,
468+
deck_id: int | None,
469+
notes_data: bytes | None,
470+
occlusions: str | None,
471+
header: str | None,
472+
notes: str | None,
473+
tags: list[str] | None
474+
) -> bool:
475+
request = image_occlusion_pb2.AddImageOcclusionNotesRequest(
476+
image_path=image_path,
477+
deck_id=deck_id,
478+
notes_data=notes_data,
479+
occlusions=occlusions,
480+
header=header,
481+
notes=notes,
482+
tags=tags
483+
)
484+
return self._backend.add_image_occlusion_notes(request)
485+
457486
# Object helpers
458487
##########################################################################
459488

pylib/tools/genbackend.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import anki.decks_pb2
2121
import anki.i18n_pb2
2222
import anki.import_export_pb2
23+
import anki.image_occlusion_pb2
2324
import anki.links_pb2
2425
import anki.media_pb2
2526
import anki.notes_pb2
@@ -187,6 +188,7 @@ def render_service(
187188
MEDIA=anki.media_pb2,
188189
LINKS=anki.links_pb2,
189190
IMPORT_EXPORT=anki.import_export_pb2,
191+
IMAGE_OCCLUSION=anki.image_occlusion_pb2,
190192
)
191193

192194
for service in anki.backend_pb2.ServiceIndex.DESCRIPTOR.values:
@@ -238,6 +240,7 @@ def render_service(
238240
import anki.tags_pb2
239241
import anki.media_pb2
240242
import anki.import_export_pb2
243+
import anki.image_occlusion_pb2
241244
242245
class RustBackendGenerated:
243246
def _run_command(self, service: int, method: int, input: Any) -> bytes:

qt/aqt/editor.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
tr,
5757
)
5858
from aqt.webview import AnkiWebView
59+
from aqt.imageocclusion import ImageOcclusionDialog
5960

6061
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico")
6162
audio = (
@@ -1171,6 +1172,27 @@ def collapseTags(self) -> None:
11711172
def expandTags(self) -> None:
11721173
aqt.mw.pm.set_tags_collapsed(self.editorMode, False)
11731174

1175+
def onImageOcclusion(self) -> None:
1176+
"""Show a file selection screen, then get selected image path."""
1177+
extension_filter = " ".join(
1178+
f"*.{extension}" for extension in sorted(itertools.chain(pics))
1179+
)
1180+
filter = f"{tr.editing_media()} ({extension_filter})"
1181+
1182+
def accept(file: str) -> None:
1183+
diag = ImageOcclusionDialog(self.mw, file)
1184+
1185+
1186+
file = getFile(
1187+
parent=self.widget,
1188+
title=tr.editing_add_media(),
1189+
cb=cast(Callable[[Any], None], accept),
1190+
filter=filter,
1191+
key="media",
1192+
)
1193+
1194+
self.parentWindow.activateWindow()
1195+
11741196
# Links from HTML
11751197
######################################################################
11761198

@@ -1202,6 +1224,7 @@ def _init_links(self) -> None:
12021224
toggleCloseHTMLTags=Editor.toggleCloseHTMLTags,
12031225
expandTags=Editor.expandTags,
12041226
collapseTags=Editor.collapseTags,
1227+
imageOcclusion=Editor.onImageOcclusion,
12051228
)
12061229

12071230

qt/aqt/imageocclusion.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright: Ankitects Pty Ltd and contributors
2+
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3+
4+
from __future__ import annotations
5+
6+
import aqt
7+
import aqt.deckconf
8+
import aqt.main
9+
import aqt.operations
10+
from aqt.qt import *
11+
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
12+
from aqt.webview import AnkiWebView
13+
14+
15+
class ImageOcclusionDialog(QDialog):
16+
17+
TITLE = "image occlusion"
18+
silentlyClose = True
19+
20+
def __init__(
21+
self,
22+
mw: aqt.main.AnkiQt,
23+
path: str,
24+
) -> None:
25+
QDialog.__init__(self, mw)
26+
self.mw = mw
27+
self._setup_ui(path)
28+
self.show()
29+
30+
def _setup_ui(self, path: str) -> None:
31+
self.setWindowModality(Qt.WindowModality.ApplicationModal)
32+
self.mw.garbage_collect_on_dialog_finish(self)
33+
self.setMinimumSize(400, 300)
34+
disable_help_button(self)
35+
restoreGeom(self, self.TITLE)
36+
addCloseShortcut(self)
37+
38+
self.web = AnkiWebView(title=self.TITLE)
39+
self.web.setVisible(False)
40+
self.web.load_ts_page("image-occlusion")
41+
layout = QVBoxLayout()
42+
layout.setContentsMargins(0, 0, 0, 0)
43+
layout.addWidget(self.web)
44+
self.setLayout(layout)
45+
46+
self.web.eval(f"anki.setupImageOcclusion('{path}');")
47+
self.setWindowTitle(self.TITLE)
48+
49+
def reject(self) -> None:
50+
self.web.cleanup()
51+
self.web = None
52+
saveGeom(self, self.TITLE)
53+
QDialog.reject(self)

qt/aqt/mediasrv.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,17 @@ def handle_on_main() -> None:
447447
return b""
448448

449449

450+
def image_occlusion() -> bytes:
451+
data = request.data
452+
453+
def handle_on_main() -> None:
454+
window = aqt.mw.app.activeWindow()
455+
if isinstance(window, ImageOcclusionDialog):
456+
window.do_image_occlusion(data)
457+
458+
aqt.mw.taskman.run_on_main(handle_on_main)
459+
return b""
460+
450461
post_handler_list = [
451462
congrats_info,
452463
get_deck_configs_for_update,
@@ -455,6 +466,7 @@ def handle_on_main() -> None:
455466
set_scheduling_states,
456467
change_notetype,
457468
import_csv,
469+
image_occlusion,
458470
]
459471

460472

@@ -478,6 +490,9 @@ def handle_on_main() -> None:
478490
"set_graph_preferences",
479491
# TagsService
480492
"complete_tag",
493+
# ImageOcclusionService
494+
"get_image_cloze_metadata",
495+
"add_image_occlusion_notes"
481496
]
482497

483498

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright: Ankitects Pty Ltd and contributors
2+
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3+
4+
use super::Backend;
5+
pub(super) use crate::pb::image_occlusion::imageocclusion_service::Service as ImageOcclusionService;
6+
use crate::{
7+
pb::{self as pb},
8+
prelude::*,
9+
};
10+
11+
impl ImageOcclusionService for Backend {
12+
fn get_image_cloze_metadata(
13+
&self,
14+
input: pb::image_occlusion::ImageClozeMetadataRequest,
15+
) -> Result<pb::image_occlusion::ImageClozeMetadata> {
16+
self.with_col(|col| col.get_image_cloze_metadata(&input.path))
17+
}
18+
19+
fn add_image_occlusion_notes(
20+
&self,
21+
input: pb::image_occlusion::AddImageOcclusionNotesRequest,
22+
) -> Result<pb::collection::OpChanges> {
23+
self.with_col(|col| {
24+
col.add_image_occlusion_notes(
25+
&input.image_path,
26+
input.deck_id.into(),
27+
&input.occlusions,
28+
&input.header,
29+
&input.notes,
30+
input.tags,
31+
)
32+
})
33+
.map(Into::into)
34+
}
35+
}

0 commit comments

Comments
 (0)