f6486da233
* 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
144 lines
5.1 KiB
TypeScript
144 lines
5.1 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import * as tr from "@tslib/ftl";
|
|
import type { ImageOcclusion } from "@tslib/proto";
|
|
import { fabric } from "fabric";
|
|
import type { PanZoom } from "panzoom";
|
|
import protobuf from "protobufjs";
|
|
import { get } from "svelte/store";
|
|
|
|
import { getImageForOcclusion, getImageOcclusionNote } from "./lib";
|
|
import { notesDataStore, tagsWritable, zoomResetValue } from "./store";
|
|
import Toast from "./Toast.svelte";
|
|
import { enableSelectable, moveShapeToCanvasBoundaries } from "./tools/lib";
|
|
import { generateShapeFromCloze } from "./tools/shape-generate";
|
|
import { undoRedoInit } from "./tools/tool-undo-redo";
|
|
|
|
export const setupMaskEditor = async (path: string, instance: PanZoom): Promise<fabric.Canvas> => {
|
|
const imageData = await getImageForOcclusion(path!);
|
|
const canvas = initCanvas();
|
|
|
|
// get image width and height
|
|
const image = document.getElementById("image") as HTMLImageElement;
|
|
image.src = getImageData(imageData.data!);
|
|
image.onload = function() {
|
|
const size = limitSize({ width: image.width, height: image.height });
|
|
canvas.setWidth(size.width);
|
|
canvas.setHeight(size.height);
|
|
image.height = size.height;
|
|
image.width = size.width;
|
|
setCanvasZoomRatio(canvas, instance);
|
|
};
|
|
|
|
return canvas;
|
|
};
|
|
|
|
export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): Promise<fabric.Canvas> => {
|
|
const clozeNoteResponse: ImageOcclusion.GetImageOcclusionNoteResponse = await getImageOcclusionNote(noteId);
|
|
if (clozeNoteResponse.error) {
|
|
new Toast({
|
|
target: document.body,
|
|
props: {
|
|
message: tr.notetypesErrorGettingImagecloze(),
|
|
type: "error",
|
|
},
|
|
}).$set({ showToast: true });
|
|
return;
|
|
}
|
|
|
|
const clozeNote = clozeNoteResponse.note!;
|
|
const canvas = initCanvas();
|
|
|
|
// get image width and height
|
|
const image = document.getElementById("image") as HTMLImageElement;
|
|
image.src = getImageData(clozeNote.imageData!);
|
|
image.onload = function() {
|
|
const size = limitSize({ width: image.width, height: image.height });
|
|
canvas.setWidth(size.width);
|
|
canvas.setHeight(size.height);
|
|
image.height = size.height;
|
|
image.width = size.width;
|
|
|
|
setCanvasZoomRatio(canvas, instance);
|
|
generateShapeFromCloze(canvas, clozeNote.occlusions);
|
|
enableSelectable(canvas, true);
|
|
addClozeNotesToTextEditor(clozeNote.header, clozeNote.backExtra, clozeNote.tags);
|
|
};
|
|
|
|
return canvas;
|
|
};
|
|
|
|
const initCanvas = (): fabric.Canvas => {
|
|
const canvas = new fabric.Canvas("canvas");
|
|
tagsWritable.set([]);
|
|
globalThis.canvas = canvas;
|
|
// enables uniform scaling by default without the need for the Shift key
|
|
canvas.uniformScaling = false;
|
|
canvas.uniScaleKey = "none";
|
|
moveShapeToCanvasBoundaries(canvas);
|
|
undoRedoInit(canvas);
|
|
return canvas;
|
|
};
|
|
|
|
const getImageData = (imageData): string => {
|
|
const b64encoded = protobuf.util.base64.encode(
|
|
imageData,
|
|
0,
|
|
imageData.length,
|
|
);
|
|
return "data:image/png;base64," + b64encoded;
|
|
};
|
|
|
|
const setCanvasZoomRatio = (
|
|
canvas: fabric.Canvas,
|
|
instance: PanZoom,
|
|
): void => {
|
|
const zoomRatioW = (innerWidth - 60) / canvas.width!;
|
|
const zoomRatioH = (innerHeight - 100) / canvas.height!;
|
|
const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH;
|
|
zoomResetValue.set(zoomRatio);
|
|
instance.smoothZoom(0, 0, zoomRatio);
|
|
};
|
|
|
|
const addClozeNotesToTextEditor = (header: string, backExtra: string, tags: string[]) => {
|
|
const noteFieldsData: { id: string; title: string; divValue: string; textareaValue: string }[] = get(
|
|
notesDataStore,
|
|
);
|
|
noteFieldsData[0].divValue = header;
|
|
noteFieldsData[1].divValue = backExtra;
|
|
noteFieldsData[0].textareaValue = header;
|
|
noteFieldsData[1].textareaValue = backExtra;
|
|
tagsWritable.set(tags);
|
|
|
|
noteFieldsData.forEach((note) => {
|
|
const divId = `${note.id}--div`;
|
|
const textAreaId = `${note.id}--textarea`;
|
|
const divElement = document.getElementById(divId)!;
|
|
const textAreaElement = document.getElementById(textAreaId)! as HTMLTextAreaElement;
|
|
divElement.innerHTML = note.divValue;
|
|
textAreaElement.value = note.textareaValue;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Fix for safari browser,
|
|
* Canvas area exceeds the maximum limit (width * height > 16777216),
|
|
* Following function also added in reviewer ts,
|
|
* so update both, if it changes
|
|
*/
|
|
const limitSize = (size: { width: number; height: number }): { width: number; height: number; scalar: number } => {
|
|
const maximumPixels = 1000000;
|
|
const { width, height } = size;
|
|
|
|
const requiredPixels = width * height;
|
|
if (requiredPixels <= maximumPixels) return { width, height, scalar: 1 };
|
|
|
|
const scalar = Math.sqrt(maximumPixels) / Math.sqrt(requiredPixels);
|
|
return {
|
|
width: Math.floor(width * scalar),
|
|
height: Math.floor(height * scalar),
|
|
scalar: scalar,
|
|
};
|
|
};
|