Various tweaks to I/O code (#2478)

* Allow user to select I/O notetype instead of enforcing a specific name

* Display a clearer error when I/O note is missing an image

Opening the card layout screen from "manage notetypes" was showing an
error about the Anki version being too old.

Replacement error is not currently translatable.

* Preserve existing notetype when adding I/O notetype

* Add a 'from clipboard' string

The intention is to use this in the future to allow an image occlusion
to be created from an image on the clipboard.

* Tweak I/O init

- Use union type instead of multiple nullable values
- Pass the notetype id in to initialization

* Fix image insertion in I/O note

- The regex expected double quotes, and we were using single ones
- Image tags don't need to be closed

* Use more consistent naming in image_occlusion.proto

* Tweaks to default I/O notetype

- Show the header on the front side as well (I presume this is what
users expect; if not am happy to revert)
- Don't show comments on card (again, I presume users expect to use
this field to add notes that aren't displayed during review, as they
can use back extra for that)

* Fix sticky footer missing background

Caused by earlier CSS refactoring
This commit is contained in:
Damien Elmes 2023-04-19 15:30:18 +10:00 committed by GitHub
parent ed334fa45d
commit f6486da233
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 257 additions and 194 deletions

View file

@ -66,6 +66,7 @@ editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will no
editing-mathjax-preview = MathJax Preview editing-mathjax-preview = MathJax Preview
editing-shrink-images = Shrink Images editing-shrink-images = Shrink Images
editing-close-html-tags = Auto-close HTML tags 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. ## You don't need to translate these strings, as they will be replaced with different ones soon.

View file

@ -13,20 +13,23 @@ import "anki/notes.proto";
import "anki/generic.proto"; import "anki/generic.proto";
service ImageOcclusionService { service ImageOcclusionService {
rpc GetImageForOcclusion(GetImageForOcclusionRequest) returns (ImageData); rpc GetImageForOcclusion(GetImageForOcclusionRequest)
returns (GetImageForOcclusionResponse);
rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest) rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest)
returns (collection.OpChanges); returns (collection.OpChanges);
rpc GetImageClozeNote(GetImageOcclusionNoteRequest) rpc GetImageOcclusionNote(GetImageOcclusionNoteRequest)
returns (ImageClozeNoteResponse); returns (GetImageOcclusionNoteResponse);
rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest) rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest)
returns (collection.OpChanges); returns (collection.OpChanges);
// Adds an I/O notetype if none exists in the collection.
rpc AddImageOcclusionNotetype(generic.Empty) returns (collection.OpChanges);
} }
message GetImageForOcclusionRequest { message GetImageForOcclusionRequest {
string path = 1; string path = 1;
} }
message ImageData { message GetImageForOcclusionResponse {
bytes data = 1; bytes data = 1;
string name = 2; string name = 2;
} }
@ -37,20 +40,28 @@ message AddImageOcclusionNoteRequest {
string header = 3; string header = 3;
string back_extra = 4; string back_extra = 4;
repeated string tags = 5; repeated string tags = 5;
} int64 notetype_id = 6;
message ImageClozeNote {
bytes image_data = 1;
string occlusions = 2;
string header = 3;
string back_extra = 4;
repeated string tags = 5;
} }
message GetImageOcclusionNoteRequest { message GetImageOcclusionNoteRequest {
int64 note_id = 1; 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 { message UpdateImageOcclusionNoteRequest {
int64 note_id = 1; int64 note_id = 1;
string occlusions = 2; string occlusions = 2;
@ -58,10 +69,3 @@ message UpdateImageOcclusionNoteRequest {
string back_extra = 4; string back_extra = 4;
repeated string tags = 5; repeated string tags = 5;
} }
message ImageClozeNoteResponse {
oneof value {
ImageClozeNote note = 1;
string error = 2;
}
}

View file

@ -41,9 +41,9 @@ CsvMetadata = import_export_pb2.CsvMetadata
DupeResolution = CsvMetadata.DupeResolution DupeResolution = CsvMetadata.DupeResolution
Delimiter = import_export_pb2.CsvMetadata.Delimiter Delimiter = import_export_pb2.CsvMetadata.Delimiter
TtsVoice = card_rendering_pb2.AllTtsVoicesResponse.TtsVoice TtsVoice = card_rendering_pb2.AllTtsVoicesResponse.TtsVoice
ImageData = image_occlusion_pb2.ImageData GetImageForOcclusionResponse = image_occlusion_pb2.GetImageForOcclusionResponse
AddImageOcclusionNoteRequest = image_occlusion_pb2.AddImageOcclusionNoteRequest AddImageOcclusionNoteRequest = image_occlusion_pb2.AddImageOcclusionNoteRequest
ImageClozeNoteResponse = image_occlusion_pb2.ImageClozeNoteResponse GetImageOcclusionNoteResponse = image_occlusion_pb2.GetImageOcclusionNoteResponse
import copy import copy
import os import os
@ -462,18 +462,21 @@ class Collection(DeprecatedNamesMixin):
# Image Occlusion # 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) return self._backend.get_image_for_occlusion(path=path)
def add_image_occlusion_note( def add_image_occlusion_note(
self, self,
image_path: str | None, notetype_id: int,
occlusions: str | None, image_path: str,
header: str | None, occlusions: str,
back_extra: str | None, header: str,
tags: list[str] | None, back_extra: str,
tags: list[str],
) -> OpChanges: ) -> OpChanges:
return self._backend.add_image_occlusion_note( return self._backend.add_image_occlusion_note(
notetype_id=notetype_id,
image_path=image_path, image_path=image_path,
occlusions=occlusions, occlusions=occlusions,
header=header, header=header,
@ -481,8 +484,10 @@ class Collection(DeprecatedNamesMixin):
tags=tags, tags=tags,
) )
def get_image_cloze_note(self, note_id: int | None) -> ImageClozeNoteResponse: def get_image_occlusion_note(
return self._backend.get_image_cloze_note(note_id=note_id) self, note_id: int | None
) -> GetImageOcclusionNoteResponse:
return self._backend.get_image_occlusion_note(note_id=note_id)
def update_image_occlusion_note( def update_image_occlusion_note(
self, self,

View file

@ -481,7 +481,7 @@ exposed_backend_list = [
# ImageOcclusionService # ImageOcclusionService
"get_image_for_occlusion", "get_image_for_occlusion",
"add_image_occlusion_note", "add_image_occlusion_note",
"get_image_cloze_note", "get_image_occlusion_note",
"update_image_occlusion_note", "update_image_occlusion_note",
] ]

View file

@ -10,7 +10,7 @@ impl ImageOcclusionService for Backend {
fn get_image_for_occlusion( fn get_image_for_occlusion(
&self, &self,
input: pb::image_occlusion::GetImageForOcclusionRequest, input: pb::image_occlusion::GetImageForOcclusionRequest,
) -> Result<pb::image_occlusion::ImageData> { ) -> Result<pb::image_occlusion::GetImageForOcclusionResponse> {
self.with_col(|col| col.get_image_for_occlusion(&input.path)) self.with_col(|col| col.get_image_for_occlusion(&input.path))
} }
@ -20,6 +20,7 @@ impl ImageOcclusionService for Backend {
) -> Result<pb::collection::OpChanges> { ) -> Result<pb::collection::OpChanges> {
self.with_col(|col| { self.with_col(|col| {
col.add_image_occlusion_note( col.add_image_occlusion_note(
input.notetype_id.into(),
&input.image_path, &input.image_path,
&input.occlusions, &input.occlusions,
&input.header, &input.header,
@ -30,11 +31,11 @@ impl ImageOcclusionService for Backend {
.map(Into::into) .map(Into::into)
} }
fn get_image_cloze_note( fn get_image_occlusion_note(
&self, &self,
input: pb::image_occlusion::GetImageOcclusionNoteRequest, input: pb::image_occlusion::GetImageOcclusionNoteRequest,
) -> Result<pb::image_occlusion::ImageClozeNoteResponse> { ) -> Result<pb::image_occlusion::GetImageOcclusionNoteResponse> {
self.with_col(|col| col.get_image_cloze_note(input.note_id.into())) self.with_col(|col| col.get_image_occlusion_note(input.note_id.into()))
} }
fn update_image_occlusion_note( fn update_image_occlusion_note(
@ -52,4 +53,12 @@ impl ImageOcclusionService for Backend {
}) })
.map(Into::into) .map(Into::into)
} }
fn add_image_occlusion_notetype(
&self,
_input: pb::generic::Empty,
) -> Result<pb::collection::OpChanges> {
self.with_col(|col| col.add_image_occlusion_notetype())
.map(Into::into)
}
} }

View file

@ -3,27 +3,22 @@
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use regex::Regex; use regex::Regex;
use crate::io::metadata; use crate::io::metadata;
use crate::io::read_file; use crate::io::read_file;
use crate::media::MediaManager; use crate::media::MediaManager;
use crate::notetype::stock::empty_stock;
use crate::notetype::CardGenContext; use crate::notetype::CardGenContext;
use crate::notetype::Notetype; use crate::pb::image_occlusion::get_image_occlusion_note_response::ImageClozeNote;
use crate::notetype::NotetypeKind; use crate::pb::image_occlusion::get_image_occlusion_note_response::Value;
use crate::pb::image_occlusion::image_cloze_note_response::Value; use crate::pb::image_occlusion::GetImageForOcclusionResponse;
use crate::pb::image_occlusion::ImageClozeNote; use crate::pb::image_occlusion::GetImageOcclusionNoteResponse;
use crate::pb::image_occlusion::ImageClozeNoteResponse;
pub use crate::pb::image_occlusion::ImageData;
use crate::pb::notetypes::stock_notetype::OriginalStockKind;
use crate::prelude::*; use crate::prelude::*;
impl Collection { impl Collection {
pub fn get_image_for_occlusion(&mut self, path: &str) -> Result<ImageData> { pub fn get_image_for_occlusion(&mut self, path: &str) -> Result<GetImageForOcclusionResponse> {
let mut metadata = ImageData { let mut metadata = GetImageForOcclusionResponse {
..Default::default() ..Default::default()
}; };
metadata.data = read_file(path)?; metadata.data = read_file(path)?;
@ -33,6 +28,7 @@ impl Collection {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn add_image_occlusion_note( pub fn add_image_occlusion_note(
&mut self, &mut self,
notetype_id: NotetypeId,
image_path: &str, image_path: &str,
occlusions: &str, occlusions: &str,
header: &str, header: &str,
@ -52,13 +48,20 @@ impl Collection {
let actual_image_name_after_adding = mgr.add_file(&image_filename, &image_bytes)?; let actual_image_name_after_adding = mgr.add_file(&image_filename, &image_bytes)?;
let image_tag = format!( let image_tag = format!(
"<img id='img' src='{}'></img>", r#"<img id="img" src="{}">"#,
&actual_image_name_after_adding &actual_image_name_after_adding
); );
let current_deck = self.get_current_deck()?; let current_deck = self.get_current_deck()?;
self.transact(Op::ImageOcclusion, |col| { 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(); let mut note = nt.new_note();
note.set_field(0, occlusions)?; note.set_field(0, occlusions)?;
@ -76,47 +79,18 @@ impl Collection {
}) })
} }
fn get_or_create_io_notetype(&mut self) -> Result<Arc<Notetype>> { pub fn get_image_occlusion_note(
let tr = &self.tr; &mut self,
let name = format!("{}", tr.notetypes_image_occlusion_name()); note_id: NoteId,
let nt = match self.get_notetype_by_name(&name)? { ) -> Result<GetImageOcclusionNoteResponse> {
Some(nt) => nt, let value = match self.get_image_occlusion_note_inner(note_id) {
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<ImageClozeNoteResponse> {
let value = match self.get_image_cloze_note_inner(note_id) {
Ok(note) => Value::Note(note), Ok(note) => Value::Note(note),
Err(err) => Value::Error(format!("{:?}", err)), 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<ImageClozeNote> { pub fn get_image_occlusion_note_inner(&mut self, note_id: NoteId) -> Result<ImageClozeNote> {
let note = self.storage.get_note(note_id)?.or_not_found(note_id)?; let note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
let mut cloze_note = ImageClozeNote::default(); let mut cloze_note = ImageClozeNote::default();
@ -131,9 +105,9 @@ impl Collection {
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].clone(); let image_file_name = &fields[1];
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());
let final_path = self.media_folder.join(src); let final_path = self.media_folder.join(src);
@ -190,58 +164,3 @@ impl Collection {
Ok(false) 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!(
"<div style=\"display: none\">{{{{cloze:{}}}}}</div>
<div id=container>
{{{{{}}}}}
<canvas id=\"canvas\" class=\"image-occlusion-canvas\"></canvas>
</div>
<div id=\"err\"></div>
<script>
try {{
anki.setupImageCloze();
}} catch (exc) {{
document.getElementById(\"err\").innerHTML = `{}<br><br>${{exc}}`;
}}
</script>
",
occlusion,
image,
tr.notetypes_error_loading_image_occlusion(),
);
let afmt = format!(
"{{{{{}}}}}
{}
<button id=\"toggle\">{}</button>
<br>
{{{{{}}}}}
<br>
{{{{{}}}}}",
header,
qfmt,
tr.notetypes_toggle_masks(),
back_extra,
comments,
);
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}

View file

@ -3,3 +3,4 @@
pub mod imagedata; pub mod imagedata;
pub mod imageocclusion; pub mod imageocclusion;
pub(crate) mod notetype;

View file

@ -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<OpOutput<()>> {
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<Arc<Notetype>> {
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<Option<Arc<Notetype>>> {
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}}}}}<div>{{{{{header}}}}}</div>{{{{/{header}}}}}
<div style=\"display: none\">{{{{cloze:{occlusion}}}}}</div>
<div id=\"err\"></div>
<div id=container>
{{{{{image}}}}}
<canvas id=\"canvas\" class=\"image-occlusion-canvas\"></canvas>
</div>
<script>
try {{
anki.setupImageCloze();
}} catch (exc) {{
document.getElementById(\"err\").innerHTML = `{err_loading}<br><br>${{exc}}`;
}}
</script>
"
);
let toggle_masks = tr.notetypes_toggle_masks();
let afmt = format!(
"\
{qfmt}
<div><button id=\"toggle\">{toggle_masks}</button></div>
{{{{#{back_extra}}}}}<div>{{{{{back_extra}}}}}</div>{{{{/{back_extra}}}}}
",
);
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}
fn io_notetype_if_valid(nt: Arc<Notetype>) -> Result<Arc<Notetype>> {
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)
}

View file

@ -6,7 +6,7 @@ use crate::config::ConfigEntry;
use crate::config::ConfigKey; use crate::config::ConfigKey;
use crate::error::Result; use crate::error::Result;
use crate::i18n::I18n; 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::invalid_input;
use crate::notetype::Notetype; use crate::notetype::Notetype;
use crate::pb::notetypes::notetype::config::Kind as NotetypeKind; use crate::pb::notetypes::notetype::config::Kind as NotetypeKind;

View file

@ -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 * as tr from "@tslib/ftl";
import Container from "../components/Container.svelte"; 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 MasksEditor from "./MaskEditor.svelte";
import Notes from "./Notes.svelte"; import Notes from "./Notes.svelte";
import StickyFooter from "./StickyFooter.svelte"; import StickyFooter from "./StickyFooter.svelte";
export let path: string | null; export let mode: IOMode;
export let noteId: number | null;
async function hideAllGuessOne(): Promise<void> { async function hideAllGuessOne(): Promise<void> {
saveImageNotes(path!, noteId!, false); addOrUpdateNote(mode, false);
} }
async function hideOneGuessOne(): Promise<void> { async function hideOneGuessOne(): Promise<void> {
saveImageNotes(path!, noteId!, true); addOrUpdateNote(mode, true);
} }
const items = [ const items = [
@ -41,12 +41,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</ul> </ul>
<div hidden={activeTabValue != 1}> <div hidden={activeTabValue != 1}>
<MasksEditor {path} {noteId} /> <MasksEditor {mode} />
</div> </div>
<div hidden={activeTabValue != 2}> <div hidden={activeTabValue != 2}>
<div class="notes-page"><Notes /></div> <div class="notes-page"><Notes /></div>
<StickyFooter {hideAllGuessOne} {hideOneGuessOne} {noteId} /> <StickyFooter
{hideAllGuessOne}
{hideOneGuessOne}
editing={mode.kind == "edit"}
/>
</div> </div>
</Container> </Container>

View file

@ -6,11 +6,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { PanZoom } from "panzoom"; import type { PanZoom } from "panzoom";
import panzoom from "panzoom"; import panzoom from "panzoom";
import type { IOMode } from "./lib";
import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor"; import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor";
import SideToolbar from "./SideToolbar.svelte"; import SideToolbar from "./SideToolbar.svelte";
export let path: string | null; export let mode: IOMode;
export let noteId: number | null;
let instance: PanZoom; let instance: PanZoom;
let innerWidth = 0; let innerWidth = 0;
@ -26,14 +26,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}); });
instance.pause(); instance.pause();
if (path) { if (mode.kind == "add") {
setupMaskEditor(path, instance).then((canvas1) => { setupMaskEditor(mode.imagePath, instance).then((canvas1) => {
canvas = canvas1; canvas = canvas1;
}); });
} } else {
setupMaskEditorForEdit(mode.noteId, instance).then((canvas1) => {
if (noteId) {
setupMaskEditorForEdit(noteId, instance).then((canvas1) => {
canvas = canvas1; canvas = canvas1;
}); });
} }

View file

@ -10,12 +10,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let hideAllGuessOne: () => void; export let hideAllGuessOne: () => void;
export let hideOneGuessOne: () => void; export let hideOneGuessOne: () => void;
export let noteId: number | null; export let editing: boolean;
</script> </script>
<div style:flex-grow="1" /> <div style:flex-grow="1" />
<div class="sticky-footer"> <div class="sticky-footer">
{#if noteId} {#if editing}
<div class="update-note-text"> <div class="update-note-text">
{tr.actionsUpdateNote()} {tr.actionsUpdateNote()}
</div> </div>
@ -49,7 +49,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
z-index: 99; z-index: 99;
margin: 0; margin: 0;
padding: 0.25rem; padding: 0.25rem;
background: var(--window-bg); background: var(--canvas);
border-style: solid none none; border-style: solid none none;
border-color: var(--border); border-color: var(--border);
border-width: thin; border-width: thin;

View file

@ -6,6 +6,7 @@ import { fabric } from "fabric";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { Collection } from "../lib/proto"; import type { Collection } from "../lib/proto";
import type { IOMode } from "./lib";
import { addImageOcclusionNote, updateImageOcclusionNote } from "./lib"; import { addImageOcclusionNote, updateImageOcclusionNote } from "./lib";
import { notesDataStore, tagsWritable } from "./store"; import { notesDataStore, tagsWritable } from "./store";
import Toast from "./Toast.svelte"; import Toast from "./Toast.svelte";
@ -107,9 +108,8 @@ const getObjectPositionInGroup = (group, object): { top: number; left: number }
return { top, left }; return { top, left };
}; };
export const saveImageNotes = async function( export const addOrUpdateNote = async function(
imagePath: string, mode: IOMode,
noteId: number,
hideInactive: boolean, hideInactive: boolean,
): Promise<void> { ): Promise<void> {
const { occlusionCloze, noteCount } = generate(hideInactive); const { occlusionCloze, noteCount } = generate(hideInactive);
@ -125,29 +125,30 @@ export const saveImageNotes = async function(
header = header ? `<div>${header}</div>` : ""; header = header ? `<div>${header}</div>` : "";
backExtra = header ? `<div>${backExtra}</div>` : ""; backExtra = header ? `<div>${backExtra}</div>` : "";
if (noteId) { if (mode.kind == "edit") {
const result = await updateImageOcclusionNote( const result = await updateImageOcclusionNote(
noteId, mode.noteId,
occlusionCloze, occlusionCloze,
header, header,
backExtra, backExtra,
tags, tags,
); );
showResult(noteId, result, noteCount); showResult(mode.noteId, result, noteCount);
} else { } else {
const result = await addImageOcclusionNote( const result = await addImageOcclusionNote(
imagePath, mode.notetypeId,
mode.imagePath,
occlusionCloze, occlusionCloze,
header, header,
backExtra, backExtra,
tags, tags,
); );
showResult(noteId, result, noteCount); showResult(null, result, noteCount);
} }
}; };
// show toast message // show toast message
const showResult = (noteId: number, result: Collection.OpChanges, count: number) => { const showResult = (noteId: number | null, result: Collection.OpChanges, count: number) => {
const toastComponent = new Toast({ const toastComponent = new Toast({
target: document.body, target: document.body,
props: { props: {

View file

@ -7,6 +7,7 @@ import { ModuleName, setupI18n } from "@tslib/i18n";
import { checkNightMode } from "../lib/nightmode"; import { checkNightMode } from "../lib/nightmode";
import ImageOcclusionPage from "./ImageOcclusionPage.svelte"; import ImageOcclusionPage from "./ImageOcclusionPage.svelte";
import type { IOMode } from "./lib";
const i18n = setupI18n({ const i18n = setupI18n({
modules: [ modules: [
@ -19,38 +20,24 @@ const i18n = setupI18n({
], ],
}); });
export async function setupImageOcclusion(path: string): Promise<ImageOcclusionPage> { export async function setupImageOcclusion(mode: IOMode): Promise<ImageOcclusionPage> {
checkNightMode(); checkNightMode();
await i18n; await i18n;
return new ImageOcclusionPage({ return new ImageOcclusionPage({
target: document.body, target: document.body,
props: { props: {
path: path, mode,
noteId: null,
},
});
}
export async function setupImageOcclusionForEdit(noteId: number): Promise<ImageOcclusionPage> {
checkNightMode();
await i18n;
return new ImageOcclusionPage({
target: document.body,
props: {
path: null,
noteId: noteId,
}, },
}); });
} }
if (window.location.hash.startsWith("#test-")) { if (window.location.hash.startsWith("#test-")) {
const path = window.location.hash.replace("#test-", ""); const imagePath = window.location.hash.replace("#test-", "");
setupImageOcclusion(path); setupImageOcclusion({ kind: "add", imagePath, notetypeId: 0 });
} }
if (window.location.hash.startsWith("#testforedit-")) { if (window.location.hash.startsWith("#testforedit-")) {
const noteId = parseInt(window.location.hash.replace("#testforedit-", "")); const noteId = parseInt(window.location.hash.replace("#testforedit-", ""));
setupImageOcclusionForEdit(noteId); setupImageOcclusion({ kind: "edit", noteId });
} }

View file

@ -4,9 +4,22 @@
import type { Collection } from "../lib/proto"; import type { Collection } from "../lib/proto";
import { ImageOcclusion, imageOcclusion } from "../lib/proto"; import { ImageOcclusion, imageOcclusion } from "../lib/proto";
export interface IOAddingMode {
kind: "add";
notetypeId: number;
imagePath: string;
}
export interface IOEditingMode {
kind: "edit";
noteId: number;
}
export type IOMode = IOAddingMode | IOEditingMode;
export async function getImageForOcclusion( export async function getImageForOcclusion(
path: string, path: string,
): Promise<ImageOcclusion.ImageData> { ): Promise<ImageOcclusion.GetImageForOcclusionResponse> {
return imageOcclusion.getImageForOcclusion( return imageOcclusion.getImageForOcclusion(
ImageOcclusion.GetImageForOcclusionRequest.create({ ImageOcclusion.GetImageForOcclusionRequest.create({
path, path,
@ -15,6 +28,7 @@ export async function getImageForOcclusion(
} }
export async function addImageOcclusionNote( export async function addImageOcclusionNote(
notetypeId: number,
imagePath: string, imagePath: string,
occlusions: string, occlusions: string,
header: string, header: string,
@ -23,6 +37,7 @@ export async function addImageOcclusionNote(
): Promise<Collection.OpChanges> { ): Promise<Collection.OpChanges> {
return imageOcclusion.addImageOcclusionNote( return imageOcclusion.addImageOcclusionNote(
ImageOcclusion.AddImageOcclusionNoteRequest.create({ ImageOcclusion.AddImageOcclusionNoteRequest.create({
notetypeId,
imagePath, imagePath,
occlusions, occlusions,
header, header,
@ -32,10 +47,10 @@ export async function addImageOcclusionNote(
); );
} }
export async function getImageClozeNote( export async function getImageOcclusionNote(
noteId: number, noteId: number,
): Promise<ImageOcclusion.ImageClozeNoteResponse> { ): Promise<ImageOcclusion.GetImageOcclusionNoteResponse> {
return imageOcclusion.getImageClozeNote( return imageOcclusion.getImageOcclusionNote(
ImageOcclusion.GetImageOcclusionNoteRequest.create({ ImageOcclusion.GetImageOcclusionNoteRequest.create({
noteId, noteId,
}), }),

View file

@ -8,7 +8,7 @@ import type { PanZoom } from "panzoom";
import protobuf from "protobufjs"; import protobuf from "protobufjs";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { getImageClozeNote, getImageForOcclusion } from "./lib"; import { getImageForOcclusion, getImageOcclusionNote } from "./lib";
import { notesDataStore, tagsWritable, zoomResetValue } from "./store"; import { notesDataStore, tagsWritable, zoomResetValue } from "./store";
import Toast from "./Toast.svelte"; import Toast from "./Toast.svelte";
import { enableSelectable, moveShapeToCanvasBoundaries } from "./tools/lib"; import { enableSelectable, moveShapeToCanvasBoundaries } from "./tools/lib";
@ -35,7 +35,7 @@ export const setupMaskEditor = async (path: string, instance: PanZoom): Promise<
}; };
export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): Promise<fabric.Canvas> => { export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): Promise<fabric.Canvas> => {
const clozeNoteResponse: ImageOcclusion.ImageClozeNoteResponse = await getImageClozeNote(noteId); const clozeNoteResponse: ImageOcclusion.GetImageOcclusionNoteResponse = await getImageOcclusionNote(noteId);
if (clozeNoteResponse.error) { if (clozeNoteResponse.error) {
new Toast({ new Toast({
target: document.body, target: document.body,

View file

@ -11,13 +11,18 @@ export function setupImageCloze(): void {
canvas.style.maxHeight = "95vh"; canvas.style.maxHeight = "95vh";
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!; const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
const container = document.getElementById("container") as HTMLDivElement;
const image = document.getElementById("img") as HTMLImageElement; const image = document.getElementById("img") as HTMLImageElement;
if (image == null) {
container.innerText = "No image to show.";
return;
}
const size = limitSize({ width: image.naturalWidth, height: image.naturalHeight }); const size = limitSize({ width: image.naturalWidth, height: image.naturalHeight });
canvas.width = size.width; canvas.width = size.width;
canvas.height = size.height; canvas.height = size.height;
// set height for div container (used 'relative' in css) // set height for div container (used 'relative' in css)
const container = document.getElementById("container") as HTMLDivElement;
container.style.height = `${image.height}px`; container.style.height = `${image.height}px`;
// setup button for toggle image occlusion // setup button for toggle image occlusion