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
168 lines
5.2 KiB
TypeScript
168 lines
5.2 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 { fabric } from "fabric";
|
|
import { get } from "svelte/store";
|
|
|
|
import type { Collection } from "../lib/proto";
|
|
import type { IOMode } from "./lib";
|
|
import { addImageOcclusionNote, updateImageOcclusionNote } from "./lib";
|
|
import { notesDataStore, tagsWritable } from "./store";
|
|
import Toast from "./Toast.svelte";
|
|
import { makeMaskTransparent } from "./tools/lib";
|
|
|
|
const divData = [
|
|
"height",
|
|
"left",
|
|
"top",
|
|
"type",
|
|
"width",
|
|
];
|
|
|
|
// Defines the number of fraction digits to use when serializing object values
|
|
fabric.Object.NUM_FRACTION_DIGITS = 2;
|
|
|
|
export function generate(hideInactive: boolean): { occlusionCloze: string; noteCount: number } {
|
|
const canvas = globalThis.canvas;
|
|
const canvasObjects = canvas.getObjects();
|
|
if (canvasObjects.length < 1) {
|
|
return { occlusionCloze: "", noteCount: 0 };
|
|
}
|
|
|
|
let occlusionCloze = "";
|
|
let clozeData = "";
|
|
let noteCount = 0;
|
|
|
|
makeMaskTransparent(canvas, false);
|
|
|
|
canvasObjects.forEach((object, index) => {
|
|
const obJson = object.toJSON();
|
|
noteCount++;
|
|
if (obJson.type === "group") {
|
|
clozeData += getGroupCloze(object, index, hideInactive);
|
|
} else {
|
|
clozeData += getCloze(object, index, null, hideInactive);
|
|
}
|
|
});
|
|
|
|
occlusionCloze += clozeData;
|
|
return { occlusionCloze, noteCount };
|
|
}
|
|
|
|
const getCloze = (object, index, relativePos, hideInactive): string => {
|
|
const obJson = object.toJSON();
|
|
let clozeData = "";
|
|
|
|
// generate cloze data in form of
|
|
// {{c1::image-occlusion:rect:top=100:left=100:width=100:height=100}}
|
|
Object.keys(obJson).forEach(function(key) {
|
|
if (divData.includes(key)) {
|
|
if (key === "type") {
|
|
clozeData += `:${obJson[key]}`;
|
|
|
|
if (obJson[key] === "ellipse") {
|
|
clozeData += `:rx=${obJson.rx.toFixed(2)}:ry=${obJson.ry.toFixed(2)}`;
|
|
}
|
|
|
|
if (obJson[key] === "polygon") {
|
|
const points = obJson.points;
|
|
let pnts = "";
|
|
points.forEach((point: { x: number; y: number }) => {
|
|
pnts += point.x.toFixed(2) + "," + point.y.toFixed(2) + " ";
|
|
});
|
|
clozeData += `:points=${pnts.trim()}`;
|
|
}
|
|
} else if (relativePos && key === "top") {
|
|
clozeData += `:top=${relativePos.top}`;
|
|
} else if (relativePos && key === "left") {
|
|
clozeData += `:left=${relativePos.left}`;
|
|
} else {
|
|
clozeData += `:${key}=${obJson[key]}`;
|
|
}
|
|
}
|
|
});
|
|
|
|
clozeData += `:hideinactive=${hideInactive}`;
|
|
clozeData = `{{c${index + 1}::image-occlusion${clozeData}}}<br>`;
|
|
return clozeData;
|
|
};
|
|
|
|
const getGroupCloze = (group, index, hideInactive): string => {
|
|
let clozeData = "";
|
|
const objects = group._objects;
|
|
|
|
objects.forEach((object) => {
|
|
const { top, left } = getObjectPositionInGroup(group, object);
|
|
clozeData += getCloze(object, index, { top, left }, hideInactive);
|
|
});
|
|
|
|
return clozeData;
|
|
};
|
|
|
|
const getObjectPositionInGroup = (group, object): { top: number; left: number } => {
|
|
let left = object.left + group.left + group.width / 2;
|
|
let top = object.top + group.top + group.height / 2;
|
|
left = left.toFixed(2);
|
|
top = top.toFixed(2);
|
|
return { top, left };
|
|
};
|
|
|
|
export const addOrUpdateNote = async function(
|
|
mode: IOMode,
|
|
hideInactive: boolean,
|
|
): Promise<void> {
|
|
const { occlusionCloze, noteCount } = generate(hideInactive);
|
|
if (noteCount === 0) {
|
|
return;
|
|
}
|
|
|
|
const fieldsData: { id: string; title: string; divValue: string; textareaValue: string }[] = get(notesDataStore);
|
|
const tags = get(tagsWritable);
|
|
let header = fieldsData[0].textareaValue;
|
|
let backExtra = fieldsData[1].textareaValue;
|
|
|
|
header = header ? `<div>${header}</div>` : "";
|
|
backExtra = header ? `<div>${backExtra}</div>` : "";
|
|
|
|
if (mode.kind == "edit") {
|
|
const result = await updateImageOcclusionNote(
|
|
mode.noteId,
|
|
occlusionCloze,
|
|
header,
|
|
backExtra,
|
|
tags,
|
|
);
|
|
showResult(mode.noteId, result, noteCount);
|
|
} else {
|
|
const result = await addImageOcclusionNote(
|
|
mode.notetypeId,
|
|
mode.imagePath,
|
|
occlusionCloze,
|
|
header,
|
|
backExtra,
|
|
tags,
|
|
);
|
|
showResult(null, result, noteCount);
|
|
}
|
|
};
|
|
|
|
// show toast message
|
|
const showResult = (noteId: number | null, result: Collection.OpChanges, count: number) => {
|
|
const toastComponent = new Toast({
|
|
target: document.body,
|
|
props: {
|
|
message: "",
|
|
type: "error",
|
|
},
|
|
});
|
|
|
|
if (result.note) {
|
|
const msg = noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count });
|
|
toastComponent.$set({ message: msg, type: "success", showToast: true });
|
|
} else {
|
|
const msg = tr.notetypesErrorGeneratingCloze();
|
|
toastComponent.$set({ message: msg, showToast: true });
|
|
}
|
|
};
|