mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
image occlusion button in note editor (#2485)
* setup mask editor in note editor - add image on mask button click (only one time) - show hide add button for io on notetype change - hide field in io notetype - icon for toggle and replace image * add update io notes * Tidy up i/o notetype check and fix error - Make it a method on editor - Use .get(), because the setting doesn't exist on older notetypes - Pass the bool value into the ts code, instead of the enum * reset io page after adding * remove adjust function & add target for mask editor * handle browse mode & merged sidetoolbar and toptoolbar to toolbar * fix: shape, button click in browse, dropdown menu * add arrow to add button * store for handling visiblity of maskeditor - remove update button in edit mode, implement autoupdate * update var name * simplify store
This commit is contained in:
parent
0a418e0612
commit
135de7f9ed
23 changed files with 520 additions and 240 deletions
|
@ -51,3 +51,5 @@ notetypes-error-generating-cloze = An error occurred when generating an image oc
|
|||
notetypes-error-getting-imagecloze = An error occurred while fetching an image occlusion note
|
||||
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-io-select-image = Select Image
|
||||
|
|
|
@ -466,6 +466,10 @@ class Collection(DeprecatedNamesMixin):
|
|||
def get_image_for_occlusion(self, path: str | None) -> GetImageForOcclusionResponse:
|
||||
return self._backend.get_image_for_occlusion(path=path)
|
||||
|
||||
def add_image_occlusion_notetype(self) -> None:
|
||||
"Add notetype if missing."
|
||||
self._backend.add_image_occlusion_notetype()
|
||||
|
||||
def add_image_occlusion_note(
|
||||
self,
|
||||
notetype_id: int,
|
||||
|
|
|
@ -27,6 +27,7 @@ NotetypeNameIdUseCount = notetypes_pb2.NotetypeNameIdUseCount
|
|||
NotetypeNames = notetypes_pb2.NotetypeNames
|
||||
ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo
|
||||
ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest
|
||||
StockNotetype = notetypes_pb2.StockNotetype
|
||||
|
||||
# legacy types
|
||||
NotetypeDict = dict[str, Any]
|
||||
|
|
|
@ -48,9 +48,10 @@ class AddCards(QMainWindow):
|
|||
self.setMinimumWidth(400)
|
||||
self.setup_choosers()
|
||||
self.setupEditor()
|
||||
self.setupButtons()
|
||||
add_close_shortcut(self)
|
||||
self._load_new_note()
|
||||
self.setupButtons()
|
||||
self.col.add_image_occlusion_notetype()
|
||||
self.history: list[NoteId] = []
|
||||
self._last_added_note: Optional[Note] = None
|
||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||
|
@ -112,6 +113,12 @@ class AddCards(QMainWindow):
|
|||
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
|
||||
qconnect(self.compat_add_shorcut.activated, self.addButton.click)
|
||||
self.addButton.setToolTip(shortcut(tr.adding_add_shortcut_ctrlandenter()))
|
||||
|
||||
# add io button
|
||||
self.io_add_button = bb.addButton(f"{tr.actions_add()} {downArrow()}", ar)
|
||||
qconnect(self.io_add_button.clicked, self.onAddIo)
|
||||
self.io_add_button.setShortcut(QKeySequence("Ctrl+Shift+I"))
|
||||
|
||||
# close
|
||||
self.closeButton = QPushButton(tr.actions_close())
|
||||
self.closeButton.setAutoDefault(False)
|
||||
|
@ -133,6 +140,17 @@ class AddCards(QMainWindow):
|
|||
b.setEnabled(False)
|
||||
self.historyButton = b
|
||||
|
||||
# hide io buttons for note type other than image occlusion
|
||||
self.show_hide_add_buttons()
|
||||
|
||||
def show_hide_add_buttons(self) -> None:
|
||||
if self.editor.current_notetype_is_image_occlusion():
|
||||
self.addButton.setVisible(False)
|
||||
self.io_add_button.setVisible(True)
|
||||
else:
|
||||
self.addButton.setVisible(True)
|
||||
self.io_add_button.setVisible(False)
|
||||
|
||||
def setAndFocusNote(self, note: Note) -> None:
|
||||
self.editor.set_note(note, focusTo=0)
|
||||
|
||||
|
@ -192,6 +210,9 @@ class AddCards(QMainWindow):
|
|||
self, old_note.note_type(), new_note.note_type()
|
||||
)
|
||||
|
||||
# update buttons for image occlusion on note type change
|
||||
self.show_hide_add_buttons()
|
||||
|
||||
def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None:
|
||||
note = self._new_note()
|
||||
if old_note := sticky_fields_from:
|
||||
|
@ -283,7 +304,10 @@ class AddCards(QMainWindow):
|
|||
# no problem, duplicate, and confirmed cloze cases
|
||||
problem = None
|
||||
if result == NoteFieldsCheckResult.EMPTY:
|
||||
problem = tr.adding_the_first_field_is_empty()
|
||||
if self.editor.current_notetype_is_image_occlusion():
|
||||
problem = tr.notetypes_no_occlusion_created()
|
||||
else:
|
||||
problem = tr.adding_the_first_field_is_empty()
|
||||
elif result == NoteFieldsCheckResult.MISSING_CLOZE:
|
||||
if not askUser(tr.adding_you_have_a_cloze_deletion_note()):
|
||||
return False
|
||||
|
@ -348,6 +372,24 @@ class AddCards(QMainWindow):
|
|||
|
||||
self.ifCanClose(doClose)
|
||||
|
||||
def onAddIo(self) -> None:
|
||||
m = QMenu(self)
|
||||
a = m.addAction(tr.notetypes_hide_all_guess_one())
|
||||
qconnect(a.triggered, self.add_io_hide_all_note)
|
||||
a = m.addAction(tr.notetypes_hide_one_guess_one())
|
||||
qconnect(a.triggered, self.add_io_hide_one_note)
|
||||
m.popup(QCursor.pos())
|
||||
|
||||
def add_io_hide_all_note(self) -> None:
|
||||
self.editor.web.eval("setOcclusionField(true)")
|
||||
self.add_current_note()
|
||||
self.editor.web.eval("resetIOImageLoaded()")
|
||||
|
||||
def add_io_hide_one_note(self) -> None:
|
||||
self.editor.web.eval("setOcclusionField(false)")
|
||||
self.add_current_note()
|
||||
self.editor.web.eval("resetIOImageLoaded()")
|
||||
|
||||
# legacy aliases
|
||||
|
||||
@property
|
||||
|
|
|
@ -21,9 +21,6 @@ class EditCurrent(QDialog):
|
|||
disable_help_button(self)
|
||||
self.setMinimumHeight(400)
|
||||
self.setMinimumWidth(250)
|
||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut(
|
||||
QKeySequence("Ctrl+Return")
|
||||
)
|
||||
self.editor = aqt.editor.Editor(
|
||||
self.mw,
|
||||
self.form.fieldsArea,
|
||||
|
@ -33,6 +30,9 @@ class EditCurrent(QDialog):
|
|||
self.editor.card = self.mw.reviewer.card
|
||||
self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0)
|
||||
restoreGeom(self, "editcurrent")
|
||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut(
|
||||
QKeySequence("Ctrl+Return")
|
||||
)
|
||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||
self.show()
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ from anki.collection import Config, SearchNode
|
|||
from anki.consts import MODEL_CLOZE
|
||||
from anki.hooks import runFilter
|
||||
from anki.httpclient import HttpClient
|
||||
from anki.models import StockNotetype
|
||||
from anki.notes import Note, NoteFieldsCheckResult
|
||||
from anki.utils import checksum, is_lin, is_win, namedtmp
|
||||
from aqt import AnkiQt, colors, gui_hooks
|
||||
|
@ -549,17 +550,33 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
setShrinkImages({json.dumps(self.mw.col.get_config("shrinkEditorImages", True))});
|
||||
setCloseHTMLTags({json.dumps(self.mw.col.get_config("closeHTMLTags", True))});
|
||||
triggerChanges();
|
||||
setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())});
|
||||
setIsEditMode({json.dumps(self.editorMode != EditorMode.ADD_CARDS)});
|
||||
"""
|
||||
|
||||
if self.addMode:
|
||||
sticky = [field["sticky"] for field in self.note.note_type()["flds"]]
|
||||
js += " setSticky(%s);" % json.dumps(sticky)
|
||||
|
||||
if (
|
||||
self.editorMode != EditorMode.ADD_CARDS
|
||||
and self.current_notetype_is_image_occlusion()
|
||||
):
|
||||
options = {"kind": "edit", "noteId": self.note.id}
|
||||
options = {"mode": options}
|
||||
js += " setupMaskEditor(%s);" % json.dumps(options)
|
||||
|
||||
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
||||
self.web.evalWithCallback(
|
||||
f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback
|
||||
)
|
||||
|
||||
def current_notetype_is_image_occlusion(self) -> bool:
|
||||
return bool(self.note) and (
|
||||
self.note.note_type().get("originalStockKind", None)
|
||||
== StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
|
||||
)
|
||||
|
||||
def _save_current_note(self) -> None:
|
||||
"Call after note is updated with data from webview."
|
||||
update_note(parent=self.widget, note=self.note).run_in_background(
|
||||
|
@ -1175,6 +1192,34 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
def setTagsCollapsed(self, collapsed: bool) -> None:
|
||||
aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed)
|
||||
|
||||
def onAddImageForOcclusion(self) -> None:
|
||||
"""Show a file selection screen, then get selected image path."""
|
||||
extension_filter = " ".join(
|
||||
f"*.{extension}" for extension in sorted(itertools.chain(pics))
|
||||
)
|
||||
filter = f"{tr.editing_media()} ({extension_filter})"
|
||||
|
||||
def accept(file: str) -> None:
|
||||
try:
|
||||
html = self._addMedia(file)
|
||||
mode = {"kind": "add", "imagePath": file, "notetypeId": 0}
|
||||
# pass both html and options
|
||||
options = {"html": html, "mode": mode}
|
||||
self.web.eval(f"setupMaskEditor({json.dumps(options)})")
|
||||
except Exception as e:
|
||||
showWarning(str(e))
|
||||
return
|
||||
|
||||
file = getFile(
|
||||
parent=self.widget,
|
||||
title=tr.editing_add_media(),
|
||||
cb=cast(Callable[[Any], None], accept),
|
||||
filter=filter,
|
||||
key="media",
|
||||
)
|
||||
|
||||
self.parentWindow.activateWindow()
|
||||
|
||||
# Links from HTML
|
||||
######################################################################
|
||||
|
||||
|
@ -1204,6 +1249,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
toggleMathjax=Editor.toggleMathjax,
|
||||
toggleShrinkImages=Editor.toggleShrinkImages,
|
||||
toggleCloseHTMLTags=Editor.toggleCloseHTMLTags,
|
||||
addImageForOcclusion=Editor.onAddImageForOcclusion,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1452,4 +1498,14 @@ def set_cloze_button(editor: Editor) -> None:
|
|||
)
|
||||
|
||||
|
||||
def set_image_occlusion_button(editor: Editor) -> None:
|
||||
action = "show" if editor.current_notetype_is_image_occlusion() else "hide"
|
||||
editor.web.eval(
|
||||
'require("anki/ui").loaded.then(() =>'
|
||||
f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("image-occlusion-button")'
|
||||
"); "
|
||||
)
|
||||
|
||||
|
||||
gui_hooks.editor_did_load_note.append(set_cloze_button)
|
||||
gui_hooks.editor_did_load_note.append(set_image_occlusion_button)
|
||||
|
|
|
@ -15,6 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
plainText: boolean;
|
||||
description: string;
|
||||
collapsed: boolean;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface EditorFieldAPI {
|
||||
|
@ -87,7 +88,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
onDestroy(() => api?.destroy());
|
||||
</script>
|
||||
|
||||
<div class="field-container" on:mouseenter on:mouseleave>
|
||||
<div class="field-container" class:hide={field.hidden} on:mouseenter on:mouseleave>
|
||||
<slot name="field-label" />
|
||||
|
||||
<Collapsible collapse={collapsed} let:collapsed={hidden}>
|
||||
|
@ -127,6 +128,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-container.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-field {
|
||||
overflow: hidden;
|
||||
/* make room for thicker focus border */
|
||||
|
|
|
@ -237,6 +237,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
return noteId;
|
||||
}
|
||||
|
||||
let isImageOcclusion = false;
|
||||
function setIsImageOcclusion(val: boolean) {
|
||||
isImageOcclusion = val;
|
||||
$ioMaskEditorVisible = val;
|
||||
}
|
||||
|
||||
let isEditMode = false;
|
||||
function setIsEditMode(val: boolean) {
|
||||
isEditMode = val;
|
||||
}
|
||||
|
||||
let cols: ("dupe" | "")[] = [];
|
||||
export function setBackgrounds(cls: ("dupe" | "")[]): void {
|
||||
cols = cls;
|
||||
|
@ -255,6 +266,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),
|
||||
})) as FieldData[];
|
||||
|
||||
function saveTags({ detail }: CustomEvent): void {
|
||||
|
@ -286,6 +298,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function saveNow(): void {
|
||||
updateIONoteInEditMode();
|
||||
closeMathjaxEditor?.();
|
||||
$commitTagEdits();
|
||||
saveFieldNow();
|
||||
|
@ -372,12 +385,68 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
import { wrapInternal } from "@tslib/wrap";
|
||||
import LabelButton from "components/LabelButton.svelte";
|
||||
import Shortcut from "components/Shortcut.svelte";
|
||||
import ImageOcclusionPage from "image-occlusion/ImageOcclusionPage.svelte";
|
||||
import type { IOMode } from "image-occlusion/lib";
|
||||
import { exportShapesToClozeDeletions } from "image-occlusion/shapes/to-cloze";
|
||||
import { ioMaskEditorVisible } from "image-occlusion/store";
|
||||
|
||||
import { mathjaxConfig } from "../editable/mathjax-element";
|
||||
import CollapseLabel from "./CollapseLabel.svelte";
|
||||
import * as oldEditorAdapter from "./old-editor-adapter";
|
||||
|
||||
let isIOImageLoaded = false;
|
||||
let imageOcclusionMode: IOMode | undefined;
|
||||
async function setupMaskEditor(options: { html: string; mode: IOMode }) {
|
||||
imageOcclusionMode = options.mode;
|
||||
if (options.mode.kind === "add") {
|
||||
fieldStores[1].set(options.html);
|
||||
}
|
||||
isIOImageLoaded = true;
|
||||
}
|
||||
|
||||
// update cloze deletions and set occlusion fields, it call in saveNow to update cloze deletions
|
||||
function updateIONoteInEditMode() {
|
||||
if (isEditMode) {
|
||||
const clozeNote = get(fieldStores[0]);
|
||||
if (clozeNote.includes("oi=1")) {
|
||||
setOcclusionField(true);
|
||||
} else {
|
||||
setOcclusionField(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset for new occlusion in add mode
|
||||
function resetIOImageLoaded() {
|
||||
isIOImageLoaded = false;
|
||||
globalThis.canvas.clear();
|
||||
const page = document.querySelector(".image-occlusion");
|
||||
if (page) {
|
||||
page.remove();
|
||||
}
|
||||
}
|
||||
globalThis.resetIOImageLoaded = resetIOImageLoaded;
|
||||
|
||||
function setOcclusionField(occludeInactive: boolean) {
|
||||
// set fields data for occlusion and image fields for io notes type
|
||||
if (isImageOcclusion) {
|
||||
const occlusionsData = exportShapesToClozeDeletions(occludeInactive);
|
||||
fieldStores[0].set(occlusionsData.clozes);
|
||||
}
|
||||
}
|
||||
|
||||
// hide first two fields for occlusion type, first contains occlusion data and second contains image
|
||||
function hideFieldInOcclusionType(index: number) {
|
||||
if (isImageOcclusion) {
|
||||
if (index == 0 || index == 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function wrap(before: string, after: string): void {
|
||||
if (!$focusedInput || !editingInputIsRichText($focusedInput)) {
|
||||
|
@ -411,6 +480,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
setShrinkImages,
|
||||
setCloseHTMLTags,
|
||||
triggerChanges,
|
||||
setIsImageOcclusion,
|
||||
setIsEditMode,
|
||||
setupMaskEditor,
|
||||
setOcclusionField,
|
||||
...oldEditorAdapter,
|
||||
});
|
||||
|
||||
|
@ -464,137 +537,158 @@ the AddCards dialog) should be implemented in the user of this component.
|
|||
</Absolute>
|
||||
{/if}
|
||||
|
||||
<Fields>
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
{#if imageOcclusionMode}
|
||||
<div style="display: {$ioMaskEditorVisible ? 'block' : 'none'}">
|
||||
<ImageOcclusionPage mode={imageOcclusionMode} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<EditorField
|
||||
{field}
|
||||
{content}
|
||||
flipInputs={plainTextDefaults[index]}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
setAddonButtonsDisabled(false);
|
||||
bridgeCommand(`focus:${index}`);
|
||||
}}
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
setAddonButtonsDisabled(true);
|
||||
bridgeCommand(
|
||||
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
|
||||
get(content),
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
$hoveredField = fields[index];
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
$hoveredField = null;
|
||||
}}
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
dupe={cols[index] === "dupe"}
|
||||
--description-font-size="{field.fontSize}px"
|
||||
--description-content={`"${field.description}"`}
|
||||
{#if $ioMaskEditorVisible && isImageOcclusion && !isIOImageLoaded}
|
||||
<div id="io-select-image-div" style="padding-top: 60px; text-align: center;">
|
||||
<LabelButton
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="5px"
|
||||
class="io-select-image-btn"
|
||||
on:click={() => bridgeCommand("addImageForOcclusion")}
|
||||
>
|
||||
<svelte:fragment slot="field-label">
|
||||
<LabelContainer
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
on:toggle={() => toggleField(index)}
|
||||
--icon-align="bottom"
|
||||
>
|
||||
<svelte:fragment slot="field-name">
|
||||
<LabelName>
|
||||
{field.name}
|
||||
</LabelName>
|
||||
</svelte:fragment>
|
||||
<FieldState>
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
{#if plainTextDefaults[index]}
|
||||
<RichTextBadge
|
||||
show={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={richTextsHidden[index]}
|
||||
on:toggle={() => toggleRichTextInput(index)}
|
||||
{tr.notetypesIoSelectImage()}
|
||||
</LabelButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !$ioMaskEditorVisible}
|
||||
<Fields>
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
|
||||
<EditorField
|
||||
{field}
|
||||
{content}
|
||||
flipInputs={plainTextDefaults[index]}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
setAddonButtonsDisabled(false);
|
||||
bridgeCommand(`focus:${index}`);
|
||||
}}
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
setAddonButtonsDisabled(true);
|
||||
bridgeCommand(
|
||||
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
|
||||
get(content),
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
$hoveredField = fields[index];
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
$hoveredField = null;
|
||||
}}
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
dupe={cols[index] === "dupe"}
|
||||
--description-font-size="{field.fontSize}px"
|
||||
--description-content={`"${field.description}"`}
|
||||
>
|
||||
<svelte:fragment slot="field-label">
|
||||
<LabelContainer
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
on:toggle={() => toggleField(index)}
|
||||
--icon-align="bottom"
|
||||
>
|
||||
<svelte:fragment slot="field-name">
|
||||
<LabelName>
|
||||
{field.name}
|
||||
</LabelName>
|
||||
</svelte:fragment>
|
||||
<FieldState>
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
{#if plainTextDefaults[index]}
|
||||
<RichTextBadge
|
||||
show={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={richTextsHidden[index]}
|
||||
on:toggle={() => toggleRichTextInput(index)}
|
||||
/>
|
||||
{:else}
|
||||
<PlainTextBadge
|
||||
show={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={plainTextsHidden[index]}
|
||||
on:toggle={() => togglePlainTextInput(index)}
|
||||
/>
|
||||
{/if}
|
||||
<slot
|
||||
name="field-state"
|
||||
{field}
|
||||
{index}
|
||||
show={fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField}
|
||||
/>
|
||||
{:else}
|
||||
<PlainTextBadge
|
||||
show={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={plainTextsHidden[index]}
|
||||
on:toggle={() => togglePlainTextInput(index)}
|
||||
/>
|
||||
{/if}
|
||||
<slot
|
||||
name="field-state"
|
||||
{field}
|
||||
{index}
|
||||
show={fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField}
|
||||
</FieldState>
|
||||
</LabelContainer>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="rich-text-input">
|
||||
<Collapsible
|
||||
collapse={richTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
toggleDisplay
|
||||
>
|
||||
<RichTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={richTextInputs[index]}
|
||||
/>
|
||||
</FieldState>
|
||||
</LabelContainer>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="rich-text-input">
|
||||
<Collapsible
|
||||
collapse={richTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
toggleDisplay
|
||||
>
|
||||
<RichTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={richTextInputs[index]}
|
||||
/>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="plain-text-input">
|
||||
<Collapsible
|
||||
collapse={plainTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
toggleDisplay
|
||||
>
|
||||
<PlainTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={plainTextInputs[index]}
|
||||
/>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
</EditorField>
|
||||
{/each}
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="plain-text-input">
|
||||
<Collapsible
|
||||
collapse={plainTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
toggleDisplay
|
||||
>
|
||||
<PlainTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={plainTextInputs[index]}
|
||||
/>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
</EditorField>
|
||||
{/each}
|
||||
|
||||
<MathjaxOverlay />
|
||||
<ImageOverlay maxWidth={250} maxHeight={125} />
|
||||
</Fields>
|
||||
<MathjaxOverlay />
|
||||
<ImageOverlay maxWidth={250} maxHeight={125} />
|
||||
</Fields>
|
||||
|
||||
<Shortcut
|
||||
keyCombination="Control+Shift+T"
|
||||
on:action={() => {
|
||||
updateTagsCollapsed(false);
|
||||
}}
|
||||
/>
|
||||
<CollapseLabel
|
||||
collapsed={$tagsCollapsed}
|
||||
tooltip={$tagsCollapsed ? tr.editingExpand() : tr.editingCollapse()}
|
||||
on:toggle={() => updateTagsCollapsed(!$tagsCollapsed)}
|
||||
>
|
||||
{@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`}
|
||||
</CollapseLabel>
|
||||
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
|
||||
<TagEditor {tags} on:tagsupdate={saveTags} />
|
||||
</Collapsible>
|
||||
<Shortcut
|
||||
keyCombination="Control+Shift+T"
|
||||
on:action={() => {
|
||||
updateTagsCollapsed(false);
|
||||
}}
|
||||
/>
|
||||
<CollapseLabel
|
||||
collapsed={$tagsCollapsed}
|
||||
tooltip={$tagsCollapsed ? tr.editingExpand() : tr.editingCollapse()}
|
||||
on:toggle={() => updateTagsCollapsed(!$tagsCollapsed)}
|
||||
>
|
||||
{@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`}
|
||||
</CollapseLabel>
|
||||
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
|
||||
<TagEditor {tags} on:tagsupdate={saveTags} />
|
||||
</Collapsible>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -603,4 +697,32 @@ the AddCards dialog) should be implemented in the user of this component.
|
|||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.image-occlusion) {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
:global(.image-occlusion .tab-buttons) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:global(.image-occlusion .top-tool-bar-container) {
|
||||
margin-left: 28px !important;
|
||||
}
|
||||
:global(.top-tool-bar-container .icon-button) {
|
||||
height: 36px !important;
|
||||
}
|
||||
:global(.image-occlusion .tool-bar-container) {
|
||||
top: unset !important;
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
:global(.io-select-image-btn) {
|
||||
margin: auto;
|
||||
padding: 0px 8px 0px 8px !important;
|
||||
}
|
||||
|
||||
:global(.image-occlusion .sticky-footer) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,6 +38,8 @@ export const editorModules = [
|
|||
ModuleName.KEYBOARD,
|
||||
ModuleName.ACTIONS,
|
||||
ModuleName.BROWSING,
|
||||
ModuleName.NOTETYPES,
|
||||
ModuleName.IMPORTING,
|
||||
];
|
||||
|
||||
export const components = {
|
||||
|
|
|
@ -55,6 +55,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
import BlockButtons from "./BlockButtons.svelte";
|
||||
import ImageOcclusionButton from "./ImageOcclusionButton.svelte";
|
||||
import InlineButtons from "./InlineButtons.svelte";
|
||||
import NotetypeButtons from "./NotetypeButtons.svelte";
|
||||
import OptionsButtons from "./OptionsButtons.svelte";
|
||||
|
@ -120,6 +121,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<Item id="cloze">
|
||||
<RichTextClozeButtons />
|
||||
</Item>
|
||||
|
||||
<Item id="image-occlusion-button">
|
||||
<ImageOcclusionButton />
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</ButtonToolbar>
|
||||
</div>
|
||||
|
|
48
ts/editor/editor-toolbar/ImageOcclusionButton.svelte
Normal file
48
ts/editor/editor-toolbar/ImageOcclusionButton.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import DynamicallySlottable from "components/DynamicallySlottable.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import { ioMaskEditorVisible } from "image-occlusion/store";
|
||||
|
||||
import ButtonGroupItem, {
|
||||
createProps,
|
||||
setSlotHostContext,
|
||||
updatePropsList,
|
||||
} from "../../components/ButtonGroupItem.svelte";
|
||||
import { mdiViewDashboard } from "./icons";
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
||||
<ButtonGroup>
|
||||
<DynamicallySlottable
|
||||
slotHost={ButtonGroupItem}
|
||||
{createProps}
|
||||
{updatePropsList}
|
||||
{setSlotHostContext}
|
||||
{api}
|
||||
>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
id="io-mask-btn"
|
||||
class={$ioMaskEditorVisible ? "active-io-btn" : ""}
|
||||
on:click={() => {
|
||||
$ioMaskEditorVisible = !$ioMaskEditorVisible;
|
||||
}}
|
||||
>
|
||||
{@html mdiViewDashboard}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
</DynamicallySlottable>
|
||||
</ButtonGroup>
|
||||
|
||||
<style>
|
||||
:global(.active-io-btn) {
|
||||
background: var(--button-primary-bg) !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
|
@ -11,6 +11,8 @@ export { default as subscriptIcon } from "@mdi/svg/svg/format-subscript.svg";
|
|||
export { default as superscriptIcon } from "@mdi/svg/svg/format-superscript.svg";
|
||||
export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg";
|
||||
export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg";
|
||||
export { default as mdiRefresh } from "@mdi/svg/svg/refresh.svg";
|
||||
export { default as mdiViewDashboard } from "@mdi/svg/svg/view-dashboard.svg";
|
||||
export { default as eraserIcon } from "bootstrap-icons/icons/eraser.svg";
|
||||
export { default as justifyFullIcon } from "bootstrap-icons/icons/justify.svg";
|
||||
export { default as olIcon } from "bootstrap-icons/icons/list-ol.svg";
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
{ "path": "../sveltelib" },
|
||||
{ "path": "../editable" },
|
||||
{ "path": "../html-filter" },
|
||||
{ "path": "../tag-editor" }
|
||||
{ "path": "../tag-editor" },
|
||||
{ "path": "../image-occlusion" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import type { IOMode } from "./lib";
|
||||
import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor";
|
||||
import SideToolbar from "./SideToolbar.svelte";
|
||||
import Toolbar from "./Toolbar.svelte";
|
||||
|
||||
export let mode: IOMode;
|
||||
|
||||
const iconSize = 80;
|
||||
let instance: PanZoom;
|
||||
let innerWidth = 0;
|
||||
const startingTool = mode.kind === "add" ? "draw-rectangle" : "cursor";
|
||||
|
@ -39,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
</script>
|
||||
|
||||
<SideToolbar {instance} {canvas} activeTool={startingTool} />
|
||||
<Toolbar {canvas} {instance} {iconSize} activeTool={startingTool} />
|
||||
<div class="editor-main" bind:clientWidth={innerWidth}>
|
||||
<div class="editor-container" use:init>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
|
|
|
@ -85,7 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
.note-toolbar {
|
||||
margin-left: 98px;
|
||||
margin-left: 106px;
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
|
||||
import { enableSelectable, stopDraw } from "./tools/lib";
|
||||
import { tools } from "./tools/tool-buttons";
|
||||
import TopToolbar from "./TopToolbar.svelte";
|
||||
|
||||
export let instance;
|
||||
export let canvas;
|
||||
|
||||
const iconSize = 80;
|
||||
|
||||
export let activeTool = "cursor";
|
||||
|
||||
// handle tool changes after initialization
|
||||
$: if (instance && canvas) {
|
||||
disableFunctions();
|
||||
enableSelectable(canvas, true);
|
||||
|
||||
switch (activeTool) {
|
||||
case "magnify":
|
||||
enableSelectable(canvas, false);
|
||||
instance.resume();
|
||||
break;
|
||||
case "draw-rectangle":
|
||||
drawRectangle(canvas);
|
||||
break;
|
||||
case "draw-ellipse":
|
||||
drawEllipse(canvas);
|
||||
break;
|
||||
case "draw-polygon":
|
||||
drawPolygon(canvas, instance);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const disableFunctions = () => {
|
||||
instance.pause();
|
||||
stopDraw(canvas);
|
||||
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
|
||||
};
|
||||
</script>
|
||||
|
||||
<TopToolbar {canvas} {instance} {iconSize} />
|
||||
|
||||
<div class="tool-bar-container">
|
||||
{#each tools as tool}
|
||||
<IconButton
|
||||
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
|
||||
{iconSize}
|
||||
active={activeTool === tool.id}
|
||||
on:click={() => {
|
||||
activeTool = tool.id;
|
||||
}}
|
||||
>
|
||||
{@html tool.icon}
|
||||
</IconButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tool-bar-container {
|
||||
position: fixed;
|
||||
top: 42px;
|
||||
left: 2px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
width: 32px;
|
||||
z-index: 99;
|
||||
background: var(--canvas-elevated);
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
:global(.tool-icon-button) {
|
||||
border: unset;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: unset;
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
:global(.active-tool) {
|
||||
color: white !important;
|
||||
background: var(--button-primary-bg) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.2em !important;
|
||||
height: 0.2em !important;
|
||||
}
|
||||
</style>
|
|
@ -5,18 +5,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script>
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
import { mdiEye, mdiFormatAlignCenter } from "./icons";
|
||||
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
|
||||
import { makeMaskTransparent } from "./tools/lib";
|
||||
import { enableSelectable, stopDraw } from "./tools/lib";
|
||||
import {
|
||||
alignTools,
|
||||
deleteDuplicateTools,
|
||||
groupUngroupTools,
|
||||
zoomTools,
|
||||
} from "./tools/more-tools";
|
||||
import { tools } from "./tools/tool-buttons";
|
||||
import { undoRedoTools } from "./tools/tool-undo-redo";
|
||||
|
||||
export let canvas;
|
||||
export let instance;
|
||||
export let iconSize;
|
||||
export let activeTool = "cursor";
|
||||
let showAlignTools = false;
|
||||
let leftPos = 82;
|
||||
let maksOpacity = false;
|
||||
|
@ -27,8 +31,53 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
showAlignTools = false;
|
||||
}
|
||||
});
|
||||
|
||||
// handle tool changes after initialization
|
||||
$: if (instance && canvas) {
|
||||
disableFunctions();
|
||||
enableSelectable(canvas, true);
|
||||
|
||||
switch (activeTool) {
|
||||
case "magnify":
|
||||
enableSelectable(canvas, false);
|
||||
instance.resume();
|
||||
break;
|
||||
case "draw-rectangle":
|
||||
drawRectangle(canvas);
|
||||
break;
|
||||
case "draw-ellipse":
|
||||
drawEllipse(canvas);
|
||||
break;
|
||||
case "draw-polygon":
|
||||
drawPolygon(canvas, instance);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const disableFunctions = () => {
|
||||
instance.pause();
|
||||
stopDraw(canvas);
|
||||
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="tool-bar-container">
|
||||
{#each tools as tool}
|
||||
<IconButton
|
||||
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
|
||||
{iconSize}
|
||||
active={activeTool === tool.id}
|
||||
on:click={() => {
|
||||
activeTool = tool.id;
|
||||
}}
|
||||
>
|
||||
{@html tool.icon}
|
||||
</IconButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="top-tool-bar-container">
|
||||
<!-- undo & redo tools -->
|
||||
<div class="undo-redo-button">
|
||||
|
@ -141,7 +190,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
display: flex;
|
||||
overflow-y: scroll;
|
||||
z-index: 99;
|
||||
margin-left: 98px;
|
||||
margin-left: 106px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
@ -171,6 +220,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
height: 32px;
|
||||
margin: unset;
|
||||
padding: 6px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
|
@ -189,4 +239,31 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
width: 0.1em !important;
|
||||
height: 0.1em !important;
|
||||
}
|
||||
|
||||
.tool-bar-container {
|
||||
position: fixed;
|
||||
top: 42px;
|
||||
left: 2px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
width: 32px;
|
||||
z-index: 99;
|
||||
background: var(--canvas-elevated);
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
:global(.tool-icon-button) {
|
||||
border: unset;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: unset;
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
:global(.active-tool) {
|
||||
color: white !important;
|
||||
background: var(--button-primary-bg) !important;
|
||||
}
|
||||
</style>
|
|
@ -20,12 +20,12 @@ const i18n = setupI18n({
|
|||
],
|
||||
});
|
||||
|
||||
export async function setupImageOcclusion(mode: IOMode): Promise<ImageOcclusionPage> {
|
||||
export async function setupImageOcclusion(mode: IOMode, target = document.body): Promise<ImageOcclusionPage> {
|
||||
checkNightMode();
|
||||
await i18n;
|
||||
|
||||
return new ImageOcclusionPage({
|
||||
target: document.body,
|
||||
target: target,
|
||||
props: {
|
||||
mode,
|
||||
},
|
||||
|
|
|
@ -54,6 +54,7 @@ export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom):
|
|||
|
||||
// get image width and height
|
||||
const image = document.getElementById("image") as HTMLImageElement;
|
||||
image.style.visibility = "hidden";
|
||||
image.src = getImageData(clozeNote.imageData!);
|
||||
image.onload = function() {
|
||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
||||
|
@ -66,6 +67,9 @@ export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom):
|
|||
addShapesToCanvasFromCloze(canvas, clozeNote.occlusions);
|
||||
enableSelectable(canvas, true);
|
||||
addClozeNotesToTextEditor(clozeNote.header, clozeNote.backExtra, clozeNote.tags);
|
||||
window.requestAnimationFrame(() => {
|
||||
image.style.visibility = "visible";
|
||||
});
|
||||
};
|
||||
|
||||
return canvas;
|
||||
|
|
|
@ -6,5 +6,8 @@
|
|||
* for up to widths/heights of 10kpx.
|
||||
*/
|
||||
export function floatToDisplay(number: number): string {
|
||||
if (Number.isNaN(number) || number == 0) {
|
||||
return ".0000";
|
||||
}
|
||||
return number.toFixed(4).replace(/^0+|0+$/g, "");
|
||||
}
|
||||
|
|
|
@ -108,8 +108,8 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
|
|||
type ShapeType = "rect" | "ellipse" | "polygon";
|
||||
|
||||
function buildShape(type: ShapeType, props: Record<string, any>): Shape {
|
||||
props.left = parseFloat(props.left);
|
||||
props.top = parseFloat(props.top);
|
||||
props.left = parseFloat(Number.isNaN(Number(props.left)) ? ".0000" : props.left);
|
||||
props.top = parseFloat(Number.isNaN(Number(props.top)) ? ".0000" : props.top);
|
||||
switch (type) {
|
||||
case "rect": {
|
||||
return new Rectangle({ ...props, width: parseFloat(props.width), height: parseFloat(props.height) });
|
||||
|
|
|
@ -81,6 +81,9 @@ function fabricObjectToBaseShapeOrShapes(
|
|||
function shapeOrShapesToCloze(shapeOrShapes: ShapeOrShapes, index: number): string {
|
||||
let text = "";
|
||||
function addKeyValue(key: string, value: string) {
|
||||
if (Number.isNaN(Number(value))) {
|
||||
value = ".0000";
|
||||
}
|
||||
text += `:${key}=${value}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,3 +9,5 @@ export const notesDataStore = writable({ id: "", title: "", divValue: "", textar
|
|||
export const zoomResetValue = writable(1);
|
||||
// it stores the tags for the note in note editor
|
||||
export const tagsWritable = writable([""]);
|
||||
// it stores the visibility of mask editor
|
||||
export const ioMaskEditorVisible = writable(true);
|
||||
|
|
Loading…
Reference in a new issue