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/*"],
globals: {
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?
notetype-error-no-image-to-show = No image to show.
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

View File

@ -13,14 +13,18 @@ import "anki/generic.proto";
service ImageOcclusionService {
rpc GetImageForOcclusion(GetImageForOcclusionRequest)
returns (GetImageForOcclusionResponse);
rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest)
returns (collection.OpChanges);
rpc GetImageOcclusionNote(GetImageOcclusionNoteRequest)
returns (GetImageOcclusionNoteResponse);
rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest)
returns (collection.OpChanges);
rpc GetImageOcclusionFields(GetImageOcclusionFieldsRequest)
returns (GetImageOcclusionFieldsResponse);
// Adds an I/O notetype if none exists in the collection.
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
@ -71,3 +75,18 @@ message UpdateImageOcclusionNoteRequest {
string back_extra = 4;
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
if result == NoteFieldsCheckResult.EMPTY:
if self.editor.current_notetype_is_image_occlusion():
problem = tr.notetypes_no_occlusion_created()
problem = tr.notetypes_no_occlusion_created2()
else:
problem = tr.adding_the_first_field_is_empty()
elif result == NoteFieldsCheckResult.MISSING_CLOZE:

View File

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

View File

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

View File

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

View File

@ -3,47 +3,42 @@
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
use anki_proto::image_occlusion::GetImageForOcclusionRequest;
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::GetImageOcclusionNoteResponse;
use anki_proto::image_occlusion::UpdateImageOcclusionNoteRequest;
use crate::collection::Collection;
use crate::error;
use crate::error::Result;
use crate::prelude::*;
impl crate::services::ImageOcclusionService for Collection {
fn get_image_for_occlusion(
&mut self,
input: GetImageForOcclusionRequest,
) -> error::Result<GetImageForOcclusionResponse> {
) -> Result<GetImageForOcclusionResponse> {
self.get_image_for_occlusion(&input.path)
}
fn add_image_occlusion_note(
&mut self,
input: AddImageOcclusionNoteRequest,
) -> error::Result<anki_proto::collection::OpChanges> {
self.add_image_occlusion_note(
input.notetype_id.into(),
&input.image_path,
&input.occlusions,
&input.header,
&input.back_extra,
input.tags,
)
.map(Into::into)
) -> Result<anki_proto::collection::OpChanges> {
self.add_image_occlusion_note(input).map(Into::into)
}
fn get_image_occlusion_note(
&mut self,
input: GetImageOcclusionNoteRequest,
) -> error::Result<GetImageOcclusionNoteResponse> {
) -> Result<GetImageOcclusionNoteResponse> {
self.get_image_occlusion_note(input.note_id.into())
}
fn update_image_occlusion_note(
&mut self,
input: UpdateImageOcclusionNoteRequest,
) -> error::Result<anki_proto::collection::OpChanges> {
) -> Result<anki_proto::collection::OpChanges> {
self.update_image_occlusion_note(
input.note_id.into(),
&input.occlusions,
@ -54,7 +49,18 @@ impl crate::services::ImageOcclusionService for Collection {
.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)
}
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],
direction: fonts[index][2] ? "rtl" : "ltr",
collapsed: fieldsCollapsed[index],
hidden: hideFieldInOcclusionType(index),
hidden: hideFieldInOcclusionType(index, ioFields),
})) as FieldData[];
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 LabelButton from "components/LabelButton.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 imageOcclusionMode: IOMode | undefined;
let ioFields = new ImageOcclusionFieldIndexes({});
async function setupMaskEditor(options: { html: string; mode: IOMode }) {
imageOcclusionMode = undefined;
await tick();
const getIoFields = getImageOcclusionFields({
notetypeId: BigInt(notetypeMeta.id),
}).then((r) => (ioFields = r.fields!));
await Promise.all([tick(), getIoFields]);
imageOcclusionMode = options.mode;
if (options.mode.kind === "add") {
fieldStores[1].set(options.html);
fieldStores[ioFields.image].set(options.html);
// new image is being added
if (isIOImageLoaded) {
resetIOImage(options.mode.imagePath);
}
} else {
const clozeNote = get(fieldStores[0]);
const clozeNote = get(fieldStores[ioFields.occlusions]);
if (clozeNote.includes("oi=1")) {
$hideAllGuessOne = true;
} else {
@ -422,14 +428,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
function setImageField(html) {
fieldStores[1].set(html);
fieldStores[ioFields.image].set(html);
}
globalThis.setImageField = setImageField;
// update cloze deletions and set occlusion fields, it call in saveNow to update cloze deletions
function updateIONoteInEditMode() {
if (isEditMode) {
const clozeNote = get(fieldStores[0]);
const clozeNote = get(fieldStores[ioFields.occlusions]);
if (clozeNote.includes("oi=1")) {
setOcclusionField(true);
} else {
@ -441,7 +447,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function setOcclusionFieldInner() {
if (isImageOcclusion) {
const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
fieldStores[0].set(occlusionsData.clozes);
fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
}
}
// 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
if (isImageOcclusion) {
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
function hideFieldInOcclusionType(index: number) {
/** hide occlusions and image */
function hideFieldInOcclusionType(
index: number,
ioFields: ImageOcclusionFieldIndexes,
) {
if (isImageOcclusion) {
if (index == 0 || index == 1) {
if (index === ioFields.occlusions || index === ioFields.image) {
return true;
}
}