mirror of
https://github.com/ankitects/anki.git
synced 2025-11-08 13:47:13 -05:00
* Update to latest Node LTS * Add sveltekit * Split tslib into separate @generated and @tslib components SvelteKit's path aliases don't support multiple locations, so our old approach of using @tslib to refer to both ts/lib and out/ts/lib will no longer work. Instead, all generated sources and their includes are placed in a separate out/ts/generated folder, and imported via @generated instead. This also allows us to generate .ts files, instead of needing to output separate .d.ts and .js files. * Switch package.json to module type * Avoid usage of baseUrl Incompatible with SvelteKit * Move sass into ts; use relative links SvelteKit's default sass support doesn't allow overriding loadPaths * jest->vitest, graphs example working with yarn dev * most pages working in dev mode * Some fixes after rebasing * Fix/silence some svelte-check errors * Get image-occlusion working with Fabric types * Post-rebase lock changes * Editor is now checked * SvelteKit build integrated into ninja * Use the new SvelteKit entrypoint for pages like congrats/deck options/etc * Run eslint once for ts/**; fix some tests * Fix a bunch of issues introduced when rebasing over latest main * Run eslint fix * Fix remaining eslint+pylint issues; tests now all pass * Fix some issues with a clean build * Latest bufbuild no longer requires @__PURE__ hack * Add a few missed dependencies * Add yarn.bat to fix Windows build * Fix pages failing to show when ANKI_API_PORT not defined * Fix svelte-check and vitest on Windows * Set node path in ./yarn * Move svelte-kit output to ts/.svelte-kit Sadly, I couldn't figure out a way to store it in out/ if out/ is a symlink, as it breaks module resolution when SvelteKit is run. * Allow HMR inside Anki * Skip SvelteKit build when HMR is defined * Fix some post-rebase issues I should have done a normal merge instead.
156 lines
4 KiB
TypeScript
156 lines
4 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 "@generated/ftl";
|
|
import { type fabric } from "fabric";
|
|
import { writable } from "svelte/store";
|
|
|
|
import { mdiRedo, mdiUndo } from "../icons";
|
|
import { emitChangeSignal } from "../MaskEditor.svelte";
|
|
import { redoKeyCombination, undoKeyCombination } from "./shortcuts";
|
|
|
|
/**
|
|
* Undo redo for rectangle and ellipse handled here,
|
|
* view tool-polygon for handling undo redo in case of polygon
|
|
*/
|
|
|
|
type UndoState = {
|
|
undoable: boolean;
|
|
redoable: boolean;
|
|
};
|
|
|
|
const shapeType = ["rect", "ellipse", "i-text", "group"];
|
|
|
|
const validShape = (shape: fabric.Object): boolean => {
|
|
if (shape.width! <= 5 || shape.height! <= 5) {
|
|
return false;
|
|
}
|
|
if (shapeType.indexOf(shape.type!) === -1) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
class UndoStack {
|
|
private stack: string[] = [];
|
|
private index = -1;
|
|
private canvas: fabric.Canvas | undefined;
|
|
private locked = false;
|
|
private shapeIds = new Set<string>();
|
|
/** used to make the toolbar buttons reactive */
|
|
private state = writable<UndoState>({ undoable: false, redoable: false });
|
|
subscribe: typeof this.state.subscribe;
|
|
|
|
constructor() {
|
|
// allows an instance of the class to act as a store
|
|
this.subscribe = this.state.subscribe;
|
|
}
|
|
|
|
setCanvas(canvas: fabric.Canvas): void {
|
|
this.canvas = canvas;
|
|
this.canvas.on("object:modified", (opts) => this.maybePush(opts));
|
|
this.canvas.on("object:removed", (opts) => {
|
|
// @ts-expect-error `destroyed` is a custom property set on groups in the ungrouping routine to avoid adding a spurious undo entry
|
|
if (!opts.target!.group && !opts.target!.destroyed) {
|
|
this.maybePush(opts);
|
|
}
|
|
});
|
|
}
|
|
|
|
reset(): void {
|
|
this.shapeIds.clear();
|
|
this.stack.length = 0;
|
|
this.index = -1;
|
|
this.push();
|
|
this.updateState();
|
|
}
|
|
|
|
private canUndo(): boolean {
|
|
return this.index > 0;
|
|
}
|
|
|
|
private canRedo(): boolean {
|
|
return this.index < this.stack.length - 1;
|
|
}
|
|
|
|
private updateState(): void {
|
|
this.state.set({
|
|
undoable: this.canUndo(),
|
|
redoable: this.canRedo(),
|
|
});
|
|
}
|
|
|
|
private updateCanvas(): void {
|
|
this.locked = true;
|
|
this.canvas?.loadFromJSON(this.stack[this.index], () => {
|
|
this.canvas?.renderAll();
|
|
emitChangeSignal();
|
|
this.locked = false;
|
|
});
|
|
}
|
|
|
|
onObjectAdded(id: string): void {
|
|
if (!this.shapeIds.has(id)) {
|
|
this.push();
|
|
}
|
|
this.shapeIds.add(id);
|
|
emitChangeSignal();
|
|
}
|
|
|
|
onObjectModified(): void {
|
|
this.push();
|
|
emitChangeSignal();
|
|
}
|
|
|
|
private maybePush(obj: fabric.IEvent<MouseEvent>): void {
|
|
if (!this.locked && validShape(obj.target!)) {
|
|
this.push();
|
|
}
|
|
}
|
|
|
|
private push(): void {
|
|
const entry = JSON.stringify(this.canvas);
|
|
if (entry === this.stack[this.stack.length - 1]) {
|
|
return;
|
|
}
|
|
this.stack.length = this.index + 1;
|
|
this.stack.push(entry);
|
|
this.index++;
|
|
this.updateState();
|
|
}
|
|
|
|
undo(): void {
|
|
if (this.canUndo()) {
|
|
this.index--;
|
|
this.updateState();
|
|
this.updateCanvas();
|
|
}
|
|
}
|
|
|
|
redo(): void {
|
|
if (this.canRedo()) {
|
|
this.index++;
|
|
this.updateState();
|
|
this.updateCanvas();
|
|
}
|
|
}
|
|
}
|
|
|
|
export const undoStack = new UndoStack();
|
|
|
|
export const undoRedoTools = [
|
|
{
|
|
name: "undo",
|
|
icon: mdiUndo,
|
|
action: () => undoStack.undo(),
|
|
tooltip: tr.undoUndo,
|
|
shortcut: undoKeyCombination,
|
|
},
|
|
{
|
|
name: "redo",
|
|
icon: mdiRedo,
|
|
action: () => undoStack.redo(),
|
|
tooltip: tr.undoRedo,
|
|
shortcut: redoKeyCombination,
|
|
},
|
|
];
|