diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 89493690a..e220ca37b 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -66,6 +66,7 @@ editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will no editing-mathjax-preview = MathJax Preview editing-shrink-images = Shrink Images editing-close-html-tags = Auto-close HTML tags +editing-from-clipboard = From Clipboard ## You don't need to translate these strings, as they will be replaced with different ones soon. diff --git a/proto/anki/image_occlusion.proto b/proto/anki/image_occlusion.proto index 07c8f0571..34dcaa580 100644 --- a/proto/anki/image_occlusion.proto +++ b/proto/anki/image_occlusion.proto @@ -13,20 +13,23 @@ import "anki/notes.proto"; import "anki/generic.proto"; service ImageOcclusionService { - rpc GetImageForOcclusion(GetImageForOcclusionRequest) returns (ImageData); + rpc GetImageForOcclusion(GetImageForOcclusionRequest) + returns (GetImageForOcclusionResponse); rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest) returns (collection.OpChanges); - rpc GetImageClozeNote(GetImageOcclusionNoteRequest) - returns (ImageClozeNoteResponse); + rpc GetImageOcclusionNote(GetImageOcclusionNoteRequest) + returns (GetImageOcclusionNoteResponse); rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest) returns (collection.OpChanges); + // Adds an I/O notetype if none exists in the collection. + rpc AddImageOcclusionNotetype(generic.Empty) returns (collection.OpChanges); } message GetImageForOcclusionRequest { string path = 1; } -message ImageData { +message GetImageForOcclusionResponse { bytes data = 1; string name = 2; } @@ -37,20 +40,28 @@ message AddImageOcclusionNoteRequest { string header = 3; string back_extra = 4; repeated string tags = 5; -} - -message ImageClozeNote { - bytes image_data = 1; - string occlusions = 2; - string header = 3; - string back_extra = 4; - repeated string tags = 5; + int64 notetype_id = 6; } message GetImageOcclusionNoteRequest { int64 note_id = 1; } +message GetImageOcclusionNoteResponse { + message ImageClozeNote { + bytes image_data = 1; + string occlusions = 2; + string header = 3; + string back_extra = 4; + repeated string tags = 5; + } + + oneof value { + ImageClozeNote note = 1; + string error = 2; + } +} + message UpdateImageOcclusionNoteRequest { int64 note_id = 1; string occlusions = 2; @@ -58,10 +69,3 @@ message UpdateImageOcclusionNoteRequest { string back_extra = 4; repeated string tags = 5; } - -message ImageClozeNoteResponse { - oneof value { - ImageClozeNote note = 1; - string error = 2; - } -} diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 748ae9824..ae14152aa 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -41,9 +41,9 @@ CsvMetadata = import_export_pb2.CsvMetadata DupeResolution = CsvMetadata.DupeResolution Delimiter = import_export_pb2.CsvMetadata.Delimiter TtsVoice = card_rendering_pb2.AllTtsVoicesResponse.TtsVoice -ImageData = image_occlusion_pb2.ImageData +GetImageForOcclusionResponse = image_occlusion_pb2.GetImageForOcclusionResponse AddImageOcclusionNoteRequest = image_occlusion_pb2.AddImageOcclusionNoteRequest -ImageClozeNoteResponse = image_occlusion_pb2.ImageClozeNoteResponse +GetImageOcclusionNoteResponse = image_occlusion_pb2.GetImageOcclusionNoteResponse import copy import os @@ -462,18 +462,21 @@ class Collection(DeprecatedNamesMixin): # Image Occlusion ########################################################################## - def get_image_for_occlusion(self, path: str | None) -> ImageData: + + def get_image_for_occlusion(self, path: str | None) -> GetImageForOcclusionResponse: return self._backend.get_image_for_occlusion(path=path) def add_image_occlusion_note( self, - image_path: str | None, - occlusions: str | None, - header: str | None, - back_extra: str | None, - tags: list[str] | None, + notetype_id: int, + image_path: str, + occlusions: str, + header: str, + back_extra: str, + tags: list[str], ) -> OpChanges: return self._backend.add_image_occlusion_note( + notetype_id=notetype_id, image_path=image_path, occlusions=occlusions, header=header, @@ -481,8 +484,10 @@ class Collection(DeprecatedNamesMixin): tags=tags, ) - def get_image_cloze_note(self, note_id: int | None) -> ImageClozeNoteResponse: - return self._backend.get_image_cloze_note(note_id=note_id) + def get_image_occlusion_note( + self, note_id: int | None + ) -> GetImageOcclusionNoteResponse: + return self._backend.get_image_occlusion_note(note_id=note_id) def update_image_occlusion_note( self, diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 2ae38ac2a..c46aef4bf 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -481,7 +481,7 @@ exposed_backend_list = [ # ImageOcclusionService "get_image_for_occlusion", "add_image_occlusion_note", - "get_image_cloze_note", + "get_image_occlusion_note", "update_image_occlusion_note", ] diff --git a/rslib/src/backend/image_occlusion.rs b/rslib/src/backend/image_occlusion.rs index 06aa34ecd..0ec876475 100644 --- a/rslib/src/backend/image_occlusion.rs +++ b/rslib/src/backend/image_occlusion.rs @@ -10,7 +10,7 @@ impl ImageOcclusionService for Backend { fn get_image_for_occlusion( &self, input: pb::image_occlusion::GetImageForOcclusionRequest, - ) -> Result { + ) -> Result { self.with_col(|col| col.get_image_for_occlusion(&input.path)) } @@ -20,6 +20,7 @@ impl ImageOcclusionService for Backend { ) -> Result { self.with_col(|col| { col.add_image_occlusion_note( + input.notetype_id.into(), &input.image_path, &input.occlusions, &input.header, @@ -30,11 +31,11 @@ impl ImageOcclusionService for Backend { .map(Into::into) } - fn get_image_cloze_note( + fn get_image_occlusion_note( &self, input: pb::image_occlusion::GetImageOcclusionNoteRequest, - ) -> Result { - self.with_col(|col| col.get_image_cloze_note(input.note_id.into())) + ) -> Result { + self.with_col(|col| col.get_image_occlusion_note(input.note_id.into())) } fn update_image_occlusion_note( @@ -52,4 +53,12 @@ impl ImageOcclusionService for Backend { }) .map(Into::into) } + + fn add_image_occlusion_notetype( + &self, + _input: pb::generic::Empty, + ) -> Result { + self.with_col(|col| col.add_image_occlusion_notetype()) + .map(Into::into) + } } diff --git a/rslib/src/image_occlusion/imagedata.rs b/rslib/src/image_occlusion/imagedata.rs index ad7594196..9e103f27b 100644 --- a/rslib/src/image_occlusion/imagedata.rs +++ b/rslib/src/image_occlusion/imagedata.rs @@ -3,27 +3,22 @@ use std::path::Path; use std::path::PathBuf; -use std::sync::Arc; use regex::Regex; use crate::io::metadata; use crate::io::read_file; use crate::media::MediaManager; -use crate::notetype::stock::empty_stock; use crate::notetype::CardGenContext; -use crate::notetype::Notetype; -use crate::notetype::NotetypeKind; -use crate::pb::image_occlusion::image_cloze_note_response::Value; -use crate::pb::image_occlusion::ImageClozeNote; -use crate::pb::image_occlusion::ImageClozeNoteResponse; -pub use crate::pb::image_occlusion::ImageData; -use crate::pb::notetypes::stock_notetype::OriginalStockKind; +use crate::pb::image_occlusion::get_image_occlusion_note_response::ImageClozeNote; +use crate::pb::image_occlusion::get_image_occlusion_note_response::Value; +use crate::pb::image_occlusion::GetImageForOcclusionResponse; +use crate::pb::image_occlusion::GetImageOcclusionNoteResponse; use crate::prelude::*; impl Collection { - pub fn get_image_for_occlusion(&mut self, path: &str) -> Result { - let mut metadata = ImageData { + pub fn get_image_for_occlusion(&mut self, path: &str) -> Result { + let mut metadata = GetImageForOcclusionResponse { ..Default::default() }; metadata.data = read_file(path)?; @@ -33,6 +28,7 @@ impl Collection { #[allow(clippy::too_many_arguments)] pub fn add_image_occlusion_note( &mut self, + notetype_id: NotetypeId, image_path: &str, occlusions: &str, header: &str, @@ -52,13 +48,20 @@ impl Collection { let actual_image_name_after_adding = mgr.add_file(&image_filename, &image_bytes)?; let image_tag = format!( - "", + r#""#, &actual_image_name_after_adding ); let current_deck = self.get_current_deck()?; self.transact(Op::ImageOcclusion, |col| { - let nt = col.get_or_create_io_notetype()?; + let nt = if notetype_id.0 == 0 { + // when testing via .html page, use first available notetype + col.add_image_occlusion_notetype_inner()?; + col.get_first_io_notetype()? + .or_invalid("expected an i/o notetype to exist")? + } else { + col.get_io_notetype_by_id(notetype_id)? + }; let mut note = nt.new_note(); note.set_field(0, occlusions)?; @@ -76,47 +79,18 @@ impl Collection { }) } - fn get_or_create_io_notetype(&mut self) -> Result> { - let tr = &self.tr; - let name = format!("{}", tr.notetypes_image_occlusion_name()); - let nt = match self.get_notetype_by_name(&name)? { - Some(nt) => nt, - None => { - self.add_io_notetype()?; - if let Some(nt) = self.get_notetype_by_name(&name)? { - nt - } else { - return Err(AnkiError::TemplateError { - info: "IO notetype not found".to_string(), - }); - } - } - }; - if nt.fields.len() < 4 { - Err(AnkiError::TemplateError { - info: "IO notetype must have 4+ fields".to_string(), - }) - } else { - Ok(nt) - } - } - - fn add_io_notetype(&mut self) -> Result<()> { - let usn = self.usn()?; - let mut nt = image_occlusion_notetype(&self.tr); - self.add_notetype_inner(&mut nt, usn, false)?; - Ok(()) - } - - pub fn get_image_cloze_note(&mut self, note_id: NoteId) -> Result { - let value = match self.get_image_cloze_note_inner(note_id) { + pub fn get_image_occlusion_note( + &mut self, + note_id: NoteId, + ) -> Result { + let value = match self.get_image_occlusion_note_inner(note_id) { Ok(note) => Value::Note(note), Err(err) => Value::Error(format!("{:?}", err)), }; - Ok(ImageClozeNoteResponse { value: Some(value) }) + Ok(GetImageOcclusionNoteResponse { value: Some(value) }) } - pub fn get_image_cloze_note_inner(&mut self, note_id: NoteId) -> Result { + pub fn get_image_occlusion_note_inner(&mut self, note_id: NoteId) -> Result { let note = self.storage.get_note(note_id)?.or_not_found(note_id)?; let mut cloze_note = ImageClozeNote::default(); @@ -131,9 +105,9 @@ impl Collection { cloze_note.image_data = "".into(); cloze_note.tags = note.tags.clone(); - let image_file_name = fields[1].clone(); + let image_file_name = &fields[1]; let src = self - .extract_img_src(&image_file_name) + .extract_img_src(image_file_name) .unwrap_or_else(|| "".to_owned()); let final_path = self.media_folder.join(src); @@ -190,58 +164,3 @@ impl Collection { Ok(false) } } - -pub(crate) fn image_occlusion_notetype(tr: &I18n) -> Notetype { - const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css"); - let mut nt = empty_stock( - NotetypeKind::Cloze, - OriginalStockKind::ImageOcclusion, - tr.notetypes_image_occlusion_name(), - ); - nt.config.css = IMAGE_CLOZE_CSS.to_string(); - let occlusion = tr.notetypes_occlusion(); - nt.add_field(occlusion.as_ref()); - let image = tr.notetypes_image(); - nt.add_field(image.as_ref()); - let header = tr.notetypes_header(); - nt.add_field(header.as_ref()); - let back_extra = tr.notetypes_back_extra_field(); - nt.add_field(back_extra.as_ref()); - let comments = tr.notetypes_comments_field(); - nt.add_field(comments.as_ref()); - let qfmt = format!( - "
{{{{cloze:{}}}}}
-
- {{{{{}}}}} - -
-
- -", - occlusion, - image, - tr.notetypes_error_loading_image_occlusion(), - ); - let afmt = format!( - "{{{{{}}}}} -{} - -
-{{{{{}}}}} -
-{{{{{}}}}}", - header, - qfmt, - tr.notetypes_toggle_masks(), - back_extra, - comments, - ); - nt.add_template(nt.name.clone(), qfmt, afmt); - nt -} diff --git a/rslib/src/image_occlusion/mod.rs b/rslib/src/image_occlusion/mod.rs index d71f09987..0983198b1 100644 --- a/rslib/src/image_occlusion/mod.rs +++ b/rslib/src/image_occlusion/mod.rs @@ -3,3 +3,4 @@ pub mod imagedata; pub mod imageocclusion; +pub(crate) mod notetype; diff --git a/rslib/src/image_occlusion/notetype.rs b/rslib/src/image_occlusion/notetype.rs new file mode 100644 index 000000000..e0e2febb7 --- /dev/null +++ b/rslib/src/image_occlusion/notetype.rs @@ -0,0 +1,114 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::sync::Arc; + +use crate::notetype::stock::empty_stock; +use crate::notetype::Notetype; +use crate::notetype::NotetypeKind; +use crate::pb::notetypes::stock_notetype::OriginalStockKind; +use crate::prelude::*; + +impl Collection { + pub fn add_image_occlusion_notetype(&mut self) -> Result> { + self.transact(Op::UpdateNotetype, |col| { + col.add_image_occlusion_notetype_inner() + }) + } + + pub fn add_image_occlusion_notetype_inner(&mut self) -> Result<()> { + if self.get_first_io_notetype()?.is_none() { + let mut nt = image_occlusion_notetype(&self.tr); + let current_id = self.get_current_notetype_id(); + self.add_notetype_inner(&mut nt, self.usn()?, false)?; + if let Some(current_id) = current_id { + // preserve previous default + self.set_current_notetype_id(current_id)?; + } + } + Ok(()) + } + + /// Returns the I/O notetype with the provided id, checking to make sure it + /// is valid. + pub(crate) fn get_io_notetype_by_id( + &mut self, + notetype_id: NotetypeId, + ) -> Result> { + let nt = self.get_notetype(notetype_id)?.or_not_found(notetype_id)?; + io_notetype_if_valid(nt) + } + + pub(crate) fn get_first_io_notetype(&mut self) -> Result>> { + for (_, nt) in self.get_all_notetypes()? { + if nt.config.original_stock_kind() == OriginalStockKind::ImageOcclusion { + return Some(io_notetype_if_valid(nt)).transpose(); + } + } + + Ok(None) + } +} + +pub(crate) fn image_occlusion_notetype(tr: &I18n) -> Notetype { + const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css"); + let mut nt = empty_stock( + NotetypeKind::Cloze, + OriginalStockKind::ImageOcclusion, + tr.notetypes_image_occlusion_name(), + ); + nt.config.css = IMAGE_CLOZE_CSS.to_string(); + let occlusion = tr.notetypes_occlusion(); + nt.add_field(occlusion.as_ref()); + let image = tr.notetypes_image(); + nt.add_field(image.as_ref()); + let header = tr.notetypes_header(); + nt.add_field(header.as_ref()); + let back_extra = tr.notetypes_back_extra_field(); + nt.add_field(back_extra.as_ref()); + let comments = tr.notetypes_comments_field(); + nt.add_field(comments.as_ref()); + + let err_loading = tr.notetypes_error_loading_image_occlusion(); + let qfmt = format!( + "\ +{{{{#{header}}}}}
{{{{{header}}}}}
{{{{/{header}}}}} +
{{{{cloze:{occlusion}}}}}
+
+
+ {{{{{image}}}}} + +
+ +" + ); + + let toggle_masks = tr.notetypes_toggle_masks(); + let afmt = format!( + "\ +{qfmt} +
+{{{{#{back_extra}}}}}
{{{{{back_extra}}}}}
{{{{/{back_extra}}}}} +", + ); + nt.add_template(nt.name.clone(), qfmt, afmt); + nt +} + +fn io_notetype_if_valid(nt: Arc) -> Result> { + if nt.config.original_stock_kind() != OriginalStockKind::ImageOcclusion { + invalid_input!("Not an image occlusion notetype"); + } + if nt.fields.len() < 4 { + return Err(AnkiError::TemplateError { + info: "IO notetype must have 4+ fields".to_string(), + }); + } + Ok(nt) +} diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs index 1769895d1..bb69fe927 100644 --- a/rslib/src/notetype/stock.rs +++ b/rslib/src/notetype/stock.rs @@ -6,7 +6,7 @@ use crate::config::ConfigEntry; use crate::config::ConfigKey; use crate::error::Result; use crate::i18n::I18n; -use crate::image_occlusion::imagedata::image_occlusion_notetype; +use crate::image_occlusion::notetype::image_occlusion_notetype; use crate::invalid_input; use crate::notetype::Notetype; use crate::pb::notetypes::notetype::config::Kind as NotetypeKind; diff --git a/ts/image-occlusion/ImageOcclusionPage.svelte b/ts/image-occlusion/ImageOcclusionPage.svelte index 02502953d..802610962 100644 --- a/ts/image-occlusion/ImageOcclusionPage.svelte +++ b/ts/image-occlusion/ImageOcclusionPage.svelte @@ -6,20 +6,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import * as tr from "@tslib/ftl"; import Container from "../components/Container.svelte"; - import { saveImageNotes } from "./generate"; + import { addOrUpdateNote } from "./generate"; + import type { IOMode } from "./lib"; import MasksEditor from "./MaskEditor.svelte"; import Notes from "./Notes.svelte"; import StickyFooter from "./StickyFooter.svelte"; - export let path: string | null; - export let noteId: number | null; + export let mode: IOMode; async function hideAllGuessOne(): Promise { - saveImageNotes(path!, noteId!, false); + addOrUpdateNote(mode, false); } async function hideOneGuessOne(): Promise { - saveImageNotes(path!, noteId!, true); + addOrUpdateNote(mode, true); } const items = [ @@ -41,12 +41,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/image-occlusion/MaskEditor.svelte b/ts/image-occlusion/MaskEditor.svelte index a598246d7..ec8d3446f 100644 --- a/ts/image-occlusion/MaskEditor.svelte +++ b/ts/image-occlusion/MaskEditor.svelte @@ -6,11 +6,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { PanZoom } from "panzoom"; import panzoom from "panzoom"; + import type { IOMode } from "./lib"; import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor"; import SideToolbar from "./SideToolbar.svelte"; - export let path: string | null; - export let noteId: number | null; + export let mode: IOMode; let instance: PanZoom; let innerWidth = 0; @@ -26,14 +26,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }); instance.pause(); - if (path) { - setupMaskEditor(path, instance).then((canvas1) => { + if (mode.kind == "add") { + setupMaskEditor(mode.imagePath, instance).then((canvas1) => { canvas = canvas1; }); - } - - if (noteId) { - setupMaskEditorForEdit(noteId, instance).then((canvas1) => { + } else { + setupMaskEditorForEdit(mode.noteId, instance).then((canvas1) => { canvas = canvas1; }); } diff --git a/ts/image-occlusion/StickyFooter.svelte b/ts/image-occlusion/StickyFooter.svelte index be36d9416..5336b68ac 100644 --- a/ts/image-occlusion/StickyFooter.svelte +++ b/ts/image-occlusion/StickyFooter.svelte @@ -10,12 +10,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let hideAllGuessOne: () => void; export let hideOneGuessOne: () => void; - export let noteId: number | null; + export let editing: boolean;