Use field tags instead of hard-coding occlusion fields

+ Don't protect the comments field

It's not required by our current code. We can remove the protection
from Header and Back Extra in the future too, once we no longer depend
on them.

Closes #2621
This commit is contained in:
Damien Elmes 2023-09-17 15:21:20 +10:00
parent 906a937faf
commit a7b4c90146
10 changed files with 134 additions and 58 deletions

View file

@ -49,7 +49,7 @@ module.exports = {
}, },
}, },
], ],
env: { browser: true }, env: { browser: true, es2020: true },
ignorePatterns: ["backend_proto.d.ts", "*.svelte.d.ts", "vendor", "extra/*"], ignorePatterns: ["backend_proto.d.ts", "*.svelte.d.ts", "vendor", "extra/*"],
globals: { globals: {
globalThis: false, globalThis: false,

View file

@ -52,4 +52,5 @@ notetypes-error-getting-imagecloze = An error occurred while fetching an image o
notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date? notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date?
notetype-error-no-image-to-show = No image to show. notetype-error-no-image-to-show = No image to show.
notetypes-no-occlusion-created = You must make at least one occlusion. notetypes-no-occlusion-created = You must make at least one occlusion.
notetypes-no-occlusion-created2 = Unable to add. Either you have not added any occlusions, or the first field is empty.
notetypes-io-select-image = Select Image notetypes-io-select-image = Select Image

View file

@ -13,14 +13,18 @@ import "anki/generic.proto";
service ImageOcclusionService { service ImageOcclusionService {
rpc GetImageForOcclusion(GetImageForOcclusionRequest) rpc GetImageForOcclusion(GetImageForOcclusionRequest)
returns (GetImageForOcclusionResponse); returns (GetImageForOcclusionResponse);
rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest)
returns (collection.OpChanges);
rpc GetImageOcclusionNote(GetImageOcclusionNoteRequest) rpc GetImageOcclusionNote(GetImageOcclusionNoteRequest)
returns (GetImageOcclusionNoteResponse); returns (GetImageOcclusionNoteResponse);
rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest) rpc GetImageOcclusionFields(GetImageOcclusionFieldsRequest)
returns (collection.OpChanges); returns (GetImageOcclusionFieldsResponse);
// Adds an I/O notetype if none exists in the collection. // Adds an I/O notetype if none exists in the collection.
rpc AddImageOcclusionNotetype(generic.Empty) returns (collection.OpChanges); rpc AddImageOcclusionNotetype(generic.Empty) returns (collection.OpChanges);
// These two are used by the standalone I/O page, but not used when using
// I/O inside Anki's editor
rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest)
returns (collection.OpChanges);
rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest)
returns (collection.OpChanges);
} }
// Implicitly includes any of the above methods that are not listed in the // Implicitly includes any of the above methods that are not listed in the
@ -71,3 +75,18 @@ message UpdateImageOcclusionNoteRequest {
string back_extra = 4; string back_extra = 4;
repeated string tags = 5; repeated string tags = 5;
} }
message GetImageOcclusionFieldsRequest {
int64 notetype_id = 1;
}
message GetImageOcclusionFieldsResponse {
ImageOcclusionFieldIndexes fields = 1;
}
message ImageOcclusionFieldIndexes {
uint32 occlusions = 1;
uint32 image = 2;
uint32 header = 3;
uint32 back_extra = 4;
}

View file

@ -305,7 +305,7 @@ class AddCards(QMainWindow):
problem = None problem = None
if result == NoteFieldsCheckResult.EMPTY: if result == NoteFieldsCheckResult.EMPTY:
if self.editor.current_notetype_is_image_occlusion(): if self.editor.current_notetype_is_image_occlusion():
problem = tr.notetypes_no_occlusion_created() problem = tr.notetypes_no_occlusion_created2()
else: else:
problem = tr.adding_the_first_field_is_empty() problem = tr.adding_the_first_field_is_empty()
elif result == NoteFieldsCheckResult.MISSING_CLOZE: elif result == NoteFieldsCheckResult.MISSING_CLOZE:

View file

@ -544,6 +544,7 @@ exposed_backend_list = [
"add_image_occlusion_note", "add_image_occlusion_note",
"get_image_occlusion_note", "get_image_occlusion_note",
"update_image_occlusion_note", "update_image_occlusion_note",
"get_image_occlusion_fields",
# SchedulerService # SchedulerService
"compute_fsrs_weights", "compute_fsrs_weights",
"compute_optimal_retention", "compute_optimal_retention",

View file

@ -6,6 +6,7 @@ use std::sync::Arc;
use anki_i18n::I18n; use anki_i18n::I18n;
use anki_proto::notetypes::stock_notetype::OriginalStockKind; use anki_proto::notetypes::stock_notetype::OriginalStockKind;
use anki_proto::notetypes::ImageOcclusionField;
use itertools::Itertools; use itertools::Itertools;
use tracing::debug; use tracing::debug;
@ -442,7 +443,7 @@ impl Collection {
let conf = &mut nt.fields[i].config; let conf = &mut nt.fields[i].config;
if !conf.prevent_deletion { if !conf.prevent_deletion {
changed = true; changed = true;
conf.prevent_deletion = true; conf.prevent_deletion = i != ImageOcclusionField::Comments as usize;
conf.tag = Some(i as u32); conf.tag = Some(i as u32);
} }
} }

View file

@ -8,8 +8,11 @@ use anki_io::metadata;
use anki_io::read_file; use anki_io::read_file;
use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageClozeNote; use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageClozeNote;
use anki_proto::image_occlusion::get_image_occlusion_note_response::Value; use anki_proto::image_occlusion::get_image_occlusion_note_response::Value;
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
use anki_proto::image_occlusion::GetImageForOcclusionResponse; use anki_proto::image_occlusion::GetImageForOcclusionResponse;
use anki_proto::image_occlusion::GetImageOcclusionNoteResponse; use anki_proto::image_occlusion::GetImageOcclusionNoteResponse;
use anki_proto::image_occlusion::ImageOcclusionFieldIndexes;
use anki_proto::notetypes::ImageOcclusionField;
use regex::Regex; use regex::Regex;
use crate::media::MediaManager; use crate::media::MediaManager;
@ -24,19 +27,13 @@ impl Collection {
Ok(metadata) Ok(metadata)
} }
#[allow(clippy::too_many_arguments)]
pub fn add_image_occlusion_note( pub fn add_image_occlusion_note(
&mut self, &mut self,
notetype_id: NotetypeId, req: AddImageOcclusionNoteRequest,
image_path: &str,
occlusions: &str,
header: &str,
back_extra: &str,
tags: Vec<String>,
) -> Result<OpOutput<()>> { ) -> Result<OpOutput<()>> {
// image file // image file
let image_bytes = read_file(image_path)?; let image_bytes = read_file(&req.image_path)?;
let image_filename = Path::new(&image_path) let image_filename = Path::new(&req.image_path)
.file_name() .file_name()
.or_not_found("expected filename")? .or_not_found("expected filename")?
.to_str() .to_str()
@ -49,6 +46,7 @@ impl Collection {
let image_tag = format!(r#"<img src="{}">"#, &actual_image_name_after_adding); let image_tag = format!(r#"<img src="{}">"#, &actual_image_name_after_adding);
let current_deck = self.get_current_deck()?; let current_deck = self.get_current_deck()?;
let notetype_id: NotetypeId = req.notetype_id.into();
self.transact(Op::ImageOcclusion, |col| { self.transact(Op::ImageOcclusion, |col| {
let nt = if notetype_id.0 == 0 { let nt = if notetype_id.0 == 0 {
// when testing via .html page, use first available notetype // when testing via .html page, use first available notetype
@ -60,11 +58,12 @@ impl Collection {
}; };
let mut note = nt.new_note(); let mut note = nt.new_note();
note.set_field(0, occlusions)?; let idxs = nt.get_io_field_indexes()?;
note.set_field(1, &image_tag)?; note.set_field(idxs.occlusions as usize, req.occlusions)?;
note.set_field(2, header)?; note.set_field(idxs.image as usize, image_tag)?;
note.set_field(3, back_extra)?; note.set_field(idxs.header as usize, req.header)?;
note.tags = tags; note.set_field(idxs.back_extra as usize, req.back_extra)?;
note.tags = req.tags;
col.add_note_inner(&mut note, current_deck.id)?; col.add_note_inner(&mut note, current_deck.id)?;
Ok(()) Ok(())
@ -87,17 +86,19 @@ impl Collection {
let mut cloze_note = ImageClozeNote::default(); let mut cloze_note = ImageClozeNote::default();
let fields = note.fields(); let fields = note.fields();
if fields.len() < 4 {
invalid_input!("Note does not have 4 fields");
}
cloze_note.occlusions = fields[0].clone(); let nt = self
cloze_note.header = fields[2].clone(); .get_notetype(note.notetype_id)?
cloze_note.back_extra = fields[3].clone(); .or_not_found(note.notetype_id)?;
let idxs = nt.get_io_field_indexes()?;
cloze_note.occlusions = fields[idxs.occlusions as usize].clone();
cloze_note.header = fields[idxs.header as usize].clone();
cloze_note.back_extra = fields[idxs.back_extra as usize].clone();
cloze_note.image_data = "".into(); cloze_note.image_data = "".into();
cloze_note.tags = note.tags.clone(); cloze_note.tags = note.tags.clone();
let image_file_name = &fields[1]; let image_file_name = &fields[idxs.image as usize];
let src = self let src = self
.extract_img_src(image_file_name) .extract_img_src(image_file_name)
.unwrap_or_else(|| "".to_owned()); .unwrap_or_else(|| "".to_owned());
@ -120,9 +121,13 @@ impl Collection {
) -> Result<OpOutput<()>> { ) -> Result<OpOutput<()>> {
let mut note = self.storage.get_note(note_id)?.or_not_found(note_id)?; let mut note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
self.transact(Op::ImageOcclusion, |col| { self.transact(Op::ImageOcclusion, |col| {
note.set_field(0, occlusions)?; let nt = col
note.set_field(2, header)?; .get_notetype(note.notetype_id)?
note.set_field(3, back_extra)?; .or_not_found(note.notetype_id)?;
let idxs = nt.get_io_field_indexes()?;
note.set_field(idxs.occlusions as usize, occlusions)?;
note.set_field(idxs.header as usize, header)?;
note.set_field(idxs.back_extra as usize, back_extra)?;
note.tags = tags; note.tags = tags;
col.update_note_inner(&mut note)?; col.update_note_inner(&mut note)?;
Ok(()) Ok(())
@ -156,3 +161,37 @@ impl Collection {
Ok(false) Ok(false)
} }
} }
impl Notetype {
pub(crate) fn get_io_field_indexes(&self) -> Result<ImageOcclusionFieldIndexes> {
get_field_indexes_by_tag(self).or_else(|_| {
if self.fields.len() < 4 {
return Err(AnkiError::DatabaseCheckRequired);
}
Ok(ImageOcclusionFieldIndexes {
occlusions: 0,
image: 1,
header: 2,
back_extra: 3,
})
})
}
}
fn get_field_indexes_by_tag(nt: &Notetype) -> Result<ImageOcclusionFieldIndexes> {
Ok(ImageOcclusionFieldIndexes {
occlusions: get_field_index(nt, ImageOcclusionField::Occlusions)?,
image: get_field_index(nt, ImageOcclusionField::Image)?,
header: get_field_index(nt, ImageOcclusionField::Header)?,
back_extra: get_field_index(nt, ImageOcclusionField::BackExtra)?,
})
}
fn get_field_index(nt: &Notetype, field: ImageOcclusionField) -> Result<u32> {
nt.fields
.iter()
.enumerate()
.find(|(_idx, f)| f.config.tag == Some(field as u32))
.map(|(idx, _)| idx as u32)
.ok_or(AnkiError::DatabaseCheckRequired)
}

View file

@ -86,7 +86,7 @@ pub(crate) fn image_occlusion_notetype(tr: &I18n) -> Notetype {
let comments = tr.notetypes_comments_field(); let comments = tr.notetypes_comments_field();
config = nt.add_field(comments.as_ref()); config = nt.add_field(comments.as_ref());
config.tag = Some(ImageOcclusionField::Comments as u32); config.tag = Some(ImageOcclusionField::Comments as u32);
config.prevent_deletion = true; config.prevent_deletion = false;
let err_loading = tr.notetypes_error_loading_image_occlusion(); let err_loading = tr.notetypes_error_loading_image_occlusion();
let qfmt = format!( let qfmt = format!(

View file

@ -3,47 +3,42 @@
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest; use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
use anki_proto::image_occlusion::GetImageForOcclusionRequest; use anki_proto::image_occlusion::GetImageForOcclusionRequest;
use anki_proto::image_occlusion::GetImageForOcclusionResponse; use anki_proto::image_occlusion::GetImageForOcclusionResponse;
use anki_proto::image_occlusion::GetImageOcclusionFieldsRequest;
use anki_proto::image_occlusion::GetImageOcclusionFieldsResponse;
use anki_proto::image_occlusion::GetImageOcclusionNoteRequest; use anki_proto::image_occlusion::GetImageOcclusionNoteRequest;
use anki_proto::image_occlusion::GetImageOcclusionNoteResponse; use anki_proto::image_occlusion::GetImageOcclusionNoteResponse;
use anki_proto::image_occlusion::UpdateImageOcclusionNoteRequest; use anki_proto::image_occlusion::UpdateImageOcclusionNoteRequest;
use crate::collection::Collection; use crate::collection::Collection;
use crate::error; use crate::error::Result;
use crate::prelude::*;
impl crate::services::ImageOcclusionService for Collection { impl crate::services::ImageOcclusionService for Collection {
fn get_image_for_occlusion( fn get_image_for_occlusion(
&mut self, &mut self,
input: GetImageForOcclusionRequest, input: GetImageForOcclusionRequest,
) -> error::Result<GetImageForOcclusionResponse> { ) -> Result<GetImageForOcclusionResponse> {
self.get_image_for_occlusion(&input.path) self.get_image_for_occlusion(&input.path)
} }
fn add_image_occlusion_note( fn add_image_occlusion_note(
&mut self, &mut self,
input: AddImageOcclusionNoteRequest, input: AddImageOcclusionNoteRequest,
) -> error::Result<anki_proto::collection::OpChanges> { ) -> Result<anki_proto::collection::OpChanges> {
self.add_image_occlusion_note( self.add_image_occlusion_note(input).map(Into::into)
input.notetype_id.into(),
&input.image_path,
&input.occlusions,
&input.header,
&input.back_extra,
input.tags,
)
.map(Into::into)
} }
fn get_image_occlusion_note( fn get_image_occlusion_note(
&mut self, &mut self,
input: GetImageOcclusionNoteRequest, input: GetImageOcclusionNoteRequest,
) -> error::Result<GetImageOcclusionNoteResponse> { ) -> Result<GetImageOcclusionNoteResponse> {
self.get_image_occlusion_note(input.note_id.into()) self.get_image_occlusion_note(input.note_id.into())
} }
fn update_image_occlusion_note( fn update_image_occlusion_note(
&mut self, &mut self,
input: UpdateImageOcclusionNoteRequest, input: UpdateImageOcclusionNoteRequest,
) -> error::Result<anki_proto::collection::OpChanges> { ) -> Result<anki_proto::collection::OpChanges> {
self.update_image_occlusion_note( self.update_image_occlusion_note(
input.note_id.into(), input.note_id.into(),
&input.occlusions, &input.occlusions,
@ -54,7 +49,18 @@ impl crate::services::ImageOcclusionService for Collection {
.map(Into::into) .map(Into::into)
} }
fn add_image_occlusion_notetype(&mut self) -> error::Result<anki_proto::collection::OpChanges> { fn add_image_occlusion_notetype(&mut self) -> Result<anki_proto::collection::OpChanges> {
self.add_image_occlusion_notetype().map(Into::into) self.add_image_occlusion_notetype().map(Into::into)
} }
fn get_image_occlusion_fields(
&mut self,
input: GetImageOcclusionFieldsRequest,
) -> Result<GetImageOcclusionFieldsResponse> {
let ntid = NotetypeId::from(input.notetype_id);
let nt = self.get_notetype(ntid)?.or_not_found(ntid)?;
Ok(GetImageOcclusionFieldsResponse {
fields: Some(nt.get_io_field_indexes()?),
})
}
} }

View file

@ -267,7 +267,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
fontSize: fonts[index][1], fontSize: fonts[index][1],
direction: fonts[index][2] ? "rtl" : "ltr", direction: fonts[index][2] ? "rtl" : "ltr",
collapsed: fieldsCollapsed[index], collapsed: fieldsCollapsed[index],
hidden: hideFieldInOcclusionType(index), hidden: hideFieldInOcclusionType(index, ioFields),
})) as FieldData[]; })) as FieldData[];
function saveTags({ detail }: CustomEvent): void { function saveTags({ detail }: CustomEvent): void {
@ -384,6 +384,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}); });
} }
import { ImageOcclusionFieldIndexes } from "@tslib/anki/image_occlusion_pb";
import { getImageOcclusionFields } from "@tslib/backend";
import { wrapInternal } from "@tslib/wrap"; import { wrapInternal } from "@tslib/wrap";
import LabelButton from "components/LabelButton.svelte"; import LabelButton from "components/LabelButton.svelte";
import Shortcut from "components/Shortcut.svelte"; import Shortcut from "components/Shortcut.svelte";
@ -398,19 +400,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let isIOImageLoaded = false; let isIOImageLoaded = false;
let imageOcclusionMode: IOMode | undefined; let imageOcclusionMode: IOMode | undefined;
let ioFields = new ImageOcclusionFieldIndexes({});
async function setupMaskEditor(options: { html: string; mode: IOMode }) { async function setupMaskEditor(options: { html: string; mode: IOMode }) {
imageOcclusionMode = undefined; imageOcclusionMode = undefined;
await tick(); const getIoFields = getImageOcclusionFields({
notetypeId: BigInt(notetypeMeta.id),
}).then((r) => (ioFields = r.fields!));
await Promise.all([tick(), getIoFields]);
imageOcclusionMode = options.mode; imageOcclusionMode = options.mode;
if (options.mode.kind === "add") { if (options.mode.kind === "add") {
fieldStores[1].set(options.html); fieldStores[ioFields.image].set(options.html);
// new image is being added // new image is being added
if (isIOImageLoaded) { if (isIOImageLoaded) {
resetIOImage(options.mode.imagePath); resetIOImage(options.mode.imagePath);
} }
} else { } else {
const clozeNote = get(fieldStores[0]); const clozeNote = get(fieldStores[ioFields.occlusions]);
if (clozeNote.includes("oi=1")) { if (clozeNote.includes("oi=1")) {
$hideAllGuessOne = true; $hideAllGuessOne = true;
} else { } else {
@ -422,14 +428,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
function setImageField(html) { function setImageField(html) {
fieldStores[1].set(html); fieldStores[ioFields.image].set(html);
} }
globalThis.setImageField = setImageField; globalThis.setImageField = setImageField;
// update cloze deletions and set occlusion fields, it call in saveNow to update cloze deletions // update cloze deletions and set occlusion fields, it call in saveNow to update cloze deletions
function updateIONoteInEditMode() { function updateIONoteInEditMode() {
if (isEditMode) { if (isEditMode) {
const clozeNote = get(fieldStores[0]); const clozeNote = get(fieldStores[ioFields.occlusions]);
if (clozeNote.includes("oi=1")) { if (clozeNote.includes("oi=1")) {
setOcclusionField(true); setOcclusionField(true);
} else { } else {
@ -441,7 +447,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function setOcclusionFieldInner() { function setOcclusionFieldInner() {
if (isImageOcclusion) { if (isImageOcclusion) {
const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne); const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
fieldStores[0].set(occlusionsData.clozes); fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
} }
} }
// global for calling this method in desktop note editor // global for calling this method in desktop note editor
@ -462,14 +468,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// set fields data for occlusion and image fields for io notes type // set fields data for occlusion and image fields for io notes type
if (isImageOcclusion) { if (isImageOcclusion) {
const occlusionsData = exportShapesToClozeDeletions(occludeInactive); const occlusionsData = exportShapesToClozeDeletions(occludeInactive);
fieldStores[0].set(occlusionsData.clozes); fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
} }
} }
// hide first two fields for occlusion type, first contains occlusion data and second contains image /** hide occlusions and image */
function hideFieldInOcclusionType(index: number) { function hideFieldInOcclusionType(
index: number,
ioFields: ImageOcclusionFieldIndexes,
) {
if (isImageOcclusion) { if (isImageOcclusion) {
if (index == 0 || index == 1) { if (index === ioFields.occlusions || index === ioFields.image) {
return true; return true;
} }
} }