Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97967f5b70 | ||
|
|
9323e1fad4 | ||
|
|
3e4e741b54 | ||
|
|
4e2d8374e6 | ||
|
|
2b3037402a | ||
|
|
bc67c27a82 | ||
|
|
ffb8f6f00f | ||
|
|
d179dfe703 | ||
|
|
5701020901 | ||
|
|
1f2d795b58 | ||
|
|
c123b3ef51 | ||
|
|
d565c7e9e9 | ||
|
|
710a40c36b | ||
|
|
64f9a5dd7d | ||
|
|
323fe33c2f | ||
|
|
2470f6f531 | ||
|
|
42968d8299 | ||
|
|
7a50b6c77e | ||
|
|
0a10d5fbc9 | ||
|
|
40d2345501 | ||
|
|
ab97ae5ebc | ||
|
|
7c93e90fc0 | ||
|
|
d44cf3306b | ||
|
|
324609999f | ||
|
|
3f54b851ae | ||
|
|
8bbb04b421 | ||
|
|
dc396c8707 | ||
|
|
52cc5d3aa7 | ||
|
|
87b6335905 | ||
|
|
17358f16c8 | ||
|
|
23eb268031 | ||
|
|
27f4cb248d | ||
|
|
bbaf4f7a34 | ||
|
|
559455bf5b | ||
|
|
bd519aff08 | ||
|
|
febeb787b5 | ||
|
|
9f8a9bfa8a | ||
|
|
8b1daed0ef | ||
|
|
44c828c7e7 | ||
|
|
afabeaa2f3 | ||
|
|
e72c1676c2 | ||
|
|
5a17eb7054 | ||
|
|
75d52c07b8 | ||
|
|
4dc6c17486 | ||
|
|
e780930799 | ||
|
|
49cd6a36a1 | ||
|
|
d4830983e2 | ||
|
|
a69fefffdc | ||
|
|
1d0466dae7 | ||
|
|
6e5a853d0f | ||
|
|
0c702ddf7b | ||
|
|
fdbffce1f9 | ||
|
|
2872b4e3ce | ||
|
|
0ba55e51e9 | ||
|
|
5887bf377d | ||
|
|
c440dd9cf0 | ||
|
|
21bc1f7fa6 | ||
|
|
4279b13554 | ||
|
|
0e106b7c7b | ||
|
|
4d7d1fba3a | ||
|
|
a35ea5e9da | ||
|
|
48466f624d | ||
|
|
f306b20449 | ||
|
|
fc4fd685ba | ||
|
|
7449df6ac6 | ||
|
|
1390333c4c | ||
|
|
a9f545a1b2 | ||
|
|
f80a96c703 | ||
|
|
f291c15bbc | ||
|
|
18821e1a67 | ||
|
|
5dd65d691c | ||
|
|
8f96dbc21d | ||
|
|
f71623f8a1 | ||
|
|
b380420cac | ||
|
|
21cccd4475 | ||
|
|
06475aea78 | ||
|
|
84af0c2d5c | ||
|
|
84648f5a56 | ||
|
|
60b7988860 | ||
|
|
732b7ad424 | ||
|
|
a664c34418 | ||
|
|
eaf3b7f7d7 | ||
|
|
03edbaf545 | ||
|
|
c3edf23023 | ||
|
|
5f765609f9 | ||
|
|
3c1beac822 | ||
|
|
f30f66969c | ||
|
|
4e6fb48bf0 | ||
|
|
53225ba5a7 | ||
|
|
c04967d775 | ||
|
|
6bb7a3c0e5 | ||
|
|
2c85191671 | ||
|
|
7b178ce2c8 |
1
.gitignore
vendored
@@ -13,6 +13,7 @@ stats.html
|
||||
hot-reload.bat
|
||||
data.json
|
||||
lib
|
||||
dist
|
||||
|
||||
#VSCode
|
||||
.vscode
|
||||
|
||||
@@ -4,7 +4,9 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
|
||||
|
||||
## Video Walkthrough
|
||||
|
||||
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
|
||||
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
|
||||
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
|
||||
|
||||
|
||||
<details><summary>10 Part (slightly outdated) Video Walkthrough</summary>
|
||||
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/> 1 Getting Started</a><br>
|
||||
|
||||
772
docs/API/ExcalidrawAutomate.d.ts
vendored
Normal file
@@ -0,0 +1,772 @@
|
||||
/// <reference types="react" />
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { TInput } from "colormaster/types";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
|
||||
import { PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
export declare class ExcalidrawAutomate {
|
||||
/**
|
||||
* Utility function that returns the Obsidian Module object.
|
||||
*/
|
||||
get obsidian(): typeof obsidian_module;
|
||||
get DEVICE(): DeviceType;
|
||||
getAttachmentFilepath(filename: string): Promise<string>;
|
||||
/**
|
||||
* Prompts the user with a dialog to select new file action.
|
||||
* - create markdown file
|
||||
* - create excalidraw file
|
||||
* - cancel action
|
||||
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
|
||||
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
|
||||
* targetPane control which leaf will be used for the new file.
|
||||
* Returns the TFile for the new file or null if the user cancelled the action.
|
||||
* @param newFileNameOrPath
|
||||
* @param shouldOpenNewFile
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @param parentFile
|
||||
* @returns
|
||||
*/
|
||||
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
*/
|
||||
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
|
||||
/**
|
||||
* Returns the editor or leaf.view of the currently active embedded obsidian file.
|
||||
* If view is not provided, ea.targetView is used.
|
||||
* If the embedded file is a markdown document the function will return
|
||||
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
|
||||
* @param view
|
||||
* @returns
|
||||
*/
|
||||
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
|
||||
view: any;
|
||||
} | {
|
||||
file: TFile;
|
||||
editor: Editor;
|
||||
} | null;
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {
|
||||
[key: string]: any;
|
||||
};
|
||||
imagesDict: {
|
||||
[key: FileId]: any;
|
||||
};
|
||||
mostRecentMarkdownSVG: SVGSVGElement;
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness?: StrokeRoundness;
|
||||
roundness: null | {
|
||||
type: RoundnessType;
|
||||
value?: number;
|
||||
};
|
||||
fontFamily: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
};
|
||||
canvas: {
|
||||
theme: string;
|
||||
viewBackgroundColor: string;
|
||||
gridSize: number;
|
||||
};
|
||||
colorPalette: {};
|
||||
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
|
||||
/**
|
||||
*
|
||||
* @returns the last recorded pointer position on the Excalidraw canvas
|
||||
*/
|
||||
getViewLastPointerPosition(): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getAPI(view?: ExcalidrawView): ExcalidrawAutomate;
|
||||
/**
|
||||
* @param val //0:"hachure", 1:"cross-hatch" 2:"solid"
|
||||
* @returns
|
||||
*/
|
||||
setFillStyle(val: number): "hachure" | "cross-hatch" | "solid";
|
||||
/**
|
||||
* @param val //0:"solid", 1:"dashed", 2:"dotted"
|
||||
* @returns
|
||||
*/
|
||||
setStrokeStyle(val: number): "solid" | "dashed" | "dotted";
|
||||
/**
|
||||
* @param val //0:"round", 1:"sharp"
|
||||
* @returns
|
||||
*/
|
||||
setStrokeSharpness(val: number): "round" | "sharp";
|
||||
/**
|
||||
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
|
||||
* @returns
|
||||
*/
|
||||
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "LocalFont";
|
||||
/**
|
||||
* @param val //0:"light", 1:"dark"
|
||||
* @returns
|
||||
*/
|
||||
setTheme(val: number): "light" | "dark";
|
||||
/**
|
||||
* @param objectIds
|
||||
* @returns
|
||||
*/
|
||||
addToGroup(objectIds: string[]): string;
|
||||
/**
|
||||
* @param templatePath
|
||||
*/
|
||||
toClipboard(templatePath?: string): Promise<void>;
|
||||
/**
|
||||
* @param file: TFile
|
||||
* @returns ExcalidrawScene
|
||||
*/
|
||||
getSceneFromFile(file: TFile): Promise<{
|
||||
elements: ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
}>;
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
*/
|
||||
getElements(): ExcalidrawElement[];
|
||||
/**
|
||||
* get single element from ExcalidrawAutomate elementsDict
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
getElement(id: string): ExcalidrawElement;
|
||||
/**
|
||||
* create a drawing and save it to filename
|
||||
* @param params
|
||||
* filename: if null, default filename as defined in Excalidraw settings
|
||||
* foldername: if null, default folder as defined in Excalidraw settings
|
||||
* @returns
|
||||
*/
|
||||
create(params?: {
|
||||
filename?: string;
|
||||
foldername?: string;
|
||||
templatePath?: string;
|
||||
onNewPane?: boolean;
|
||||
frontmatterKeys?: {
|
||||
"excalidraw-plugin"?: "raw" | "parsed";
|
||||
"excalidraw-link-prefix"?: string;
|
||||
"excalidraw-link-brackets"?: boolean;
|
||||
"excalidraw-url-prefix"?: string;
|
||||
"excalidraw-export-transparent"?: boolean;
|
||||
"excalidraw-export-dark"?: boolean;
|
||||
"excalidraw-export-padding"?: number;
|
||||
"excalidraw-export-pngscale"?: number;
|
||||
"excalidraw-default-mode"?: "view" | "zen";
|
||||
"excalidraw-onload-script"?: string;
|
||||
"excalidraw-linkbutton-opacity"?: number;
|
||||
"excalidraw-autoexport"?: boolean;
|
||||
};
|
||||
plaintext?: string;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param templatePath
|
||||
* @param embedFont
|
||||
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
* @param theme
|
||||
* @returns
|
||||
*/
|
||||
createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<SVGSVGElement>;
|
||||
/**
|
||||
*
|
||||
* @param templatePath
|
||||
* @param scale
|
||||
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
* @param theme
|
||||
* @returns
|
||||
*/
|
||||
createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<any>;
|
||||
/**
|
||||
*
|
||||
* @param text
|
||||
* @param lineLen
|
||||
* @returns
|
||||
*/
|
||||
wrapText(text: string, lineLen: number): string;
|
||||
private boxedElement;
|
||||
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addRect(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addDiamond(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addEllipse(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addBlob(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
* Refresh the size of a text element to fit its contents
|
||||
* @param id - the id of the text element
|
||||
*/
|
||||
refreshTextElementSize(id: string): void;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param text
|
||||
* @param formatting
|
||||
* box: if !null, text will be boxed
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
addText(topX: number, topY: number, text: string, formatting?: {
|
||||
wrapAt?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
|
||||
boxPadding?: number;
|
||||
boxStrokeColor?: string;
|
||||
textVerticalAlign?: "top" | "middle" | "bottom";
|
||||
}, id?: string): string;
|
||||
/**
|
||||
*
|
||||
* @param points
|
||||
* @returns
|
||||
*/
|
||||
addLine(points: [[x: number, y: number]]): string;
|
||||
/**
|
||||
*
|
||||
* @param points
|
||||
* @param formatting
|
||||
* @returns
|
||||
*/
|
||||
addArrow(points: [x: number, y: number][], formatting?: {
|
||||
startArrowHead?: string;
|
||||
endArrowHead?: string;
|
||||
startObjectId?: string;
|
||||
endObjectId?: string;
|
||||
}): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param imageFile
|
||||
* @returns
|
||||
*/
|
||||
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
anchor?: boolean): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param tex
|
||||
* @returns
|
||||
*/
|
||||
addLaTex(topX: number, topY: number, tex: string): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param objectA
|
||||
* @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
|
||||
* @param objectB
|
||||
* @param connectionB when passed null, Excalidraw will automatically decide
|
||||
* @param formatting
|
||||
* numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
|
||||
* startArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
|
||||
* endArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
|
||||
* padding:
|
||||
* @returns
|
||||
*/
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint | null, objectB: string, connectionB: ConnectionPoint | null, formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
padding?: number;
|
||||
}): string;
|
||||
/**
|
||||
* Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line)
|
||||
* @param lineId id of the line or arrow object in elementsDict
|
||||
* @param label the label text
|
||||
* @returns undefined (if unsuccessful) or the id of the new text element
|
||||
*/
|
||||
addLabelToLine(lineId: string, label: string): string;
|
||||
/**
|
||||
* clear elementsDict and imagesDict only
|
||||
*/
|
||||
clear(): void;
|
||||
/**
|
||||
* clear() + reset all style values to default
|
||||
*/
|
||||
reset(): void;
|
||||
/**
|
||||
* returns true if MD file is an Excalidraw file
|
||||
* @param f
|
||||
* @returns
|
||||
*/
|
||||
isExcalidrawFile(f: TFile): boolean;
|
||||
targetView: ExcalidrawView;
|
||||
/**
|
||||
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
|
||||
* if view is null or undefined, the function will first try setView("active"), then setView("first").
|
||||
* @param view
|
||||
* @returns targetView
|
||||
*/
|
||||
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
|
||||
/**
|
||||
*
|
||||
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
|
||||
*/
|
||||
getExcalidrawAPI(): any;
|
||||
/**
|
||||
* get elements in View
|
||||
* @returns
|
||||
*/
|
||||
getViewElements(): ExcalidrawElement[];
|
||||
/**
|
||||
*
|
||||
* @param elToDelete
|
||||
* @returns
|
||||
*/
|
||||
deleteViewElements(elToDelete: ExcalidrawElement[]): boolean;
|
||||
/**
|
||||
* get the selected element in the view, if more are selected, get the first
|
||||
* @returns
|
||||
*/
|
||||
getViewSelectedElement(): any;
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getViewSelectedElements(): any[];
|
||||
/**
|
||||
*
|
||||
* @param el
|
||||
* @returns TFile file handle for the image element
|
||||
*/
|
||||
getViewFileForImageElement(el: ExcalidrawElement): TFile | null;
|
||||
/**
|
||||
* copies elements from view to elementsDict for editing
|
||||
* @param elements
|
||||
*/
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;
|
||||
/**
|
||||
*
|
||||
* @param forceViewMode
|
||||
* @returns
|
||||
*/
|
||||
viewToggleFullScreen(forceViewMode?: boolean): void;
|
||||
setViewModeEnabled(enabled: boolean): void;
|
||||
/**
|
||||
* This function gives you a more hands on access to Excalidraw.
|
||||
* @param scene - The scene you want to load to Excalidraw
|
||||
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
|
||||
* @returns
|
||||
*/
|
||||
viewUpdateScene(scene: {
|
||||
elements?: ExcalidrawElement[];
|
||||
appState?: AppState;
|
||||
files?: BinaryFileData;
|
||||
commitToHistory?: boolean;
|
||||
}, restore?: boolean): void;
|
||||
/**
|
||||
* connect an object to the selected element in the view
|
||||
* @param objectA ID of the element
|
||||
* @param connectionA
|
||||
* @param connectionB
|
||||
* @param formatting
|
||||
* @returns
|
||||
*/
|
||||
connectObjectWithViewSelectedElement(objectA: string, connectionA: ConnectionPoint | null, connectionB: ConnectionPoint | null, formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
padding?: number;
|
||||
}): boolean;
|
||||
/**
|
||||
* zoom tarteView to fit elements provided as input
|
||||
* elements === [] will zoom to fit the entire scene
|
||||
* selectElements toggles whether the elements should be in a selected state at the end of the operation
|
||||
* @param selectElements
|
||||
* @param elements
|
||||
*/
|
||||
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
|
||||
/**
|
||||
* Adds elements from elementsDict to the current view
|
||||
* @param repositionToCursor default is false
|
||||
* @param save default is true
|
||||
* @param newElementsOnTop controls whether elements created with ExcalidrawAutomate
|
||||
* are added at the bottom of the stack or the top of the stack of elements already in the view
|
||||
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
|
||||
* position in the stack of elements in the view even if modified using EA
|
||||
* default is false, i.e. the new elements get to the bottom of the stack
|
||||
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
|
||||
* @returns
|
||||
*/
|
||||
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
|
||||
/**
|
||||
* Register instance of EA to use for hooks with TargetView
|
||||
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
|
||||
* Using this event you can set a different instance of Excalidraw Automate for hooks
|
||||
* @returns true if successful
|
||||
*/
|
||||
registerThisAsViewEA(): boolean;
|
||||
/**
|
||||
* Sets the targetView EA to window.ExcalidrawAutomate
|
||||
* @returns true if successful
|
||||
*/
|
||||
deregisterThisAsViewEA(): boolean;
|
||||
/**
|
||||
* If set, this callback is triggered when the user closes an Excalidraw view.
|
||||
*/
|
||||
onViewUnloadHook: (view: ExcalidrawView) => void;
|
||||
/**
|
||||
* If set, this callback is triggered, when the user changes the view mode.
|
||||
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
|
||||
*/
|
||||
onViewModeChangeHook: (isViewModeEnabled: boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void;
|
||||
/**
|
||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
||||
*/
|
||||
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when the user clicks a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
|
||||
*/
|
||||
onLinkClickHook: (element: ExcalidrawElement, linkText: string, event: MouseEvent, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
|
||||
* You can use this callback in case you want to do something additional when the onDrop event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
|
||||
*/
|
||||
onDropHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
event: React.DragEvent<HTMLDivElement>;
|
||||
draggable: any;
|
||||
type: "file" | "text" | "unknown";
|
||||
payload: {
|
||||
files: TFile[];
|
||||
text: string;
|
||||
};
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
pointerPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
*/
|
||||
onPasteHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
payload: ClipboardData;
|
||||
event: ClipboardEvent;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
pointerPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}) => boolean;
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
*/
|
||||
onFileOpenHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
*/
|
||||
onFileCreateHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
*/
|
||||
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
|
||||
color: string) => void;
|
||||
/**
|
||||
* utility function to generate EmbeddedFilesLoader object
|
||||
* @param isDark
|
||||
* @returns
|
||||
*/
|
||||
getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader;
|
||||
/**
|
||||
* utility function to generate ExportSettings object
|
||||
* @param withBackground
|
||||
* @param withTheme
|
||||
* @returns
|
||||
*/
|
||||
getExportSettings(withBackground: boolean, withTheme: boolean): ExportSettings;
|
||||
/**
|
||||
* get bounding box of elements
|
||||
* bounding box is the box encapsulating all of the elements completely
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
getBoundingBox(elements: ExcalidrawElement[]): {
|
||||
topX: number;
|
||||
topY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
/**
|
||||
* elements grouped by the highest level groups
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][];
|
||||
/**
|
||||
* gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
|
||||
/**
|
||||
* @param element
|
||||
* @param a
|
||||
* @param b
|
||||
* @param gap
|
||||
* @returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
* and the `element`, in ascending order of distance from `a`.
|
||||
*/
|
||||
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
|
||||
/**
|
||||
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
|
||||
* @param elements
|
||||
* @returns null or the groupId
|
||||
*/
|
||||
getCommonGroupForElements(elements: ExcalidrawElement[]): string;
|
||||
/**
|
||||
* Gets all the elements from elements[] that share one or more groupIds with element.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
/**
|
||||
* See OCR plugin for example on how to use scriptSettings
|
||||
* Set by the ScriptEngine
|
||||
*/
|
||||
activeScript: string;
|
||||
/**
|
||||
*
|
||||
* @returns script settings. Saves settings in plugin settings, under the activeScript key
|
||||
*/
|
||||
getScriptSettings(): {};
|
||||
/**
|
||||
* sets script settings.
|
||||
* @param settings
|
||||
* @returns
|
||||
*/
|
||||
setScriptSettings(settings: any): Promise<void>;
|
||||
/**
|
||||
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
|
||||
* @param file
|
||||
* @param openState - if not provided {active: true} will be used
|
||||
* @returns
|
||||
*/
|
||||
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
|
||||
/**
|
||||
* measure text size based on current style settings
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
measureText(text: string): {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
/**
|
||||
* Returns the size of the image element at 100% (i.e. the original size)
|
||||
* @param imageElement an image element from the active scene on targetView
|
||||
*/
|
||||
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
/**
|
||||
* verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||
* recommended use:
|
||||
* if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
|
||||
* @param requiredVersion
|
||||
* @returns
|
||||
*/
|
||||
verifyMinimumPluginVersion(requiredVersion: string): boolean;
|
||||
/**
|
||||
* Check if view is instance of ExcalidrawView
|
||||
* @param view
|
||||
* @returns
|
||||
*/
|
||||
isExcalidrawView(view: any): boolean;
|
||||
/**
|
||||
* sets selection in view
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
|
||||
/**
|
||||
* @returns an 8 character long random id
|
||||
*/
|
||||
generateElementId(): string;
|
||||
/**
|
||||
* @param element
|
||||
* @returns a clone of the element with a new id
|
||||
*/
|
||||
cloneElement(element: ExcalidrawElement): ExcalidrawElement;
|
||||
/**
|
||||
* Moves the element to a specific position in the z-index
|
||||
*/
|
||||
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hexStringToRgb(color: string): number[];
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHexString(color: number[]): string;
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hslToRgb(color: number[]): number[];
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHsl(color: number[]): number[];
|
||||
/**
|
||||
*
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
colorNameToHex(color: string): string;
|
||||
/**
|
||||
* https://github.com/lbragile/ColorMaster
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
getCM(color: TInput): ColorMaster;
|
||||
importSVG(svgString: string): boolean;
|
||||
}
|
||||
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
|
||||
export declare function destroyExcalidrawAutomate(): void;
|
||||
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
|
||||
w: number;
|
||||
h: number;
|
||||
baseline: number;
|
||||
};
|
||||
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
|
||||
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
|
||||
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
|
||||
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
|
||||
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
|
||||
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
|
||||
export declare const search: (view: ExcalidrawView) => Promise<void>;
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
|
||||
export declare const cloneElement: (el: ExcalidrawElement) => any;
|
||||
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;
|
||||
@@ -2,36 +2,110 @@
|
||||
## Attributes and functions overview
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
```typescript
|
||||
export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
plugin: ExcalidrawPlugin;
|
||||
targetView: ExcalidrawView = null; //the view currently edited
|
||||
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
|
||||
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
|
||||
mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
|
||||
style: {
|
||||
strokeColor: string; //https://www.w3schools.com/colors/default.asp
|
||||
backgroundColor: string;
|
||||
angle: number; //radian
|
||||
fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp"
|
||||
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
|
||||
fontSize: number;
|
||||
textAlign: string; //"left"|"right"|"center"
|
||||
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
|
||||
startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null
|
||||
endArrowHead: string;
|
||||
};
|
||||
canvas: {
|
||||
theme: string; //"dark"|"light"
|
||||
viewBackgroundColor: string;
|
||||
gridSize: number;
|
||||
};
|
||||
You can find the source file here: [ExcalidrawAutomate.d.ts](ExcalidrawAutomate.d.ts).
|
||||
|
||||
```javascript
|
||||
/// <reference types="react" />
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { TInput } from "colormaster/types";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
import { PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
export declare class ExcalidrawAutomate {
|
||||
/**
|
||||
* Utility function that returns the Obsidian Module object.
|
||||
*/
|
||||
get obsidian(): typeof obsidian_module;
|
||||
get DEVICE(): DeviceType;
|
||||
getAttachmentFilepath(filename: string): Promise<string>;
|
||||
/**
|
||||
* Prompts the user with a dialog to select new file action.
|
||||
* - create markdown file
|
||||
* - create excalidraw file
|
||||
* - cancel action
|
||||
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
|
||||
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
|
||||
* targetPane control which leaf will be used for the new file.
|
||||
* Returns the TFile for the new file or null if the user cancelled the action.
|
||||
* @param newFileNameOrPath
|
||||
* @param shouldOpenNewFile
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @param parentFile
|
||||
* @returns
|
||||
*/
|
||||
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
*/
|
||||
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
|
||||
/**
|
||||
* Returns the editor or leaf.view of the currently active embedded obsidian file.
|
||||
* If view is not provided, ea.targetView is used.
|
||||
* If the embedded file is a markdown document the function will return
|
||||
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
|
||||
* @param view
|
||||
* @returns
|
||||
*/
|
||||
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
|
||||
view: any;
|
||||
} | {
|
||||
file: TFile;
|
||||
editor: Editor;
|
||||
} | null;
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {
|
||||
[key: string]: any;
|
||||
};
|
||||
imagesDict: {
|
||||
[key: FileId]: any;
|
||||
};
|
||||
mostRecentMarkdownSVG: SVGSVGElement;
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness?: StrokeRoundness;
|
||||
roundness: null | {
|
||||
type: RoundnessType;
|
||||
value?: number;
|
||||
};
|
||||
fontFamily: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
};
|
||||
canvas: {
|
||||
theme: string;
|
||||
viewBackgroundColor: string;
|
||||
gridSize: number;
|
||||
};
|
||||
colorPalette: {};
|
||||
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
|
||||
/**
|
||||
*
|
||||
* @returns the last recorded pointer position on the Excalidraw canvas
|
||||
*/
|
||||
getViewLastPointerPosition(): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
@@ -71,6 +145,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @param templatePath
|
||||
*/
|
||||
toClipboard(templatePath?: string): Promise<void>;
|
||||
/**
|
||||
* @param file: TFile
|
||||
* @returns ExcalidrawScene
|
||||
*/
|
||||
getSceneFromFile(file: TFile): Promise<{
|
||||
elements: ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
}>;
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
@@ -101,10 +183,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
"excalidraw-url-prefix"?: string;
|
||||
"excalidraw-export-transparent"?: boolean;
|
||||
"excalidraw-export-dark"?: boolean;
|
||||
"excalidraw-export-svgpadding"?: number;
|
||||
"excalidraw-export-padding"?: number;
|
||||
"excalidraw-export-pngscale"?: number;
|
||||
"excalidraw-default-mode"?: "view" | "zen";
|
||||
"excalidraw-onload-script"?: string;
|
||||
"excalidraw-linkbutton-opacity"?: number;
|
||||
"excalidraw-autoexport"?: boolean;
|
||||
};
|
||||
plaintext?: string;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
*
|
||||
@@ -134,6 +220,16 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
*/
|
||||
wrapText(text: string, lineLen: number): string;
|
||||
private boxedElement;
|
||||
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
@@ -170,6 +266,11 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @returns
|
||||
*/
|
||||
addBlob(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
* Refresh the size of a text element to fit its contents
|
||||
* @param id - the id of the text element
|
||||
*/
|
||||
refreshTextElementSize(id: string): void;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
@@ -184,9 +285,11 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
wrapAt?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
textAlign?: string;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
|
||||
boxPadding?: number;
|
||||
boxStrokeColor?: string;
|
||||
textVerticalAlign?: "top" | "middle" | "bottom";
|
||||
}, id?: string): string;
|
||||
/**
|
||||
*
|
||||
@@ -213,7 +316,8 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @param imageFile
|
||||
* @returns
|
||||
*/
|
||||
addImage(topX: number, topY: number, imageFile: TFile): Promise<string>;
|
||||
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
anchor?: boolean): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
@@ -262,12 +366,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @returns
|
||||
*/
|
||||
isExcalidrawFile(f: TFile): boolean;
|
||||
targetView: ExcalidrawView;
|
||||
/**
|
||||
*
|
||||
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
|
||||
* if view is null or undefined, the function will first try setView("active"), then setView("first").
|
||||
* @param view
|
||||
* @returns
|
||||
* @returns targetView
|
||||
*/
|
||||
setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView;
|
||||
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
|
||||
/**
|
||||
*
|
||||
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
|
||||
@@ -311,6 +417,19 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @returns
|
||||
*/
|
||||
viewToggleFullScreen(forceViewMode?: boolean): void;
|
||||
setViewModeEnabled(enabled: boolean): void;
|
||||
/**
|
||||
* This function gives you a more hands on access to Excalidraw.
|
||||
* @param scene - The scene you want to load to Excalidraw
|
||||
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
|
||||
* @returns
|
||||
*/
|
||||
viewUpdateScene(scene: {
|
||||
elements?: ExcalidrawElement[];
|
||||
appState?: AppState;
|
||||
files?: BinaryFileData;
|
||||
commitToHistory?: boolean;
|
||||
}, restore?: boolean): void;
|
||||
/**
|
||||
* connect an object to the selected element in the view
|
||||
* @param objectA ID of the element
|
||||
@@ -325,6 +444,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
padding?: number;
|
||||
}): boolean;
|
||||
/**
|
||||
* zoom tarteView to fit elements provided as input
|
||||
* elements === [] will zoom to fit the entire scene
|
||||
* selectElements toggles whether the elements should be in a selected state at the end of the operation
|
||||
* @param selectElements
|
||||
* @param elements
|
||||
*/
|
||||
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
|
||||
/**
|
||||
* Adds elements from elementsDict to the current view
|
||||
* @param repositionToCursor default is false
|
||||
@@ -334,9 +461,10 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
|
||||
* position in the stack of elements in the view even if modified using EA
|
||||
* default is false, i.e. the new elements get to the bottom of the stack
|
||||
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
|
||||
* @returns
|
||||
*/
|
||||
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean): Promise<boolean>;
|
||||
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
|
||||
/**
|
||||
* Register instance of EA to use for hooks with TargetView
|
||||
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
|
||||
@@ -362,7 +490,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return true, it will stop the native excalidraw onLinkHover management flow.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
||||
*/
|
||||
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
||||
/**
|
||||
@@ -394,6 +522,49 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
y: number;
|
||||
};
|
||||
}) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
*/
|
||||
onPasteHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
payload: ClipboardData;
|
||||
event: ClipboardEvent;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
pointerPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}) => boolean;
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
*/
|
||||
onFileOpenHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
*/
|
||||
onFileCreateHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
*/
|
||||
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
|
||||
color: string) => void;
|
||||
/**
|
||||
* utility function to generate EmbeddedFilesLoader object
|
||||
* @param isDark
|
||||
@@ -431,6 +602,15 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @returns
|
||||
*/
|
||||
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
|
||||
/**
|
||||
* @param element
|
||||
* @param a
|
||||
* @param b
|
||||
* @param gap
|
||||
* @returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
* and the `element`, in ascending order of distance from `a`.
|
||||
*/
|
||||
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
|
||||
/**
|
||||
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
|
||||
* @param elements
|
||||
@@ -445,14 +625,12 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
*/
|
||||
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param a
|
||||
* @param b
|
||||
* @param gap
|
||||
* @returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
* and the `element`, in ascending order of distance from `a`.
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
/**
|
||||
* See OCR plugin for example on how to use scriptSettings
|
||||
* Set by the ScriptEngine
|
||||
@@ -472,9 +650,10 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
/**
|
||||
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
|
||||
* @param file
|
||||
* @param openState - if not provided {active: true} will be used
|
||||
* @returns
|
||||
*/
|
||||
openFileInNewOrAdjacentLeaf(file: TFile): WorkspaceLeaf;
|
||||
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
|
||||
/**
|
||||
* measure text size based on current style settings
|
||||
* @param text
|
||||
@@ -484,6 +663,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
/**
|
||||
* Returns the size of the image element at 100% (i.e. the original size)
|
||||
* @param imageElement an image element from the active scene on targetView
|
||||
*/
|
||||
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
/**
|
||||
* verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||
* recommended use:
|
||||
@@ -503,7 +690,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
selectElementsInView(elements: ExcalidrawElement[]): void;
|
||||
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
|
||||
/**
|
||||
* @returns an 8 character long random id
|
||||
*/
|
||||
@@ -518,25 +705,25 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
*/
|
||||
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
|
||||
/**
|
||||
*
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hexStringToRgb(color: string): number[];
|
||||
/**
|
||||
*
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHexString(color: number[]): string;
|
||||
/**
|
||||
*
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hslToRgb(color: number[]): number[];
|
||||
/**
|
||||
*
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
@@ -547,5 +734,47 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @returns
|
||||
*/
|
||||
colorNameToHex(color: string): string;
|
||||
}```
|
||||
|
||||
/**
|
||||
* https://github.com/lbragile/ColorMaster
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
getCM(color: TInput): ColorMaster;
|
||||
importSVG(svgString: string): boolean;
|
||||
}
|
||||
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
|
||||
export declare function destroyExcalidrawAutomate(): void;
|
||||
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
|
||||
w: number;
|
||||
h: number;
|
||||
baseline: number;
|
||||
};
|
||||
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
|
||||
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
|
||||
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
|
||||
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
|
||||
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
|
||||
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
|
||||
export declare const search: (view: ExcalidrawView) => Promise<void>;
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
|
||||
export declare const cloneElement: (el: ExcalidrawElement) => any;
|
||||
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;
|
||||
```
|
||||
@@ -21,7 +21,7 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
|
||||
|
||||
You can change the styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using that. There would be no point in setting all these parameters each time you add an element.
|
||||
|
||||
### Before we dive deeper, here are three a simple example [Templater](https://github.com/SilentVoid13/Templater) scripts
|
||||
### Before we dive deeper, here are three simple example [Templater](https://github.com/SilentVoid13/Templater) scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
|
||||
@@ -18,13 +18,21 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.20")) {
|
||||
}
|
||||
const ShadowGroupMarker = "ShadowCloneOf-";
|
||||
|
||||
const elements = ea.getViewSelectedElements().filter(
|
||||
el=>["ellipse", "rectangle", "diamond"].includes(el.type) ||
|
||||
el.groupIds.some(id => id.startsWith(ShadowGroupMarker)) ||
|
||||
(["line", "arrow"].includes(el.type) && el.roundness === null)
|
||||
);
|
||||
if(elements.length === 0) {
|
||||
new Notice ("Select ellipses, rectangles or diamonds");
|
||||
return;
|
||||
}
|
||||
|
||||
const PolyBool = ea.getPolybool();
|
||||
const PolyBool = ea.getPolyBool();
|
||||
const polyboolAction = await utils.suggester(["union (a + b)", "intersect (a && b)", "diffrence (a - b)", "reversed diffrence (b - a)", "xor"], [
|
||||
PolyBool.union, PolyBool.intersect, PolyBool.difference, PolyBool.differenceRev, PolyBool.xor
|
||||
], "What would you like todo with the object");
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
const shadowClones = elements.filter(element => element.groupIds.some(id => id.startsWith(ShadowGroupMarker)));
|
||||
shadowClones.forEach(shadowClone => {
|
||||
let parentId = shadowClone.groupIds
|
||||
|
||||
102
ea-scripts/Concatenate lines.md
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Connects two lines. Lines may be type of arrow or line. The resulting line will carry the style of the line higher in the drawing layers (bring to front the one you want to control the look and feel). Arrows are connected intelligently.
|
||||

|
||||
```js*/
|
||||
const lines = ea.getViewSelectedElements().filter(el=>el.type==="line" || el.type==="arrow");
|
||||
if(lines.length !== 2) {
|
||||
new Notice ("Select two lines or arrows");
|
||||
return;
|
||||
}
|
||||
|
||||
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||
const rotate = (point, element) => {
|
||||
const [x1, y1] = point;
|
||||
const x2 = element.x + element.width/2;
|
||||
const y2 = element.y - element.height/2;
|
||||
const angle = element.angle;
|
||||
return [
|
||||
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
|
||||
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
|
||||
];
|
||||
}
|
||||
|
||||
const points = lines.map(
|
||||
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
|
||||
);
|
||||
|
||||
const last = (p) => p[p.length-1];
|
||||
const first = (p) => p[0];
|
||||
const distance = (p1,p2) => Math.sqrt((p1[0]-p2[0])**2+(p1[1]-p2[1])**2);
|
||||
|
||||
const distances = [
|
||||
distance(first(points[0]),first(points[1])),
|
||||
distance(first(points[0]),last (points[1])),
|
||||
distance(last (points[0]),first(points[1])),
|
||||
distance(last (points[0]),last (points[1]))
|
||||
];
|
||||
|
||||
const connectDirection = distances.indexOf(Math.min(...distances));
|
||||
|
||||
let newPoints = [];
|
||||
switch(connectDirection) {
|
||||
case 0: //first-first
|
||||
newPoints = [...points[0].reverse(),...points[1].slice(1)];
|
||||
break;
|
||||
case 1: //first-last
|
||||
newPoints = [...points[0].reverse(),...points[1].reverse().slice(1)];
|
||||
break;
|
||||
case 2: //last-first
|
||||
newPoints = [...points[0],...points[1].slice(1)];
|
||||
break;
|
||||
case 3: //last-last
|
||||
newPoints = [...points[0],...points[1].reverse().slice(1)];
|
||||
break;
|
||||
}
|
||||
|
||||
["strokeColor", "backgrounColor", "fillStyle", "roundness", "roughness", "strokeWidth", "strokeStyle", "opacity"].forEach(prop=>{
|
||||
ea.style[prop] = lines[1][prop];
|
||||
})
|
||||
|
||||
ea.style.startArrowHead = null;
|
||||
ea.style.endArrowHead = null;
|
||||
|
||||
ea.copyViewElementsToEAforEditing(lines);
|
||||
ea.getElements().forEach(el=>{el.isDeleted = true});
|
||||
|
||||
const lineTypes = parseInt(lines.map(line => line.type === "line" ? '1' : '0').join(''),2);
|
||||
|
||||
switch (lineTypes) {
|
||||
case 0: //arrow - arrow
|
||||
ea.addArrow(
|
||||
newPoints,
|
||||
connectDirection === 0 //first-first
|
||||
? { startArrowHead: lines[0].endArrowhead, endArrowHead: lines[1].endArrowhead }
|
||||
: connectDirection === 1 //first-last
|
||||
? { startArrowHead: lines[0].endArrowhead, endArrowHead: lines[1].startArrowhead }
|
||||
: connectDirection === 2 //last-first
|
||||
? { startArrowHead: lines[0].startArrowhead, endArrowHead: lines[1].endArrowhead }
|
||||
//3: last-last
|
||||
: { startArrowHead: lines[0].startArrowhead, endArrowHead: lines[1].startArrowhead }
|
||||
);
|
||||
break;
|
||||
case 1: //arrow - line
|
||||
reverse = connectDirection === 0 || connectDirection === 1;
|
||||
ea.addArrow(newPoints,{
|
||||
startArrowHead: reverse ? lines[0].endArrowhead : lines[0].startArrowhead,
|
||||
endArrowHead: reverse ? lines[0].startArrowhead : lines[0].endArrowhead
|
||||
});
|
||||
break;
|
||||
case 2: //line - arrow
|
||||
reverse = connectDirection === 1 || connectDirection === 3;
|
||||
ea.addArrow(newPoints,{
|
||||
startArrowHead: reverse ? lines[1].endArrowhead : lines[1].startArrowhead,
|
||||
endArrowHead: reverse ? lines[1].startArrowhead : lines[1].endArrowhead
|
||||
});
|
||||
break;
|
||||
case 3: //line - line
|
||||
ea.addLine(newPoints);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
ea.addElementsToView();
|
||||
17
ea-scripts/Concatenate lines.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72.75819749055177 80.03703336574608" width="72.75819749055177" height="80.03703336574608">
|
||||
<!-- svg-source:excalidraw -->
|
||||
|
||||
<defs>
|
||||
<style class="style-fonts">
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://excalidraw.com/Virgil.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
||||
}
|
||||
</style>
|
||||
|
||||
</defs>
|
||||
<g stroke-linecap="round"><g transform="translate(4 4) rotate(0 12.71901889991409 17.183109917454658)"><path d="M0 0 C0 7.02, 0 14.05, 0 34.37 M0 34.37 C7.62 34.37, 15.24 34.37, 25.44 34.37" stroke="black" stroke-width="4.5" fill="none" stroke-dasharray="1.5 10"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(51.379765518092086 61.93633577499986) rotate(0 5.684341886080802e-14 7.050348795373111)"><path d="M0 0 C0 4.06, 0 8.11, 0 14.1" stroke="black" stroke-width="4.5" fill="none" stroke-dasharray="1.5 10"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(34.0013341989918 20.987787610339183) rotate(0 17.378431645779926 17.378431645779983)"><path d="M34.76 17.38 C34.76 18.38, 34.67 19.41, 34.49 20.4 C34.32 21.39, 34.05 22.38, 33.71 23.32 C33.36 24.27, 32.93 25.2, 32.43 26.07 C31.93 26.94, 31.34 27.78, 30.69 28.55 C30.04 29.32, 29.32 30.04, 28.55 30.69 C27.78 31.34, 26.94 31.93, 26.07 32.43 C25.2 32.93, 24.27 33.36, 23.32 33.71 C22.38 34.05, 21.39 34.32, 20.4 34.49 C19.41 34.67, 18.38 34.76, 17.38 34.76 C16.37 34.76, 15.35 34.67, 14.36 34.49 C13.37 34.32, 12.38 34.05, 11.43 33.71 C10.49 33.36, 9.56 32.93, 8.69 32.43 C7.82 31.93, 6.98 31.34, 6.21 30.69 C5.44 30.04, 4.71 29.32, 4.07 28.55 C3.42 27.78, 2.83 26.94, 2.33 26.07 C1.83 25.2, 1.39 24.27, 1.05 23.32 C0.7 22.38, 0.44 21.39, 0.26 20.4 C0.09 19.41, 0 18.38, 0 17.38 C0 16.37, 0.09 15.35, 0.26 14.36 C0.44 13.37, 0.7 12.38, 1.05 11.43 C1.39 10.49, 1.83 9.56, 2.33 8.69 C2.83 7.82, 3.42 6.98, 4.07 6.21 C4.71 5.44, 5.44 4.71, 6.21 4.07 C6.98 3.42, 7.82 2.83, 8.69 2.33 C9.56 1.83, 10.49 1.39, 11.43 1.05 C12.38 0.7, 13.37 0.44, 14.36 0.26 C15.35 0.09, 16.37 0, 17.38 0 C18.38 0, 19.41 0.09, 20.4 0.26 C21.39 0.44, 22.38 0.7, 23.32 1.05 C24.27 1.39, 25.2 1.83, 26.07 2.33 C26.94 2.83, 27.78 3.42, 28.55 4.07 C29.32 4.71, 30.04 5.44, 30.69 6.21 C31.34 6.98, 31.93 7.82, 32.43 8.69 C32.93 9.56, 33.36 10.49, 33.71 11.43 C34.05 12.38, 34.32 13.37, 34.49 14.36 C34.67 15.35, 34.71 16.88, 34.76 17.38 C34.8 17.88, 34.8 16.88, 34.76 17.38" stroke="black" stroke-width="4" fill="none"></path></g><g stroke-linecap="round"><g transform="translate(41.72257566145686 38.36621939788711) rotate(0 9.65718949485347 0)"><path d="M0 0 C4.11 0, 8.22 0, 19.31 0 M0 0 C6.95 0, 13.9 0, 19.31 0" stroke="black" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(41.72257587602678 38.36622004108449) rotate(89.99999999999994 9.65718949485347 0)"><path d="M0 0 C5.31 0, 10.62 0, 19.31 0 M0 0 C4.56 0, 9.13 0, 19.31 0" stroke="black" stroke-width="4" fill="none"></path></g></g><mask></mask></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
673
ea-scripts/ExcaliAI.md
Normal file
@@ -0,0 +1,673 @@
|
||||
/*
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||

|
||||
```js*/
|
||||
let dirty=false;
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.12")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
const outputTypes = {
|
||||
"html": {
|
||||
instruction: "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
|
||||
blocktype: "html"
|
||||
},
|
||||
"mermaid": {
|
||||
instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
|
||||
blocktype: "mermaid"
|
||||
},
|
||||
"svg": {
|
||||
instruction: "Return a single message containing only the SVG code in an html codeblock.",
|
||||
blocktype: "svg"
|
||||
},
|
||||
"image-gen": {
|
||||
instruction: "Return a single message with the generated image prompt in a codeblock",
|
||||
blocktype: "image"
|
||||
},
|
||||
"image-edit": {
|
||||
instruction: "",
|
||||
blocktype: "image"
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompts = {
|
||||
"Challenge my thinking": {
|
||||
prompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
|
||||
type: "mermaid",
|
||||
help: "Translate your image and optional text prompt into a Mermaid mindmap. If there are conversion errors, edit the Mermaid script under 'More Tools'."
|
||||
},
|
||||
"Convert sketch to shapes": {
|
||||
prompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
|
||||
type: "svg",
|
||||
help: "Convert selected scribbles into shapes; works better with fewer shapes. Experimental and may not produce good drawings."
|
||||
},
|
||||
"Create a simple Excalidraw icon": {
|
||||
prompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
|
||||
type: "svg",
|
||||
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
|
||||
},
|
||||
"Edit an image": {
|
||||
prompt: null,
|
||||
type: "image-edit",
|
||||
help: "Image elements will be used as the Image. Shapes on top of the image will be the Mask. Use the prompt to instruct Dall-e about the changes. Dall-e-2 model will be used."
|
||||
},
|
||||
"Generate an image from image and prompt": {
|
||||
prompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
|
||||
type: "image-gen",
|
||||
help: "Generate an image based on the drawing and prompt using ChatGPT-Vision and Dall-e. Provide a contextual text-prompt for accurate interpretation."
|
||||
},
|
||||
"Generate an image from prompt": {
|
||||
prompt: null,
|
||||
type: "image-gen",
|
||||
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
|
||||
},
|
||||
"Generate an image to illustrate a quote": {
|
||||
prompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
|
||||
type: "image-gen",
|
||||
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
|
||||
},
|
||||
"Visual brainstorm": {
|
||||
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
|
||||
type: "image-gen",
|
||||
help: "Use ChatGPT Visions and Dall-e to create an image based on your text prompt and image to spark new ideas."
|
||||
},
|
||||
"Wireframe to code": {
|
||||
prompt: `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
|
||||
type: "html",
|
||||
help: "Use GPT Visions to interpret the wireframe and generate a web application. YOu may copy the resulting code from the active embeddable's top left menu."
|
||||
},
|
||||
}
|
||||
|
||||
const IMAGE_WARNING = "The generated image is linked through a temporary OpenAI URL and will be removed in approximately 30 minutes. To save it permanently, choose 'Save image from URL to local file' from the Obsidian Command Palette."
|
||||
// --------------------------------------
|
||||
// Initialize values and settings
|
||||
// --------------------------------------
|
||||
let settings = ea.getScriptSettings();
|
||||
|
||||
if(!settings["Agent's Task"]) {
|
||||
settings = {
|
||||
"Agent's Task": "Wireframe to code",
|
||||
"User Prompt": "",
|
||||
};
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
|
||||
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
|
||||
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
|
||||
return;
|
||||
}
|
||||
|
||||
let userPrompt = settings["User Prompt"] ?? "";
|
||||
let agentTask = settings["Agent's Task"];
|
||||
let imageSize = settings["Image Size"]??"1024x1024";
|
||||
|
||||
if(!systemPrompts.hasOwnProperty(agentTask)) {
|
||||
agentTask = Object.keys(systemPrompts)[0];
|
||||
}
|
||||
let imageModel, valideSizes;
|
||||
|
||||
const setImageModelAndSizes = () => {
|
||||
imageModel = systemPrompts[agentTask].type === "image-edit"
|
||||
? "dall-e-2"
|
||||
: ea.plugin.settings.openAIDefaultImageGenerationModel;
|
||||
validSizes = imageModel === "dall-e-2"
|
||||
? [`256x256`, `512x512`, `1024x1024`]
|
||||
: (imageModel === "dall-e-3"
|
||||
? [`1024x1024`, `1792x1024`, `1024x1792`]
|
||||
: [`1024x1024`])
|
||||
if(!validSizes.includes(imageSize)) {
|
||||
imageSize = "1024x1024";
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
setImageModelAndSizes();
|
||||
|
||||
// --------------------------------------
|
||||
// Generate Image Blob From Selected Excalidraw Elements
|
||||
// --------------------------------------
|
||||
const calculateImageScale = (elements) => {
|
||||
const bb = ea.getBoundingBox(elements);
|
||||
const size = (bb.width*bb.height);
|
||||
const minRatio = Math.sqrt(360000/size);
|
||||
const maxRatio = Math.sqrt(size/16000000);
|
||||
return minRatio > 1
|
||||
? minRatio
|
||||
: (
|
||||
maxRatio > 1
|
||||
? 1/maxRatio
|
||||
: 1
|
||||
);
|
||||
}
|
||||
|
||||
const createMask = async (dataURL) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// If opaque (alpha > 0), make it transparent
|
||||
if (data[i + 3] > 0) {
|
||||
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
||||
} else if (data[i + 3] === 0) {
|
||||
// If fully transparent, make it red
|
||||
data[i] = 255; // Red
|
||||
data[i + 1] = 0; // Green
|
||||
data[i + 2] = 0; // Blue
|
||||
data[i + 3] = 255; // make it opaque
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
const maskDataURL = canvas.toDataURL();
|
||||
|
||||
resolve(maskDataURL);
|
||||
};
|
||||
|
||||
img.onerror = error => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
img.src = dataURL;
|
||||
});
|
||||
}
|
||||
|
||||
//https://platform.openai.com/docs/api-reference/images/createEdit
|
||||
//dall-e-2 image edit only works on square images
|
||||
//if targetDalleImageEdit === true then the image and the mask will be returned in two separate dataURLs
|
||||
let squareBB;
|
||||
|
||||
const generateCanvasDataURL = async (view, targetDalleImageEdit=false) => {
|
||||
let PADDING = 5;
|
||||
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
|
||||
const viewElements = ea.getViewSelectedElements();
|
||||
if(viewElements.length === 0) {
|
||||
return {imageDataURL: null, maskDataURL: null} ;
|
||||
}
|
||||
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
|
||||
|
||||
let maskDataURL;
|
||||
const loader = ea.getEmbeddedFilesLoader(false);
|
||||
let scale = calculateImageScale(ea.getElements());
|
||||
const bb = ea.getBoundingBox(viewElements);
|
||||
if(ea.getElements()
|
||||
.filter(el=>el.type==="image")
|
||||
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
|
||||
) { PADDING = 0; }
|
||||
|
||||
let exportSettings = {withBackground: true, withTheme: true};
|
||||
|
||||
if(targetDalleImageEdit) {
|
||||
PADDING = 0;
|
||||
const strokeColor = ea.style.strokeColor;
|
||||
const backgroundColor = ea.style.backgroundColor;
|
||||
ea.style.backgroundColor = "transparent";
|
||||
ea.style.strokeColor = "transparent";
|
||||
let rectID;
|
||||
if(bb.height > bb.width) {
|
||||
rectID = ea.addRect(bb.topX-(bb.height-bb.width)/2, bb.topY,bb.height, bb.height);
|
||||
}
|
||||
if(bb.width > bb.height) {
|
||||
rectID = ea.addRect(bb.topX, bb.topY-(bb.width-bb.height)/2,bb.width, bb.width);
|
||||
}
|
||||
if(bb.height === bb.width) {
|
||||
rectID = ea.addRect(bb.topX, bb.topY, bb.width, bb.height);
|
||||
}
|
||||
const rect = ea.getElement(rectID);
|
||||
squareBB = {topX: rect.x-PADDING, topY: rect.y-PADDING, width: rect.width + 2*PADDING, height: rect.height + 2*PADDING};
|
||||
ea.style.strokeColor = strokeColor;
|
||||
ea.style.backgroundColor = backgroundColor;
|
||||
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
|
||||
|
||||
dalleWidth = parseInt(imageSize.split("x")[0]);
|
||||
scale = dalleWidth/squareBB.width;
|
||||
exportSettings = {withBackground: false, withTheme: true};
|
||||
maskDataURL= await ea.createPNGBase64(
|
||||
null, scale, exportSettings, loader, "light", PADDING
|
||||
);
|
||||
maskDataURL = await createMask(maskDataURL)
|
||||
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
|
||||
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
|
||||
}
|
||||
|
||||
const imageDataURL = await ea.createPNGBase64(
|
||||
null, scale, exportSettings, loader, "light", PADDING
|
||||
);
|
||||
ea.clear();
|
||||
return {imageDataURL, maskDataURL};
|
||||
}
|
||||
|
||||
let {imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[agentTask].type === "image-edit");
|
||||
|
||||
// --------------------------------------
|
||||
// Support functions - embeddable spinner and error
|
||||
// --------------------------------------
|
||||
const spinner = await ea.convertStringToDataURL(`
|
||||
<html><head><style>
|
||||
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
|
||||
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
|
||||
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
|
||||
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
|
||||
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
|
||||
@keyframes rotate {100% {transform: rotate(360deg);}}
|
||||
@keyframes dash {
|
||||
0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;}
|
||||
50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;}
|
||||
100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}
|
||||
}
|
||||
</style></head><body>
|
||||
<div class="Spinner">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Generating...</div>
|
||||
</body></html>`);
|
||||
|
||||
const errorMessage = async (spinnerID, message) => {
|
||||
const error = "Something went wrong! Check developer console for more.";
|
||||
const details = message ? `<p>${message}</p>` : "";
|
||||
const errorDataURL = await ea.convertStringToDataURL(`
|
||||
<html><head><style>
|
||||
html, body {height: 100%;}
|
||||
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
|
||||
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
|
||||
</style></head><body>
|
||||
<h1>Error!</h1>
|
||||
<h3>${error}</h3>${details}
|
||||
</body></html>`);
|
||||
new Notice (error);
|
||||
ea.getElement(spinnerID).link = errorDataURL;
|
||||
ea.addElementsToView(false,true);
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// Utility to write Mermaid to dialog
|
||||
// --------------------------------------
|
||||
const EDITOR_LS_KEYS = {
|
||||
OAI_API_KEY: "excalidraw-oai-api-key",
|
||||
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
|
||||
PUBLISH_LIBRARY: "publish-library-data",
|
||||
};
|
||||
|
||||
const setMermaidDataToStorage = (mermaidDefinition) => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
|
||||
JSON.stringify(mermaidDefinition)
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`localStorage.setItem error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// --------------------------------------
|
||||
// Submit Prompt
|
||||
// --------------------------------------
|
||||
const generateImage = async(text, spinnerID, bb) => {
|
||||
const requestObject = {
|
||||
text,
|
||||
imageGenerationProperties: {
|
||||
size: imageSize,
|
||||
//quality: "standard", //not supported by dall-e-2
|
||||
n:1,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await ea.postOpenAI(requestObject);
|
||||
console.log({result, json:result?.json});
|
||||
|
||||
if(!result?.json?.data?.[0]?.url) {
|
||||
await errorMessage(spinnerID, result?.json?.error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ea.getElement(spinnerID)
|
||||
spinner.isDeleted = true;
|
||||
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
|
||||
const imageEl = ea.getElement(imageID);
|
||||
const revisedPrompt = result.json.data[0].revised_prompt;
|
||||
if(revisedPrompt) {
|
||||
ea.style.fontSize = 16;
|
||||
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
|
||||
width: imageEl.width-30,
|
||||
textAlign: "center",
|
||||
textVerticalAlign: "top",
|
||||
box: true,
|
||||
})
|
||||
ea.getElement(rectID).strokeColor = "transparent";
|
||||
ea.getElement(rectID).backgroundColor = "transparent";
|
||||
ea.addToGroup(ea.getElements().filter(el=>el.id !== spinnerID).map(el=>el.id));
|
||||
}
|
||||
|
||||
await ea.addElementsToView(false, true, true);
|
||||
ea.getExcalidrawAPI().setToast({
|
||||
message: IMAGE_WARNING,
|
||||
duration: 15000,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
const run = async (text) => {
|
||||
if(!text && !imageDataURL) {
|
||||
new Notice("No prompt, aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
const systemPrompt = systemPrompts[agentTask];
|
||||
const outputType = outputTypes[systemPrompt.type];
|
||||
const isImageGenRequest = outputType.blocktype === "image";
|
||||
const isImageEditRequest = systemPrompt.type === "image-edit";
|
||||
|
||||
if(isImageEditRequest) {
|
||||
if(!text) {
|
||||
new Notice("You must provide a text prompt with instructions for how the image should be modified");
|
||||
return;
|
||||
}
|
||||
if(!imageDataURL || !maskDataURL) {
|
||||
new Notice("You must provide an image and a mask");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//place spinner next to selected elements
|
||||
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
|
||||
const spinnerID = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
|
||||
|
||||
//this block is in an async call using the isEACompleted flag because otherwise during debug Obsidian
|
||||
//goes black (not freezes, but does not get a new frame for some reason)
|
||||
//palcing this in an async call solves this issue
|
||||
//If you know why this is happening and can offer a better solution, please reach out to @zsviczian
|
||||
let isEACompleted = false;
|
||||
setTimeout(async()=>{
|
||||
await ea.addElementsToView(false,true);
|
||||
ea.clear();
|
||||
const embeddable = ea.getViewElements().filter(el=>el.id===spinnerID);
|
||||
ea.copyViewElementsToEAforEditing(embeddable);
|
||||
const els = ea.getViewSelectedElements();
|
||||
ea.viewZoomToElements(false, els.concat(embeddable));
|
||||
isEACompleted = true;
|
||||
});
|
||||
|
||||
if(isImageGenRequest && !systemPrompt.prompt && !isImageEditRequest) {
|
||||
generateImage(text,spinnerID,bb);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestObject = isImageEditRequest
|
||||
? {
|
||||
...imageDataURL ? {image: imageDataURL} : {},
|
||||
...(text && text.trim() !== "") ? {text} : {},
|
||||
imageGenerationProperties: {
|
||||
size: imageSize,
|
||||
//quality: "standard", //not supported by dall-e-2
|
||||
n:1,
|
||||
mask: maskDataURL,
|
||||
},
|
||||
}
|
||||
: {
|
||||
...imageDataURL ? {image: imageDataURL} : {},
|
||||
...(text && text.trim() !== "") ? {text} : {},
|
||||
systemPrompt: systemPrompt.prompt,
|
||||
instruction: outputType.instruction,
|
||||
}
|
||||
|
||||
//Get result from GPT
|
||||
const result = await ea.postOpenAI(requestObject);
|
||||
console.log({result, json:result?.json});
|
||||
|
||||
//checking that EA has completed. Because the postOpenAI call is an async await
|
||||
//I don't expect EA not to be completed by now. However the devil never sleeps.
|
||||
//This (the insomnia of the Devil) is why I have a watchdog here as well
|
||||
let counter = 0
|
||||
while(!isEACompleted && counter++<10) sleep(50);
|
||||
if(!isEACompleted) {
|
||||
await errorMessage(spinnerID, "Unexpected issue with ExcalidrawAutomate");
|
||||
return;
|
||||
}
|
||||
|
||||
if(isImageEditRequest) {
|
||||
if(!result?.json?.data?.[0]?.url) {
|
||||
await errorMessage(spinnerID, result?.json?.error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ea.getElement(spinnerID)
|
||||
spinner.isDeleted = true;
|
||||
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
|
||||
await ea.addElementsToView(false, true, true);
|
||||
ea.getExcalidrawAPI().setToast({
|
||||
message: IMAGE_WARNING,
|
||||
duration: 15000,
|
||||
closable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!result?.json?.hasOwnProperty("choices")) {
|
||||
await errorMessage(spinnerID, result?.json?.error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
//exctract codeblock and display result
|
||||
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
|
||||
|
||||
if(!content) {
|
||||
await errorMessage(spinnerID);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isImageGenRequest) {
|
||||
generateImage(content,spinnerID,bb);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(outputType.blocktype) {
|
||||
case "html":
|
||||
ea.getElement(spinnerID).link = await ea.convertStringToDataURL(content);
|
||||
ea.addElementsToView(false,true);
|
||||
break;
|
||||
case "svg":
|
||||
ea.getElement(spinnerID).isDeleted = true;
|
||||
ea.importSVG(content);
|
||||
ea.addToGroup(ea.getElements().map(el=>el.id));
|
||||
if(ea.getViewSelectedElements().length>0) {
|
||||
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY};
|
||||
}
|
||||
ea.addElementsToView(true, false);
|
||||
break;
|
||||
case "mermaid":
|
||||
if(content.startsWith("mermaid")) {
|
||||
content = content.replace(/^mermaid/,"").trim();
|
||||
}
|
||||
|
||||
try {
|
||||
result = await ea.addMermaid(content);
|
||||
if(typeof result === "string") {
|
||||
await errorMessage(spinnerID, "Open [More Tools / Mermaid to Excalidraw] to manually fix the received mermaid script<br><br>" + result);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
ea.addText(0,0,content);
|
||||
}
|
||||
ea.getElement(spinnerID).isDeleted = true;
|
||||
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height};
|
||||
await ea.addElementsToView(true, false);
|
||||
setMermaidDataToStorage(content);
|
||||
new Notice("Open More Tools/Mermaid to Excalidraw in the top tools menu to edit the generated diagram",8000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// User Interface
|
||||
// --------------------------------------
|
||||
let previewDiv;
|
||||
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-edit";
|
||||
const addPreviewImage = () => {
|
||||
if(!previewDiv) return;
|
||||
previewDiv.empty();
|
||||
previewDiv.createEl("img",{
|
||||
attr: {
|
||||
style: `max-width: 100%;max-height: 30vh;`,
|
||||
src: imageDataURL,
|
||||
}
|
||||
});
|
||||
if(maskDataURL) {
|
||||
previewDiv.createEl("img",{
|
||||
attr: {
|
||||
style: `max-width: 100%;max-height: 30vh;`,
|
||||
src: maskDataURL,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const configModal = new ea.obsidian.Modal(app);
|
||||
configModal.modalEl.style.width="100%";
|
||||
configModal.modalEl.style.maxWidth="1000px";
|
||||
|
||||
configModal.onOpen = async () => {
|
||||
const contentEl = configModal.contentEl;
|
||||
contentEl.createEl("h1", {text: "ExcaliAI"});
|
||||
|
||||
let systemPromptTextArea, systemPromptDiv, imageSizeSetting, imageSizeSettingDropdown, helpEl;
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("What would you like to do?")
|
||||
.addDropdown(dropdown=>{
|
||||
Object.keys(systemPrompts).forEach(key=>dropdown.addOption(key,key));
|
||||
dropdown
|
||||
.setValue(agentTask)
|
||||
.onChange(async (value) => {
|
||||
dirty = true;
|
||||
const prevTask = agentTask;
|
||||
agentTask = value;
|
||||
if(
|
||||
(systemPrompts[prevTask].type === "image-edit" && systemPrompts[value].type !== "image-edit") ||
|
||||
(systemPrompts[prevTask].type !== "image-edit" && systemPrompts[value].type === "image-edit")
|
||||
) {
|
||||
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[value].type === "image-edit"));
|
||||
addPreviewImage();
|
||||
setImageModelAndSizes();
|
||||
while (imageSizeSettingDropdown.selectEl.options.length > 0) { imageSizeSettingDropdown.selectEl.remove(0); }
|
||||
validSizes.forEach(size=>imageSizeSettingDropdown.addOption(size,size));
|
||||
imageSizeSettingDropdown.setValue(imageSize);
|
||||
}
|
||||
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
|
||||
const prompt = systemPrompts[value].prompt;
|
||||
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[value].help;
|
||||
if(prompt) {
|
||||
systemPromptDiv.style.display = "";
|
||||
systemPromptTextArea.setValue(systemPrompts[value].prompt);
|
||||
} else {
|
||||
systemPromptDiv.style.display = "none";
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
helpEl = contentEl.createEl("p");
|
||||
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[agentTask].help;
|
||||
|
||||
systemPromptDiv = contentEl.createDiv();
|
||||
systemPromptDiv.createEl("h4", {text: "Customize System Prompt"});
|
||||
systemPromptDiv.createEl("span", {text: "Unless you know what you are doing I do not recommend changing the system prompt"})
|
||||
const systemPromptSetting = new ea.obsidian.Setting(systemPromptDiv)
|
||||
.addTextArea(text => {
|
||||
systemPromptTextArea = text;
|
||||
const prompt = systemPrompts[agentTask].prompt;
|
||||
text.inputEl.style.minHeight = "10em";
|
||||
text.inputEl.style.width = "100%";
|
||||
text.setValue(prompt);
|
||||
text.onChange(value => {
|
||||
systemPrompts[value].prompt = value;
|
||||
});
|
||||
if(!prompt) systemPromptDiv.style.display = "none";
|
||||
})
|
||||
systemPromptSetting.nameEl.style.display = "none";
|
||||
systemPromptSetting.descEl.style.display = "none";
|
||||
systemPromptSetting.infoEl.style.display = "none";
|
||||
|
||||
contentEl.createEl("h4", {text: "User Prompt"});
|
||||
const userPromptSetting = new ea.obsidian.Setting(contentEl)
|
||||
.addTextArea(text => {
|
||||
text.inputEl.style.minHeight = "10em";
|
||||
text.inputEl.style.width = "100%";
|
||||
text.setValue(userPrompt);
|
||||
text.onChange(value => {
|
||||
userPrompt = value;
|
||||
dirty = true;
|
||||
})
|
||||
})
|
||||
userPromptSetting.nameEl.style.display = "none";
|
||||
userPromptSetting.descEl.style.display = "none";
|
||||
userPromptSetting.infoEl.style.display = "none";
|
||||
|
||||
imageSizeSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName("Select image size")
|
||||
.setDesc(fragWithHTML("<mark>⚠️ Important ⚠️</mark>: " + IMAGE_WARNING))
|
||||
.addDropdown(dropdown=>{
|
||||
validSizes.forEach(size=>dropdown.addOption(size,size));
|
||||
imageSizeSettingDropdown = dropdown;
|
||||
dropdown
|
||||
.setValue(imageSize)
|
||||
.onChange(async (value) => {
|
||||
dirty = true;
|
||||
imageSize = value;
|
||||
if(systemPrompts[agentTask].type === "image-edit") {
|
||||
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, true));
|
||||
addPreviewImage();
|
||||
}
|
||||
});
|
||||
})
|
||||
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
|
||||
|
||||
if(imageDataURL) {
|
||||
previewDiv = contentEl.createDiv({
|
||||
attr: {
|
||||
style: "text-align: center;",
|
||||
}
|
||||
});
|
||||
addPreviewImage();
|
||||
} else {
|
||||
contentEl.createEl("h4", {text: "No elements are selected from your canvas"});
|
||||
contentEl.createEl("span", {text: "Because there are no Excalidraw elements selected on the canvas, only the text prompt will be sent to OpenAI."});
|
||||
}
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText("Run")
|
||||
.onClick((event)=>{
|
||||
run(userPrompt); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
|
||||
configModal.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
configModal.onClose = () => {
|
||||
if(dirty) {
|
||||
settings["User Prompt"] = userPrompt;
|
||||
settings["Agent's Task"] = agentTask;
|
||||
settings["Image Size"] = imageSize;
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
configModal.open();
|
||||
1
ea-scripts/ExcaliAI.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>
|
||||
|
After Width: | Height: | Size: 694 B |
673
ea-scripts/GPT-Draw-a-UI.md
Normal file
@@ -0,0 +1,673 @@
|
||||
/*
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||

|
||||
```js*/
|
||||
let dirty=false;
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.12")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
const outputTypes = {
|
||||
"html": {
|
||||
instruction: "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
|
||||
blocktype: "html"
|
||||
},
|
||||
"mermaid": {
|
||||
instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
|
||||
blocktype: "mermaid"
|
||||
},
|
||||
"svg": {
|
||||
instruction: "Return a single message containing only the SVG code in an html codeblock.",
|
||||
blocktype: "svg"
|
||||
},
|
||||
"image-gen": {
|
||||
instruction: "Return a single message with the generated image prompt in a codeblock",
|
||||
blocktype: "image"
|
||||
},
|
||||
"image-edit": {
|
||||
instruction: "",
|
||||
blocktype: "image"
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompts = {
|
||||
"Challenge my thinking": {
|
||||
prompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
|
||||
type: "mermaid",
|
||||
help: "Translate your image and optional text prompt into a Mermaid mindmap. If there are conversion errors, edit the Mermaid script under 'More Tools'."
|
||||
},
|
||||
"Convert sketch to shapes": {
|
||||
prompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
|
||||
type: "svg",
|
||||
help: "Convert selected scribbles into shapes; works better with fewer shapes. Experimental and may not produce good drawings."
|
||||
},
|
||||
"Create a simple Excalidraw icon": {
|
||||
prompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
|
||||
type: "svg",
|
||||
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
|
||||
},
|
||||
"Edit an image": {
|
||||
prompt: null,
|
||||
type: "image-edit",
|
||||
help: "Image elements will be used as the Image. Shapes on top of the image will be the Mask. Use the prompt to instruct Dall-e about the changes. Dall-e-2 model will be used."
|
||||
},
|
||||
"Generate an image from image and prompt": {
|
||||
prompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
|
||||
type: "image-gen",
|
||||
help: "Generate an image based on the drawing and prompt using ChatGPT-Vision and Dall-e. Provide a contextual text-prompt for accurate interpretation."
|
||||
},
|
||||
"Generate an image from prompt": {
|
||||
prompt: null,
|
||||
type: "image-gen",
|
||||
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
|
||||
},
|
||||
"Generate an image to illustrate a quote": {
|
||||
prompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
|
||||
type: "image-gen",
|
||||
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
|
||||
},
|
||||
"Visual brainstorm": {
|
||||
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
|
||||
type: "image-gen",
|
||||
help: "Use ChatGPT Visions and Dall-e to create an image based on your text prompt and image to spark new ideas."
|
||||
},
|
||||
"Wireframe to code": {
|
||||
prompt: `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
|
||||
type: "html",
|
||||
help: "Use GPT Visions to interpret the wireframe and generate a web application. YOu may copy the resulting code from the active embeddable's top left menu."
|
||||
},
|
||||
}
|
||||
|
||||
const IMAGE_WARNING = "The generated image is linked through a temporary OpenAI URL and will be removed in approximately 30 minutes. To save it permanently, choose 'Save image from URL to local file' from the Obsidian Command Palette."
|
||||
// --------------------------------------
|
||||
// Initialize values and settings
|
||||
// --------------------------------------
|
||||
let settings = ea.getScriptSettings();
|
||||
|
||||
if(!settings["Agent's Task"]) {
|
||||
settings = {
|
||||
"Agent's Task": "Wireframe to code",
|
||||
"User Prompt": "",
|
||||
};
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
|
||||
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
|
||||
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
|
||||
return;
|
||||
}
|
||||
|
||||
let userPrompt = settings["User Prompt"] ?? "";
|
||||
let agentTask = settings["Agent's Task"];
|
||||
let imageSize = settings["Image Size"]??"1024x1024";
|
||||
|
||||
if(!systemPrompts.hasOwnProperty(agentTask)) {
|
||||
agentTask = Object.keys(systemPrompts)[0];
|
||||
}
|
||||
let imageModel, valideSizes;
|
||||
|
||||
const setImageModelAndSizes = () => {
|
||||
imageModel = systemPrompts[agentTask].type === "image-edit"
|
||||
? "dall-e-2"
|
||||
: ea.plugin.settings.openAIDefaultImageGenerationModel;
|
||||
validSizes = imageModel === "dall-e-2"
|
||||
? [`256x256`, `512x512`, `1024x1024`]
|
||||
: (imageModel === "dall-e-3"
|
||||
? [`1024x1024`, `1792x1024`, `1024x1792`]
|
||||
: [`1024x1024`])
|
||||
if(!validSizes.includes(imageSize)) {
|
||||
imageSize = "1024x1024";
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
setImageModelAndSizes();
|
||||
|
||||
// --------------------------------------
|
||||
// Generate Image Blob From Selected Excalidraw Elements
|
||||
// --------------------------------------
|
||||
const calculateImageScale = (elements) => {
|
||||
const bb = ea.getBoundingBox(elements);
|
||||
const size = (bb.width*bb.height);
|
||||
const minRatio = Math.sqrt(360000/size);
|
||||
const maxRatio = Math.sqrt(size/16000000);
|
||||
return minRatio > 1
|
||||
? minRatio
|
||||
: (
|
||||
maxRatio > 1
|
||||
? 1/maxRatio
|
||||
: 1
|
||||
);
|
||||
}
|
||||
|
||||
const createMask = async (dataURL) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// If opaque (alpha > 0), make it transparent
|
||||
if (data[i + 3] > 0) {
|
||||
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
||||
} else if (data[i + 3] === 0) {
|
||||
// If fully transparent, make it red
|
||||
data[i] = 255; // Red
|
||||
data[i + 1] = 0; // Green
|
||||
data[i + 2] = 0; // Blue
|
||||
data[i + 3] = 255; // make it opaque
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
const maskDataURL = canvas.toDataURL();
|
||||
|
||||
resolve(maskDataURL);
|
||||
};
|
||||
|
||||
img.onerror = error => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
img.src = dataURL;
|
||||
});
|
||||
}
|
||||
|
||||
//https://platform.openai.com/docs/api-reference/images/createEdit
|
||||
//dall-e-2 image edit only works on square images
|
||||
//if targetDalleImageEdit === true then the image and the mask will be returned in two separate dataURLs
|
||||
let squareBB;
|
||||
|
||||
const generateCanvasDataURL = async (view, targetDalleImageEdit=false) => {
|
||||
let PADDING = 5;
|
||||
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
|
||||
const viewElements = ea.getViewSelectedElements();
|
||||
if(viewElements.length === 0) {
|
||||
return {imageDataURL: null, maskDataURL: null} ;
|
||||
}
|
||||
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
|
||||
|
||||
let maskDataURL;
|
||||
const loader = ea.getEmbeddedFilesLoader(false);
|
||||
let scale = calculateImageScale(ea.getElements());
|
||||
const bb = ea.getBoundingBox(viewElements);
|
||||
if(ea.getElements()
|
||||
.filter(el=>el.type==="image")
|
||||
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
|
||||
) { PADDING = 0; }
|
||||
|
||||
let exportSettings = {withBackground: true, withTheme: true};
|
||||
|
||||
if(targetDalleImageEdit) {
|
||||
PADDING = 0;
|
||||
const strokeColor = ea.style.strokeColor;
|
||||
const backgroundColor = ea.style.backgroundColor;
|
||||
ea.style.backgroundColor = "transparent";
|
||||
ea.style.strokeColor = "transparent";
|
||||
let rectID;
|
||||
if(bb.height > bb.width) {
|
||||
rectID = ea.addRect(bb.topX-(bb.height-bb.width)/2, bb.topY,bb.height, bb.height);
|
||||
}
|
||||
if(bb.width > bb.height) {
|
||||
rectID = ea.addRect(bb.topX, bb.topY-(bb.width-bb.height)/2,bb.width, bb.width);
|
||||
}
|
||||
if(bb.height === bb.width) {
|
||||
rectID = ea.addRect(bb.topX, bb.topY, bb.width, bb.height);
|
||||
}
|
||||
const rect = ea.getElement(rectID);
|
||||
squareBB = {topX: rect.x-PADDING, topY: rect.y-PADDING, width: rect.width + 2*PADDING, height: rect.height + 2*PADDING};
|
||||
ea.style.strokeColor = strokeColor;
|
||||
ea.style.backgroundColor = backgroundColor;
|
||||
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
|
||||
|
||||
dalleWidth = parseInt(imageSize.split("x")[0]);
|
||||
scale = dalleWidth/squareBB.width;
|
||||
exportSettings = {withBackground: false, withTheme: true};
|
||||
maskDataURL= await ea.createPNGBase64(
|
||||
null, scale, exportSettings, loader, "light", PADDING
|
||||
);
|
||||
maskDataURL = await createMask(maskDataURL)
|
||||
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
|
||||
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
|
||||
}
|
||||
|
||||
const imageDataURL = await ea.createPNGBase64(
|
||||
null, scale, exportSettings, loader, "light", PADDING
|
||||
);
|
||||
ea.clear();
|
||||
return {imageDataURL, maskDataURL};
|
||||
}
|
||||
|
||||
let {imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[agentTask].type === "image-edit");
|
||||
|
||||
// --------------------------------------
|
||||
// Support functions - embeddable spinner and error
|
||||
// --------------------------------------
|
||||
const spinner = await ea.convertStringToDataURL(`
|
||||
<html><head><style>
|
||||
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
|
||||
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
|
||||
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
|
||||
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
|
||||
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
|
||||
@keyframes rotate {100% {transform: rotate(360deg);}}
|
||||
@keyframes dash {
|
||||
0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;}
|
||||
50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;}
|
||||
100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}
|
||||
}
|
||||
</style></head><body>
|
||||
<div class="Spinner">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Generating...</div>
|
||||
</body></html>`);
|
||||
|
||||
const errorMessage = async (spinnerID, message) => {
|
||||
const error = "Something went wrong! Check developer console for more.";
|
||||
const details = message ? `<p>${message}</p>` : "";
|
||||
const errorDataURL = await ea.convertStringToDataURL(`
|
||||
<html><head><style>
|
||||
html, body {height: 100%;}
|
||||
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
|
||||
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
|
||||
</style></head><body>
|
||||
<h1>Error!</h1>
|
||||
<h3>${error}</h3>${details}
|
||||
</body></html>`);
|
||||
new Notice (error);
|
||||
ea.getElement(spinnerID).link = errorDataURL;
|
||||
ea.addElementsToView(false,true);
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// Utility to write Mermaid to dialog
|
||||
// --------------------------------------
|
||||
const EDITOR_LS_KEYS = {
|
||||
OAI_API_KEY: "excalidraw-oai-api-key",
|
||||
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
|
||||
PUBLISH_LIBRARY: "publish-library-data",
|
||||
};
|
||||
|
||||
const setMermaidDataToStorage = (mermaidDefinition) => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
|
||||
JSON.stringify(mermaidDefinition)
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`localStorage.setItem error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// --------------------------------------
|
||||
// Submit Prompt
|
||||
// --------------------------------------
|
||||
const generateImage = async(text, spinnerID, bb) => {
|
||||
const requestObject = {
|
||||
text,
|
||||
imageGenerationProperties: {
|
||||
size: imageSize,
|
||||
//quality: "standard", //not supported by dall-e-2
|
||||
n:1,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await ea.postOpenAI(requestObject);
|
||||
console.log({result, json:result?.json});
|
||||
|
||||
if(!result?.json?.data?.[0]?.url) {
|
||||
await errorMessage(spinnerID, result?.json?.error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ea.getElement(spinnerID)
|
||||
spinner.isDeleted = true;
|
||||
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
|
||||
const imageEl = ea.getElement(imageID);
|
||||
const revisedPrompt = result.json.data[0].revised_prompt;
|
||||
if(revisedPrompt) {
|
||||
ea.style.fontSize = 16;
|
||||
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
|
||||
width: imageEl.width-30,
|
||||
textAlign: "center",
|
||||
textVerticalAlign: "top",
|
||||
box: true,
|
||||
})
|
||||
ea.getElement(rectID).strokeColor = "transparent";
|
||||
ea.getElement(rectID).backgroundColor = "transparent";
|
||||
ea.addToGroup(ea.getElements().filter(el=>el.id !== spinnerID).map(el=>el.id));
|
||||
}
|
||||
|
||||
await ea.addElementsToView(false, true, true);
|
||||
ea.getExcalidrawAPI().setToast({
|
||||
message: IMAGE_WARNING,
|
||||
duration: 15000,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
const run = async (text) => {
|
||||
if(!text && !imageDataURL) {
|
||||
new Notice("No prompt, aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
const systemPrompt = systemPrompts[agentTask];
|
||||
const outputType = outputTypes[systemPrompt.type];
|
||||
const isImageGenRequest = outputType.blocktype === "image";
|
||||
const isImageEditRequest = systemPrompt.type === "image-edit";
|
||||
|
||||
if(isImageEditRequest) {
|
||||
if(!text) {
|
||||
new Notice("You must provide a text prompt with instructions for how the image should be modified");
|
||||
return;
|
||||
}
|
||||
if(!imageDataURL || !maskDataURL) {
|
||||
new Notice("You must provide an image and a mask");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//place spinner next to selected elements
|
||||
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
|
||||
const spinnerID = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
|
||||
|
||||
//this block is in an async call using the isEACompleted flag because otherwise during debug Obsidian
|
||||
//goes black (not freezes, but does not get a new frame for some reason)
|
||||
//palcing this in an async call solves this issue
|
||||
//If you know why this is happening and can offer a better solution, please reach out to @zsviczian
|
||||
let isEACompleted = false;
|
||||
setTimeout(async()=>{
|
||||
await ea.addElementsToView(false,true);
|
||||
ea.clear();
|
||||
const embeddable = ea.getViewElements().filter(el=>el.id===spinnerID);
|
||||
ea.copyViewElementsToEAforEditing(embeddable);
|
||||
const els = ea.getViewSelectedElements();
|
||||
ea.viewZoomToElements(false, els.concat(embeddable));
|
||||
isEACompleted = true;
|
||||
});
|
||||
|
||||
if(isImageGenRequest && !systemPrompt.prompt && !isImageEditRequest) {
|
||||
generateImage(text,spinnerID,bb);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestObject = isImageEditRequest
|
||||
? {
|
||||
...imageDataURL ? {image: imageDataURL} : {},
|
||||
...(text && text.trim() !== "") ? {text} : {},
|
||||
imageGenerationProperties: {
|
||||
size: imageSize,
|
||||
//quality: "standard", //not supported by dall-e-2
|
||||
n:1,
|
||||
mask: maskDataURL,
|
||||
},
|
||||
}
|
||||
: {
|
||||
...imageDataURL ? {image: imageDataURL} : {},
|
||||
...(text && text.trim() !== "") ? {text} : {},
|
||||
systemPrompt: systemPrompt.prompt,
|
||||
instruction: outputType.instruction,
|
||||
}
|
||||
|
||||
//Get result from GPT
|
||||
const result = await ea.postOpenAI(requestObject);
|
||||
console.log({result, json:result?.json});
|
||||
|
||||
//checking that EA has completed. Because the postOpenAI call is an async await
|
||||
//I don't expect EA not to be completed by now. However the devil never sleeps.
|
||||
//This (the insomnia of the Devil) is why I have a watchdog here as well
|
||||
let counter = 0
|
||||
while(!isEACompleted && counter++<10) sleep(50);
|
||||
if(!isEACompleted) {
|
||||
await errorMessage(spinnerID, "Unexpected issue with ExcalidrawAutomate");
|
||||
return;
|
||||
}
|
||||
|
||||
if(isImageEditRequest) {
|
||||
if(!result?.json?.data?.[0]?.url) {
|
||||
await errorMessage(spinnerID, result?.json?.error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ea.getElement(spinnerID)
|
||||
spinner.isDeleted = true;
|
||||
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
|
||||
await ea.addElementsToView(false, true, true);
|
||||
ea.getExcalidrawAPI().setToast({
|
||||
message: IMAGE_WARNING,
|
||||
duration: 15000,
|
||||
closable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!result?.json?.hasOwnProperty("choices")) {
|
||||
await errorMessage(spinnerID, result?.json?.error?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
//exctract codeblock and display result
|
||||
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
|
||||
|
||||
if(!content) {
|
||||
await errorMessage(spinnerID);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isImageGenRequest) {
|
||||
generateImage(content,spinnerID,bb);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(outputType.blocktype) {
|
||||
case "html":
|
||||
ea.getElement(spinnerID).link = await ea.convertStringToDataURL(content);
|
||||
ea.addElementsToView(false,true);
|
||||
break;
|
||||
case "svg":
|
||||
ea.getElement(spinnerID).isDeleted = true;
|
||||
ea.importSVG(content);
|
||||
ea.addToGroup(ea.getElements().map(el=>el.id));
|
||||
if(ea.getViewSelectedElements().length>0) {
|
||||
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY};
|
||||
}
|
||||
ea.addElementsToView(true, false);
|
||||
break;
|
||||
case "mermaid":
|
||||
if(content.startsWith("mermaid")) {
|
||||
content = content.replace(/^mermaid/,"").trim();
|
||||
}
|
||||
|
||||
try {
|
||||
result = await ea.addMermaid(content);
|
||||
if(typeof result === "string") {
|
||||
await errorMessage(spinnerID, "Open [More Tools / Mermaid to Excalidraw] to manually fix the received mermaid script<br><br>" + result);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
ea.addText(0,0,content);
|
||||
}
|
||||
ea.getElement(spinnerID).isDeleted = true;
|
||||
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height};
|
||||
await ea.addElementsToView(true, false);
|
||||
setMermaidDataToStorage(content);
|
||||
new Notice("Open More Tools/Mermaid to Excalidraw in the top tools menu to edit the generated diagram",8000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// User Interface
|
||||
// --------------------------------------
|
||||
let previewDiv;
|
||||
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-edit";
|
||||
const addPreviewImage = () => {
|
||||
if(!previewDiv) return;
|
||||
previewDiv.empty();
|
||||
previewDiv.createEl("img",{
|
||||
attr: {
|
||||
style: `max-width: 100%;max-height: 30vh;`,
|
||||
src: imageDataURL,
|
||||
}
|
||||
});
|
||||
if(maskDataURL) {
|
||||
previewDiv.createEl("img",{
|
||||
attr: {
|
||||
style: `max-width: 100%;max-height: 30vh;`,
|
||||
src: maskDataURL,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const configModal = new ea.obsidian.Modal(app);
|
||||
configModal.modalEl.style.width="100%";
|
||||
configModal.modalEl.style.maxWidth="1000px";
|
||||
|
||||
configModal.onOpen = async () => {
|
||||
const contentEl = configModal.contentEl;
|
||||
contentEl.createEl("h1", {text: "ExcaliAI"});
|
||||
|
||||
let systemPromptTextArea, systemPromptDiv, imageSizeSetting, imageSizeSettingDropdown, helpEl;
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("What would you like to do?")
|
||||
.addDropdown(dropdown=>{
|
||||
Object.keys(systemPrompts).forEach(key=>dropdown.addOption(key,key));
|
||||
dropdown
|
||||
.setValue(agentTask)
|
||||
.onChange(async (value) => {
|
||||
dirty = true;
|
||||
const prevTask = agentTask;
|
||||
agentTask = value;
|
||||
if(
|
||||
(systemPrompts[prevTask].type === "image-edit" && systemPrompts[value].type !== "image-edit") ||
|
||||
(systemPrompts[prevTask].type !== "image-edit" && systemPrompts[value].type === "image-edit")
|
||||
) {
|
||||
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[value].type === "image-edit"));
|
||||
addPreviewImage();
|
||||
setImageModelAndSizes();
|
||||
while (imageSizeSettingDropdown.selectEl.options.length > 0) { imageSizeSettingDropdown.selectEl.remove(0); }
|
||||
validSizes.forEach(size=>imageSizeSettingDropdown.addOption(size,size));
|
||||
imageSizeSettingDropdown.setValue(imageSize);
|
||||
}
|
||||
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
|
||||
const prompt = systemPrompts[value].prompt;
|
||||
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[value].help;
|
||||
if(prompt) {
|
||||
systemPromptDiv.style.display = "";
|
||||
systemPromptTextArea.setValue(systemPrompts[value].prompt);
|
||||
} else {
|
||||
systemPromptDiv.style.display = "none";
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
helpEl = contentEl.createEl("p");
|
||||
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[agentTask].help;
|
||||
|
||||
systemPromptDiv = contentEl.createDiv();
|
||||
systemPromptDiv.createEl("h4", {text: "Customize System Prompt"});
|
||||
systemPromptDiv.createEl("span", {text: "Unless you know what you are doing I do not recommend changing the system prompt"})
|
||||
const systemPromptSetting = new ea.obsidian.Setting(systemPromptDiv)
|
||||
.addTextArea(text => {
|
||||
systemPromptTextArea = text;
|
||||
const prompt = systemPrompts[agentTask].prompt;
|
||||
text.inputEl.style.minHeight = "10em";
|
||||
text.inputEl.style.width = "100%";
|
||||
text.setValue(prompt);
|
||||
text.onChange(value => {
|
||||
systemPrompts[value].prompt = value;
|
||||
});
|
||||
if(!prompt) systemPromptDiv.style.display = "none";
|
||||
})
|
||||
systemPromptSetting.nameEl.style.display = "none";
|
||||
systemPromptSetting.descEl.style.display = "none";
|
||||
systemPromptSetting.infoEl.style.display = "none";
|
||||
|
||||
contentEl.createEl("h4", {text: "User Prompt"});
|
||||
const userPromptSetting = new ea.obsidian.Setting(contentEl)
|
||||
.addTextArea(text => {
|
||||
text.inputEl.style.minHeight = "10em";
|
||||
text.inputEl.style.width = "100%";
|
||||
text.setValue(userPrompt);
|
||||
text.onChange(value => {
|
||||
userPrompt = value;
|
||||
dirty = true;
|
||||
})
|
||||
})
|
||||
userPromptSetting.nameEl.style.display = "none";
|
||||
userPromptSetting.descEl.style.display = "none";
|
||||
userPromptSetting.infoEl.style.display = "none";
|
||||
|
||||
imageSizeSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName("Select image size")
|
||||
.setDesc(fragWithHTML("<mark>⚠️ Important ⚠️</mark>: " + IMAGE_WARNING))
|
||||
.addDropdown(dropdown=>{
|
||||
validSizes.forEach(size=>dropdown.addOption(size,size));
|
||||
imageSizeSettingDropdown = dropdown;
|
||||
dropdown
|
||||
.setValue(imageSize)
|
||||
.onChange(async (value) => {
|
||||
dirty = true;
|
||||
imageSize = value;
|
||||
if(systemPrompts[agentTask].type === "image-edit") {
|
||||
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, true));
|
||||
addPreviewImage();
|
||||
}
|
||||
});
|
||||
})
|
||||
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
|
||||
|
||||
if(imageDataURL) {
|
||||
previewDiv = contentEl.createDiv({
|
||||
attr: {
|
||||
style: "text-align: center;",
|
||||
}
|
||||
});
|
||||
addPreviewImage();
|
||||
} else {
|
||||
contentEl.createEl("h4", {text: "No elements are selected from your canvas"});
|
||||
contentEl.createEl("span", {text: "Because there are no Excalidraw elements selected on the canvas, only the text prompt will be sent to OpenAI."});
|
||||
}
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText("Run")
|
||||
.onClick((event)=>{
|
||||
run(userPrompt); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
|
||||
configModal.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
configModal.onClose = () => {
|
||||
if(dirty) {
|
||||
settings["User Prompt"] = userPrompt;
|
||||
settings["Agent's Task"] = agentTask;
|
||||
settings["Image Size"] = imageSize;
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
configModal.open();
|
||||
1
ea-scripts/GPT-Draw-a-UI.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>
|
||||
|
After Width: | Height: | Size: 694 B |
673
ea-scripts/Golden Ratio.md
Normal file
@@ -0,0 +1,673 @@
|
||||
/*
|
||||
The script performs two different functions depending on the elements selected in the view.
|
||||
1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again.
|
||||
2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.
|
||||
|
||||

|
||||
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/2SHn_ruax-s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
|
||||
Gravitational point of spiral: $$\left[x,y\right]=\left[ x + \frac{{\text{width} \cdot \phi^2}}{{\phi^2 + 1}}\;, \; y + \frac{{\text{height} \cdot \phi^2}}{{\phi^2 + 1}} \right]$$
|
||||
Dimensions of inner rectangles in case of Double Spiral: $$[width, height] = \left[\frac{width\cdot(\phi^2+1)}{2\phi^2}\;, \;\frac{height\cdot(\phi^2+1)}{2\phi^2}\right]$$
|
||||
|
||||
```js*/
|
||||
const phi = (1 + Math.sqrt(5)) / 2; // Golden Ratio (φ)
|
||||
const inversePhi = (1-1/phi);
|
||||
const pointsPerCurve = 20; // Number of points per curve segment
|
||||
const ownerWindow = ea.targetView.ownerWindow;
|
||||
const hostLeaf = ea.targetView.leaf;
|
||||
let dirty = false;
|
||||
const ids = [];
|
||||
|
||||
const textEls = ea.getViewSelectedElements().filter(el=>el.type === "text");
|
||||
let rect = ea.getViewSelectedElements().length === 1 ? ea.getViewSelectedElement() : null;
|
||||
if(!rect || rect.type !== "rectangle") {
|
||||
//Fontsize cycle
|
||||
if(textEls.length>0) {
|
||||
if(window.excalidrawGoldenRatio) {
|
||||
clearTimeout(window.excalidrawGoldenRatio?.timer);
|
||||
} else {
|
||||
window.excalidrawGoldenRatio = {timer: null, cycle:-1};
|
||||
}
|
||||
window.excalidrawGoldenRatio.timer = setTimeout(()=>{delete window.excalidrawGoldenRatio;},2000);
|
||||
window.excalidrawGoldenRatio.cycle = (window.excalidrawGoldenRatio.cycle+1)%5;
|
||||
|
||||
ea.copyViewElementsToEAforEditing(textEls);
|
||||
ea.getElements().forEach(el=> {
|
||||
el.fontSize = window.excalidrawGoldenRatio.cycle === 2
|
||||
? el.fontSize / Math.pow(phi,4)
|
||||
: el.fontSize * phi;
|
||||
const font = ExcalidrawLib.getFontString(el);
|
||||
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
|
||||
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
|
||||
el.width = width;
|
||||
el.height = height;
|
||||
el.baseline = baseline;
|
||||
});
|
||||
ea.addElementsToView();
|
||||
return;
|
||||
}
|
||||
new Notice("Select text elements, or a select a single rectangle");
|
||||
return;
|
||||
}
|
||||
ea.copyViewElementsToEAforEditing([rect]);
|
||||
rect = ea.getElement(rect.id);
|
||||
ea.style.strokeColor = rect.strokeColor;
|
||||
ea.style.strokeWidth = rect.strokeWidth;
|
||||
ea.style.roughness = rect.roughness;
|
||||
ea.style.angle = rect.angle;
|
||||
let {x,y,width,height} = rect;
|
||||
|
||||
// --------------------------------------------
|
||||
// Load Settings
|
||||
// --------------------------------------------
|
||||
let settings = ea.getScriptSettings();
|
||||
if(!settings["Horizontal Grid"]) {
|
||||
settings = {
|
||||
"Horizontal Grid" : {
|
||||
value: "left-right",
|
||||
valueset: ["none","letf-right","right-left","center-out","center-in"]
|
||||
},
|
||||
"Vertical Grid": {
|
||||
value: "none",
|
||||
valueset: ["none","top-down","bottom-up","center-out","center-in"]
|
||||
},
|
||||
"Size": {
|
||||
value: "6",
|
||||
valueset: ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]
|
||||
},
|
||||
"Aspect Choice": {
|
||||
value: "none",
|
||||
valueset: ["none","adjust-width","adjust-height"]
|
||||
},
|
||||
"Type": "grid",
|
||||
"Spiral Orientation": {
|
||||
value: "top-left",
|
||||
valueset: ["double","top-left","top-right","bottom-right","bottom-left"]
|
||||
},
|
||||
"Lock Elements": false,
|
||||
"Send to Back": false,
|
||||
"Update Style": false,
|
||||
"Bold Spiral": false,
|
||||
};
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
let hDirection = settings["Horizontal Grid"].value;
|
||||
let vDirection = settings["Vertical Grid"].value;
|
||||
let aspectChoice = settings["Aspect Choice"].value;
|
||||
let type = settings["Type"];
|
||||
let spiralOrientation = settings["Spiral Orientation"].value;
|
||||
let lockElements = settings["Lock Elements"];
|
||||
let sendToBack = settings["Send to Back"];
|
||||
let size = parseInt(settings["Size"].value);
|
||||
let updateStyle = settings["Update Style"];
|
||||
let boldSpiral = settings["Bold Spiral"];
|
||||
|
||||
// --------------------------------------------
|
||||
// Rotation
|
||||
// --------------------------------------------
|
||||
let centerX, centerY;
|
||||
const rotatePointAndAddToElementList = (elementID) => {
|
||||
ids.push(elementID);
|
||||
const line = ea.getElement(elementID);
|
||||
|
||||
// Calculate the initial position of the line's center
|
||||
const lineCenterX = line.x + line.width / 2;
|
||||
const lineCenterY = line.y + line.height / 2;
|
||||
|
||||
// Calculate the difference between the line's center and the rectangle's center
|
||||
const diffX = lineCenterX - (rect.x + rect.width / 2);
|
||||
const diffY = lineCenterY - (rect.y + rect.height / 2);
|
||||
|
||||
// Apply the rotation to the difference
|
||||
const cosTheta = Math.cos(rect.angle);
|
||||
const sinTheta = Math.sin(rect.angle);
|
||||
const rotatedX = diffX * cosTheta - diffY * sinTheta;
|
||||
const rotatedY = diffX * sinTheta + diffY * cosTheta;
|
||||
|
||||
// Calculate the new position of the line's center with respect to the rectangle's center
|
||||
const newLineCenterX = rotatedX + (rect.x + rect.width / 2);
|
||||
const newLineCenterY = rotatedY + (rect.y + rect.height / 2);
|
||||
|
||||
// Update the line's coordinates by adjusting for the change in the center
|
||||
line.x += newLineCenterX - lineCenterX;
|
||||
line.y += newLineCenterY - lineCenterY;
|
||||
}
|
||||
|
||||
const rotatePointsWithinRectangle = (points) => {
|
||||
const centerX = rect.x + rect.width / 2;
|
||||
const centerY = rect.y + rect.height / 2;
|
||||
|
||||
const cosTheta = Math.cos(rect.angle);
|
||||
const sinTheta = Math.sin(rect.angle);
|
||||
|
||||
const rotatedPoints = points.map(([x, y]) => {
|
||||
// Translate the point relative to the rectangle's center
|
||||
const translatedX = x - centerX;
|
||||
const translatedY = y - centerY;
|
||||
|
||||
// Apply the rotation to the translated coordinates
|
||||
const rotatedX = translatedX * cosTheta - translatedY * sinTheta;
|
||||
const rotatedY = translatedX * sinTheta + translatedY * cosTheta;
|
||||
|
||||
// Translate back to the original coordinate system
|
||||
const finalX = rotatedX + centerX;
|
||||
const finalY = rotatedY + centerY;
|
||||
|
||||
return [finalX, finalY];
|
||||
});
|
||||
|
||||
return rotatedPoints;
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Grid
|
||||
// --------------------------------------------
|
||||
const calculateGoldenSum = (baseOfGoldenGrid, pow) => {
|
||||
const ratio = 1 / phi;
|
||||
const geometricSum = baseOfGoldenGrid * ((1 - Math.pow(ratio, pow)) / (1 - ratio));
|
||||
return geometricSum;
|
||||
};
|
||||
|
||||
const findBaseForGoldenGrid = (targetValue, n, scenario) => {
|
||||
const ratio = 1 / phi;
|
||||
if (scenario === "center-out") {
|
||||
return targetValue * (2-2*ratio) / (1 + ratio + 2*Math.pow(ratio,n));
|
||||
} else if (scenario === "center-in") {
|
||||
return targetValue*2*(1-ratio)*Math.pow(phi,n-1) /(2*Math.pow(phi,n-1)*(1-Math.pow(ratio,n))-1+ratio);
|
||||
} else {
|
||||
return targetValue * (1-ratio)/(1-Math.pow(ratio,n));
|
||||
}
|
||||
}
|
||||
|
||||
const calculateOffsetVertical = (scenario, base) => {
|
||||
if (scenario === "center-out") return base / 2;
|
||||
if (scenario === "center-in") return base / Math.pow(phi, size + 1) / 2;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const horizontal = (direction, scenario) => {
|
||||
const base = findBaseForGoldenGrid(width, size + 1, scenario);
|
||||
const totalGridWidth = calculateGoldenSum(base, size + 1);
|
||||
|
||||
for (i = 1; i <= size; i++) {
|
||||
const offset =
|
||||
scenario === "center-out"
|
||||
? totalGridWidth - calculateGoldenSum(base, i)
|
||||
: calculateGoldenSum(base, size + 1 - i);
|
||||
|
||||
const x2 =
|
||||
direction === "left"
|
||||
? x + offset
|
||||
: x + width - offset;
|
||||
|
||||
rotatePointAndAddToElementList(
|
||||
ea.addLine([
|
||||
[x2, y],
|
||||
[x2, y + height],
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const vertical = (direction, scenario) => {
|
||||
const base = findBaseForGoldenGrid(height, size + 1, scenario);
|
||||
const totalGridWidth = calculateGoldenSum(base, size + 1);
|
||||
|
||||
for (i = 1; i <= size; i++) {
|
||||
const offset =
|
||||
scenario === "center-out"
|
||||
? totalGridWidth - calculateGoldenSum(base, i)
|
||||
: calculateGoldenSum(base, size + 1 - i);
|
||||
|
||||
const y2 =
|
||||
direction === "top"
|
||||
? y + offset
|
||||
: y + height - offset;
|
||||
|
||||
rotatePointAndAddToElementList(
|
||||
ea.addLine([
|
||||
[x, y2],
|
||||
[x+width, y2],
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const centerHorizontal = (scenario) => {
|
||||
width = width / 2;
|
||||
horizontal("left", scenario);
|
||||
x += width;
|
||||
horizontal("right", scenario);
|
||||
x -= width;
|
||||
width = 2*width;
|
||||
|
||||
};
|
||||
|
||||
const centerVertical = (scenario) => {
|
||||
height = height / 2;
|
||||
vertical("top", scenario);
|
||||
y += height;
|
||||
vertical("bottom", scenario);
|
||||
y -= height;
|
||||
height = 2*height;
|
||||
};
|
||||
|
||||
const drawGrid = () => {
|
||||
switch(hDirection) {
|
||||
case "none": break;
|
||||
case "left-right": horizontal("left"); break;
|
||||
case "right-left": horizontal("right"); break;
|
||||
case "center-out": centerHorizontal("center-out"); break;
|
||||
case "center-in": centerHorizontal("center-in"); break;
|
||||
}
|
||||
switch(vDirection) {
|
||||
case "none": break;
|
||||
case "top-down": vertical("top"); break;
|
||||
case "bottom-up": vertical("bottom"); break;
|
||||
case "center-out": centerVertical("center-out"); break;
|
||||
case "center-in": centerVertical("center-in"); break;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Draw Spiral
|
||||
// --------------------------------------------
|
||||
const drawSpiral = () => {
|
||||
let nextX, nextY, nextW, nextH;
|
||||
let spiralPoints = [];
|
||||
let curveEndX, curveEndY, curveX, curveY;
|
||||
|
||||
const phaseShift = {
|
||||
"bottom-right": 0,
|
||||
"bottom-left": 2,
|
||||
"top-left": 2,
|
||||
"top-right": 0,
|
||||
}[spiralOrientation];
|
||||
|
||||
let curveStartX = {
|
||||
"bottom-right": x,
|
||||
"bottom-left": x+width,
|
||||
"top-left": x+width,
|
||||
"top-right": x,
|
||||
}[spiralOrientation];
|
||||
|
||||
let curveStartY = {
|
||||
"bottom-right": y+height,
|
||||
"bottom-left": y+height,
|
||||
"top-left": y,
|
||||
"top-right": y,
|
||||
}[spiralOrientation];
|
||||
|
||||
const mirror = spiralOrientation === "bottom-left" || spiralOrientation === "top-right";
|
||||
for (let i = phaseShift; i < size+phaseShift; i++) {
|
||||
const curvePhase = i%4;
|
||||
const linePhase = mirror?[0,3,2,1][curvePhase]:curvePhase;
|
||||
const longHorizontal = width/phi;
|
||||
const shortHorizontal = width*inversePhi;
|
||||
const longVertical = height/phi;
|
||||
const shortVertical = height*inversePhi;
|
||||
switch(linePhase) {
|
||||
case 0: //right
|
||||
nextX = x + longHorizontal;
|
||||
nextY = y;
|
||||
nextW = shortHorizontal;
|
||||
nextH = height;
|
||||
break;
|
||||
case 1: //down
|
||||
nextX = x;
|
||||
nextY = y + longVertical;
|
||||
nextW = width;
|
||||
nextH = shortVertical;
|
||||
break;
|
||||
case 2: //left
|
||||
nextX = x;
|
||||
nextY = y;
|
||||
nextW = shortHorizontal;
|
||||
nextH = height;
|
||||
break;
|
||||
case 3: //up
|
||||
nextX = x;
|
||||
nextY = y;
|
||||
nextW = width;
|
||||
nextH = shortVertical;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(curvePhase) {
|
||||
case 0: //right
|
||||
curveEndX = nextX;
|
||||
curveEndY = mirror ? nextY + nextH : nextY;
|
||||
break;
|
||||
case 1: //down
|
||||
curveEndX = nextX + nextW;
|
||||
curveEndY = mirror ? nextY + nextH : nextY;
|
||||
break;
|
||||
case 2: //left
|
||||
curveEndX = nextX + nextW;
|
||||
curveEndY = mirror ? nextY : nextY + nextH;
|
||||
break;
|
||||
case 3: //up
|
||||
curveEndX = nextX;
|
||||
curveEndY = mirror ? nextY : nextY + nextH;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add points for the curve segment
|
||||
|
||||
for (let j = 0; j <= pointsPerCurve; j++) {
|
||||
const t = j / pointsPerCurve;
|
||||
const angle = -Math.PI / 2 * t;
|
||||
|
||||
switch(curvePhase) {
|
||||
case 0:
|
||||
curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle);
|
||||
curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle);
|
||||
break;
|
||||
case 1:
|
||||
curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle);
|
||||
curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle);
|
||||
break;
|
||||
case 2:
|
||||
curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle);
|
||||
curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle);
|
||||
break;
|
||||
case 3:
|
||||
curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle);
|
||||
curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle);
|
||||
break;
|
||||
}
|
||||
spiralPoints.push([curveX, curveY]);
|
||||
}
|
||||
x = nextX;
|
||||
y = nextY;
|
||||
curveStartX = curveEndX;
|
||||
curveStartY = curveEndY;
|
||||
width = nextW;
|
||||
height = nextH;
|
||||
switch(linePhase) {
|
||||
case 0: rotatePointAndAddToElementList(ea.addLine([[x,y],[x,y+height]]));break;
|
||||
case 1: rotatePointAndAddToElementList(ea.addLine([[x,y],[x+width,y]]));break;
|
||||
case 2: rotatePointAndAddToElementList(ea.addLine([[x+width,y],[x+width,y+height]]));break;
|
||||
case 3: rotatePointAndAddToElementList(ea.addLine([[x,y+height],[x+width,y+height]]));break;
|
||||
}
|
||||
}
|
||||
const strokeWidth = ea.style.strokeWidth;
|
||||
ea.style.strokeWidth = strokeWidth * (boldSpiral ? 3 : 1);
|
||||
const angle = ea.style.angle;
|
||||
ea.style.angle = 0;
|
||||
ids.push(ea.addLine(rotatePointsWithinRectangle(spiralPoints)));
|
||||
ea.style.angle = angle;
|
||||
ea.style.strokeWidth = strokeWidth;
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Update Aspect Ratio
|
||||
// --------------------------------------------
|
||||
const updateAspectRatio = () => {
|
||||
switch(aspectChoice) {
|
||||
case "none": break;
|
||||
case "adjust-width": rect.width = rect.height/phi; break;
|
||||
case "adjust-height": rect.height = rect.width/phi; break;
|
||||
}
|
||||
({x,y,width,height} = rect);
|
||||
centerX = x + width/2;
|
||||
centerY = y + height/2;
|
||||
}
|
||||
// --------------------------------------------
|
||||
// UI
|
||||
// --------------------------------------------
|
||||
draw = async () => {
|
||||
if(updateStyle) {
|
||||
ea.style.strokeWidth = 0.5; rect.strokeWidth;
|
||||
ea.style.roughness = 0; rect.roughness;
|
||||
ea.style.roundness = null;
|
||||
rect.strokeWidth = 0.5;
|
||||
rect.roughness = 0;
|
||||
rect.roundness = null;
|
||||
}
|
||||
updateAspectRatio();
|
||||
switch(type) {
|
||||
case "grid": drawGrid(); break;
|
||||
case "spiral":
|
||||
if(spiralOrientation === "double") {
|
||||
wInner = width * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2));
|
||||
hInner = height * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2));
|
||||
x2 = width - wInner + x;
|
||||
y2 = height - hInner + y;
|
||||
width = wInner;
|
||||
height = hInner;
|
||||
rotatePointAndAddToElementList(ea.addRect(x,y,width,height));
|
||||
spiralOrientation = "bottom-right";
|
||||
drawSpiral();
|
||||
x = x2;
|
||||
y = y2;
|
||||
width = wInner;
|
||||
height = hInner;
|
||||
rotatePointAndAddToElementList(ea.addRect(x,y,width,height));
|
||||
spiralOrientation = "top-left";
|
||||
drawSpiral();
|
||||
spiralOrientation = "double";
|
||||
} else {
|
||||
drawSpiral();
|
||||
}
|
||||
break;
|
||||
}
|
||||
ea.addToGroup(ids);
|
||||
ids.push(rect.id);
|
||||
ea.addToGroup(ids);
|
||||
lockElements && ea.getElements().forEach(el=>{el.locked = true;});
|
||||
await ea.addElementsToView(false,false,!sendToBack);
|
||||
!lockElements && ea.selectElementsInView(ea.getViewElements().filter(el => ids.includes(el.id)));
|
||||
}
|
||||
|
||||
const modal = new ea.obsidian.Modal(app);
|
||||
|
||||
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
|
||||
const keydownListener = (e) => {
|
||||
if(hostLeaf !== app.workspace.activeLeaf) return;
|
||||
if(hostLeaf.width === 0 && hostLeaf.height === 0) return;
|
||||
if(e.key === "Enter" && (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey)) {
|
||||
e.preventDefault();
|
||||
modal.close();
|
||||
draw()
|
||||
}
|
||||
}
|
||||
ownerWindow.addEventListener('keydown',keydownListener);
|
||||
|
||||
modal.onOpen = async () => {
|
||||
const contentEl = modal.contentEl;
|
||||
contentEl.createEl("h1", {text: "Golden Ratio"});
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("Adjust Rectangle Aspect Ratio to Golden Ratio")
|
||||
.addDropdown(dropdown=>dropdown
|
||||
.addOption("none","None")
|
||||
.addOption("adjust-width","Adjust Width")
|
||||
.addOption("adjust-height","Adjust Height")
|
||||
.setValue(aspectChoice)
|
||||
.onChange(value => {
|
||||
aspectChoice = value;
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("Change Line Style To: thin, architect, sharp")
|
||||
.addToggle(toggle=>
|
||||
toggle
|
||||
.setValue(updateStyle)
|
||||
.onChange(value => {
|
||||
dirty = true;
|
||||
updateStyle = value;
|
||||
})
|
||||
)
|
||||
|
||||
let sizeEl;
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("Number of lines")
|
||||
.addSlider(slider => slider
|
||||
.setLimits(2, 20, 1)
|
||||
.setValue(size)
|
||||
.onChange(value => {
|
||||
sizeEl.innerText = ` ${value.toString()}`;
|
||||
size = value;
|
||||
dirty = true;
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", el => {
|
||||
sizeEl = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${size.toString()}`;
|
||||
});
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("Lock Rectangle and Gridlines")
|
||||
.addToggle(toggle=>
|
||||
toggle
|
||||
.setValue(lockElements)
|
||||
.onChange(value => {
|
||||
dirty = true;
|
||||
lockElements = value;
|
||||
})
|
||||
)
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName("Send to Back")
|
||||
.addToggle(toggle=>
|
||||
toggle
|
||||
.setValue(sendToBack)
|
||||
.onChange(value => {
|
||||
dirty = true;
|
||||
sendToBack = value;
|
||||
})
|
||||
)
|
||||
|
||||
let bGrid, bSpiral;
|
||||
let sHGrid, sVGrid, sSpiral, sBoldSpiral;
|
||||
const showGridSettings = (value) => {
|
||||
value
|
||||
? (bGrid.setCta(), bSpiral.removeCta())
|
||||
: (bGrid.removeCta(), bSpiral.setCta());
|
||||
sHGrid.settingEl.style.display = value ? "" : "none";
|
||||
sVGrid.settingEl.style.display = value ? "" : "none";
|
||||
sSpiral.settingEl.style.display = !value ? "" : "none";
|
||||
sBoldSpiral.settingEl.style.display = !value ? "" : "none";
|
||||
}
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName(fragWithHTML("<h3>Output Type</h3>"))
|
||||
.addButton(button => {
|
||||
bGrid = button;
|
||||
button
|
||||
.setButtonText("Grid")
|
||||
.setCta(type === "grid")
|
||||
.onClick(event => {
|
||||
type = "grid";
|
||||
showGridSettings(true);
|
||||
dirty = true;
|
||||
})
|
||||
})
|
||||
.addButton(button => {
|
||||
bSpiral = button;
|
||||
button
|
||||
.setButtonText("Spiral")
|
||||
.setCta(type === "spiral")
|
||||
.onClick(event => {
|
||||
type = "spiral";
|
||||
showGridSettings(false);
|
||||
dirty = true;
|
||||
})
|
||||
});
|
||||
|
||||
sSpiral = new ea.obsidian.Setting(contentEl)
|
||||
.setName("Spiral Orientation")
|
||||
.addDropdown(dropdown=>dropdown
|
||||
.addOption("double","Double")
|
||||
.addOption("top-left","Top left")
|
||||
.addOption("top-right","Top right")
|
||||
.addOption("bottom-right","Bottom right")
|
||||
.addOption("bottom-left","Bottom left")
|
||||
.setValue(spiralOrientation)
|
||||
.onChange(value => {
|
||||
spiralOrientation = value;
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
sBoldSpiral = new ea.obsidian.Setting(contentEl)
|
||||
.setName("Spiral with Bold Line")
|
||||
.addToggle(toggle=>
|
||||
toggle
|
||||
.setValue(boldSpiral)
|
||||
.onChange(value => {
|
||||
dirty = true;
|
||||
boldSpiral = value;
|
||||
})
|
||||
)
|
||||
|
||||
sHGrid = new ea.obsidian.Setting(contentEl)
|
||||
.setName("Horizontal Grid")
|
||||
.addDropdown(dropdown=>dropdown
|
||||
.addOption("none","None")
|
||||
.addOption("left-right","Left to right")
|
||||
.addOption("right-left","Right to left")
|
||||
.addOption("center-out","Center out")
|
||||
.addOption("center-in","Center in")
|
||||
.setValue(hDirection)
|
||||
.onChange(value => {
|
||||
hDirection = value;
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
sVGrid = new ea.obsidian.Setting(contentEl)
|
||||
.setName("Vertical Grid")
|
||||
.addDropdown(dropdown=>dropdown
|
||||
.addOption("none","None")
|
||||
.addOption("top-down","Top down")
|
||||
.addOption("bottom-up","Bootom up")
|
||||
.addOption("center-out","Center out")
|
||||
.addOption("center-in","Center in")
|
||||
.setValue(vDirection)
|
||||
.onChange(value => {
|
||||
vDirection = value;
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
showGridSettings(type === "grid");
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText("Run")
|
||||
.setCta(true)
|
||||
.onClick(async (event) => {
|
||||
draw();
|
||||
modal.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
modal.onClose = () => {
|
||||
if(dirty) {
|
||||
settings["Horizontal Grid"].value = hDirection;
|
||||
settings["Vertical Grid"].value = vDirection;
|
||||
settings["Size"].value = size.toString();
|
||||
settings["Aspect Choice"].value = aspectChoice;
|
||||
settings["Type"] = type;
|
||||
settings["Spiral Orientation"].value = spiralOrientation;
|
||||
settings["Lock Elements"] = lockElements;
|
||||
settings["Send to Back"] = sendToBack;
|
||||
settings["Update Style"] = updateStyle;
|
||||
settings["Bold Spiral"] = boldSpiral;
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
ownerWindow.removeEventListener('keydown',keydownListener);
|
||||
}
|
||||
|
||||
modal.open();
|
||||
1
ea-scripts/Golden Ratio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 642.8 373.7" stroke="currentColor"><path stroke-linecap="round" stroke-width="4" d="M5 5h633M5 5h633m0 0v364m0-364v364m0 0H5m633 0H5m0 0V5m0 364V5m242 0v364m0-364v364M5 144h242M5 144h242M154 5v139m0-139v139m0-53h93m-93 0h93m-57 0v53m0-53v53m-36-33h36m-36 0h36m-14-20v20m0-20v20m0-8h14m-14 0h14"/><path stroke-linecap="round" stroke-width="12" d="m638 5-5 57m5-57-5 57m0 0-14 55m14-55-14 55m0 0-24 53m24-53-24 53m0 0-32 49m32-49-32 49m0 0-40 43m40-43-40 43m0 0-46 37m46-37-46 37m0 0-53 30m53-30-53 30m0 0-56 22m56-22-56 22m0 0-60 13m60-13-60 13m0 0-61 5m61-5-61 5m0 0s0 0 0 0m0 0s0 0 0 0m0 0-38-3m38 3-38-3m0 0-37-8m37 8-37-8m0 0-35-14m35 14-35-14m0 0-32-18m32 18-32-18m0 0-29-23m29 23-29-23m0 0-25-27m25 27-25-27m0 0-20-30m20 30-20-30m0 0-14-33m14 33-14-33m0 0-9-34m9 34-9-34m0 0-3-35m3 35-3-35m0 0s0 0 0 0m0 0s0 0 0 0m0 0 2-22m-2 22 2-22m0 0 5-21m-5 21 5-21m0 0 9-20m-9 20 9-20m0 0 13-19M21 81l13-19m0 0 15-16M34 62l15-16m0 0 18-14M49 46l18-14m0 0 20-12M67 32c4-3 8-6 20-12m0 0 21-8m-21 8 21-8m0 0 23-5m-23 5 23-5m0 0 23-2m-23 2 23-2m0 0s0 0 0 0m0 0s0 0 0 0m0 0 15 1m-15-1 15 1m0 0 14 3m-14-3 14 3m0 0 13 5m-13-5 13 5m0 0 13 7m-13-7 13 7m0 0 11 9m-11-9 11 9m0 0 9 10m-9-10 9 10m0 0 8 12m-8-12 8 12m0 0 5 12m-5-12 5 12m0 0 4 13m-4-13 4 13m0 0 1 14m-1-14 1 14m0 0s0 0 0 0m0 0s0 0 0 0m0 0-1 8m1-8-1 8m0 0-2 8m2-8-2 8m0 0-4 8m4-8-4 8m0 0-4 7m4-7-4 7m0 0-6 6m6-6-6 6m0 0-7 6m7-6-7 6m0 0-7 4m7-4-7 4m0 0-9 3m9-3-9 3m0 0-8 2m8-2-8 2m0 0-9 1m9-1-9 1m0 0s0 0 0 0m0 0s0 0 0 0m0 0h-6m6 0h-6m0 0-5-2m5 2-5-2m0 0-5-2m5 2-5-2m0 0-5-2m5 2-5-2m0 0-4-4m4 4-4-4m0 0-4-4m4 4-4-4m0 0-3-4m3 4-3-4m0 0-2-5m2 5-2-5m0 0-1-5m1 5-1-5m0 0-1-5m1 5-1-5m0 0s0 0 0 0m0 0s0 0 0 0m0 0 1-3m-1 3 1-3m0 0v-3m0 3v-3m0 0 2-3m-2 3 2-3m0 0 2-3m-2 3 2-3m0 0 2-2m-2 2 2-2m0 0 2-2m-2 2 2-2m0 0 3-2m-3 2 3-2m0 0 3-1m-3 1 3-1m0 0 4-1m-4 1 4-1m0 0h3m-3 0h3m0 0s0 0 0 0m0 0s0 0 0 0m0 0h2m-2 0h2m0 0h2m-2 0h2m0 0 2 1m-2-1 2 1m0 0 2 1m-2-1 2 1m0 0 2 2m-2-2 2 2m0 0 1 1m-1-1 1 1m0 0 1 2m-1-2 1 2m0 0 1 2m-1-2 1 2m0 0v1m0-1v1m0 0 1 2m-1-2 1 2"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -29,7 +29,7 @@ try {
|
||||
elHeight /= 1.1;
|
||||
}
|
||||
} else if (elWidth * elHeight < areaAvailable) {
|
||||
while (elWidth * elHeight > areaAvailable) {
|
||||
while (elWidth * elHeight < areaAvailable) {
|
||||
elWidth *= 1.1;
|
||||
elHeight *= 1.1;
|
||||
}
|
||||
@@ -64,4 +64,4 @@ try {
|
||||
ea.addElementsToView(false, true, true);
|
||||
} catch (err) {
|
||||
_ = new Notice(err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
40
ea-scripts/Relative Font Size Cycle.md
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
The script will cycle through S, M, L, XL font sizes scaled to the current canvas zoom.
|
||||
```js*/
|
||||
const FONTSIZES = [16, 20, 28, 36];
|
||||
const api = ea.getExcalidrawAPI();
|
||||
const st = api.getAppState();
|
||||
const zoom = st.zoom.value;
|
||||
const currentItemFontSize = st.currentItemFontSize;
|
||||
|
||||
const fontsizes = FONTSIZES.map(s=>s/zoom);
|
||||
const els = ea.getViewSelectedElements().filter(el=>el.type === "text");
|
||||
|
||||
const findClosestIndex = (val, list) => {
|
||||
let closestIndex = 0;
|
||||
let closestDifference = Math.abs(list[0] - val);
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
const difference = Math.abs(list[i] - val);
|
||||
if (difference <= closestDifference) {
|
||||
closestDifference = difference;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
ea.viewUpdateScene({appState:{currentItemFontSize: fontsizes[(findClosestIndex(currentItemFontSize, fontsizes)+1) % fontsizes.length] }});
|
||||
|
||||
if(els.length>0) {
|
||||
ea.copyViewElementsToEAforEditing(els);
|
||||
ea.getElements().forEach(el=> {
|
||||
el.fontSize = fontsizes[(findClosestIndex(el.fontSize, fontsizes)+1) % fontsizes.length];
|
||||
const font = ExcalidrawLib.getFontString(el);
|
||||
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
|
||||
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
|
||||
el.width = width;
|
||||
el.height = height;
|
||||
el.baseline = baseline;
|
||||
});
|
||||
ea.addElementsToView();
|
||||
}
|
||||
4
ea-scripts/Relative Font Size Cycle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 50 30" xmlns="http://www.w3.org/2000/svg">
|
||||
<text fill="currentColor" x="10" y="30" font-size="16px" font-weight="light">A</text>
|
||||
<text fill="currentColor" x="22" y="30" font-size="36px" font-weight="light">A</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
96
ea-scripts/Repeat Texts.md
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
|
||||

|
||||
In the following script, we address the concept of repetition through the lens of numerical progression. As visualized by the image, where multiple circles each labeled with an even task number are being condensed into a linear sequence, our script will similarly iterate through a set of numbers.
|
||||
|
||||
Inspired from [Repeat Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Elements.md)
|
||||
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.19")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
let repeatNum = parseInt(await utils.inputPrompt("repeat times?","number","5"));
|
||||
if(!repeatNum) {
|
||||
new Notice("Please enter a number.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedElements = ea.getViewSelectedElements().sort((lha,rha) => lha.x === rha.x ? lha.y - rha.y : lha.x - rha.x);
|
||||
|
||||
const selectedBounds = selectedElements.filter(e => e.type !== "text");
|
||||
const selectedTexts = selectedElements.filter(e => e.type === "text");
|
||||
const selectedTextsById = selectedTexts.reduce((prev, next) => (prev[next.id] = next, prev), {})
|
||||
|
||||
|
||||
if(selectedTexts.length !== 2 || ![0, 2].includes(selectedBounds.length)) {
|
||||
new Notice("Please select only 2 text elements.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(selectedBounds.length === 2) {
|
||||
if(selectedBounds[0].type !== selectedBounds[1].type) {
|
||||
new Notice("The selected elements must be of the same type.");
|
||||
return;
|
||||
}
|
||||
if (!selectedBounds.every(e => e.boundElements?.length === 1)) {
|
||||
new Notice("Only support the bound element with 1 text element.");
|
||||
return;
|
||||
}
|
||||
if (!selectedBounds.every(e => !!selectedTextsById[e.boundElements?.[0]?.id])) {
|
||||
new Notice("Bound element must refer to the text element.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const prevBoundEl = selectedBounds.length ? selectedBounds[0] : selectedTexts[0];
|
||||
const nextBoundEl = selectedBounds.length ? selectedBounds[1] : selectedTexts[1];
|
||||
const prevTextEl = prevBoundEl.type === 'text' ? prevBoundEl : selectedTextsById[prevBoundEl.boundElements[0].id]
|
||||
const nextTextEl = nextBoundEl.type === 'text' ? nextBoundEl : selectedTextsById[nextBoundEl.boundElements[0].id]
|
||||
|
||||
const xDistance = nextBoundEl.x - prevBoundEl.x;
|
||||
const yDistance = nextBoundEl.y - prevBoundEl.y;
|
||||
|
||||
const numReg = /\d+/
|
||||
let textNumDiff
|
||||
try {
|
||||
const num0 = +prevTextEl.text.match(numReg)
|
||||
const num1 = +nextTextEl.text.match(numReg)
|
||||
textNumDiff = num1 - num0
|
||||
} catch(e) {
|
||||
new Notice("Text must include a number!")
|
||||
return;
|
||||
}
|
||||
|
||||
const repeatEl = (newEl, step) => {
|
||||
ea.elementsDict[newEl.id] = newEl;
|
||||
newEl.x += xDistance * (step + 1);
|
||||
newEl.y += yDistance * (step + 1);
|
||||
|
||||
if(newEl.text) {
|
||||
const text = newEl.text.replace(numReg, (match) => +match + (step + 1) * textNumDiff)
|
||||
newEl.originalText = text
|
||||
newEl.rawText = text
|
||||
newEl.text = text
|
||||
}
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(selectedBounds);
|
||||
for(let i=0; i<repeatNum; i++) {
|
||||
const newTextEl = ea.cloneElement(nextTextEl);
|
||||
repeatEl(newTextEl, i)
|
||||
|
||||
if (selectedBounds.length) {
|
||||
const newBoundEl = ea.cloneElement(selectedBounds[1]);
|
||||
newBoundEl.boundElements[0].id = newTextEl.id
|
||||
newTextEl.containerId = newBoundEl.id
|
||||
repeatEl(newBoundEl, i)
|
||||
}
|
||||
}
|
||||
|
||||
await ea.addElementsToView(false, false, true);
|
||||
|
||||
7
ea-scripts/Repeat Texts.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
|
||||
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g><g><g><path fill="#000000" d="M101.2,18.7c-2,0.9-38,36.6-39.2,39c-1,1.9-1.1,4.2-0.4,6.2c0.6,1.5,37.3,38.6,39.3,39.6c1.7,0.9,5.2,0.8,7.1-0.1c1.9-0.9,3.7-3.5,4.1-5.8c0.6-3.5-0.2-4.5-12.5-16.8L88.2,69.2h44.3c38.7,0,44.8,0.1,48.6,0.7c24.2,4.2,43.2,22.6,48.1,46.8c3.3,16.4-1,34.2-11.5,47.6c-3.5,4.6-4.1,6.9-2.4,10.4c1.8,3.7,6.6,5.3,10.4,3.5c2.8-1.3,8.7-9.4,12.5-17.2c10.5-20.8,10.5-45.2,0-66.2c-10.3-20.6-28.6-34.8-51.8-40c-4.6-1.1-4.8-1.1-51.3-1.2c-25.7-0.1-46.7-0.3-46.7-0.5s5.2-5.5,11.5-11.9c9.4-9.4,11.6-11.9,12-13.3C113.6,21.6,107.3,16.1,101.2,18.7z"/><path fill="#000000" d="M34.9,73.4c-1.8,0.5-5.8,4.4-9.7,9.6c-7.9,10.4-12.8,22.4-14.5,35.4c-5.3,40.1,22.9,77.3,63.1,83.3c4,0.6,11.3,0.7,45.4,0.7c24,0,40.6,0.2,40.6,0.4c0,0.2-5,5.3-11.1,11.1c-11.3,11-12.9,13-12.9,16c0,4.1,3.8,7.9,7.9,7.9c0.7,0,2.1-0.3,3.1-0.7c2.3-1,38.1-36.6,39.3-39.2c1-2.1,1.1-4.2,0.4-6.1c-0.6-1.5-37.3-38.6-39.3-39.7c-1.9-1-6.1-0.6-8.1,0.8c-2.9,2-4,6.4-2.5,9.4c0.4,0.8,5.9,6.6,12.1,12.9l11.4,11.4h-40.4c-35,0-40.9-0.1-44.7-0.7c-24.2-4.1-43.4-22.9-48.2-47.1c-1.1-5.5-1.1-16.4,0-21.9c2.1-10.7,7-20.2,14.6-28.7c2.1-2.3,3.5-4.3,3.8-5.4c1-3.6-0.7-7.3-4.2-9C38.7,72.8,37.4,72.7,34.9,73.4z"/></g></g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -10,11 +10,13 @@ If there are frames, the script will use the frames for the presentation. Frames
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.17")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.23")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
const hostLeaf = ea.targetView.leaf;
|
||||
const hostView = hostLeaf.view;
|
||||
const statusBarElement = document.querySelector("div.status-bar");
|
||||
const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierKeyDown.metaKey;
|
||||
const altKey = ea.targetView.modifierKeyDown.altKey || ctrlKey;
|
||||
@@ -36,10 +38,13 @@ const SVG_LEFT_ARROW = ea.obsidian.getIcon("lucide-arrow-left").outerHTML;
|
||||
const SVG_EDIT = ea.obsidian.getIcon("lucide-pencil").outerHTML;
|
||||
const SVG_MAXIMIZE = ea.obsidian.getIcon("lucide-maximize").outerHTML;
|
||||
const SVG_MINIMIZE = ea.obsidian.getIcon("lucide-minimize").outerHTML;
|
||||
const SVG_LASER_ON = ea.obsidian.getIcon("lucide-hand").outerHTML;
|
||||
const SVG_LASER_OFF = ea.obsidian.getIcon("lucide-wand").outerHTML;
|
||||
|
||||
//-------------------------------
|
||||
//utility & convenience functions
|
||||
//-------------------------------
|
||||
let isLaserOn = false;
|
||||
let slide = 0;
|
||||
let isFullscreen = false;
|
||||
const ownerDocument = ea.targetView.ownerDocument;
|
||||
@@ -283,7 +288,7 @@ let busy = false;
|
||||
const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = TRANSITION_STEP_COUNT) => {
|
||||
const startTimer = Date.now();
|
||||
let watchdog = 0;
|
||||
while(busy && watchdog++<15) await(100);
|
||||
while(busy && watchdog++<15) await sleep(100);
|
||||
if(busy && watchdog >= 15) return;
|
||||
busy = true;
|
||||
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:true}});
|
||||
@@ -310,6 +315,9 @@ const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = TRANSIT
|
||||
}
|
||||
}
|
||||
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:false}});
|
||||
if(isLaserOn) {
|
||||
excalidrawAPI.setActiveTool({type: "laser"});
|
||||
}
|
||||
busy = false;
|
||||
}
|
||||
|
||||
@@ -450,6 +458,22 @@ const createPresentationNavigationPanel = () => {
|
||||
margin: 0px auto;`
|
||||
}
|
||||
});
|
||||
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
title: "Toggle Laser Pointer and Panning Mode"
|
||||
}
|
||||
}, button => {
|
||||
button.innerHTML = isLaserOn ? SVG_LASER_ON : SVG_LASER_OFF;
|
||||
button.onclick = () => {
|
||||
isLaserOn = !isLaserOn;
|
||||
excalidrawAPI.setActiveTool({
|
||||
type: isLaserOn ? "laser" : "selection"
|
||||
})
|
||||
button.innerHTML = isLaserOn ? SVG_LASER_ON : SVG_LASER_OFF;
|
||||
}
|
||||
});
|
||||
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
title: "Toggle fullscreen. If you hold ALT/OPT when starting the presentation it will not go fullscreen."
|
||||
@@ -504,7 +528,8 @@ const createPresentationNavigationPanel = () => {
|
||||
// keyboard navigation
|
||||
//--------------------
|
||||
const keydownListener = (e) => {
|
||||
if(ea.targetView.leaf !== app.workspace.activeLeaf) return;
|
||||
if(hostLeaf !== app.workspace.activeLeaf) return;
|
||||
if(hostLeaf.width === 0 && hostLeaf.height === 0) return;
|
||||
e.preventDefault();
|
||||
switch(e.key) {
|
||||
case "Escape":
|
||||
@@ -630,6 +655,9 @@ const initializeEventListners = () => {
|
||||
// Exit presentation
|
||||
//----------------------------
|
||||
const exitPresentation = async (openForEdit = false) => {
|
||||
//this is a hack, not sure why ea loses target view when other scripts are executed while the presentation is running
|
||||
ea.targetView = hostView;
|
||||
isLaserOn = false;
|
||||
statusBarElement.style.display = "inherit";
|
||||
if(openForEdit) ea.targetView.preventAutozoom();
|
||||
await exitFullscreen();
|
||||
@@ -680,7 +708,8 @@ const exitPresentation = async (openForEdit = false) => {
|
||||
//Resets pointer offsets. Ugly solution.
|
||||
//During testing offsets were wrong after presentation, but don't know why.
|
||||
//This should solve it even if they are wrong.
|
||||
ea.targetView.refresh();
|
||||
hostView.refresh();
|
||||
excalidrawAPI.setActiveTool({type: "selection"});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -717,4 +746,4 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
|
||||
timestamp
|
||||
};
|
||||
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20spacing.svg"/></div>|[[#Fixed spacing]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance%20between%20centers.svg"/></div>|[[#Fixed vertical distance between centers]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance.svg"/></div>|[[#Fixed vertical distance]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.svg"/></div>|[[#Golden Ratio]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
|
||||
@@ -54,6 +55,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
| | |
|
||||
|----|-----|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Concatenate%20lines.svg"></div>|[[#Concatenate lines]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20connector.svg"/></div>|[[#Mindmap connector]]|
|
||||
@@ -66,6 +68,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
| | |
|
||||
|----|-----|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Relative%20Font%20Size%20Cycle.svg"/></div>|[[#Relative Font Size Cycle]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|
||||
@@ -114,11 +117,14 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.svg"/></div>|[[#Boolean Operations]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Texts.svg"/></div>|[[#Repeat Texts]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Elements%20of%20Type.svg"/></div>|[[#Select Elements of Type]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.svg"/></div>|[[#Select Similar Elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.svg"/></div>|[[#Slideshow]]|
|
||||
@@ -205,6 +211,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Change%20shape%20of%20selected%20elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script allows you to change the shape and fill style of selected Rectangles, Diamonds, Ellipses, Lines, Arrows and Freedraw.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-change-shape.jpg'></td></tr></table>
|
||||
|
||||
## Concatenate lines
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Concatenate%20lines.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Concatenate%20lines.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will connect two objects with an arrow. If either of the objects are a set of grouped elements (e.g. a text element grouped with an encapsulating rectangle), the script will identify these groups, and connect the arrow to the largest object in the group (assuming you want to connect the arrow to the box around the text element).<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-concatenate-lines.png'></td></tr></table>
|
||||
|
||||
## Connect elements
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.md
|
||||
@@ -337,12 +349,33 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script adds the `Folder Note Core: Make current document folder note` function to Excalidraw drawings. Running this script will convert the active Excalidraw drawing into a folder note. If you already have embedded images in your drawing, those attachments will not be moved when the folder note is created. You need to take care of those attachments separately, or convert the drawing to a folder note prior to adding the attachments. The script requires the <a href="https://github.com/aidenlx/folder-note-core" target="_blank">Folder Note Core</a> plugin.</td></tr></table>
|
||||
|
||||
## Golden Ratio
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Golden%20Ratio.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script performs two different functions depending on the elements selected in the view.<br>
|
||||
1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again.<br>
|
||||
2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/golden-ratio.jpg'><br><iframe width="400" height="225" src="https://www.youtube.com/embed/2SHn_ruax-s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
|
||||
## Grid selected images
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/7flash'>@7flash</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Grid%20Selected%20Images.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script arranges selected images into compact grid view, removing gaps in-between, resizing when necessary and breaking into multiple rows/columns.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-grid-selected-images.png'></td></tr></table>
|
||||
|
||||
## ExcaliAI
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
|
||||
|
||||
## GPT Draw-a-UI
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/GPT-Draw-a-UI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script was discontinued in favor of ExcaliAI. Draw a UI and let GPT create the code for you.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
|
||||
|
||||
|
||||
## Hardware Eraser Support
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.md
|
||||
@@ -409,6 +442,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><a href='https://youtu.be/Kwt_8WdOUT4' target='_blank'>Link to video on YouTube</a></td></tr></table>
|
||||
|
||||
## Relative Font Size Cycle
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Relative%20Font%20Size%20Cycle.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Relative%20Font%20Size%20Cycle.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will cycle through S, M, L, XL font sizes scaled to the current canvas zoom.</td></tr></table>
|
||||
|
||||
## Rename Image
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.md
|
||||
@@ -421,6 +460,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will detect the difference between 2 selected elements, including position, size, angle, stroke and background color, and create several elements that repeat these differences based on the number of repetitions entered by the user.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-elements.png'></td></tr></table>
|
||||
|
||||
## Repeat Texts
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Texts.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/soraliu'>@soraliu</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Texts.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">In the following script, we address the concept of repetition through the lens of numerical progression. As visualized by the image, where multiple circles each labeled with an even task number are being condensed into a linear sequence, our script will similarly iterate through a set of numbers</td></tr></table>
|
||||
|
||||
## Reverse arrows
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.md
|
||||
@@ -539,4 +584,4 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)</td></tr></table>
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)</td></tr></table>
|
||||
|
||||
|
Before Width: | Height: | Size: 373 KiB After Width: | Height: | Size: 341 KiB |
BIN
images/golden-ratio.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
images/scripts-concatenate-lines.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
images/scripts-draw-a-ui.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
images/scripts-repeat-texts.png
Executable file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/thumbnail-getting-started.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.9.19-mermaid-2",
|
||||
"version": "2.0.1-beta-2",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.9.19",
|
||||
"version": "2.0.14",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
"fundingUrl": "https://ko-fi.com/zsolt",
|
||||
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-excalidraw-plugin",
|
||||
"version": "1.9.15",
|
||||
"version": "2.0.14",
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
@@ -18,23 +18,23 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zsviczian/excalidraw": "0.15.3-obsidian-1",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-9",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colormaster": "^1.2.1",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"lz-string": "^1.5.0",
|
||||
"monkey-around": "^2.3.0",
|
||||
"polybooljs": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"roughjs": "^4.5.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"nanoid": "^4.0.2",
|
||||
"lucide-react": "^0.263.1"
|
||||
"lucide-react": "^0.263.1",
|
||||
"mathjax-full": "^3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lz-string": "^1.5.0",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
@@ -47,8 +47,9 @@
|
||||
"@rollup/plugin-typescript": "^11.1.2",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/js-beautify": "^1.14.0",
|
||||
"@types/node": "^20.5.6",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
@@ -56,14 +57,16 @@
|
||||
"obsidian": "^1.4.0",
|
||||
"prettier": "^3.0.1",
|
||||
"rollup": "^2.70.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"tslib": "^2.6.1",
|
||||
"ttypescript": "^1.5.15",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.2.2",
|
||||
"cssnano": "^6.0.2"
|
||||
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/typescript-estree": "5.3.0"
|
||||
|
||||
@@ -10,7 +10,9 @@ import webWorker from "rollup-plugin-web-worker-loader";
|
||||
import fs from'fs';
|
||||
import LZString from 'lz-string';
|
||||
import postprocess from 'rollup-plugin-postprocess';
|
||||
import cssnano from 'cssnano';
|
||||
|
||||
const DIST_FOLDER = 'dist';
|
||||
const isProd = (process.env.NODE_ENV === "production")
|
||||
const isLib = (process.env.NODE_ENV === "lib");
|
||||
console.log(`Running: ${process.env.NODE_ENV}`);
|
||||
@@ -25,16 +27,34 @@ const reactdom_pkg = isLib ? "" : isProd
|
||||
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
|
||||
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
|
||||
if(!isLib) {
|
||||
const excalidraw_styles = isProd
|
||||
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.production.css", "utf8")
|
||||
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.development.css", "utf8");
|
||||
const plugin_styles = fs.readFileSync("./styles.css", "utf8")
|
||||
const styles = plugin_styles + excalidraw_styles;
|
||||
cssnano()
|
||||
.process(styles) // Process the CSS
|
||||
.then(result => {
|
||||
fs.writeFileSync(`./${DIST_FOLDER}/styles.css`, result.css);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error while processing CSS:', error);
|
||||
});
|
||||
}
|
||||
|
||||
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
|
||||
const manifest = isLib ? {} : JSON.parse(manifestStr);
|
||||
!isLib && console.log(manifest.version);
|
||||
|
||||
const packageString = isLib ? "" : ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
|
||||
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
|
||||
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
|
||||
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);' +
|
||||
'const PLUGIN_VERSION="'+manifest.version+'";';
|
||||
const packageString = isLib
|
||||
? ""
|
||||
: ';' + lzstring_pkg +
|
||||
'\nconst EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";\n' +
|
||||
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
|
||||
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
|
||||
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
|
||||
'const PLUGIN_VERSION="'+manifest.version+'";';
|
||||
|
||||
const BASE_CONFIG = {
|
||||
input: 'src/main.ts',
|
||||
@@ -52,15 +72,16 @@ const getRollupPlugins = (tsconfig, ...plugins) =>
|
||||
const BUILD_CONFIG = {
|
||||
...BASE_CONFIG,
|
||||
output: {
|
||||
dir: '.',
|
||||
sourcemap: isProd?false:'inline',
|
||||
dir: DIST_FOLDER,
|
||||
entryFileNames: 'main.js',
|
||||
//sourcemap: isProd?false:'inline',
|
||||
format: 'cjs',
|
||||
exports: 'default',
|
||||
},
|
||||
plugins: [
|
||||
typescript2({
|
||||
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
|
||||
inlineSources: !isProd
|
||||
//inlineSources: !isProd
|
||||
}),
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
@@ -78,7 +99,10 @@ const BUILD_CONFIG = {
|
||||
nodeResolve({ browser: true, preferBuiltins: false }),
|
||||
...isProd
|
||||
? [
|
||||
terser({toplevel: false, compress: {passes: 2}}),
|
||||
terser({
|
||||
toplevel: false,
|
||||
compress: {passes: 2}
|
||||
}),
|
||||
//!postprocess - the version available on npmjs does not work, need this update:
|
||||
// npm install brettz9/rollup-plugin-postprocess#update --save-dev
|
||||
// https://github.com/developit/rollup-plugin-postprocess/issues/10
|
||||
@@ -91,6 +115,12 @@ const BUILD_CONFIG = {
|
||||
[/var React = require\('react'\);/, packageString],
|
||||
])
|
||||
],
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'manifest.json', dest: DIST_FOLDER },
|
||||
],
|
||||
verbose: true, // Optional: To display copied files in the console
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
|
||||
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
|
||||
|
||||
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
|
||||
import {
|
||||
ASSISTANT_FONT,
|
||||
CASCADIA_FONT,
|
||||
VIRGIL_FONT,
|
||||
} from "./constants/constFonts";
|
||||
import {
|
||||
DEFAULT_MD_EMBED_CSS,
|
||||
fileid,
|
||||
FRONTMATTER_KEY_BORDERCOLOR,
|
||||
@@ -15,15 +19,14 @@ import {
|
||||
IMAGE_TYPES,
|
||||
nanoid,
|
||||
THEME_FILTER,
|
||||
VIRGIL_FONT,
|
||||
} from "./constants";
|
||||
} from "./constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
import { t } from "./lang/helpers";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension } from "./utils/FileUtils";
|
||||
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "./utils/FileUtils";
|
||||
import {
|
||||
errorlog,
|
||||
getDataURL,
|
||||
@@ -39,7 +42,8 @@ import {
|
||||
svgToBase64,
|
||||
} from "./utils/Utils";
|
||||
import { ValueOf } from "./types";
|
||||
import { has } from "./svgToExcalidraw/attributes";
|
||||
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
import { mermaidToExcalidraw } from "src/constants/constants";
|
||||
|
||||
//An ugly workaround for the following situation.
|
||||
//File A is a markdown file that has an embedded Excalidraw file B
|
||||
@@ -54,6 +58,7 @@ export const IMAGE_MIME_TYPES = {
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
webp: "image/webp",
|
||||
bmp: "image/bmp",
|
||||
@@ -104,8 +109,12 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
|
||||
for (const [oldColor, newColor] of Object.entries(colorMap)) {
|
||||
const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi');
|
||||
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
|
||||
const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi');
|
||||
svg = svg.replaceAll(fillStyleRegex, `fill:${newColor}`);
|
||||
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'gi');
|
||||
svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`);
|
||||
const strokeStyleRegex = new RegExp(`stroke:${oldColor}`, 'gi');
|
||||
svg = svg.replaceAll(strokeStyleRegex, `stroke:${newColor}`);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
@@ -149,7 +158,8 @@ export class EmbeddedFile {
|
||||
public linkParts: LinkParts;
|
||||
private hostPath: string;
|
||||
public attemptCounter: number = 0;
|
||||
public isHyperlink: boolean = false;
|
||||
public isHyperLink: boolean = false;
|
||||
public isLocalLink: boolean = false;
|
||||
public hyperlink:DataURL;
|
||||
public colorMap: ColorMap | null = null;
|
||||
|
||||
@@ -169,12 +179,18 @@ export class EmbeddedFile {
|
||||
this.imgInverted = this.img = "";
|
||||
this.mtime = 0;
|
||||
|
||||
if(imgPath.startsWith("https://") || imgPath.startsWith("http://")){
|
||||
this.isHyperlink = true;
|
||||
if(imgPath.startsWith("https://") || imgPath.startsWith("http://") || imgPath.startsWith("ftp://") || imgPath.startsWith("ftps://")) {
|
||||
this.isHyperLink = true;
|
||||
this.hyperlink = imgPath as DataURL;
|
||||
return;
|
||||
};
|
||||
|
||||
if(imgPath.startsWith("file://")) {
|
||||
this.isLocalLink = true;
|
||||
this.hyperlink = imgPath as DataURL;
|
||||
return;
|
||||
}
|
||||
|
||||
this.linkParts = getLinkParts(imgPath);
|
||||
this.hostPath = hostPath;
|
||||
if (!this.linkParts.path) {
|
||||
@@ -202,11 +218,11 @@ export class EmbeddedFile {
|
||||
}
|
||||
|
||||
private fileChanged(): boolean {
|
||||
if(this.isHyperlink) {
|
||||
if(this.isHyperLink || this.isLocalLink) {
|
||||
return false;
|
||||
}
|
||||
if (!this.file) {
|
||||
this.file = app.metadataCache.getFirstLinkpathDest(
|
||||
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
|
||||
this.linkParts.path,
|
||||
this.hostPath,
|
||||
); // maybe the file has synchronized in the mean time
|
||||
@@ -225,13 +241,13 @@ export class EmbeddedFile {
|
||||
isDark: boolean,
|
||||
isSVGwithBitmap: boolean,
|
||||
) {
|
||||
if (!this.file && !this.isHyperlink) {
|
||||
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
|
||||
return;
|
||||
}
|
||||
if (this.fileChanged()) {
|
||||
this.imgInverted = this.img = "";
|
||||
}
|
||||
this.mtime = this.isHyperlink ? 0 : this.file.stat.mtime;
|
||||
this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime;
|
||||
this.size = size;
|
||||
this.mimeType = mimeType;
|
||||
switch (isDark && isSVGwithBitmap) {
|
||||
@@ -246,7 +262,7 @@ export class EmbeddedFile {
|
||||
}
|
||||
|
||||
public isLoaded(isDark: boolean): boolean {
|
||||
if(!this.isHyperlink) {
|
||||
if(!this.isHyperLink && !this.isLocalLink) {
|
||||
if (!this.file) {
|
||||
this.file = app.metadataCache.getFirstLinkpathDest(
|
||||
this.linkParts.path,
|
||||
@@ -268,7 +284,7 @@ export class EmbeddedFile {
|
||||
}
|
||||
|
||||
public getImage(isDark: boolean) {
|
||||
if (!this.file && !this.isHyperlink) {
|
||||
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
|
||||
return "";
|
||||
}
|
||||
if (isDark && this.isSVGwithBitmap) {
|
||||
@@ -282,7 +298,7 @@ export class EmbeddedFile {
|
||||
* @returns true if image should scale such as the updated images has the same area as the previous images, false if the image should be displayed at 100%
|
||||
*/
|
||||
public shouldScale() {
|
||||
return this.isHyperlink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
|
||||
return this.isHyperLink || this.isLocalLink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,12 +333,86 @@ export class EmbeddedFilesLoader {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getExcalidrawSVG ({
|
||||
isDark,
|
||||
file,
|
||||
depth,
|
||||
inFile,
|
||||
hasSVGwithBitmap,
|
||||
elements = [],
|
||||
}: {
|
||||
isDark: boolean;
|
||||
file: TFile;
|
||||
depth: number;
|
||||
inFile: TFile | EmbeddedFile;
|
||||
hasSVGwithBitmap: boolean;
|
||||
elements?: ExcalidrawElement[];
|
||||
}) : Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> {
|
||||
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
|
||||
const forceTheme = hasExportTheme(this.plugin, file)
|
||||
? getExportTheme(this.plugin, file, "light")
|
||||
: undefined;
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: hasExportBackground(this.plugin, file)
|
||||
? getWithBackground(this.plugin, file)
|
||||
: false,
|
||||
withTheme: !!forceTheme,
|
||||
};
|
||||
const svg = replaceSVGColors(
|
||||
await createSVG(
|
||||
file?.path,
|
||||
true,
|
||||
exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
null,
|
||||
elements,
|
||||
this.plugin,
|
||||
depth+1,
|
||||
getExportPadding(this.plugin, file),
|
||||
),
|
||||
inFile instanceof EmbeddedFile ? inFile.colorMap : null
|
||||
) as SVGSVGElement;
|
||||
|
||||
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
|
||||
const imageList = svg.querySelectorAll(
|
||||
"image:not([href^='data:image/svg'])",
|
||||
);
|
||||
if (imageList.length > 0) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
if (hasSVGwithBitmap && isDark) {
|
||||
imageList.forEach((i) => {
|
||||
const id = i.parentElement?.id;
|
||||
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
|
||||
u.setAttribute("filter", THEME_FILTER);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
const dURL = svgToBase64(svg.outerHTML) as DataURL;
|
||||
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
|
||||
};
|
||||
|
||||
//this is a fix for backward compatibility - I messed up with generating the local link
|
||||
private getLocalPath(path: string) {
|
||||
const localPath = path.split("file://")[1]
|
||||
if(localPath.startsWith("/")) {
|
||||
return localPath.substring(1);
|
||||
}
|
||||
return localPath;
|
||||
}
|
||||
|
||||
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<ImgData> {
|
||||
if (!this.plugin || !inFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isHyperlink = inFile instanceof EmbeddedFile ? inFile.isHyperlink : false;
|
||||
const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false;
|
||||
const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false;
|
||||
const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : "";
|
||||
const file: TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile;
|
||||
if(file && markdownRendererRecursionWatcthdog.has(file)) {
|
||||
@@ -331,7 +421,7 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
const linkParts =
|
||||
isHyperlink
|
||||
isHyperLink
|
||||
? null
|
||||
: inFile instanceof EmbeddedFile
|
||||
? inFile.linkParts
|
||||
@@ -346,11 +436,11 @@ export class EmbeddedFilesLoader {
|
||||
};
|
||||
|
||||
let hasSVGwithBitmap = false;
|
||||
const isExcalidrawFile = !isHyperlink && this.plugin.isExcalidrawFile(file);
|
||||
const isPDF = !isHyperlink && file.extension.toLowerCase() === "pdf";
|
||||
const isExcalidrawFile = !isHyperLink && !isLocalLink && this.plugin.isExcalidrawFile(file);
|
||||
const isPDF = !isHyperLink && !isLocalLink && file.extension.toLowerCase() === "pdf";
|
||||
|
||||
if (
|
||||
!isHyperlink && !isPDF &&
|
||||
!isHyperLink && !isPDF && !isLocalLink &&
|
||||
!(
|
||||
IMAGE_TYPES.contains(file.extension) ||
|
||||
isExcalidrawFile ||
|
||||
@@ -359,63 +449,26 @@ export class EmbeddedFilesLoader {
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const ab = isHyperlink || isPDF
|
||||
const ab = isHyperLink || isPDF
|
||||
? null
|
||||
: await app.vault.readBinary(file);
|
||||
: isLocalLink
|
||||
? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink))
|
||||
: await app.vault.readBinary(file);
|
||||
|
||||
const getExcalidrawSVG = async (isDark: boolean) => {
|
||||
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
|
||||
const forceTheme = hasExportTheme(this.plugin, file)
|
||||
? getExportTheme(this.plugin, file, "light")
|
||||
: undefined;
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: hasExportBackground(this.plugin, file)
|
||||
? getWithBackground(this.plugin, file)
|
||||
: false,
|
||||
withTheme: !!forceTheme,
|
||||
};
|
||||
const svg = replaceSVGColors(
|
||||
await createSVG(
|
||||
file.path,
|
||||
true,
|
||||
exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
this.plugin,
|
||||
depth+1,
|
||||
getExportPadding(this.plugin, file),
|
||||
),
|
||||
inFile instanceof EmbeddedFile ? inFile.colorMap : null
|
||||
) as SVGSVGElement;
|
||||
let dURL: DataURL = null;
|
||||
if (isExcalidrawFile) {
|
||||
const res = await this.getExcalidrawSVG({
|
||||
isDark: this.isDark,
|
||||
file,
|
||||
depth,
|
||||
inFile,
|
||||
hasSVGwithBitmap,
|
||||
});
|
||||
dURL = res.dataURL;
|
||||
hasSVGwithBitmap = res.hasSVGwithBitmap;
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
|
||||
const imageList = svg.querySelectorAll(
|
||||
"image:not([href^='data:image/svg'])",
|
||||
);
|
||||
if (imageList.length > 0) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
if (hasSVGwithBitmap && isDark) {
|
||||
imageList.forEach((i) => {
|
||||
const id = i.parentElement?.id;
|
||||
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
|
||||
u.setAttribute("filter", THEME_FILTER);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
const dURL = svgToBase64(svg.outerHTML) as DataURL;
|
||||
return dURL as DataURL;
|
||||
};
|
||||
|
||||
const excalidrawSVG = isExcalidrawFile
|
||||
? await getExcalidrawSVG(this.isDark)
|
||||
: null;
|
||||
const excalidrawSVG = isExcalidrawFile ? dURL : null;
|
||||
|
||||
const [pdfDataURL, pdfSize] = isPDF
|
||||
? await this.pdfToDataURL(file,linkParts)
|
||||
@@ -425,7 +478,7 @@ export class EmbeddedFilesLoader {
|
||||
? "image/png"
|
||||
: "image/svg+xml";
|
||||
|
||||
const extension = isHyperlink
|
||||
const extension = isHyperLink || isLocalLink
|
||||
? getURLImageExtension(hyperlink)
|
||||
: file.extension;
|
||||
if (!isExcalidrawFile && !isPDF) {
|
||||
@@ -433,20 +486,20 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
let dataURL =
|
||||
isHyperlink
|
||||
isHyperLink
|
||||
? (
|
||||
inFile instanceof EmbeddedFile
|
||||
? await getDataURLFromURL(inFile.hyperlink, mimeType)
|
||||
: null
|
||||
)
|
||||
: excalidrawSVG ?? pdfDataURL ??
|
||||
(file.extension === "svg"
|
||||
(file?.extension === "svg"
|
||||
? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null)
|
||||
: file.extension === "md"
|
||||
: file?.extension === "md"
|
||||
? null
|
||||
: await getDataURL(ab, mimeType));
|
||||
|
||||
if(!isHyperlink && !dataURL) {
|
||||
if(!isHyperLink && !dataURL && !isLocalLink) {
|
||||
markdownRendererRecursionWatcthdog.add(file);
|
||||
const result = await this.convertMarkdownToSVG(this.plugin, file, linkParts, depth);
|
||||
markdownRendererRecursionWatcthdog.delete(file);
|
||||
@@ -458,10 +511,10 @@ export class EmbeddedFilesLoader {
|
||||
return {
|
||||
mimeType,
|
||||
fileId: await generateIdFromFile(
|
||||
isHyperlink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
|
||||
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
|
||||
),
|
||||
dataURL,
|
||||
created: isHyperlink ? 0 : file.stat.mtime,
|
||||
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
|
||||
hasSVGwithBitmap,
|
||||
size,
|
||||
};
|
||||
@@ -492,7 +545,7 @@ export class EmbeddedFilesLoader {
|
||||
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"});
|
||||
const data = await this._getObsidianImage(embeddedFile, depth);
|
||||
if (data) {
|
||||
const fileData = {
|
||||
const fileData: FileData = {
|
||||
mimeType: data.mimeType,
|
||||
id: entry.value[0],
|
||||
dataURL: data.dataURL,
|
||||
@@ -534,7 +587,7 @@ export class EmbeddedFilesLoader {
|
||||
while (!this.terminate && !(equation = equations.next()).done) {
|
||||
if (!excalidrawData.getEquation(equation.value[0]).isLoaded) {
|
||||
const latex = equation.value[1].latex;
|
||||
const data = await tex2dataURL(latex, this.plugin);
|
||||
const data = await tex2dataURL(latex);
|
||||
if (data) {
|
||||
const fileData = {
|
||||
mimeType: data.mimeType,
|
||||
@@ -550,6 +603,59 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
if(shouldRenderMermaid()) {
|
||||
const mermaidElements = getMermaidImageElements(excalidrawData.scene.elements);
|
||||
for(const element of mermaidElements) {
|
||||
if(this.terminate) {
|
||||
continue;
|
||||
}
|
||||
const data = getMermaidText(element);
|
||||
const result = await mermaidToExcalidraw(data, {fontSize: 20}, true);
|
||||
if(!result) {
|
||||
continue;
|
||||
}
|
||||
if(result?.files) {
|
||||
for (const key in result.files) {
|
||||
const fileData = {
|
||||
...result.files[key],
|
||||
id: element.fileId,
|
||||
created: Date.now(),
|
||||
hasSVGwithBitmap: false,
|
||||
shouldScale: true,
|
||||
size: await getImageSize(result.files[key].dataURL),
|
||||
};
|
||||
files.push(fileData);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(result?.elements) {
|
||||
//handle case that mermaidToExcalidraw has implemented this type of diagram in the mean time
|
||||
const res = await this.getExcalidrawSVG({
|
||||
isDark: this.isDark,
|
||||
file: null,
|
||||
depth,
|
||||
inFile: null,
|
||||
hasSVGwithBitmap: false,
|
||||
elements: result.elements
|
||||
});
|
||||
if(res?.dataURL) {
|
||||
const size = await getImageSize(res.dataURL);
|
||||
const fileData:FileData = {
|
||||
mimeType: "image/svg+xml",
|
||||
id: element.fileId,
|
||||
dataURL: res.dataURL,
|
||||
created: Date.now(),
|
||||
hasSVGwithBitmap: res.hasSVGwithBitmap,
|
||||
size,
|
||||
shouldScale: true,
|
||||
};
|
||||
files.push(fileData);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.emptyPDFDocsMap();
|
||||
if (this.terminate) {
|
||||
return;
|
||||
@@ -650,6 +756,9 @@ export class EmbeddedFilesLoader {
|
||||
case "Cascadia":
|
||||
fontDef = CASCADIA_FONT;
|
||||
break;
|
||||
case "Assistant":
|
||||
fontDef = ASSISTANT_FONT;
|
||||
break;
|
||||
case "":
|
||||
fontDef = "";
|
||||
break;
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
ExcalidrawTextElement,
|
||||
StrokeRoundness,
|
||||
RoundnessType,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import { Editor, normalizePath, Notice, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
|
||||
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings, TextMode } from "src/ExcalidrawView";
|
||||
import { ExcalidrawData, getMarkdownDrawingSection, REGEX_LINK } from "src/ExcalidrawData";
|
||||
@@ -34,8 +34,8 @@ import {
|
||||
REG_LINKINDEX_INVALIDCHARS,
|
||||
THEME_FILTER,
|
||||
mermaidToExcalidraw,
|
||||
} from "src/constants";
|
||||
import { getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
|
||||
} from "src/constants/constants";
|
||||
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
|
||||
import {
|
||||
//debug,
|
||||
embedFontsInSVG,
|
||||
@@ -51,10 +51,10 @@ import {
|
||||
wrapTextAtCharLength,
|
||||
} from "src/utils/Utils";
|
||||
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark } from "src/utils/ObsidianUtils";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "src/LaTeX";
|
||||
import { NewFileActions, Prompt } from "src/dialogs/Prompt";
|
||||
import { GenericInputPrompt, NewFileActions, Prompt } from "src/dialogs/Prompt";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
@@ -74,11 +74,18 @@ import RYBPlugin from "colormaster/plugins/ryb";
|
||||
import CMYKPlugin from "colormaster/plugins/cmyk";
|
||||
import { TInput } from "colormaster/types";
|
||||
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
|
||||
import { ROUNDNESS } from "src/constants";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
import { ROUNDNESS } from "src/constants/constants";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
|
||||
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import PolyBool from "polybooljs";
|
||||
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
|
||||
import {
|
||||
AIRequest,
|
||||
postOpenAI as _postOpenAI,
|
||||
extractCodeBlocks as _extractCodeBlocks,
|
||||
} from "./utils/AIUtils";
|
||||
import { EXCALIDRAW_AUTOMATE_INFO } from "./dialogs/SuggesterInfo";
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -97,6 +104,7 @@ extendPlugins([
|
||||
]);
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
|
||||
const GAP = 4;
|
||||
|
||||
@@ -108,10 +116,115 @@ export class ExcalidrawAutomate {
|
||||
return obsidian_module;
|
||||
};
|
||||
|
||||
get LASERPOINTER() {
|
||||
return this.plugin.settings.laserSettings;
|
||||
}
|
||||
|
||||
get DEVICE():DeviceType {
|
||||
return DEVICE;
|
||||
}
|
||||
|
||||
public help(target: Function | string) {
|
||||
if (!target) {
|
||||
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName')");
|
||||
return;
|
||||
}
|
||||
|
||||
let funcInfo;
|
||||
|
||||
if (typeof target === 'function') {
|
||||
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name);
|
||||
} else if (typeof target === 'string') {
|
||||
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target);
|
||||
}
|
||||
|
||||
if(!funcInfo) {
|
||||
console.log("Usage: ea.help(ea.functionName) or\nea.help('propertyName') - notice property name is in quotes");
|
||||
return;
|
||||
}
|
||||
|
||||
if (funcInfo.desc) {
|
||||
const formattedDesc = funcInfo.desc
|
||||
.replaceAll("<br>", "\n")
|
||||
.replace(/<code>(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space
|
||||
.replace(/<b>(.*?)<\/b>/g, '%c\u200b$1%c') // Zero-width space
|
||||
.replace(/<a onclick='window\.open\("(.*?)"\)'>(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
|
||||
|
||||
const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
|
||||
|
||||
console.log(`Declaration: ${funcInfo.code}`);
|
||||
console.log(`Description: ${formattedDesc}`, ...styles);
|
||||
} else {
|
||||
console.log("Description not available for this function.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post's an AI request to the OpenAI API and returns the response.
|
||||
* @param request
|
||||
* @returns
|
||||
*/
|
||||
public async postOpenAI (request: AIRequest): Promise<RequestUrlResponse> {
|
||||
return await _postOpenAI(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the codeblock contents from the supplied markdown string.
|
||||
* @param markdown
|
||||
* @param codeblockType
|
||||
* @returns an array of dictionaries with the codeblock contents and type
|
||||
*/
|
||||
public extractCodeBlocks(markdown: string): { data: string, type: string }[] {
|
||||
return _extractCodeBlocks(markdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* converts a string to a DataURL
|
||||
* @param htmlString
|
||||
* @returns dataURL
|
||||
*/
|
||||
public async convertStringToDataURL (data:string, type: string = "text/html"):Promise<string> {
|
||||
// Create a blob from the HTML string
|
||||
const blob = new Blob([data], { type });
|
||||
|
||||
// Read the blob as Data URL
|
||||
const base64String = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if(typeof reader.result === "string") {
|
||||
const base64String = reader.result.split(',')[1];
|
||||
resolve(base64String);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
if(base64String) {
|
||||
return `data:${type};base64,${base64String}`;
|
||||
}
|
||||
return "about:blank";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the folder exists, if not, creates it.
|
||||
* @param folderpath
|
||||
* @returns
|
||||
*/
|
||||
public async checkAndCreateFolder(folderpath: string): Promise<TFolder> {
|
||||
return await checkAndCreateFolder(folderpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename.
|
||||
* @param filename
|
||||
* @param folderpath
|
||||
* @returns
|
||||
*/
|
||||
public getNewUniqueFilepath(filename: string, folderpath: string): string {
|
||||
return getNewUniqueFilepath(app.vault, filename, folderpath);
|
||||
}
|
||||
|
||||
public async getAttachmentFilepath(filename: string): Promise<string> {
|
||||
if (!this.targetView || !this.targetView?.file) {
|
||||
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
|
||||
@@ -121,6 +234,14 @@ export class ExcalidrawAutomate {
|
||||
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
|
||||
}
|
||||
|
||||
public compressToBase64(str:string):string {
|
||||
return LZString.compressToBase64(str);
|
||||
}
|
||||
|
||||
public decompressFromBase64(str:string):string {
|
||||
return LZString.decompressFromBase64(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user with a dialog to select new file action.
|
||||
* - create markdown file
|
||||
@@ -147,14 +268,14 @@ export class ExcalidrawAutomate {
|
||||
return null;
|
||||
}
|
||||
const modifierKeys = emulateKeysForLinkClick(targetPane);
|
||||
const newFilePrompt = new NewFileActions(
|
||||
this.plugin,
|
||||
newFileNameOrPath,
|
||||
modifierKeys,
|
||||
this.targetView,
|
||||
shouldOpenNewFile,
|
||||
parentFile
|
||||
)
|
||||
const newFilePrompt = new NewFileActions({
|
||||
plugin: this.plugin,
|
||||
path: newFileNameOrPath,
|
||||
keys: modifierKeys,
|
||||
view: this.targetView,
|
||||
openNewFile: shouldOpenNewFile,
|
||||
parentFile: parentFile
|
||||
})
|
||||
newFilePrompt.open();
|
||||
return await newFilePrompt.waitForClose;
|
||||
}
|
||||
@@ -217,7 +338,7 @@ export class ExcalidrawAutomate {
|
||||
fontSize: number;
|
||||
textAlign: string; //"left"|"right"|"center"
|
||||
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
|
||||
startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null
|
||||
startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
|
||||
endArrowHead: string;
|
||||
};
|
||||
canvas: {
|
||||
@@ -402,7 +523,7 @@ export class ExcalidrawAutomate {
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
*/
|
||||
getElements(): ExcalidrawElement[] {
|
||||
getElements(): Mutable<ExcalidrawElement>[] {
|
||||
const elements = [];
|
||||
const elementIds = Object.keys(this.elementsDict);
|
||||
for (let i = 0; i < elementIds.length; i++) {
|
||||
@@ -416,7 +537,7 @@ export class ExcalidrawAutomate {
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
getElement(id: string): ExcalidrawElement {
|
||||
getElement(id: string): Mutable<ExcalidrawElement> {
|
||||
return this.elementsDict[id];
|
||||
};
|
||||
|
||||
@@ -639,6 +760,7 @@ export class ExcalidrawAutomate {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param templatePath
|
||||
@@ -694,6 +816,28 @@ export class ExcalidrawAutomate {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for createPNG() that returns a base64 encoded string
|
||||
* @param templatePath
|
||||
* @param scale
|
||||
* @param exportSettings
|
||||
* @param loader
|
||||
* @param theme
|
||||
* @param padding
|
||||
* @returns
|
||||
*/
|
||||
async createPNGBase64(
|
||||
templatePath?: string,
|
||||
scale: number = 1,
|
||||
exportSettings?: ExportSettings,
|
||||
loader?: EmbeddedFilesLoader,
|
||||
theme?: string,
|
||||
padding?: number,
|
||||
): Promise<string> {
|
||||
const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding);
|
||||
return `data:image/png;base64,${await blobToBase64(png)}`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text
|
||||
@@ -712,6 +856,7 @@ export class ExcalidrawAutomate {
|
||||
w: number,
|
||||
h: number,
|
||||
link: string | null = null,
|
||||
scale?: [number, number],
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
@@ -742,6 +887,7 @@ export class ExcalidrawAutomate {
|
||||
boundElements: [] as any,
|
||||
link,
|
||||
locked: false,
|
||||
...scale ? {scale} : {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -757,7 +903,15 @@ export class ExcalidrawAutomate {
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
public addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
|
||||
public addEmbeddable(
|
||||
topX: number,
|
||||
topY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
url?: string,
|
||||
file?: TFile,
|
||||
embeddableCustomData?: EmbeddableMDCustomProps,
|
||||
): string {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "addEmbeddable()");
|
||||
@@ -784,7 +938,9 @@ export class ExcalidrawAutomate {
|
||||
false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
|
||||
)
|
||||
}]]` : "",
|
||||
[1,1],
|
||||
);
|
||||
this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults};
|
||||
return id;
|
||||
};
|
||||
|
||||
@@ -1073,8 +1229,8 @@ export class ExcalidrawAutomate {
|
||||
addArrow(
|
||||
points: [x: number, y: number][],
|
||||
formatting?: {
|
||||
startArrowHead?: string;
|
||||
endArrowHead?: string;
|
||||
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
startObjectId?: string;
|
||||
endObjectId?: string;
|
||||
},
|
||||
@@ -1142,15 +1298,18 @@ export class ExcalidrawAutomate {
|
||||
|
||||
/**
|
||||
* Adds a mermaid diagram to ExcalidrawAutomate elements
|
||||
* @param diagram
|
||||
* @returns the ids of the elements that were created
|
||||
* @param diagram string containing the mermaid diagram
|
||||
* @param groupElements default is trud. If true, the elements will be grouped
|
||||
* @returns the ids of the elements that were created or null if there was an error
|
||||
*/
|
||||
async addMermaid(
|
||||
diagram: string,
|
||||
): Promise<string[]> {
|
||||
groupElements: boolean = true,
|
||||
): Promise<string[]|string> {
|
||||
const result = await mermaidToExcalidraw(diagram, {fontSize: this.style.fontSize});
|
||||
const ids:string[] = [];
|
||||
if(!result) return ids;
|
||||
if(!result) return null;
|
||||
if(result?.error) return result.error;
|
||||
|
||||
if(result?.elements) {
|
||||
result.elements.forEach(el=>{
|
||||
@@ -1164,7 +1323,7 @@ export class ExcalidrawAutomate {
|
||||
this.imagesDict[key as FileId] = {
|
||||
...result.files[key],
|
||||
created: Date.now(),
|
||||
isHyperlink: false,
|
||||
isHyperLink: false,
|
||||
hyperlink: null,
|
||||
file: null,
|
||||
hasSVGwithBitmap: false,
|
||||
@@ -1172,6 +1331,10 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(groupElements && result?.elements && ids.length > 1) {
|
||||
this.addToGroup(ids);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -1209,7 +1372,7 @@ export class ExcalidrawAutomate {
|
||||
id: fileId,
|
||||
dataURL: image.dataURL,
|
||||
created: image.created,
|
||||
isHyperlink: typeof imageFile === "string",
|
||||
isHyperLink: typeof imageFile === "string",
|
||||
hyperlink: typeof imageFile === "string"
|
||||
? imageFile
|
||||
: null,
|
||||
@@ -1250,7 +1413,7 @@ export class ExcalidrawAutomate {
|
||||
*/
|
||||
async addLaTex(topX: number, topY: number, tex: string): Promise<string> {
|
||||
const id = nanoid();
|
||||
const image = await tex2dataURL(tex, this.plugin);
|
||||
const image = await tex2dataURL(tex);
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
@@ -1284,8 +1447,8 @@ export class ExcalidrawAutomate {
|
||||
* @param connectionB when passed null, Excalidraw will automatically decide
|
||||
* @param formatting
|
||||
* numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
|
||||
* startArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
|
||||
* endArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
|
||||
* startArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
|
||||
* endArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
|
||||
* padding:
|
||||
* @returns
|
||||
*/
|
||||
@@ -1296,8 +1459,8 @@ export class ExcalidrawAutomate {
|
||||
connectionB: ConnectionPoint | null,
|
||||
formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
|
||||
endArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
|
||||
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
padding?: number;
|
||||
},
|
||||
): string {
|
||||
@@ -1543,11 +1706,7 @@ export class ExcalidrawAutomate {
|
||||
errorMessage("targetView not set", "getViewElements()");
|
||||
return [];
|
||||
}
|
||||
const api = this.targetView.excalidrawAPI;
|
||||
if (!api) {
|
||||
return [];
|
||||
}
|
||||
return api.getSceneElements();
|
||||
return this.targetView.getViewElements();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1561,12 +1720,12 @@ export class ExcalidrawAutomate {
|
||||
errorMessage("targetView not set", "deleteViewElements()");
|
||||
return false;
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
if (!current) {
|
||||
const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if (!api) {
|
||||
return false;
|
||||
}
|
||||
const el: ExcalidrawElement[] = current.getSceneElements();
|
||||
const st: AppState = current.getAppState();
|
||||
const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
|
||||
const st: AppState = api.getAppState();
|
||||
this.targetView.updateScene({
|
||||
elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)),
|
||||
appState: st,
|
||||
@@ -1625,10 +1784,26 @@ export class ExcalidrawAutomate {
|
||||
* copies elements from view to elementsDict for editing
|
||||
* @param elements
|
||||
*/
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void {
|
||||
elements.forEach((el) => {
|
||||
this.elementsDict[el.id] = cloneElement(el);
|
||||
});
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void {
|
||||
if(copyImages && elements.some(el=>el.type === "image")) {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "copyViewElementsToEAforEditing()");
|
||||
return;
|
||||
}
|
||||
const sceneFiles = this.targetView.getScene().files;
|
||||
elements.forEach((el) => {
|
||||
this.elementsDict[el.id] = cloneElement(el);
|
||||
if(el.type === "image") {
|
||||
this.imagesDict[el.fileId] = sceneFiles?.[el.fileId];
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
elements.forEach((el) => {
|
||||
this.elementsDict[el.id] = cloneElement(el);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1710,8 +1885,8 @@ export class ExcalidrawAutomate {
|
||||
connectionB: ConnectionPoint | null,
|
||||
formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
|
||||
endArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
|
||||
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
padding?: number;
|
||||
},
|
||||
): boolean {
|
||||
@@ -2312,7 +2487,7 @@ export class ExcalidrawAutomate {
|
||||
* Gets the class PolyBool from https://github.com/velipso/polybooljs
|
||||
* @returns
|
||||
*/
|
||||
getPolyool() {
|
||||
getPolyBool() {
|
||||
const defaultEpsilon = 0.0000000001;
|
||||
PolyBool.epsilon(defaultEpsilon);
|
||||
return PolyBool;
|
||||
@@ -2590,12 +2765,12 @@ export async function createPNG(
|
||||
);
|
||||
}
|
||||
|
||||
const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
|
||||
export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
|
||||
elements: ExcalidrawElement[];
|
||||
hostFile: TFile;
|
||||
}): ExcalidrawElement[] => {
|
||||
return elements.map((el)=>{
|
||||
if(el.type!=="embeddable" && el.link && el.link.startsWith("[")) {
|
||||
if(el.link && el.link.startsWith("[")) {
|
||||
const partsArray = REGEX_LINK.getResList(el.link)[0];
|
||||
if(!partsArray?.value) return el;
|
||||
let linkText = REGEX_LINK.getLink(partsArray);
|
||||
@@ -2683,6 +2858,7 @@ export async function createSVG(
|
||||
withTheme,
|
||||
},
|
||||
padding,
|
||||
null,
|
||||
);
|
||||
|
||||
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
|
||||
@@ -2791,13 +2967,16 @@ function errorMessage(message: string, source: string) {
|
||||
export const insertLaTeXToView = (view: ExcalidrawView) => {
|
||||
const app = view.plugin.app;
|
||||
const ea = view.plugin.ea;
|
||||
const prompt = new Prompt(
|
||||
GenericInputPrompt.Prompt(
|
||||
view,
|
||||
view.plugin,
|
||||
app,
|
||||
t("ENTER_LATEX"),
|
||||
view.plugin.settings.latexBoilerplate,
|
||||
"\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
|
||||
);
|
||||
prompt.openAndGetValue(async (formula: string) => {
|
||||
view.plugin.settings.latexBoilerplate,
|
||||
undefined,
|
||||
3
|
||||
).then(async (formula: string) => {
|
||||
if (!formula) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
originalText: this is the text without added linebreaks for wrapping. This will be parsed or markup depending on view mode
|
||||
rawText: text with original markdown markup and without the added linebreaks for wrapping
|
||||
*/
|
||||
import { App, TFile } from "obsidian";
|
||||
import { App, Notice, TFile } from "obsidian";
|
||||
import {
|
||||
nanoid,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
wrapText,
|
||||
ERROR_IFRAME_CONVERSION_CANCELED,
|
||||
JSON_parse,
|
||||
} from "./constants";
|
||||
} from "./constants/constants";
|
||||
import { _measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
hasExportTheme,
|
||||
isVersionNewerThanOther,
|
||||
LinkParts,
|
||||
updateFrontmatterInString,
|
||||
wrapTextAtCharLength,
|
||||
} from "./utils/Utils";
|
||||
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
@@ -49,10 +50,11 @@ import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/types";
|
||||
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
|
||||
import { ConfirmationPrompt, Prompt } from "./dialogs/Prompt";
|
||||
import { ConfirmationPrompt } from "./dialogs/Prompt";
|
||||
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
|
||||
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
|
||||
|
||||
@@ -121,7 +123,7 @@ export const REGEX_LINK = {
|
||||
//added \n at and of DRAWING_REG: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/357
|
||||
const DRAWING_REG = /\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
|
||||
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
||||
const DRAWING_COMPRESSED_REG =
|
||||
export const DRAWING_COMPRESSED_REG =
|
||||
/(\n# Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
|
||||
const DRAWING_COMPRESSED_REG_FALLBACK =
|
||||
/(\n# Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
|
||||
@@ -242,6 +244,26 @@ const estimateMaxLineLen = (text: string, originalText: string): number => {
|
||||
const wrap = (text: string, lineLen: number) =>
|
||||
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
|
||||
|
||||
export const getExcalidrawMarkdownHeaderSection = (data:string, keys:[string,string][]):string => {
|
||||
let trimLocation = data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search(/(%%\n)?# Drawing\n/);
|
||||
}
|
||||
if (trimLocation == -1) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let header = updateFrontmatterInString(data.substring(0, trimLocation),keys);
|
||||
//this should be removed at a later time. Left it here to remediate 1.4.9 mistake
|
||||
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
|
||||
if (header.match(REG_IMG)) {
|
||||
header = header.replace(REG_IMG, "$1");
|
||||
}
|
||||
//end of remove
|
||||
return header;
|
||||
}
|
||||
|
||||
|
||||
export class ExcalidrawData {
|
||||
public textElements: Map<
|
||||
string,
|
||||
@@ -261,6 +283,7 @@ export class ExcalidrawData {
|
||||
public loaded: boolean = false;
|
||||
public files: Map<FileId, EmbeddedFile> = null; //fileId, path
|
||||
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
|
||||
private mermaids: Map<FileId, { mermaid: string; isLoaded: boolean }> = null; //fileId, path
|
||||
private compatibilityMode: boolean = false;
|
||||
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
|
||||
|
||||
@@ -270,6 +293,7 @@ export class ExcalidrawData {
|
||||
this.app = plugin.app;
|
||||
this.files = new Map<FileId, EmbeddedFile>();
|
||||
this.equations = new Map<FileId, { latex: string; isLoaded: boolean }>();
|
||||
this.mermaids = new Map<FileId, { mermaid: string; isLoaded: boolean }>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,14 +308,24 @@ export class ExcalidrawData {
|
||||
|
||||
const elements = this.scene.elements;
|
||||
for (const el of elements) {
|
||||
if(el.type === "iframe") {
|
||||
if(el.type === "iframe" && !el.customData) {
|
||||
el.type = "embeddable";
|
||||
}
|
||||
|
||||
if (el.boundElements) {
|
||||
const map = new Map<string, string>();
|
||||
let alreadyHasText:boolean = false;
|
||||
el.boundElements.forEach((item: { id: string; type: string }) => {
|
||||
map.set(item.id, item.type);
|
||||
if(item.type === "text") {
|
||||
if(!alreadyHasText) {
|
||||
map.set(item.id, item.type);
|
||||
alreadyHasText = true;
|
||||
} else {
|
||||
elements.find((el:ExcalidrawElement)=>el.id===item.id).containerId = null;
|
||||
}
|
||||
} else {
|
||||
map.set(item.id, item.type);
|
||||
}
|
||||
});
|
||||
const boundElements = Array.from(map, ([id, type]) => ({ id, type }));
|
||||
if (boundElements.length !== el.boundElements.length) {
|
||||
@@ -434,9 +468,10 @@ export class ExcalidrawData {
|
||||
>();
|
||||
this.elementLinks = new Map<string, string>();
|
||||
if (this.file != file) {
|
||||
//this is a reload - files and equations will take care of reloading when needed
|
||||
//this is a reload - files, equations and mermaids will take care of reloading when needed
|
||||
this.files.clear();
|
||||
this.equations.clear();
|
||||
this.mermaids.clear();
|
||||
}
|
||||
this.file = file;
|
||||
this.compatibilityMode = false;
|
||||
@@ -501,7 +536,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
//once off migration of legacy scenes
|
||||
if(this.scene?.elements?.some((el:any)=>el.type==="iframe")) {
|
||||
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
|
||||
const prompt = new ConfirmationPrompt(
|
||||
this.plugin,
|
||||
"This file contains embedded frames " +
|
||||
@@ -600,7 +635,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
//Load links
|
||||
const REG_LINKID_FILEPATH = /([\w\d]*):\s*(https?:\/\/[^\s]*)\n/gm;
|
||||
const REG_LINKID_FILEPATH = /([\w\d]*):\s*((?:https?|file|ftps?):\/\/[^\s]*)\n/gm;
|
||||
res = data.matchAll(REG_LINKID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
@@ -612,7 +647,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
//Load Equations
|
||||
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$(.*)(\$\$\s*\n)/gm;
|
||||
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$([\s\S]*?)(\$\$\s*\n)/gm;
|
||||
res = data.matchAll(REG_FILEID_EQUATION);
|
||||
while (!(parts = res.next()).done) {
|
||||
this.setEquation(parts.value[1] as FileId, {
|
||||
@@ -621,6 +656,16 @@ export class ExcalidrawData {
|
||||
});
|
||||
}
|
||||
|
||||
//Load Mermaids
|
||||
const mermaidElements = getMermaidImageElements(this.scene.elements);
|
||||
if(mermaidElements.length>0 && !shouldRenderMermaid()) {
|
||||
new Notice ("Mermaid images are only supported in Obsidian 1.4.14 and above. Please update Obsidian to see the mermaid images in this drawing. Obsidian mobile 1.4.14 currently only avaiable to Obsidian insiders", 5000);
|
||||
} else {
|
||||
mermaidElements.forEach(el =>
|
||||
this.setMermaid(el.fileId, {mermaid: getMermaidText(el), isLoaded: false})
|
||||
);
|
||||
}
|
||||
|
||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
||||
//e.g. if the entire text elements section was deleted.
|
||||
this.findNewTextElementsInScene();
|
||||
@@ -657,6 +702,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
this.files.clear();
|
||||
this.equations.clear();
|
||||
this.mermaids.clear();
|
||||
this.findNewTextElementsInScene();
|
||||
this.findNewElementLinksInScene();
|
||||
await this.setTextMode(TextMode.raw, true); //legacy files are always displayed in raw mode.
|
||||
@@ -984,8 +1030,9 @@ export class ExcalidrawData {
|
||||
if (parsedLink) {
|
||||
outString += parsedLink;
|
||||
if (!(urlIcon || linkIcon)) {
|
||||
if (REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) {
|
||||
urlIcon = true;
|
||||
const linkText = REGEX_LINK.getLink(parts);
|
||||
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links
|
||||
} else {
|
||||
linkIcon = true;
|
||||
}
|
||||
@@ -1061,8 +1108,9 @@ export class ExcalidrawData {
|
||||
if (parsedLink) {
|
||||
outString += parsedLink;
|
||||
if (!(urlIcon || linkIcon)) {
|
||||
if (REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) {
|
||||
urlIcon = true;
|
||||
const linkText = REGEX_LINK.getLink(parts);
|
||||
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links
|
||||
} else {
|
||||
linkIcon = true;
|
||||
}
|
||||
@@ -1103,6 +1151,7 @@ export class ExcalidrawData {
|
||||
outString += `${this.elementLinks.get(key)} ^${key}\n\n`;
|
||||
}
|
||||
|
||||
// deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
|
||||
outString +=
|
||||
this.equations.size > 0 || this.files.size > 0
|
||||
? "\n# Embedded files\n"
|
||||
@@ -1116,12 +1165,12 @@ export class ExcalidrawData {
|
||||
for (const key of this.files.keys()) {
|
||||
const PATHREG = /(^[^#\|]*)/;
|
||||
const ef = this.files.get(key);
|
||||
if(ef.isHyperlink) {
|
||||
if(ef.isHyperLink || ef.isLocalLink) {
|
||||
outString += `${key}: ${ef.hyperlink}\n`;
|
||||
} else {
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829
|
||||
const path = ef.file
|
||||
? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path))
|
||||
? ef.linkParts.original.replace(PATHREG,this.app.metadataCache.fileToLinktext(ef.file,this.file.path))
|
||||
: ef.linkParts.original;
|
||||
const colorMap = ef.colorMap ? " " + JSON.stringify(ef.colorMap) : "";
|
||||
outString += `${key}: [[${path}]]${colorMap}\n`;
|
||||
@@ -1207,11 +1256,8 @@ export class ExcalidrawData {
|
||||
const scene = this.scene as SceneDataWithFiles;
|
||||
|
||||
//remove files and equations that no longer have a corresponding image element
|
||||
const fileIds = (
|
||||
scene.elements.filter(
|
||||
(e) => e.type === "image",
|
||||
) as ExcalidrawImageElement[]
|
||||
).map((e) => e.fileId);
|
||||
const images = scene.elements.filter((e) => e.type === "image") as ExcalidrawImageElement[];
|
||||
const fileIds = (images).map((e) => e.fileId);
|
||||
this.files.forEach((value, key) => {
|
||||
if (!fileIds.contains(key)) {
|
||||
this.files.delete(key);
|
||||
@@ -1226,38 +1272,59 @@ export class ExcalidrawData {
|
||||
}
|
||||
});
|
||||
|
||||
this.mermaids.forEach((value, key) => {
|
||||
if (!fileIds.contains(key)) {
|
||||
this.mermaids.delete(key);
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//check if there are any images that need to be processed in the new scene
|
||||
if (!scene.files || Object.keys(scene.files).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//assing new fileId to duplicate equation and markdown files
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/601
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/593
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
|
||||
const processedIds = new Set<string>();
|
||||
fileIds.forEach(fileId=>{
|
||||
fileIds.forEach((fileId,idx)=>{
|
||||
if(processedIds.has(fileId)) {
|
||||
const file = this.getFile(fileId);
|
||||
//const file = this.files.get(fileId as FileId);
|
||||
const equation = this.getEquation(fileId);
|
||||
//const equation = this.equations.get(fileId as FileId);
|
||||
//images should have a single reference, but equations and markdown embeds should have as many as instances of the file in the scene
|
||||
if(file && (file.isHyperlink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
|
||||
const mermaid = this.getMermaid(fileId);
|
||||
|
||||
|
||||
|
||||
//images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene
|
||||
if(file && (file.isHyperLink || file.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
|
||||
return;
|
||||
}
|
||||
if(mermaid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(getMermaidText(images[idx])) {
|
||||
this.setMermaid(fileId, {mermaid: getMermaidText(images[idx]), isLoaded: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = fileid();
|
||||
//scene.files[newId] = {...scene.files[fileId]};
|
||||
(scene.elements.filter((el:ExcalidrawImageElement)=>el.fileId === fileId)[0] as any).fileId = newId;
|
||||
(scene
|
||||
.elements
|
||||
.filter((el:ExcalidrawImageElement)=>el.fileId === fileId)
|
||||
.sort((a,b)=>a.updated<b.updated ? 1 : -1)[0] as any)
|
||||
.fileId = newId;
|
||||
dirty = true;
|
||||
processedIds.add(newId);
|
||||
if(file) {
|
||||
this.setFile(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,file.linkParts.original));
|
||||
//this.files.set(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,file.linkParts.original))
|
||||
}
|
||||
if(equation) {
|
||||
this.setEquation(newId as FileId, {latex:equation.latex, isLoaded:false});
|
||||
//this.equations.set(newId as FileId, equation);
|
||||
}
|
||||
}
|
||||
processedIds.add(fileId);
|
||||
@@ -1265,7 +1332,8 @@ export class ExcalidrawData {
|
||||
|
||||
|
||||
for (const key of Object.keys(scene.files)) {
|
||||
if (!(this.hasFile(key as FileId) || this.hasEquation(key as FileId))) {
|
||||
const mermaidElements = getMermaidImageElements(scene.elements.filter((el:ExcalidrawImageElement)=>el.fileId === key));
|
||||
if (!(this.hasFile(key as FileId) || this.hasEquation(key as FileId) || this.hasMermaid(key as FileId) || mermaidElements.length > 0)) {
|
||||
dirty = true;
|
||||
await this.saveDataURLtoVault(
|
||||
scene.files[key].dataURL,
|
||||
@@ -1275,35 +1343,6 @@ export class ExcalidrawData {
|
||||
}
|
||||
}
|
||||
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
|
||||
/*const equations = new Set<string>();
|
||||
const duplicateEqs = new Set<string>();
|
||||
for (const key of fileIds) {
|
||||
if (this.hasEquation(key as FileId)) {
|
||||
if (equations.has(key)) {
|
||||
duplicateEqs.add(key);
|
||||
} else {
|
||||
equations.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (duplicateEqs.size > 0) {
|
||||
for (const key of duplicateEqs.keys()) {
|
||||
const elements = this.scene.elements.filter(
|
||||
(el: ExcalidrawElement) => el.type === "image" && el.fileId === key,
|
||||
);
|
||||
for (let i = 1; i < elements.length; i++) {
|
||||
const newFileId = fileid() as FileId;
|
||||
this.setEquation(newFileId, {
|
||||
latex: this.getEquation(key as FileId).latex,
|
||||
isLoaded: false,
|
||||
});
|
||||
elements[i].fileId = newFileId;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
@@ -1558,7 +1597,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
/**
|
||||
Files and equations copy/paste support
|
||||
Files, equations and mermaid copy/paste support
|
||||
This is not a complete solution, it assumes the source document is opened first
|
||||
at that time the fileId is stored in the master files/equations map
|
||||
when pasted the map is checked if the file already exists
|
||||
@@ -1573,9 +1612,10 @@ export class ExcalidrawData {
|
||||
}
|
||||
this.files.set(fileId, data);
|
||||
|
||||
if(data.isHyperlink) {
|
||||
if(data.isHyperLink || data.isLocalLink) {
|
||||
this.plugin.filesMaster.set(fileId, {
|
||||
isHyperlink: true,
|
||||
isHyperLink: data.isHyperLink,
|
||||
isLocalLink: data.isLocalLink,
|
||||
path: data.hyperlink,
|
||||
blockrefData: null,
|
||||
hasSVGwithBitmap: data.isSVGwithBitmap
|
||||
@@ -1589,7 +1629,8 @@ export class ExcalidrawData {
|
||||
|
||||
const parts = data.linkParts.original.split("#");
|
||||
this.plugin.filesMaster.set(fileId, {
|
||||
isHyperlink: false,
|
||||
isHyperLink: false,
|
||||
isLocalLink: false,
|
||||
path:data.file.path + (data.shouldScale()?"":"|100%"),
|
||||
blockrefData: parts.length === 1
|
||||
? null
|
||||
@@ -1636,7 +1677,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
if (this.plugin.filesMaster.has(fileId)) {
|
||||
const masterFile = this.plugin.filesMaster.get(fileId);
|
||||
if(masterFile.isHyperlink) {
|
||||
if(masterFile.isHyperLink || masterFile.isLocalLink) {
|
||||
this.files.set(
|
||||
fileId,
|
||||
new EmbeddedFile(this.plugin,this.file.path,masterFile.path)
|
||||
@@ -1663,6 +1704,9 @@ export class ExcalidrawData {
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------
|
||||
//Equations
|
||||
//--------------
|
||||
public setEquation(
|
||||
fileId: FileId,
|
||||
data: { latex: string; isLoaded: boolean },
|
||||
@@ -1704,6 +1748,51 @@ export class ExcalidrawData {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------
|
||||
//Mermaids
|
||||
//--------------
|
||||
public setMermaid(
|
||||
fileId: FileId,
|
||||
data: { mermaid: string; isLoaded: boolean },
|
||||
) {
|
||||
this.mermaids.set(fileId, { mermaid: data.mermaid, isLoaded: data.isLoaded });
|
||||
this.plugin.mermaidsMaster.set(fileId, data.mermaid);
|
||||
}
|
||||
|
||||
public getMermaid(fileId: FileId): { mermaid: string; isLoaded: boolean } {
|
||||
let result = this.mermaids.get(fileId);
|
||||
if(result) return result;
|
||||
const mermaid = this.plugin.mermaidsMaster.get(fileId);
|
||||
if(!mermaid) return result;
|
||||
this.mermaids.set(fileId, {mermaid, isLoaded: false});
|
||||
return {mermaid, isLoaded: false};
|
||||
}
|
||||
|
||||
public getMermaidEntries() {
|
||||
return this.mermaids.entries();
|
||||
}
|
||||
|
||||
public deleteMermaid(fileId: FileId) {
|
||||
this.mermaids.delete(fileId);
|
||||
//deliberately not deleting from plugin.mermaidsMaster
|
||||
//could be present in other drawings as well
|
||||
}
|
||||
|
||||
//Image copy/paste support
|
||||
public hasMermaid(fileId: FileId): boolean {
|
||||
if (this.mermaids.has(fileId)) {
|
||||
return true;
|
||||
}
|
||||
if (this.plugin.mermaidsMaster.has(fileId)) {
|
||||
this.mermaids.set(fileId, {
|
||||
mermaid: this.plugin.mermaidsMaster.get(fileId),
|
||||
isLoaded: false,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const getTransclusion = async (
|
||||
|
||||
19
src/ExcalidrawLib.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
import { RestoredDataState } from "@zsviczian/excalidraw/types/data/restore";
|
||||
import { ImportedDataState } from "@zsviczian/excalidraw/types/data/types";
|
||||
import { BoundingBox } from "@zsviczian/excalidraw/types/element/bounds";
|
||||
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, Theme } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
|
||||
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
|
||||
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
|
||||
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
|
||||
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
|
||||
type EmbeddedLink =
|
||||
| ({
|
||||
@@ -44,6 +44,7 @@ declare namespace ExcalidrawLib {
|
||||
appState?: AppState;
|
||||
files?: any;
|
||||
exportPadding?: number;
|
||||
exportingFrame: ExcalidrawFrameElement | null | undefined;
|
||||
renderEmbeddables?: boolean;
|
||||
}): Promise<SVGSVGElement>;
|
||||
|
||||
@@ -129,8 +130,10 @@ declare namespace ExcalidrawLib {
|
||||
function mermaidToExcalidraw(
|
||||
mermaidDefinition: string,
|
||||
opts: {fontSize: number},
|
||||
forceSVG?: boolean,
|
||||
): Promise<{
|
||||
elements: ExcalidrawElement[],
|
||||
files:any
|
||||
elements?: ExcalidrawElement[];
|
||||
files?: any;
|
||||
error?: string;
|
||||
} | undefined>;
|
||||
}
|
||||
168
src/LaTeX.ts
@@ -1,14 +1,20 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import {mathjax} from "mathjax-full/js/mathjax";
|
||||
import {TeX} from 'mathjax-full/js/input/tex.js';
|
||||
import {SVG} from 'mathjax-full/js/output/svg.js';
|
||||
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
|
||||
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
|
||||
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
|
||||
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { FileData, MimeType } from "./EmbeddedFileLoader";
|
||||
import { FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { errorlog, getImageSize, log, sleep, svgToBase64 } from "./utils/Utils";
|
||||
import { fileid } from "./constants";
|
||||
import html2canvas from "html2canvas";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
declare let window: any;
|
||||
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { getImageSize, svgToBase64 } from "./utils/Utils";
|
||||
import { fileid } from "./constants/constants";
|
||||
import { TFile } from "obsidian";
|
||||
import { MathDocument } from "mathjax-full/js/core/MathDocument";
|
||||
import { stripVTControlCharacters } from "util";
|
||||
|
||||
export const updateEquation = async (
|
||||
equation: string,
|
||||
@@ -17,7 +23,7 @@ export const updateEquation = async (
|
||||
addFiles: Function,
|
||||
plugin: ExcalidrawPlugin,
|
||||
) => {
|
||||
const data = await tex2dataURL(equation, plugin);
|
||||
const data = await tex2dataURL(equation);
|
||||
if (data) {
|
||||
const files: FileData[] = [];
|
||||
files.push({
|
||||
@@ -33,9 +39,23 @@ export const updateEquation = async (
|
||||
}
|
||||
};
|
||||
|
||||
let adaptor: LiteAdaptor;
|
||||
let input: TeX<unknown, unknown, unknown>;
|
||||
let output: SVG<unknown, unknown, unknown>;
|
||||
let html: MathDocument<any, any, any>;
|
||||
let preamble: string;
|
||||
|
||||
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
|
||||
const loadPreamble = async () => {
|
||||
const file = app.vault.getAbstractFileByPath("preamble.sty");
|
||||
preamble = file && file instanceof TFile
|
||||
? await app.vault.read(file)
|
||||
: null;
|
||||
};
|
||||
|
||||
export async function tex2dataURL(
|
||||
tex: string,
|
||||
plugin: ExcalidrawPlugin,
|
||||
scale: number = 4 // Default scale value, adjust as needed
|
||||
): Promise<{
|
||||
mimeType: MimeType;
|
||||
fileId: FileId;
|
||||
@@ -43,102 +63,44 @@ export async function tex2dataURL(
|
||||
created: number;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
//if network is slow, or not available, or mathjax has not yet fully loaded
|
||||
let counter = 0;
|
||||
while (!plugin.mathjax && !plugin.mathjaxLoaderFinished && counter < 10) {
|
||||
await sleep(100);
|
||||
counter++;
|
||||
if(!adaptor) {
|
||||
await loadPreamble();
|
||||
adaptor = liteAdaptor();
|
||||
RegisterHTMLHandler(adaptor);
|
||||
input = new TeX({
|
||||
packages: AllPackages,
|
||||
...Boolean(preamble) ? {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']]
|
||||
} : {},
|
||||
});
|
||||
output = new SVG({ fontCache: "local" });
|
||||
html = mathjax.document("", { InputJax: input, OutputJax: output });
|
||||
}
|
||||
|
||||
if(!plugin.mathjaxLoaderFinished) {
|
||||
errorlog({where: "text2dataURL", fn: tex2dataURL, message:"mathjaxLoader not ready, using fallback. Try reloading Obsidian or restarting the Excalidraw plugin"});
|
||||
}
|
||||
|
||||
//it is not clear why this works, but it seems that after loading the plugin sometimes only the third attempt is successful.
|
||||
try {
|
||||
return await mathjaxSVG(tex, plugin);
|
||||
} catch (e) {
|
||||
await sleep(100);
|
||||
try {
|
||||
return await mathjaxSVG(tex, plugin);
|
||||
} catch (e) {
|
||||
await sleep(100);
|
||||
try {
|
||||
return await mathjaxSVG(tex, plugin);
|
||||
} catch (e) {
|
||||
if (plugin.mathjax) {
|
||||
new Notice(
|
||||
"Unknown error loading LaTeX. Using fallback solution. Try closing and reopening this drawing.",
|
||||
);
|
||||
} else {
|
||||
new Notice(
|
||||
"LaTeX support did not load. Using fallback solution. Try checking your network connection.",
|
||||
);
|
||||
}
|
||||
//fallback
|
||||
return await mathjaxImage2html(tex);
|
||||
const node = html.convert(
|
||||
Boolean(preamble) ? `${preamble}${tex}` : tex,
|
||||
{ display: true, scale }
|
||||
);
|
||||
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
|
||||
if (svg) {
|
||||
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
|
||||
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
|
||||
}
|
||||
const img = svgToBase64(svg.outerHTML);
|
||||
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
|
||||
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
|
||||
const dataURL = svgToBase64(svg.outerHTML);
|
||||
return {
|
||||
mimeType: "image/svg+xml",
|
||||
fileId: fileid() as FileId,
|
||||
dataURL: dataURL as DataURL,
|
||||
created: Date.now(),
|
||||
size: await getImageSize(img),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function mathjaxSVG(
|
||||
tex: string,
|
||||
plugin: ExcalidrawPlugin,
|
||||
): Promise<{
|
||||
mimeType: MimeType;
|
||||
fileId: FileId;
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
const eq = plugin.mathjax.tex2svg(tex, { display: true, scale: 4 });
|
||||
const svg = eq.querySelector("svg");
|
||||
if (svg) {
|
||||
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
|
||||
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
|
||||
}
|
||||
const dataURL = svgToBase64(svg.outerHTML);
|
||||
return {
|
||||
mimeType: "image/svg+xml",
|
||||
fileId: fileid() as FileId,
|
||||
dataURL: dataURL as DataURL,
|
||||
created: Date.now(),
|
||||
size: await getImageSize(dataURL),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function mathjaxImage2html(tex: string): Promise<{
|
||||
mimeType: MimeType;
|
||||
fileId: FileId;
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
const div = document.body.createDiv();
|
||||
div.style.display = "table"; //this will ensure div fits width of formula exactly
|
||||
//@ts-ignore
|
||||
|
||||
const eq = window.MathJax.tex2chtml(tex, { display: true, scale: 4 }); //scale to ensure good resolution
|
||||
eq.style.margin = "3px";
|
||||
eq.style.color = "black";
|
||||
|
||||
//ipad support - removing mml as that was causing phantom double-image blur.
|
||||
const el = eq.querySelector("mjx-assistive-mml");
|
||||
if (el) {
|
||||
el.parentElement.removeChild(el);
|
||||
}
|
||||
div.appendChild(eq);
|
||||
window.MathJax.typeset();
|
||||
const canvas = await html2canvas(div, { backgroundColor: null }); //transparent
|
||||
document.body.removeChild(div);
|
||||
return {
|
||||
mimeType: "image/png",
|
||||
fileId: fileid() as FileId,
|
||||
dataURL: canvas.toDataURL() as DataURL,
|
||||
created: Date.now(),
|
||||
size: { height: canvas.height, width: canvas.width },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
TFile,
|
||||
Vault,
|
||||
} from "obsidian";
|
||||
import { RERENDER_EVENT } from "./constants";
|
||||
import { RERENDER_EVENT } from "./constants/constants";
|
||||
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
||||
import { createPNG, createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
@@ -20,17 +20,18 @@ import {
|
||||
hasExportTheme,
|
||||
convertSVGStringToElement,
|
||||
} from "./utils/Utils";
|
||||
import { getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils";
|
||||
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
|
||||
import { ImageKey, imageCache } from "./utils/ImageCache";
|
||||
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
|
||||
import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
|
||||
|
||||
interface imgElementAttributes {
|
||||
file?: TFile;
|
||||
fname: string; //Excalidraw filename
|
||||
fwidth: string; //Display width of image
|
||||
fheight: string; //Display height of image
|
||||
style: string; //css style to apply to IMG element
|
||||
style: string[]; //css style to apply to IMG element
|
||||
}
|
||||
|
||||
let plugin: ExcalidrawPlugin;
|
||||
@@ -123,8 +124,16 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
|
||||
style += `height:${imgAttributes.fheight}px;`;
|
||||
}
|
||||
if(!onCanvas) element.setAttribute("style", style);
|
||||
element.addClass(imgAttributes.style);
|
||||
element.addClass("excalidraw-embedded-img");
|
||||
element.classList.add(...Array.from(imgAttributes.style))
|
||||
if(!element.hasClass("excalidraw-embedded-img")) {
|
||||
element.addClass("excalidraw-embedded-img");
|
||||
}
|
||||
if(
|
||||
window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed &&
|
||||
!element.hasClass("excalidraw-canvas-immersive")
|
||||
) {
|
||||
element.addClass("excalidraw-canvas-immersive");
|
||||
}
|
||||
}
|
||||
|
||||
const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
|
||||
@@ -177,7 +186,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
|
||||
return null;
|
||||
}
|
||||
|
||||
svg = embedFontsInSVG(svg, plugin);
|
||||
svg = embedFontsInSVG(svg, plugin, false);
|
||||
//need to remove width and height attributes to support area= embeds
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
@@ -199,7 +208,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
maybeSVG = await imageCache.getImageFromCache(cacheKey);
|
||||
}
|
||||
|
||||
const svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
|
||||
let svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
|
||||
? maybeSVG
|
||||
: convertSVGStringToElement((await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
@@ -223,6 +232,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
return null;
|
||||
}
|
||||
|
||||
svg = embedFontsInSVG(svg, plugin, true);
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
containerElement.append(svg);
|
||||
@@ -253,7 +263,7 @@ const getIMG = async (
|
||||
const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);
|
||||
|
||||
// https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
|
||||
imgAttributes.style = imgAttributes.style.replaceAll(" ", "-");
|
||||
imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-"));
|
||||
|
||||
const forceTheme = hasExportTheme(plugin, file)
|
||||
? getExportTheme(plugin, file, "light")
|
||||
@@ -388,8 +398,9 @@ const createImgElement = async (
|
||||
fname: fileSource,
|
||||
fwidth: imgOrDiv.getAttribute("w"),
|
||||
fheight: imgOrDiv.getAttribute("h"),
|
||||
style: imgOrDiv.getAttribute("class"),
|
||||
style: [...Array.from(imgOrDiv.classList)],
|
||||
}, onCanvas);
|
||||
if(!newImg) return;
|
||||
parent.empty();
|
||||
if(!onCanvas) {
|
||||
newImg.style.maxHeight = imgMaxHeigth;
|
||||
@@ -398,6 +409,20 @@ const createImgElement = async (
|
||||
newImg.setAttribute("fileSource",fileSource);
|
||||
parent.append(newImg);
|
||||
});
|
||||
const cssClasses = getFileCSSClasses(attr.file);
|
||||
cssClasses.forEach((cssClass) => {
|
||||
if(imgOrDiv.hasClass(cssClass)) return;
|
||||
imgOrDiv.addClass(cssClass);
|
||||
});
|
||||
if(window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) {
|
||||
if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
|
||||
imgOrDiv.addClass("excalidraw-canvas-immersive");
|
||||
}
|
||||
} else {
|
||||
if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
|
||||
imgOrDiv.removeClass("excalidraw-canvas-immersive");
|
||||
}
|
||||
}
|
||||
return imgOrDiv;
|
||||
}
|
||||
|
||||
@@ -406,7 +431,7 @@ const createImageDiv = async (
|
||||
onCanvas: boolean = false
|
||||
): Promise<HTMLDivElement> => {
|
||||
const img = await createImgElement(attr, onCanvas);
|
||||
return createDiv(attr.style, (el) => el.append(img));
|
||||
return createDiv(attr.style.join(" "), (el) => el.append(img));
|
||||
};
|
||||
|
||||
const processReadingMode = async (
|
||||
@@ -448,7 +473,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
|
||||
fname: "",
|
||||
fheight: "",
|
||||
fwidth: "",
|
||||
style: "",
|
||||
style: [],
|
||||
};
|
||||
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
@@ -465,7 +490,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
|
||||
: getDefaultWidth(plugin);
|
||||
attr.fheight = internalEmbedEl.getAttribute("height");
|
||||
let alt = internalEmbedEl.getAttribute("alt");
|
||||
attr.style = "excalidraw-svg";
|
||||
attr.style = ["excalidraw-svg"];
|
||||
processAltText(src.split("#")[0],alt,attr);
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:"");
|
||||
@@ -484,14 +509,14 @@ const processAltText = (
|
||||
attr.fwidth = parts[2] ?? attr.fwidth;
|
||||
attr.fheight = parts[3] ?? attr.fheight;
|
||||
if (parts[4] && !parts[4].startsWith(fname)) {
|
||||
attr.style = `excalidraw-svg${`-${parts[4]}`}`;
|
||||
attr.style = [`excalidraw-svg${`-${parts[4]}`}`];
|
||||
}
|
||||
if (
|
||||
(!parts[4] || parts[4]==="") &&
|
||||
(!parts[2] || parts[2]==="") &&
|
||||
parts[0] && parts[0] !== ""
|
||||
) {
|
||||
attr.style = `excalidraw-svg${`-${parts[0]}`}`;
|
||||
attr.style = [`excalidraw-svg${`-${parts[0]}`}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,7 +574,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
fname: ctx.sourcePath,
|
||||
fheight: "",
|
||||
fwidth: getDefaultWidth(plugin),
|
||||
style: "excalidraw-svg",
|
||||
style: ["excalidraw-svg"],
|
||||
};
|
||||
|
||||
attr.file = file;
|
||||
@@ -604,7 +629,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
|
||||
//timer to avoid the image flickering when the user is typing
|
||||
let timer: NodeJS.Timeout = null;
|
||||
const observer = new MutationObserver((m) => {
|
||||
const markdownObserverFn: MutationCallback = (m) => {
|
||||
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
|
||||
return;
|
||||
}
|
||||
@@ -617,7 +642,10 @@ const tmpObsidianWYSIWYG = async (
|
||||
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
|
||||
internalEmbedDiv.appendChild(imgDiv);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
const observer = isDebugMode
|
||||
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
|
||||
: new MutationObserver(markdownObserverFn);
|
||||
observer.observe(internalEmbedDiv, {
|
||||
attributes: true, //configure it to listen to attribute changes
|
||||
});
|
||||
@@ -669,13 +697,16 @@ export const hoverEvent = (e: any) => {
|
||||
};
|
||||
|
||||
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
|
||||
export const observer = new MutationObserver(async (m) => {
|
||||
if (m.length == 0) {
|
||||
const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
|
||||
if (m.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.hover.linkText) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.hover.linkText.endsWith("excalidraw")) {
|
||||
return;
|
||||
}
|
||||
const file = metadataCache.getFirstLinkpathDest(
|
||||
plugin.hover.linkText,
|
||||
plugin.hover.sourcePath ? plugin.hover.sourcePath : "",
|
||||
@@ -712,9 +743,7 @@ export const observer = new MutationObserver(async (m) => {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
//@ts-ignore
|
||||
!m[0].addedNodes[0].classNames !=
|
||||
"popover hover-popover file-embed is-loaded"
|
||||
(m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -728,7 +757,7 @@ export const observer = new MutationObserver(async (m) => {
|
||||
fname: file.path,
|
||||
fwidth: "300",
|
||||
fheight: null,
|
||||
style: "excalidraw-svg",
|
||||
style: ["excalidraw-svg"],
|
||||
});
|
||||
const div = createDiv("", async (el) => {
|
||||
el.appendChild(img);
|
||||
@@ -745,5 +774,9 @@ export const observer = new MutationObserver(async (m) => {
|
||||
});
|
||||
});
|
||||
node.appendChild(div);
|
||||
});
|
||||
};
|
||||
|
||||
export const legacyExcalidrawPopoverObserver = isDebugMode
|
||||
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
|
||||
: new MutationObserver(legacyExcalidrawPopoverObserverFn);
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants";
|
||||
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
|
||||
import { getIMGFilename } from "./utils/FileUtils";
|
||||
import { splitFolderAndFilename } from "./utils/FileUtils";
|
||||
import { getEA } from "src";
|
||||
|
||||
export type ScriptIconMap = {
|
||||
[key: string]: { name: string; group: string; svgString: string };
|
||||
@@ -42,6 +43,7 @@ export class ScriptEngine {
|
||||
this.loadScript(scriptFile);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEventHandler = async (file: TFile) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
@@ -103,7 +105,7 @@ export class ScriptEngine {
|
||||
public getListofScripts(): TFile[] {
|
||||
this.scriptPath = this.plugin.settings.scriptFolderPath;
|
||||
if (!app.vault.getAbstractFileByPath(this.scriptPath)) {
|
||||
this.scriptPath = null;
|
||||
//this.scriptPath = null;
|
||||
return;
|
||||
}
|
||||
return app.vault
|
||||
@@ -199,27 +201,26 @@ export class ScriptEngine {
|
||||
|
||||
const commandId = `${PLUGIN_ID}:${basename}`;
|
||||
// @ts-ignore
|
||||
if (!app.commands.commands[commandId]) {
|
||||
if (!this.plugin.app.commands.commands[commandId]) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete app.commands.commands[commandId];
|
||||
delete this.plugin.app.commands.commands[commandId];
|
||||
}
|
||||
|
||||
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
|
||||
if (!view || !script || !title) {
|
||||
return;
|
||||
}
|
||||
this.plugin.ea.reset();
|
||||
this.plugin.ea.setView(view);
|
||||
this.plugin.ea.activeScript = title;
|
||||
const ea = getEA(view);
|
||||
ea.activeScript = title;
|
||||
|
||||
//https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
|
||||
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
||||
let result = null;
|
||||
//try {
|
||||
result = await new AsyncFunction("ea", "utils", script)(this.plugin.ea, {
|
||||
result = await new AsyncFunction("ea", "utils", script)(ea, {
|
||||
inputPrompt: (
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
@@ -233,7 +234,7 @@ export class ScriptEngine {
|
||||
ScriptEngine.inputPrompt(
|
||||
view,
|
||||
this.plugin,
|
||||
app,
|
||||
this.plugin.app,
|
||||
header,
|
||||
placeholder,
|
||||
value,
|
||||
@@ -262,7 +263,7 @@ export class ScriptEngine {
|
||||
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
|
||||
errorlog({ script: this.plugin.ea.activeScript, error: e });
|
||||
}*/
|
||||
this.plugin.ea.activeScript = null;
|
||||
//ea.activeScript = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
378
src/constants.ts
7
src/constants/constFonts.ts
Normal file
378
src/constants/constants.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { DeviceType } from "../types";
|
||||
import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
import { moment } from "obsidian";
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
|
||||
|
||||
declare const excalidrawLib: typeof ExcalidrawLib;
|
||||
|
||||
export const LOCALE = moment.locale();
|
||||
|
||||
export const obsidianToExcalidrawMap: { [key: string]: string } = {
|
||||
'en': 'en-US',
|
||||
'af': 'af-ZA', // Assuming South Africa for Afrikaans
|
||||
'am': 'am-ET', // Assuming Ethiopia for Amharic
|
||||
'ar': 'ar-SA',
|
||||
'eu': 'eu-ES',
|
||||
'be': 'be-BY', // Assuming Belarus for Belarusian
|
||||
'bg': 'bg-BG',
|
||||
'bn': 'bn-BD', // Assuming Bangladesh for Bengali
|
||||
'ca': 'ca-ES',
|
||||
'cs': 'cs-CZ',
|
||||
'da': 'da-DK', // Assuming Denmark for Danish
|
||||
'de': 'de-DE',
|
||||
'el': 'el-GR',
|
||||
'eo': 'eo-EO', // Esperanto doesn't have a country
|
||||
'es': 'es-ES',
|
||||
'fa': 'fa-IR',
|
||||
'fi-fi': 'fi-FI',
|
||||
'fr': 'fr-FR',
|
||||
'gl': 'gl-ES',
|
||||
'he': 'he-IL',
|
||||
'hi': 'hi-IN',
|
||||
'hu': 'hu-HU',
|
||||
'id': 'id-ID',
|
||||
'it': 'it-IT',
|
||||
'ja': 'ja-JP',
|
||||
'ko': 'ko-KR',
|
||||
'lv': 'lv-LV',
|
||||
'ml': 'ml-IN', // Assuming India for Malayalam
|
||||
'ms': 'ms-MY', // Assuming Malaysia for Malay
|
||||
'nl': 'nl-NL',
|
||||
'no': 'nb-NO', // Using Norwegian Bokmål for Norwegian
|
||||
'oc': 'oc-FR', // Assuming France for Occitan
|
||||
'pl': 'pl-PL',
|
||||
'pt': 'pt-PT',
|
||||
'pt-BR': 'pt-BR',
|
||||
'ro': 'ro-RO',
|
||||
'ru': 'ru-RU',
|
||||
'sr': 'sr-RS', // Assuming Serbia for Serbian
|
||||
'se': 'sv-SE', // Assuming Swedish for 'se'
|
||||
'sk': 'sk-SK',
|
||||
'sq': 'sq-AL', // Assuming Albania for Albanian
|
||||
'ta': 'ta-IN', // Assuming India for Tamil
|
||||
'te': 'te-IN', // Assuming India for Telugu
|
||||
'th': 'th-TH',
|
||||
'tr': 'tr-TR',
|
||||
'uk': 'uk-UA',
|
||||
'ur': 'ur-PK', // Assuming Pakistan for Urdu
|
||||
'vi': 'vi-VN',
|
||||
'zh': 'zh-CN',
|
||||
'zh-TW': 'zh-TW',
|
||||
};
|
||||
|
||||
|
||||
export const {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getDefaultLineHeight,
|
||||
wrapText,
|
||||
getFontString,
|
||||
getBoundTextMaxWidth,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
mutateElement,
|
||||
restore,
|
||||
mermaidToExcalidraw,
|
||||
} = excalidrawLib;
|
||||
|
||||
export function JSON_parse(x: string): any {
|
||||
return JSON.parse(x.replaceAll("[", "["));
|
||||
}
|
||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
||||
|
||||
export const DEVICE: DeviceType = {
|
||||
isDesktop: !document.body.hasClass("is-tablet") && !document.body.hasClass("is-mobile"),
|
||||
isPhone: document.body.hasClass("is-phone"),
|
||||
isTablet: document.body.hasClass("is-tablet"),
|
||||
isMobile: document.body.hasClass("is-mobile"), //running Obsidian Mobile, need to also check isTablet
|
||||
isLinux: document.body.hasClass("mod-linux") && ! document.body.hasClass("is-android"),
|
||||
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
|
||||
isWindows: document.body.hasClass("mod-windows"),
|
||||
isIOS: document.body.hasClass("is-ios"),
|
||||
isAndroid: document.body.hasClass("is-android")
|
||||
};
|
||||
|
||||
export const ROOTELEMENTSIZE = (() => {
|
||||
const tempElement = document.createElement('div');
|
||||
tempElement.style.fontSize = '1rem';
|
||||
tempElement.style.display = 'none'; // Hide the element
|
||||
document.body.appendChild(tempElement);
|
||||
const computedStyle = getComputedStyle(tempElement);
|
||||
const pixelSize = parseFloat(computedStyle.fontSize);
|
||||
document.body.removeChild(tempElement);
|
||||
return pixelSize;
|
||||
})();
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
8,
|
||||
);
|
||||
export const KEYCODE = {
|
||||
ESC: 27,
|
||||
};
|
||||
export const ROUNDNESS = { //should at one point publish @zsviczian/excalidraw/types/constants
|
||||
LEGACY: 1,
|
||||
PROPORTIONAL_RADIUS: 2,
|
||||
ADAPTIVE_RADIUS: 3,
|
||||
} as const;
|
||||
export const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
|
||||
export const GITHUB_RELEASES = "https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/";
|
||||
export const URLFETCHTIMEOUT = 3000;
|
||||
export const PLUGIN_ID = "obsidian-excalidraw-plugin";
|
||||
export const SCRIPT_INSTALL_CODEBLOCK = "excalidraw-script-install";
|
||||
export const SCRIPT_INSTALL_FOLDER = "Downloaded";
|
||||
export const fileid = customAlphabet("1234567890abcdef", 40);
|
||||
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*#]/g;
|
||||
|
||||
//taken from Obsidian source code
|
||||
export const REG_SECTION_REF_CLEAN = /([:#|^\\\r\n]|%%|\[\[|]])/g;
|
||||
export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
|
||||
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
|
||||
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
|
||||
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
|
||||
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
|
||||
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
|
||||
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
|
||||
export const MAX_IMAGE_SIZE = 500;
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const FRONTMATTER_KEY_EXPORT_TRANSPARENT =
|
||||
"excalidraw-export-transparent";
|
||||
export const FRONTMATTER_KEY_EXPORT_DARK = "excalidraw-export-dark";
|
||||
export const FRONTMATTER_KEY_EXPORT_SVGPADDING = "excalidraw-export-svgpadding"; //depricated
|
||||
export const FRONTMATTER_KEY_EXPORT_PADDING = "excalidraw-export-padding";
|
||||
export const FRONTMATTER_KEY_EXPORT_PNGSCALE = "excalidraw-export-pngscale";
|
||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
|
||||
export const FRONTMATTER_KEY_ONLOAD_SCRIPT = "excalidraw-onload-script";
|
||||
export const FRONTMATTER_KEY_LINKBUTTON_OPACITY = "excalidraw-linkbutton-opacity";
|
||||
export const FRONTMATTER_KEY_DEFAULT_MODE = "excalidraw-default-mode";
|
||||
export const FRONTMATTER_KEY_FONT = "excalidraw-font";
|
||||
export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color";
|
||||
export const FRONTMATTER_KEY_BORDERCOLOR = "excalidraw-border-color";
|
||||
export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css";
|
||||
export const FRONTMATTER_KEY_AUTOEXPORT = "excalidraw-autoexport"
|
||||
export const FRONTMATTER_KEY_EMBEDDABLE_THEME = "excalidraw-iframe-theme";
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||
export const BLANK_DRAWING =
|
||||
`{"type":"excalidraw","version":2,"source":"${GITHUB_RELEASES+PLUGIN_VERSION}","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}`;
|
||||
export const DARK_BLANK_DRAWING =
|
||||
`{"type":"excalidraw","version":2,"source":"${GITHUB_RELEASES+PLUGIN_VERSION}","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}`;
|
||||
export const FRONTMATTER = [
|
||||
"---",
|
||||
"",
|
||||
`${FRONTMATTER_KEY}: parsed`,
|
||||
"tags: [excalidraw]",
|
||||
"",
|
||||
"---",
|
||||
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==",
|
||||
"",
|
||||
"",
|
||||
].join("\n");
|
||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||
/*export const FULLSCREEN_ICON_NAME = "fullscreen";
|
||||
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";*/
|
||||
export const SCRIPTENGINE_ICON_NAME = "ScriptEngine";
|
||||
|
||||
export const KEYBOARD_EVENT_TYPES = [
|
||||
"keydown",
|
||||
"keyup",
|
||||
"keypress"
|
||||
];
|
||||
|
||||
export const EXTENDED_EVENT_TYPES = [
|
||||
/* "pointerdown",
|
||||
"pointerup",
|
||||
"pointermove",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"mouseover",
|
||||
"mouseout",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
"dblclick",
|
||||
"drag",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragexit",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"dragstart",
|
||||
"drop",*/
|
||||
"copy",
|
||||
"cut",
|
||||
"paste",
|
||||
/*"wheel",
|
||||
"touchstart",
|
||||
"touchend",
|
||||
"touchmove",*/
|
||||
];
|
||||
|
||||
//export const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
||||
|
||||
|
||||
export const COLOR_NAMES = new Map<string, string>();
|
||||
COLOR_NAMES.set("aliceblue", "#f0f8ff");
|
||||
COLOR_NAMES.set("antiquewhite", "#faebd7");
|
||||
COLOR_NAMES.set("aqua", "#00ffff");
|
||||
COLOR_NAMES.set("aquamarine", "#7fffd4");
|
||||
COLOR_NAMES.set("azure", "#f0ffff");
|
||||
COLOR_NAMES.set("beige", "#f5f5dc");
|
||||
COLOR_NAMES.set("bisque", "#ffe4c4");
|
||||
COLOR_NAMES.set("black", "#000000");
|
||||
COLOR_NAMES.set("blanchedalmond", "#ffebcd");
|
||||
COLOR_NAMES.set("blue", "#0000ff");
|
||||
COLOR_NAMES.set("blueviolet", "#8a2be2");
|
||||
COLOR_NAMES.set("brown", "#a52a2a");
|
||||
COLOR_NAMES.set("burlywood", "#deb887");
|
||||
COLOR_NAMES.set("cadetblue", "#5f9ea0");
|
||||
COLOR_NAMES.set("chartreuse", "#7fff00");
|
||||
COLOR_NAMES.set("chocolate", "#d2691e");
|
||||
COLOR_NAMES.set("coral", "#ff7f50");
|
||||
COLOR_NAMES.set("cornflowerblue", "#6495ed");
|
||||
COLOR_NAMES.set("cornsilk", "#fff8dc");
|
||||
COLOR_NAMES.set("crimson", "#dc143c");
|
||||
COLOR_NAMES.set("cyan", "#00ffff");
|
||||
COLOR_NAMES.set("darkblue", "#00008b");
|
||||
COLOR_NAMES.set("darkcyan", "#008b8b");
|
||||
COLOR_NAMES.set("darkgoldenrod", "#b8860b");
|
||||
COLOR_NAMES.set("darkgray", "#a9a9a9");
|
||||
COLOR_NAMES.set("darkgreen", "#006400");
|
||||
COLOR_NAMES.set("darkkhaki", "#bdb76b");
|
||||
COLOR_NAMES.set("darkmagenta", "#8b008b");
|
||||
COLOR_NAMES.set("darkolivegreen", "#556b2f");
|
||||
COLOR_NAMES.set("darkorange", "#ff8c00");
|
||||
COLOR_NAMES.set("darkorchid", "#9932cc");
|
||||
COLOR_NAMES.set("darkred", "#8b0000");
|
||||
COLOR_NAMES.set("darksalmon", "#e9967a");
|
||||
COLOR_NAMES.set("darkseagreen", "#8fbc8f");
|
||||
COLOR_NAMES.set("darkslateblue", "#483d8b");
|
||||
COLOR_NAMES.set("darkslategray", "#2f4f4f");
|
||||
COLOR_NAMES.set("darkturquoise", "#00ced1");
|
||||
COLOR_NAMES.set("darkviolet", "#9400d3");
|
||||
COLOR_NAMES.set("deeppink", "#ff1493");
|
||||
COLOR_NAMES.set("deepskyblue", "#00bfff");
|
||||
COLOR_NAMES.set("dimgray", "#696969");
|
||||
COLOR_NAMES.set("dodgerblue", "#1e90ff");
|
||||
COLOR_NAMES.set("firebrick", "#b22222");
|
||||
COLOR_NAMES.set("floralwhite", "#fffaf0");
|
||||
COLOR_NAMES.set("forestgreen", "#228b22");
|
||||
COLOR_NAMES.set("fuchsia", "#ff00ff");
|
||||
COLOR_NAMES.set("gainsboro", "#dcdcdc");
|
||||
COLOR_NAMES.set("ghostwhite", "#f8f8ff");
|
||||
COLOR_NAMES.set("gold", "#ffd700");
|
||||
COLOR_NAMES.set("goldenrod", "#daa520");
|
||||
COLOR_NAMES.set("gray", "#808080");
|
||||
COLOR_NAMES.set("green", "#008000");
|
||||
COLOR_NAMES.set("greenyellow", "#adff2f");
|
||||
COLOR_NAMES.set("honeydew", "#f0fff0");
|
||||
COLOR_NAMES.set("hotpink", "#ff69b4");
|
||||
COLOR_NAMES.set("indianred", "#cd5c5c");
|
||||
COLOR_NAMES.set("indigo", "#4b0082");
|
||||
COLOR_NAMES.set("ivory", "#fffff0");
|
||||
COLOR_NAMES.set("khaki", "#f0e68c");
|
||||
COLOR_NAMES.set("lavender", "#e6e6fa");
|
||||
COLOR_NAMES.set("lavenderblush", "#fff0f5");
|
||||
COLOR_NAMES.set("lawngreen", "#7cfc00");
|
||||
COLOR_NAMES.set("lemonchiffon", "#fffacd");
|
||||
COLOR_NAMES.set("lightblue", "#add8e6");
|
||||
COLOR_NAMES.set("lightcoral", "#f08080");
|
||||
COLOR_NAMES.set("lightcyan", "#e0ffff");
|
||||
COLOR_NAMES.set("lightgoldenrodyellow", "#fafad2");
|
||||
COLOR_NAMES.set("lightgrey", "#d3d3d3");
|
||||
COLOR_NAMES.set("lightgreen", "#90ee90");
|
||||
COLOR_NAMES.set("lightpink", "#ffb6c1");
|
||||
COLOR_NAMES.set("lightsalmon", "#ffa07a");
|
||||
COLOR_NAMES.set("lightseagreen", "#20b2aa");
|
||||
COLOR_NAMES.set("lightskyblue", "#87cefa");
|
||||
COLOR_NAMES.set("lightslategray", "#778899");
|
||||
COLOR_NAMES.set("lightsteelblue", "#b0c4de");
|
||||
COLOR_NAMES.set("lightyellow", "#ffffe0");
|
||||
COLOR_NAMES.set("lime", "#00ff00");
|
||||
COLOR_NAMES.set("limegreen", "#32cd32");
|
||||
COLOR_NAMES.set("linen", "#faf0e6");
|
||||
COLOR_NAMES.set("magenta", "#ff00ff");
|
||||
COLOR_NAMES.set("maroon", "#800000");
|
||||
COLOR_NAMES.set("mediumaquamarine", "#66cdaa");
|
||||
COLOR_NAMES.set("mediumblue", "#0000cd");
|
||||
COLOR_NAMES.set("mediumorchid", "#ba55d3");
|
||||
COLOR_NAMES.set("mediumpurple", "#9370d8");
|
||||
COLOR_NAMES.set("mediumseagreen", "#3cb371");
|
||||
COLOR_NAMES.set("mediumslateblue", "#7b68ee");
|
||||
COLOR_NAMES.set("mediumspringgreen", "#00fa9a");
|
||||
COLOR_NAMES.set("mediumturquoise", "#48d1cc");
|
||||
COLOR_NAMES.set("mediumvioletred", "#c71585");
|
||||
COLOR_NAMES.set("midnightblue", "#191970");
|
||||
COLOR_NAMES.set("mintcream", "#f5fffa");
|
||||
COLOR_NAMES.set("mistyrose", "#ffe4e1");
|
||||
COLOR_NAMES.set("moccasin", "#ffe4b5");
|
||||
COLOR_NAMES.set("navajowhite", "#ffdead");
|
||||
COLOR_NAMES.set("navy", "#000080");
|
||||
COLOR_NAMES.set("oldlace", "#fdf5e6");
|
||||
COLOR_NAMES.set("olive", "#808000");
|
||||
COLOR_NAMES.set("olivedrab", "#6b8e23");
|
||||
COLOR_NAMES.set("orange", "#ffa500");
|
||||
COLOR_NAMES.set("orangered", "#ff4500");
|
||||
COLOR_NAMES.set("orchid", "#da70d6");
|
||||
COLOR_NAMES.set("palegoldenrod", "#eee8aa");
|
||||
COLOR_NAMES.set("palegreen", "#98fb98");
|
||||
COLOR_NAMES.set("paleturquoise", "#afeeee");
|
||||
COLOR_NAMES.set("palevioletred", "#d87093");
|
||||
COLOR_NAMES.set("papayawhip", "#ffefd5");
|
||||
COLOR_NAMES.set("peachpuff", "#ffdab9");
|
||||
COLOR_NAMES.set("peru", "#cd853f");
|
||||
COLOR_NAMES.set("pink", "#ffc0cb");
|
||||
COLOR_NAMES.set("plum", "#dda0dd");
|
||||
COLOR_NAMES.set("powderblue", "#b0e0e6");
|
||||
COLOR_NAMES.set("purple", "#800080");
|
||||
COLOR_NAMES.set("rebeccapurple", "#663399");
|
||||
COLOR_NAMES.set("red", "#ff0000");
|
||||
COLOR_NAMES.set("rosybrown", "#bc8f8f");
|
||||
COLOR_NAMES.set("royalblue", "#4169e1");
|
||||
COLOR_NAMES.set("saddlebrown", "#8b4513");
|
||||
COLOR_NAMES.set("salmon", "#fa8072");
|
||||
COLOR_NAMES.set("sandybrown", "#f4a460");
|
||||
COLOR_NAMES.set("seagreen", "#2e8b57");
|
||||
COLOR_NAMES.set("seashell", "#fff5ee");
|
||||
COLOR_NAMES.set("sienna", "#a0522d");
|
||||
COLOR_NAMES.set("silver", "#c0c0c0");
|
||||
COLOR_NAMES.set("skyblue", "#87ceeb");
|
||||
COLOR_NAMES.set("slateblue", "#6a5acd");
|
||||
COLOR_NAMES.set("slategray", "#708090");
|
||||
COLOR_NAMES.set("snow", "#fffafa");
|
||||
COLOR_NAMES.set("springgreen", "#00ff7f");
|
||||
COLOR_NAMES.set("steelblue", "#4682b4");
|
||||
COLOR_NAMES.set("tan", "#d2b48c");
|
||||
COLOR_NAMES.set("teal", "#008080");
|
||||
COLOR_NAMES.set("thistle", "#d8bfd8");
|
||||
COLOR_NAMES.set("tomato", "#ff6347");
|
||||
COLOR_NAMES.set("turquoise", "#40e0d0");
|
||||
COLOR_NAMES.set("violet", "#ee82ee");
|
||||
COLOR_NAMES.set("wheat", "#f5deb3");
|
||||
COLOR_NAMES.set("white", "#ffffff");
|
||||
COLOR_NAMES.set("whitesmoke", "#f5f5f5");
|
||||
COLOR_NAMES.set("yellow", "#ffff00");
|
||||
COLOR_NAMES.set("yellowgreen", "#9acd32");
|
||||
export const DEFAULT_MD_EMBED_CSS = `.snw-reference{display: none;}.excalidraw-md-host{padding:0px 10px}.excalidraw-md-footer{height:5px}foreignObject{background-color:transparent}p{display:block;margin-block-start:1em;margin-block-end:1em;margin-inline-start:0px;margin-inline-end:0px;color:inherit}table,tr,th,td{color:inherit;border:1px solid;border-collapse:collapse;padding:3px}th{font-weight:bold;border-bottom:double;background-color:silver}.copy-code-button{display:none}code[class*=language-],pre[class*=language-]{color:#393a34;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;font-size:.9em;line-height:1.2em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre>code[class*=language-]{font-size:1em}pre[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,code[class*=language-] ::-moz-selection{background:#C1DEF1}pre[class*=language-]::selection,pre[class*=language-] ::selection,code[class*=language-]::selection,code[class*=language-] ::selection{background:#C1DEF1}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;background-color:#0000001a}:not(pre)>code[class*=language-]{padding:.2em;padding-top:1px;padding-bottom:1px;background:#f8f8f8;border:1px solid #dddddd}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:green;font-style:italic}.token.namespace{opacity:.7}.token.string{color:#a31515}.token.punctuation,.token.operator{color:#393a34}.token.url,.token.symbol,.token.number,.token.boolean,.token.variable,.token.constant,.token.inserted{color:#36acaa}.token.atrule,.token.keyword,.token.attr-value,.language-autohotkey .token.selector,.language-json .token.boolean,.language-json .token.number,code[class*=language-css]{color:#00f}.token.function{color:#393a34}.token.deleted,.language-autohotkey .token.tag{color:#9a050f}.token.selector,.language-autohotkey .token.keyword{color:#00009f}.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.class-name,.language-json .token.property{color:#2b91af}.token.tag,.token.selector{color:maroon}.token.attr-name,.token.property,.token.regex,.token.entity{color:red}.token.directive.tag .tag{background:#ffff00;color:#393a34}.line-numbers.line-numbers .line-numbers-rows{border-right-color:#a5a5a5}.line-numbers .line-numbers-rows>span:before{color:#2b91af}.line-highlight.line-highlight{background:rgba(193,222,241,.2);background:-webkit-linear-gradient(left,rgba(193,222,241,.2) 70%,rgba(221,222,241,0));background:linear-gradient(to right,rgba(193,222,241,.2) 70%,rgba(221,222,241,0))}blockquote{ font-style:italic;background-color:rgb(46,43,42,0.1);margin:0;margin-left:1em;border-radius:0 4px 4px 0;border:1px solid hsl(0,80%,32%);border-left-width:8px;border-top-width:0px;border-right-width:0px;border-bottom-width:0px;padding:10px 20px;margin-inline-start:30px;margin-inline-end:30px;}`;
|
||||
export const SCRIPTENGINE_ICON = `<g transform="translate(-8,-8)"><path d="M24.318 37.983c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749m.126-.104c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749" fill="none" stroke-width="2" stroke-linecap="round" stroke="currentColor"/><path d="M81.235 56.502a23.3 23.3 0 0 1-1.46 8.068 20.785 20.785 0 0 1-1.762 3.72 24.068 24.068 0 0 1-5.337 6.26 22.575 22.575 0 0 1-3.449 2.358 23.726 23.726 0 0 1-7.803 2.803 24.719 24.719 0 0 1-8.333 0 24.102 24.102 0 0 1-4.028-1.074 23.71 23.71 0 0 1-3.776-1.729 23.259 23.259 0 0 1-6.369-5.265 23.775 23.775 0 0 1-2.416-3.353 24.935 24.935 0 0 1-1.762-3.72 23.765 23.765 0 0 1-1.083-3.981 23.454 23.454 0 0 1 0-8.173c.252-1.336.604-2.698 1.083-3.956a24.935 24.935 0 0 1 1.762-3.72 22.587 22.587 0 0 1 2.416-3.378c.881-1.048 1.888-2.017 2.946-2.908a24.38 24.38 0 0 1 3.423-2.357 23.71 23.71 0 0 1 3.776-1.73 21.74 21.74 0 0 1 4.028-1.047 23.437 23.437 0 0 1 8.333 0 24.282 24.282 0 0 1 7.803 2.777 26.198 26.198 0 0 1 3.45 2.357 24.62 24.62 0 0 1 5.336 6.287 20.785 20.785 0 0 1 1.762 3.72 21.32 21.32 0 0 1 1.083 3.955c.251 1.336.302 3.405.377 4.086.05.681.05-.68 0 0" fill="none" stroke-width="4" stroke-linecap="round" stroke="currentColor"/><path d="M69.404 56.633c-6.596-3.3-13.216-6.6-19.51-9.744m19.51 9.744c-6.747-3.379-13.493-6.758-19.51-9.744m0 0v19.489m0-19.49v19.49m0 0c4.355-2.148 8.71-4.322 19.51-9.745m-19.51 9.745c3.978-1.965 7.93-3.956 19.51-9.745m0 0h0m0 0h0" fill="currentColor" stroke-linecap="round" stroke="currentColor" stroke-width="4"/></g>`;
|
||||
export const DISK_ICON_NAME = "save";
|
||||
export const EXPORT_IMG_ICON = ` <g transform="scale(4.166)" strokeWidth="1.25" fill="none" stroke="currentColor"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 8h.01"></path><path d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5"></path><path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"></path><path d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"></path><path d="M19 16v6"></path><path d="M22 19l-3 3l-3 -3"></path></g>`;
|
||||
export const EXPORT_IMG_ICON_NAME = `export-img`;
|
||||
export const EXCALIDRAW_ICON = `<path d="M24 17h121v121H24z" style="fill:none" transform="matrix(.8843 0 0 .83471 -21.223 -14.19)"/><path d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01Zm-66.93-65.3c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02Zm78.54-1.18c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" style="fill:currentColor;fill-rule:nonzero" transform="translate(-26.41 -29.49)"/>`;
|
||||
116
src/constants/startupScript.md
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
#exclude
|
||||
```js*/
|
||||
/**
|
||||
* If set, this callback is triggered when the user closes an Excalidraw view.
|
||||
* onViewUnloadHook: (view: ExcalidrawView) => void = null;
|
||||
*/
|
||||
//ea.onViewUnloadHook = (view) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user changes the view mode.
|
||||
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
|
||||
* onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
|
||||
*/
|
||||
//ea.onViewModeChangeHook = (isViewModeEnabled, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
||||
* onLinkHoverHook: (
|
||||
* element: NonDeletedExcalidrawElement,
|
||||
* linkText: string,
|
||||
* view: ExcalidrawView,
|
||||
* ea: ExcalidrawAutomate
|
||||
* ) => boolean = null;
|
||||
*/
|
||||
//ea.onLinkHoverHook = (element, linkText, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user clicks a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
|
||||
* onLinkClickHook:(
|
||||
* element: ExcalidrawElement,
|
||||
* linkText: string,
|
||||
* event: MouseEvent,
|
||||
* view: ExcalidrawView,
|
||||
* ea: ExcalidrawAutomate
|
||||
* ) => boolean = null;
|
||||
*/
|
||||
//ea.onLinkClickHook = (element,linkText,event, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
|
||||
* You can use this callback in case you want to do something additional when the onDrop event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
|
||||
* onDropHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* event: React.DragEvent<HTMLDivElement>;
|
||||
* draggable: any; //Obsidian draggable object
|
||||
* type: "file" | "text" | "unknown";
|
||||
* payload: {
|
||||
* files: TFile[]; //TFile[] array of dropped files
|
||||
* text: string; //string
|
||||
* };
|
||||
* excalidrawFile: TFile; //the file receiving the drop event
|
||||
* view: ExcalidrawView; //the excalidraw view receiving the drop
|
||||
* pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
||||
* }) => boolean = null;
|
||||
*/
|
||||
//ea.onDropHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
* onPasteHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* payload: ClipboardData;
|
||||
* event: ClipboardEvent;
|
||||
* excalidrawFile: TFile; //the file receiving the paste event
|
||||
* view: ExcalidrawView; //the excalidraw view receiving the paste
|
||||
* pointerPosition: { x: number; y: number }; //the pointer position on canvas
|
||||
* }) => boolean = null;
|
||||
*/
|
||||
//ea.onPasteHook = (data) => {};
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
* onFileOpenHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* excalidrawFile: TFile; //the file being loaded
|
||||
* view: ExcalidrawView;
|
||||
* }) => Promise<void>;
|
||||
*/
|
||||
//ea.onFileOpenHook = (data) => {};
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
* onFileCreateHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* excalidrawFile: TFile; //the file being created
|
||||
* view: ExcalidrawView;
|
||||
* }) => Promise<void>;
|
||||
*/
|
||||
//ea.onFileCreateHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
* onCanvasColorChangeHook: (
|
||||
* ea: ExcalidrawAutomate,
|
||||
* view: ExcalidrawView, //the excalidraw view
|
||||
* color: string,
|
||||
* ) => void = null;
|
||||
*/
|
||||
//ea.onCanvasColorChangeHook = (ea, view, color) => {};
|
||||
7
src/constants/starutpscript.ts
Normal file
@@ -1,12 +1,13 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants";
|
||||
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
|
||||
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants/constants";
|
||||
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
|
||||
import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils";
|
||||
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
@@ -18,13 +19,22 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
const getTheme = (view: ExcalidrawView, theme:string): string => view.excalidrawData.embeddableTheme === "dark"
|
||||
? "theme-dark"
|
||||
: view.excalidrawData.embeddableTheme === "light"
|
||||
? "theme-light"
|
||||
: view.excalidrawData.embeddableTheme === "auto"
|
||||
? theme === "dark" ? "theme-dark" : "theme-light"
|
||||
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Render webview for anything other than Vimeo and Youtube
|
||||
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
|
||||
//required to control the video
|
||||
//--------------------------------------------------------------------------------
|
||||
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
|
||||
if(DEVICE.isDesktop) {
|
||||
const isDataURL = src.startsWith("data:");
|
||||
if(DEVICE.isDesktop && !isDataURL) {
|
||||
return (
|
||||
<webview
|
||||
ref={(ref) => view.updateEmbeddableRef(id, ref)}
|
||||
@@ -46,11 +56,12 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
|
||||
title="Excalidraw Embedded Content"
|
||||
allowFullScreen={true}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
src={src}
|
||||
src={isDataURL ? null : src}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
borderRadius: "var(--embeddable-radius)",
|
||||
}}
|
||||
srcDoc={isDataURL ? atob(src.split(',')[1]) : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -59,13 +70,15 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
|
||||
//Render WorkspaceLeaf or CanvasNode
|
||||
//--------------------------------------------------------------------------------
|
||||
function RenderObsidianView(
|
||||
{ element, linkText, view, containerRef, appState, theme }:{
|
||||
{ mdProps, element, linkText, view, containerRef, activeEmbeddable, theme, canvasColor }:{
|
||||
mdProps: EmbeddableMDCustomProps;
|
||||
element: NonDeletedExcalidrawElement;
|
||||
linkText: string;
|
||||
view: ExcalidrawView;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
appState: UIAppState;
|
||||
activeEmbeddable: {element: NonDeletedExcalidrawElement; state: string};
|
||||
theme: string;
|
||||
canvasColor: string;
|
||||
}): JSX.Element {
|
||||
|
||||
const { subpath, file } = processLinkText(linkText, view);
|
||||
@@ -79,8 +92,19 @@ function RenderObsidianView(
|
||||
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
|
||||
const isEditingRef = react.useRef(false);
|
||||
const isActiveRef = react.useRef(false);
|
||||
const themeRef = react.useRef(theme);
|
||||
const elementRef = react.useRef(element);
|
||||
|
||||
// Update themeRef when theme changes
|
||||
react.useEffect(() => {
|
||||
themeRef.current = theme;
|
||||
}, [theme]);
|
||||
|
||||
// Update elementRef when element changes
|
||||
react.useEffect(() => {
|
||||
elementRef.current = element;
|
||||
}, [element]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//block propagation of events to the parent if the iframe element is active
|
||||
//--------------------------------------------------------------------------------
|
||||
@@ -192,6 +216,7 @@ function RenderObsidianView(
|
||||
//This runs only when the file is added, thus should not be a major performance issue
|
||||
await leafRef.current.leaf.setViewState({state: {file:null}})
|
||||
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
|
||||
setColors(containerRef.current, element, mdProps, canvasColor);
|
||||
} else {
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
|
||||
@@ -205,9 +230,84 @@ function RenderObsidianView(
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath, containerRef]);
|
||||
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
|
||||
if(!mdProps) return;
|
||||
if (!leafRef.current?.hasOwnProperty("node")) return;
|
||||
|
||||
const canvasNodeContainer = containerRef.current?.firstElementChild as HTMLElement;
|
||||
|
||||
if(mdProps.useObsidianDefaults) {
|
||||
canvasNode?.style.removeProperty("--canvas-background");
|
||||
canvasNodeContainer?.style.removeProperty("background-color");
|
||||
canvasNode?.style.removeProperty("--canvas-border");
|
||||
canvasNodeContainer?.style.removeProperty("border-color");
|
||||
return;
|
||||
}
|
||||
|
||||
const ea = view.plugin.ea;
|
||||
if(mdProps.backgroundMatchElement) {
|
||||
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
|
||||
const color = element?.backgroundColor
|
||||
? (element.backgroundColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
|
||||
: "transparent";
|
||||
|
||||
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
|
||||
canvasNode?.style.setProperty("--canvas-background", color);
|
||||
canvasNode?.style.setProperty("--background-primary", color);
|
||||
canvasNodeContainer?.style.setProperty("background-color", color);
|
||||
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
|
||||
const opacity = (mdProps.backgroundOpacity??100)/100;
|
||||
const color = mdProps.backgroundMatchCanvas
|
||||
? (canvasColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
|
||||
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
|
||||
|
||||
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
|
||||
canvasNode?.style.setProperty("--canvas-background", color);
|
||||
canvasNode?.style.setProperty("--background-primary", color);
|
||||
canvasNodeContainer?.style.setProperty("background-color", color);
|
||||
}
|
||||
|
||||
if(mdProps.borderMatchElement) {
|
||||
const opacity = (mdProps?.borderOpacity ?? 50)/100;
|
||||
const color = element?.strokeColor
|
||||
? (element.strokeColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
|
||||
: "transparent";
|
||||
canvasNode?.style.setProperty("--canvas-border", color);
|
||||
canvasNode?.style.setProperty("--canvas-color", color);
|
||||
canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
} else if(!(mdProps?.borderMatchElement ?? true)) {
|
||||
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
|
||||
canvasNode?.style.setProperty("--canvas-border", color);
|
||||
canvasNode?.style.setProperty("--canvas-color", color);
|
||||
canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
}
|
||||
}
|
||||
|
||||
react.useEffect(() => {
|
||||
if(!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
const element = elementRef.current;
|
||||
const canvasNode = containerRef.current;
|
||||
if(!canvasNode.hasClass("canvas-node")) return;
|
||||
setColors(canvasNode, element, mdProps, canvasColor);
|
||||
}, [
|
||||
mdProps,
|
||||
elementRef.current,
|
||||
containerRef.current,
|
||||
canvasColor,
|
||||
])
|
||||
|
||||
react.useEffect(() => {
|
||||
if(isEditingRef.current) {
|
||||
if(leafRef.current?.node) {
|
||||
containerRef.current?.addClasses(["is-editing", "is-focused"]);
|
||||
view.canvasNodeFactory.stopEditing(leafRef.current.node);
|
||||
}
|
||||
isEditingRef.current = false;
|
||||
@@ -242,10 +342,12 @@ function RenderObsidianView(
|
||||
patchMobileView(view);
|
||||
} else if (leafRef.current?.node) {
|
||||
//Handle canvas node
|
||||
view.canvasNodeFactory.startEditing(leafRef.current.node, theme);
|
||||
const newTheme = getTheme(view, themeRef.current);
|
||||
containerRef.current?.addClasses(["is-editing", "is-focused"]);
|
||||
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
|
||||
}
|
||||
}
|
||||
}, [leafRef.current?.leaf, element.id]);
|
||||
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// Set isActiveRef and switch to preview mode when the iframe is not active
|
||||
@@ -256,7 +358,7 @@ function RenderObsidianView(
|
||||
}
|
||||
|
||||
const previousIsActive = isActiveRef.current;
|
||||
isActiveRef.current = (appState.activeEmbeddable?.element.id === element.id) && (appState.activeEmbeddable?.state === "active");
|
||||
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
|
||||
|
||||
if (previousIsActive === isActiveRef.current) {
|
||||
return;
|
||||
@@ -278,20 +380,17 @@ function RenderObsidianView(
|
||||
}
|
||||
} else if (leafRef.current?.node) {
|
||||
//Handle canvas node
|
||||
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
|
||||
view.canvasNodeFactory.stopEditing(leafRef.current.node);
|
||||
}
|
||||
}, [
|
||||
containerRef,
|
||||
leafRef,
|
||||
isActiveRef,
|
||||
appState.activeEmbeddable?.element,
|
||||
appState.activeEmbeddable?.state,
|
||||
activeEmbeddable?.element,
|
||||
activeEmbeddable?.state,
|
||||
element,
|
||||
view,
|
||||
linkText,
|
||||
subpath,
|
||||
file,
|
||||
theme,
|
||||
isEditingRef,
|
||||
view.canvasNodeFactory
|
||||
]);
|
||||
@@ -299,16 +398,12 @@ function RenderObsidianView(
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
|
||||
const theme = view.excalidrawData.embeddableTheme === "dark"
|
||||
? "theme-dark"
|
||||
: view.excalidrawData.embeddableTheme === "light"
|
||||
? "theme-light"
|
||||
: view.excalidrawData.embeddableTheme === "auto"
|
||||
? appState.theme === "dark" ? "theme-dark" : "theme-light"
|
||||
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
const theme = getTheme(view, appState.theme);
|
||||
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -319,15 +414,19 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
|
||||
borderRadius: "var(--embeddable-radius)",
|
||||
color: `var(--text-normal)`,
|
||||
}}
|
||||
className={theme}
|
||||
className={`${theme} canvas-node ${
|
||||
mdProps?.filenameVisible && !mdProps.useObsidianDefaults ? "" : "excalidraw-mdEmbed-hideFilename"}`}
|
||||
>
|
||||
<RenderObsidianView
|
||||
mdProps={mdProps}
|
||||
element={element}
|
||||
linkText={linkText}
|
||||
view={view}
|
||||
containerRef={containerRef}
|
||||
appState={appState}
|
||||
theme={theme}/>
|
||||
activeEmbeddable={appState.activeEmbeddable}
|
||||
theme={appState.theme}
|
||||
canvasColor={appState.viewBackgroundColor}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
179
src/dialogs/EmbeddableMDFileCustomDataSettingsComponent.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Setting, ToggleComponent } from "obsidian";
|
||||
import { EmbeddableMDCustomProps } from "./EmbeddableSettings";
|
||||
import { fragWithHTML } from "src/utils/Utils";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
export class EmbeddalbeMDFileCustomDataSettingsComponent {
|
||||
constructor (
|
||||
private contentEl: HTMLElement,
|
||||
private mdCustomData: EmbeddableMDCustomProps,
|
||||
private update?: Function,
|
||||
private isMDFile: boolean = true,
|
||||
) {
|
||||
if(!update) this.update = () => {};
|
||||
}
|
||||
|
||||
render() {
|
||||
let detailsDIV: HTMLDivElement;
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName(t("ES_USE_OBSIDIAN_DEFAULTS"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.mdCustomData.useObsidianDefaults)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.useObsidianDefaults = value;
|
||||
detailsDIV.style.display = value ? "none" : "block";
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
this.contentEl.createEl("hr", { cls: "excalidraw-setting-hr" });
|
||||
|
||||
detailsDIV = this.contentEl.createDiv();
|
||||
detailsDIV.style.display = this.mdCustomData.useObsidianDefaults ? "none" : "block";
|
||||
|
||||
const contentEl = detailsDIV
|
||||
if(this.isMDFile) {
|
||||
new Setting(contentEl)
|
||||
.setName(t("ES_FILENAME_VISIBLE"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.mdCustomData.filenameVisible)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.filenameVisible = value;
|
||||
})
|
||||
);
|
||||
}
|
||||
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
|
||||
|
||||
let bgSetting: Setting;
|
||||
let bgMatchElementToggle: ToggleComponent;
|
||||
let bgMatchCanvasToggle: ToggleComponent;
|
||||
new Setting(contentEl)
|
||||
.setName(t("ES_BACKGROUND_MATCH_ELEMENT"))
|
||||
.addToggle(toggle => {
|
||||
bgMatchElementToggle = toggle;
|
||||
toggle
|
||||
.setValue(this.mdCustomData.backgroundMatchElement)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.backgroundMatchElement = value;
|
||||
if(value) {
|
||||
bgSetting.settingEl.style.display = "none";
|
||||
if(this.mdCustomData.backgroundMatchCanvas) {
|
||||
bgMatchCanvasToggle.setValue(false);
|
||||
}
|
||||
} else {
|
||||
if(!this.mdCustomData.backgroundMatchCanvas) {
|
||||
bgSetting.settingEl.style.display = "";
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
})
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName(t("ES_BACKGROUND_MATCH_CANVAS"))
|
||||
.addToggle(toggle => {
|
||||
bgMatchCanvasToggle = toggle;
|
||||
toggle
|
||||
.setValue(this.mdCustomData.backgroundMatchCanvas)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.backgroundMatchCanvas = value;
|
||||
if(value) {
|
||||
bgSetting.settingEl.style.display = "none";
|
||||
if(this.mdCustomData.backgroundMatchElement) {
|
||||
bgMatchElementToggle.setValue(false);
|
||||
}
|
||||
} else {
|
||||
if(!this.mdCustomData.backgroundMatchElement) {
|
||||
bgSetting.settingEl.style.display = "";
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
})
|
||||
});
|
||||
|
||||
if(this.mdCustomData.backgroundMatchElement && this.mdCustomData.backgroundMatchCanvas) {
|
||||
bgMatchCanvasToggle.setValue(false);
|
||||
}
|
||||
|
||||
bgSetting = new Setting(contentEl)
|
||||
.setName(t("ES_BACKGROUND_COLOR"))
|
||||
.addColorPicker(colorPicker =>
|
||||
colorPicker
|
||||
.setValue(this.mdCustomData.backgroundColor)
|
||||
.onChange((value) => {
|
||||
this.mdCustomData.backgroundColor = value;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
|
||||
const opacity = (value:number):DocumentFragment => {
|
||||
return fragWithHTML(`Current opacity is <b>${value}%</b>`);
|
||||
}
|
||||
|
||||
const bgOpacitySetting = new Setting(contentEl)
|
||||
.setName(t("ES_BACKGROUND_OPACITY"))
|
||||
.setDesc(opacity(this.mdCustomData.backgroundOpacity))
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(0,100,5)
|
||||
.setValue(this.mdCustomData.backgroundOpacity)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.backgroundOpacity = value;
|
||||
bgOpacitySetting.setDesc(opacity(value));
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
if(this.isMDFile) {
|
||||
contentEl.createEl("h4",{text: t("ES_BORDER_HEAD")});
|
||||
let borderSetting: Setting;
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName(t("ES_BORDER_MATCH_ELEMENT"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.mdCustomData.borderMatchElement)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.borderMatchElement = value;
|
||||
if(value) {
|
||||
borderSetting.settingEl.style.display = "none";
|
||||
} else {
|
||||
borderSetting.settingEl.style.display = "";
|
||||
}
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
borderSetting = new Setting(contentEl)
|
||||
.setName(t("ES_BORDER_COLOR"))
|
||||
.addColorPicker(colorPicker =>
|
||||
colorPicker
|
||||
.setValue(this.mdCustomData.borderColor)
|
||||
.onChange((value) => {
|
||||
this.mdCustomData.borderColor = value;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
borderSetting.settingEl.style.display = this.mdCustomData.borderMatchElement ? "none" : "";
|
||||
|
||||
const borderOpacitySetting = new Setting(contentEl)
|
||||
.setName(t("ES_BORDER_OPACITY"))
|
||||
.setDesc(opacity(this.mdCustomData.borderOpacity))
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(0,100,5)
|
||||
.setValue(this.mdCustomData.borderOpacity)
|
||||
.onChange(value => {
|
||||
this.mdCustomData.borderOpacity = value;
|
||||
borderOpacitySetting.setDesc(opacity(value));
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
220
src/dialogs/EmbeddableSettings.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { Modal, Notice, Setting, TFile, ToggleComponent } from "obsidian";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { t } from "src/lang/helpers";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename } from "src/utils/FileUtils";
|
||||
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
|
||||
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
|
||||
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
|
||||
import { isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
|
||||
export type EmbeddableMDCustomProps = {
|
||||
useObsidianDefaults: boolean;
|
||||
backgroundMatchCanvas: boolean;
|
||||
backgroundMatchElement: boolean;
|
||||
backgroundColor: string;
|
||||
backgroundOpacity: number;
|
||||
borderMatchElement: boolean;
|
||||
borderColor: string;
|
||||
borderOpacity: number;
|
||||
filenameVisible: boolean;
|
||||
}
|
||||
|
||||
export class EmbeddableSettings extends Modal {
|
||||
private ea: ExcalidrawAutomate;
|
||||
private updatedFilepath: string = null;
|
||||
private zoomValue: number;
|
||||
private isYouTube: boolean;
|
||||
private youtubeStart: string = null;
|
||||
private isMDFile: boolean;
|
||||
private notExcalidrawIsInternal: boolean;
|
||||
private isLocalURI: boolean;
|
||||
private mdCustomData: EmbeddableMDCustomProps;
|
||||
private onKeyDown: (ev: KeyboardEvent) => void;
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
private file: TFile,
|
||||
private element: ExcalidrawEmbeddableElement
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.ea = getEA(this.view);
|
||||
this.ea.copyViewElementsToEAforEditing([this.element]);
|
||||
this.zoomValue = element.scale[0];
|
||||
this.isYouTube = isYouTube(this.element.link);
|
||||
this.notExcalidrawIsInternal = this.file && !this.view.plugin.isExcalidrawFile(this.file)
|
||||
this.isMDFile = this.file && this.file.extension === "md" && !this.view.plugin.isExcalidrawFile(this.file);
|
||||
this.isLocalURI = this.element.link.startsWith("file://");
|
||||
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
|
||||
|
||||
this.mdCustomData = element.customData?.mdProps ?? view.plugin.settings.embeddableMarkdownDefaults;
|
||||
if(!element.customData?.mdProps) {
|
||||
const bgCM = this.plugin.ea.getCM(element.backgroundColor);
|
||||
this.mdCustomData.backgroundColor = bgCM.stringHEX({alpha: false});
|
||||
this.mdCustomData.backgroundOpacity = element.opacity;
|
||||
const borderCM = this.plugin.ea.getCM(element.strokeColor);
|
||||
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
|
||||
this.mdCustomData.borderOpacity = element.opacity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
//this.titleEl.setText(t("ES_TITLE"));
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.containerEl.removeEventListener("keydown",this.onKeyDown);
|
||||
}
|
||||
|
||||
async createForm() {
|
||||
|
||||
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
|
||||
|
||||
if(this.file) {
|
||||
new Setting(this.contentEl)
|
||||
.setName(t("ES_RENAME"))
|
||||
.addText(text =>
|
||||
text
|
||||
.setValue(getPathWithoutExtension(this.file))
|
||||
.onChange(async (value) => {
|
||||
this.updatedFilepath = value;
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const zoomValue = ():DocumentFragment => {
|
||||
return fragWithHTML(`${t("ES_ZOOM_100_RELATIVE_DESC")}<br>Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
|
||||
}
|
||||
|
||||
const zoomSetting = new Setting(this.contentEl)
|
||||
.setName(t("ES_ZOOM"))
|
||||
.setDesc(zoomValue())
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("ES_ZOOM_100"))
|
||||
.onClick(() => {
|
||||
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
this.zoomValue = 1/api.getAppState().zoom.value;
|
||||
zoomSetting.setDesc(zoomValue());
|
||||
})
|
||||
)
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(10,400,5)
|
||||
.setValue(this.zoomValue*100)
|
||||
.onChange(value => {
|
||||
this.zoomValue = value/100;
|
||||
zoomSetting.setDesc(zoomValue());
|
||||
})
|
||||
)
|
||||
|
||||
if(this.isYouTube) {
|
||||
new Setting(this.contentEl)
|
||||
.setName(t("ES_YOUTUBE_START"))
|
||||
.setDesc(t("ES_YOUTUBE_START_DESC"))
|
||||
.addText(text =>
|
||||
text
|
||||
.setValue(this.youtubeStart)
|
||||
.onChange(async (value) => {
|
||||
this.youtubeStart = value;
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if(this.isMDFile || this.notExcalidrawIsInternal) {
|
||||
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
|
||||
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData, undefined, this.isMDFile).render();
|
||||
}
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
|
||||
.setTooltip("ESC")
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
})
|
||||
)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_OK"))
|
||||
.setTooltip("CTRL/Opt+Enter")
|
||||
.setCta()
|
||||
.onClick(()=>this.applySettings())
|
||||
)
|
||||
|
||||
|
||||
const onKeyDown = (ev: KeyboardEvent) => {
|
||||
if(isWinCTRLorMacCMD(ev) && ev.key === "Enter") {
|
||||
this.applySettings();
|
||||
}
|
||||
}
|
||||
|
||||
this.onKeyDown = onKeyDown;
|
||||
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async applySettings() {
|
||||
let dirty = false;
|
||||
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
|
||||
if(this.updatedFilepath) {
|
||||
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
|
||||
if(newPathWithExt !== this.file.path) {
|
||||
const fnparts = splitFolderAndFilename(newPathWithExt);
|
||||
const newPath = getNewUniqueFilepath(
|
||||
this.app.vault,
|
||||
fnparts.folderpath,
|
||||
fnparts.filename,
|
||||
);
|
||||
await this.app.vault.rename(this.file,newPath);
|
||||
el.link = this.element.link.replace(
|
||||
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
|
||||
this.plugin.app.metadataCache.fileToLinktext(
|
||||
this.file,this.view.file.path,true)
|
||||
}$3`);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
|
||||
dirty = true;
|
||||
if(this.youtubeStart === "" || isValidYouTubeStart(this.youtubeStart)) {
|
||||
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
|
||||
} else {
|
||||
new Notice(t("ES_YOUTUBE_START_INVALID"));
|
||||
}
|
||||
}
|
||||
if(
|
||||
this.isMDFile && (
|
||||
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
|
||||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
|
||||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
|
||||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
|
||||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
|
||||
) {
|
||||
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if(this.zoomValue !== this.element.scale[0]) {
|
||||
dirty = true;
|
||||
|
||||
el.scale = [this.zoomValue,this.zoomValue];
|
||||
}
|
||||
if(dirty) {
|
||||
this.ea.addElementsToView();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Modal, Setting, TFile } from "obsidian";
|
||||
import { getEA } from "src";
|
||||
import { DEVICE } from "src/constants";
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
|
||||
41
src/dialogs/InsertCommandDialog.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import { t } from "../lang/helpers";
|
||||
|
||||
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.limit = 20;
|
||||
this.setInstructions([
|
||||
{
|
||||
command: t("SELECT_COMMAND"),
|
||||
purpose: "",
|
||||
},
|
||||
]);
|
||||
this.setPlaceholder(t("SELECT_COMMAND_PLACEHOLDER"));
|
||||
this.emptyStateText = t("NO_MATCHING_COMMAND");
|
||||
}
|
||||
|
||||
getItems(): any[] {
|
||||
//@ts-ignore
|
||||
return this.app.commands.listCommands();
|
||||
}
|
||||
|
||||
getItemText(item: any): string {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
onChooseItem(item: any): void {
|
||||
const cmdId = item?.id;
|
||||
this.addText(`⚙️[${item.name}](cmd://${item.id})`);
|
||||
}
|
||||
|
||||
public start(addText: Function) {
|
||||
this.addText = addText;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { isALT, scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
|
||||
import { fileURLToPath } from "url";
|
||||
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants";
|
||||
import { scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
|
||||
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
@@ -60,6 +59,7 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
|
||||
const scaleToFullsize = scaleToFullsizeModifier(event);
|
||||
(async () => {
|
||||
//this.view.currentPosition = this.position;
|
||||
await ea.addImage(0, 0, item, !scaleToFullsize);
|
||||
ea.addElementsToView(true, true, true);
|
||||
})();
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { getLink } from "src/utils/FileUtils";
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.app = plugin.app;
|
||||
this.limit = 20;
|
||||
this.setInstructions([
|
||||
{
|
||||
@@ -45,7 +47,8 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
true,
|
||||
);
|
||||
}
|
||||
this.addText(`[[${filepath + (item.alias ? `|${item.alias}` : "")}]]`);
|
||||
const link = getLink(this.plugin,{embed: false, path: filepath, alias: item.alias});
|
||||
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function) {
|
||||
|
||||
@@ -6,12 +6,15 @@ import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
|
||||
export class InsertPDFModal extends Modal {
|
||||
private borderBox: boolean = true;
|
||||
private gapSize:number = 20;
|
||||
private groupPages: boolean = false;
|
||||
private direction: "down" | "right" = "right";
|
||||
private numColumns: number = 1;
|
||||
private numRows: number = 1;
|
||||
private lockAfterImport: boolean = true;
|
||||
private pagesToImport:number[] = [];
|
||||
private pageDimensions: {width: number, height: number} = {width: 0, height: 0};
|
||||
@@ -21,7 +24,6 @@ export class InsertPDFModal extends Modal {
|
||||
private pdfFile: TFile;
|
||||
private dirty: boolean = false;
|
||||
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
@@ -47,7 +49,10 @@ export class InsertPDFModal extends Modal {
|
||||
this.plugin.settings.pdfImportScale = this.importScale;
|
||||
this.plugin.settings.pdfBorderBox = this.borderBox;
|
||||
this.plugin.settings.pdfGapSize = this.gapSize;
|
||||
this.plugin.settings.pdfGroupPages = this.groupPages;
|
||||
this.plugin.settings.pdfNumColumns = this.numColumns;
|
||||
this.plugin.settings.pdfNumRows = this.numRows;
|
||||
this.plugin.settings.pdfDirection = this.direction;
|
||||
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
|
||||
this.plugin.saveSettings();
|
||||
}
|
||||
@@ -111,7 +116,10 @@ export class InsertPDFModal extends Modal {
|
||||
await this.plugin.loadSettings();
|
||||
this.borderBox = this.plugin.settings.pdfBorderBox;
|
||||
this.gapSize = this.plugin.settings.pdfGapSize;
|
||||
this.groupPages = this.plugin.settings.pdfGroupPages;
|
||||
this.numColumns = this.plugin.settings.pdfNumColumns;
|
||||
this.numRows = this.plugin.settings.pdfNumRows;
|
||||
this.direction = this.plugin.settings.pdfDirection;
|
||||
this.lockAfterImport = this.plugin.settings.pdfLockAfterImport;
|
||||
this.importScale = this.plugin.settings.pdfImportScale;
|
||||
|
||||
@@ -211,7 +219,18 @@ export class InsertPDFModal extends Modal {
|
||||
this.borderBox = value;
|
||||
this.dirty = true;
|
||||
}))
|
||||
|
||||
|
||||
new Setting(ce)
|
||||
.setName("Group pages")
|
||||
.setDesc("This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.")
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.groupPages)
|
||||
.onChange((value) => {
|
||||
this.groupPages = value
|
||||
this.dirty = true;
|
||||
}))
|
||||
|
||||
|
||||
new Setting(ce)
|
||||
.setName("Lock pages on canvas after import")
|
||||
.addToggle(toggle => toggle
|
||||
@@ -221,8 +240,38 @@ export class InsertPDFModal extends Modal {
|
||||
this.dirty = true;
|
||||
}))
|
||||
|
||||
let columnsText: HTMLDivElement;
|
||||
let numColumnsSetting: Setting;
|
||||
let numRowsSetting: Setting;
|
||||
const colRowVisibility = () => {
|
||||
switch(this.direction) {
|
||||
case "down":
|
||||
numColumnsSetting.settingEl.style.display = "none";
|
||||
numRowsSetting.settingEl.style.display = "";
|
||||
break;
|
||||
case "right":
|
||||
numColumnsSetting.settingEl.style.display = "";
|
||||
numRowsSetting.settingEl.style.display = "none";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
new Setting(ce)
|
||||
.setName("Import direction")
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOptions({
|
||||
"down": "Top > Down",
|
||||
"right": "Left > Right"
|
||||
})
|
||||
.setValue(this.direction)
|
||||
.onChange(value => {
|
||||
this.direction = value as "down" | "right";
|
||||
colRowVisibility();
|
||||
this.dirty = true;
|
||||
}))
|
||||
|
||||
let columnsText: HTMLDivElement;
|
||||
numColumnsSetting = new Setting(ce);
|
||||
numColumnsSetting
|
||||
.setName("Number of columns")
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 100, 1)
|
||||
@@ -239,6 +288,26 @@ export class InsertPDFModal extends Modal {
|
||||
el.innerText = ` ${this.numColumns.toString()}`;
|
||||
});
|
||||
|
||||
let rowsText: HTMLDivElement;
|
||||
numRowsSetting = new Setting(ce);
|
||||
numRowsSetting
|
||||
.setName("Number of rows")
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 100, 1)
|
||||
.setValue(this.numRows)
|
||||
.onChange(value => {
|
||||
this.numRows = value;
|
||||
rowsText.innerText = ` ${value.toString()}`;
|
||||
this.dirty = true;
|
||||
}))
|
||||
.settingEl.createDiv("", (el) => {
|
||||
rowsText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.numRows.toString()}`;
|
||||
});
|
||||
colRowVisibility();
|
||||
|
||||
let gapSizeText: HTMLDivElement;
|
||||
new Setting(ce)
|
||||
.setName("Size of gap between pages")
|
||||
@@ -256,7 +325,7 @@ export class InsertPDFModal extends Modal {
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.gapSize.toString()}`;
|
||||
});
|
||||
|
||||
|
||||
const importSizeSetting = new Setting(ce)
|
||||
.setName("Imported page size")
|
||||
.setDesc(`${this.pageDimensions.width*this.importScale} x ${this.pageDimensions.height*this.importScale}`)
|
||||
@@ -303,6 +372,7 @@ export class InsertPDFModal extends Modal {
|
||||
topX,
|
||||
topY,
|
||||
this.pdfFile.path + `#page=${page}`,
|
||||
false,
|
||||
false);
|
||||
const imgEl = ea.getElement(imageID) as any;
|
||||
imgEl.width = imgWidth;
|
||||
@@ -310,9 +380,21 @@ export class InsertPDFModal extends Modal {
|
||||
if(this.lockAfterImport) imgEl.locked = true;
|
||||
|
||||
ea.addToGroup([boxID,imageID]);
|
||||
|
||||
column = (column + 1) % this.numColumns;
|
||||
if(column === 0) row++;
|
||||
|
||||
switch(this.direction) {
|
||||
case "right":
|
||||
column = (column + 1) % this.numColumns;
|
||||
if(column === 0) row++;
|
||||
break;
|
||||
case "down":
|
||||
row = (row + 1) % this.numRows;
|
||||
if(row === 0) column++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(this.groupPages) {
|
||||
const ids = ea.getElements().map(el => el.id);
|
||||
ea.addToGroup(ids);
|
||||
}
|
||||
await ea.addElementsToView(true,true,false);
|
||||
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
||||
|
||||
@@ -17,6 +17,359 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"2.0.14":`
|
||||
## New
|
||||
- Stylus button now activates the eraser function. Note: This feature is supported for styluses that comply with industry-standard button events. Unfortunately, Samsung SPEN and Apple Pencil do not support this functionality.
|
||||
|
||||
## Fixed
|
||||
- Improved handwriting quality. I have resolved the long-standing issue of closing the loop when ends of the line are close, making an "u" into an "o" ([#1529](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1529) and [#6303](https://github.com/excalidraw/excalidraw/issues/6303)).
|
||||
- Improved Excalidraw's full-screen mode behavior. Access it via the Obsidian Command Palette or the full-screen button on the Obsidian Tools Panel ([#1528](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1528)).
|
||||
- Fixed color picker overlapping with the Obsidian mobile toolbar on Obsidian-Mobile.
|
||||
- Corrected display issues with alternative font sizes (Fibonacci and Zoom relative) in the element properties panel when editing a text element (refer to [2.0.11 Release Notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.11) for details about the font-size Easter Egg).
|
||||
- Resolved the issue where Excalidraw SVG exports containing LaTeX were not loading correctly into Inkscape ([#1519](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1519)). Thanks to 🙏@HyunggyuJang for the contribution.
|
||||
`,
|
||||
"2.0.13":`
|
||||
## Fixed
|
||||
- Excalidraw crashes if you paste an image and right-click on canvas immediately after pasting.
|
||||
`,
|
||||
"2.0.12":`
|
||||
## Fixed
|
||||
- Stencil library not working [#1516](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1516), [#1517](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1517)
|
||||
- The new convert image from URL to Local File feature did not work in two situations:
|
||||
- When the embedded image is downloaded from a very slow server (e.g. OpenAIs temp image server)
|
||||
- On Android
|
||||
- The postToOpenAI function did not work in all situations on Android.
|
||||
- ExcaliAI wireframe to code did not display correctly on Android
|
||||
- Tooltips kept popping up on Android.
|
||||
|
||||
## New
|
||||
- Added "Save image from URL to local file" to the right-click context menu
|
||||
- Further [ExcaliAI](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md) improvements including support for image editing with image mask
|
||||
`,
|
||||
"2.0.11":`
|
||||
## Fixed
|
||||
- Resolved an Obsidian performance issue caused by simultaneous installations of Excalidraw and the Minimal theme. Optimized Excalidraw CSS loading into Obsidian since April 2021, resulting in noticeable performance improvements. ([#1456](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1456))
|
||||
- Removed default support for the [Sliding Panes Plugin](https://github.com/deathau/sliding-panes-obsidian) due to compatibility issues with Obsidian Workspaces. Obsidian's "Stack Tabs" feature now supersedes Sliding Panes. To re-enable sliding panes support, navigate to Compatibility Features in Plugin Settings.
|
||||
- Sometimes images referenced with URLs did not show in exported scenes and when embedding Excalidraw into a markdown note. I hope all that is now resolved.
|
||||
- ExcalidrawAutomate scripts sometimes were not able to save their settings.
|
||||
|
||||
## New
|
||||
- Introduced an "Easter Egg" feature in font-size properties:
|
||||
- Hold SHIFT while selecting font size to use scaled sizes (S, M, L, XL) based on the current canvas zoom, ensuring consistent sizes within zoom ranges.
|
||||
- Hold ALT/OPT while selecting font size to use values based on the golden mean (s:16, m:26, l:42, xl:68). ALT+SHIFT scales font sizes based on canvas zoom.
|
||||
- Scaled sizes are sticky; new text elements adjust font sizes relative to the canvas zoom. Deselect SHIFT to disable this feature.
|
||||
- For more on the Golden Scale, watch [The Golden Opportunity](https://youtu.be/2SHn_ruax-s).
|
||||
- Added two new Command Palette Actions:
|
||||
- "Decompress current Excalidraw File" in Markdown View mode helps repair corrupted, compressed Excalidraw files manually.
|
||||
- "Save image from URL to local file" saves referenced URL images to your Vault, replacing images in the drawing.
|
||||
- Updated the ExcaliAI script to generate images using ExcaliAI.
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
- Added additional documentation about functions to ea.suggester.
|
||||
- Added ea.help(). You can use this function from Developer Console to print help information about functions. Usage: ${String.fromCharCode(96)}ea.help(ea.functionName)${String.fromCharCode(96)} or ${String.fromCharCode(96)}ea.help('propertyName')${String.fromCharCode(96)} - notice property name is in quotes.
|
||||
`,
|
||||
"2.0.10":`
|
||||
One more minor tweak to support an updated ExcaliAI script - now available in the script store.
|
||||
`,
|
||||
"2.0.9":`
|
||||
This release is very minor, and I apologize for the frequent updates in a short span. I chose not to delay this fix for 1-2 weeks, waiting for my larger release. The WireframeToAI feature wasn't working in 2.0.8, but now it does.
|
||||
`,
|
||||
"2.0.8":`
|
||||
## New
|
||||
- Mermaid Class Diagrams [#7381](https://github.com/excalidraw/excalidraw/pull/7381)
|
||||
- New Scripts:
|
||||
- Repeat Texts contributed by @soraliu [#1425](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1425)
|
||||
- Relative Font Size Cycle [#1474](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1474)
|
||||
- New setting to configure the URL used to reach the OpenAI API - for setting an OpenAI API compatible local LLM URL.
|
||||
|
||||
## Fixed
|
||||
- web images with jpeg extension were not displayed. [#1486](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1486)
|
||||
- MathJax was causing errors on the file in the active editor when starting Obsidian or starting the Excalidraw Plugin. I reworked the MathJax implementation from the ground up. [#1484](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1484), [#1473](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1473)
|
||||
- Enhanced performance for resizing sticky notes (resize + ALT) on slower devices when centrally adjusting their size.
|
||||
|
||||
## New in ExcalidrawAutomate:
|
||||
- New ArrowHead types. Currently only available programmatically and when converting Mermaid Class Diagrams into Excalidraw Objects:
|
||||
${String.fromCharCode(96,96,96)}ts
|
||||
addArrow(
|
||||
points: [x: number, y: number][],
|
||||
formatting?: {
|
||||
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
startObjectId?: string;
|
||||
endObjectId?: string;
|
||||
},
|
||||
): string;
|
||||
|
||||
connectObjects(
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint | null,
|
||||
objectB: string,
|
||||
connectionB: ConnectionPoint | null,
|
||||
formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
padding?: number;
|
||||
},
|
||||
): string;
|
||||
|
||||
connectObjectWithViewSelectedElement(
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint | null,
|
||||
connectionB: ConnectionPoint | null,
|
||||
formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
|
||||
padding?: number;
|
||||
},
|
||||
): boolean;
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"2.0.7":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
# Fixed
|
||||
- The Android and iOS crash with 2.0.5 😰. I can't apologize enough for releasing a version that I did not properly test on Android and iOS. That ought to teach me something about last-minute changes before hitting release.
|
||||
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
|
||||
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
|
||||
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
|
||||
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
|
||||
|
||||
# New
|
||||
- Bonus feature compared to 2.0.4: Second-order links when clicking embedded images. I use images to connect ideas. Clicking on an image and seeing all the connections immediately is very powerful.
|
||||
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
|
||||
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
|
||||
- Configurable modifier keys for link click action and drag&drop actions.
|
||||
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
|
||||
`,
|
||||
"2.0.5":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
# Fixed
|
||||
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
|
||||
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
|
||||
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
|
||||
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
|
||||
|
||||
# New
|
||||
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
|
||||
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
|
||||
- Configurable modifier keys for link click action and drag&drop actions.
|
||||
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
|
||||
`,
|
||||
"2.0.4":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- ExcaliAI
|
||||
- You can now add ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} or ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} to the ${String.fromCharCode(96)}cssclasses:${String.fromCharCode(96)} frontmatter property in embedded markdown nodes and their font face will match the respective Excalidraw fonts.
|
||||
|
||||
## Fixed
|
||||
- Adding a script for the very first time (when the script folder did not yet exist) did not show up in the tools panel. Required an Obsidian restart.
|
||||
- Performance improvements
|
||||
|
||||
## New and updated In Excalidraw Automate
|
||||
- Added many new functions and some features to existing functions. See the [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.3) for details
|
||||
`,
|
||||
"2.0.3":`
|
||||
## Fixed
|
||||
- Mermaid to Excalidraw stopped working after installing the Obsidian 1.5.0 insider build. [#1450](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1450)
|
||||
- CTRL+Click on a Mermaid diagram did not open the Mermaid editor.
|
||||
- Embed color settings were not honored when the embedded markdown was focused on a section or block.
|
||||
- Scrollbars were visible when the embeddable was set to transparent (set background color to match element background, and set element background color to "transparent").
|
||||
`,
|
||||
"2.0.2":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/502swdqvZ2A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Resolved an issue where the Command Palette's "Toggle between Excalidraw and Markdown mode" failed to uncompress the Excalidraw JSON for editing.
|
||||
|
||||
## New
|
||||
- Scaling feature for embedded objects (markdown documents, pdfs, YouTube, etc.): Hold down the SHIFT key while resizing elements to adjust their size.
|
||||
- Expanded support for Canvas Candy. Regardless of Canvas Candy, you can apply CSS classes to embedded markdown documents for transparency, shape adjustments, text orientation, and more.
|
||||
- Added new functionalities to the active embeddable top-left menu:
|
||||
- Document Properties (cog icon)
|
||||
- File renaming
|
||||
- Basic styling options for embedded markdown documents
|
||||
- Setting YouTube start time
|
||||
- Zoom to full screen for PDFs
|
||||
- Improved immersive embedding of Excalidraw into Obsidian Canvas.
|
||||
- Introduced new Command Palette Actions:
|
||||
- Embeddable Properties
|
||||
- Scaling selected embeddable elements to 100% relative to the current canvas zoom.
|
||||
`,
|
||||
"2.0.1":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/xmqiBTrlbEM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- bug with cssclasses in frontmatter
|
||||
- styling of help screen keyboard shortcuts [#1437](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1437)
|
||||
`,
|
||||
"2.0.0":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/JC1E-jeiWhI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- Added support for applying CSS classes in frontmatter. Now, when embedding Excalidraw drawings into Obsidian Canvas, you can use [Canvas Candy](https://tfthacker.com/canvas-candy) classes. For instance, ${String.fromCharCode(96)}cssclasses: cc-border-none${String.fromCharCode(96)} removes the canvas node border around the drawing.
|
||||
- Introduced new context menu actions:
|
||||
- Navigate to link or embedded image.
|
||||
- Add any file from the vault to the canvas.
|
||||
- Convert the selected text element or sticky note to an embedded markdown file.
|
||||
- Add a link from the Vault to the selected element.
|
||||
- Frames are now rendered in exported images.
|
||||
- SVG Export includes the ${String.fromCharCode(96)}.excalidraw-svg${String.fromCharCode(96)} class, enabling post-processing of SVGs using publish.js when using custom domains with Obsidian Publish. Also, added a command palette action ${String.fromCharCode(96)}Obsidian Publish: Find SVG and PNG exports that are out of date${String.fromCharCode(96)}.
|
||||
- Added a new Command palette action to open the corresponding Excalidraw file based on the embedded SVG or PNG file. ${String.fromCharCode(96)}Open Excalidraw Drawing${String.fromCharCode(96)} [Issue #1411](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1411)
|
||||
|
||||
## Fixed and Improved
|
||||
- Resolved issue with the Mermaid Timeline graph displaying all black. [Issue #1424](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1424)
|
||||
- Enabled toggling pen mode off after activation by a pen touch.
|
||||
- Now you are able to unlock elements on mobile; previously, locked elements couldn't be selected.
|
||||
- Fixed the disabled ${String.fromCharCode(96)}complete line button${String.fromCharCode(96)} for multipoint lines on mobile.
|
||||

|
||||
|
||||
`,
|
||||
"1.9.28":`
|
||||
## Fixed & Improved
|
||||
- Fixed an issue where the toolbar lost focus, requiring two clicks. This caused a problem when the hand tool was activated from ExcalidrawAutomate script when opening a drawing, causing buttons to stop working. [#1344](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1344)
|
||||
- Resolved a caching issue affecting image area-links and group-links, making them work inconsistently. For more details, refer to the discussion on [Discord](https://discord.com/channels/1026825302900494357/1169311900308361318).
|
||||
- Improved frame colors with Dynamic Coloring.
|
||||
- Added support for multiline LaTeX formulas. [#1403](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1403)
|
||||
- Fixed the issue of Chinese characters overlapping in MathJax. [#1406](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1406)
|
||||
|
||||
## New
|
||||
- Added support for Mermaid to Excalidraw **Sequence Diagrams**.
|
||||
- If an image contains an element link, clicking on the image will now open the link chooser, allowing you to decide whether to open the image or follow the element link.
|
||||
- When hovering over an image that also has an element link, the hover preview will display the contents of the link.
|
||||
- You can now choose to **import PDFs** in columns instead of rows. Additionally, you have the option to group all pages after import, which will improve the unlocking experience if you also lock pages on import.
|
||||
- Introduced configuration options for the **Laser Tool**, including pointer color, decay length, and time. ([#1408](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1408), [#1220](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1220))
|
||||
|
||||

|
||||
`,
|
||||
"1.9.27": `
|
||||
## New
|
||||
- Restructured plugin settings, added additional comments and relevant videos
|
||||
- Added setting to change PDF to Image resolution/scale. This has an effect when embedding PDF pages to Excalidraw. A lower value will result in less-sharp pages, but better overall performance. Also, larger pages (higher scale value) were not accepted by Excalidraw.com when copying from Obsidian due to the 2MB image file limit. Find the "PDF to Image" setting under "Embedding Excalidraw into your Notes and Exporting" setting. [#1393](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1393)
|
||||
|
||||
## Fixed
|
||||
- When multiple Excalidraw Scripts were executed parallel a race condition occurred causing scripts to override each other
|
||||
- I implemented a partial fix to "text detaching from figures when dragging them" [#1400](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1400)
|
||||
- Regression: extra thin stroke removed with 1.9.26 [#1399](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1399)`,
|
||||
"1.9.26":`
|
||||
## Fixes and improvements from Excalidraw.com
|
||||
- Freedraw shape selection issue, when fill-pattern is not solid [#7193](https://github.com/excalidraw/excalidraw/pull/7193)
|
||||
- Actions panel UX improvement [#6850](https://github.com/excalidraw/excalidraw/pull/6850)
|
||||
|
||||
## Fixed in plugin
|
||||
- After inserting PDF pages as image the size of inserted images were incorrectly anchored preventing resizing of pages. The fix does not solve the issue with already imported pages, but pages you import in the future will not be anchored.
|
||||
- Mobile toolbar flashes up on tab change on desktop
|
||||
- Toolbar buttons are active on the first click after opening a drawing. This addresses the "hand" issue raised here: [#1344](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1344)
|
||||
`,
|
||||
"1.9.25":`
|
||||
## Fixed
|
||||
- Fixed issues with creating Markdown or Excalidraw files for non-existing documents [#1385](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1385)
|
||||
- Resolved a bug where changing the section/block filter after duplicating a markdown embeddable now works correctly on the first attempt [#1387](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1387)
|
||||
|
||||
## New
|
||||
- Easily create a markdown file and embed it as an embedded frame with a single click when clicking a link pointing to a non-existent file.
|
||||

|
||||
- Offline LaTeX support. The MathJax package is now included in the plugin, eliminating the need for an internet connection. [#1383](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1383), [#936](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/936), [#1289](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1289)
|
||||
|
||||
## Minor Updates from excalidraw.com
|
||||
- Improved the laser pointer in dark mode.
|
||||
- Removed bound arrows from frames.
|
||||
- Enhanced fill rendering.
|
||||
- Maintained the z-order of elements added to frames.
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
- Introduced two LZString functions in ExcalidrawAutomate:
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
compressToBase64(str:string):string;
|
||||
decompressFromBase64(str:string):string;
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"1.9.24":`
|
||||
## Fixed
|
||||
- Resolved some hidden Image and Backup Cache initialization errors.
|
||||
|
||||
## New Features
|
||||
- Introducing the ${String.fromCharCode(96)}[[cmd://cmd-id]]${String.fromCharCode(96)} link type, along with a new Command Palette Action: ${String.fromCharCode(96)}Insert Obsidian Command as a link${String.fromCharCode(96)}. With this update, you can now add any command available on the Obsidian Command palette as a link in Excalidraw. When you click the link, the corresponding command will be executed. This feature opens up exciting possibilities for automating your drawings by creating Excalidraw Scripts and attaching them to elements.
|
||||
|
||||
- I am thrilled to announce that you can now embed images directly from your local hard drive in Excalidraw. These files won't be moved into Obsidian. Please note, however, that these images won't be synchronized across your other devices. [#1365](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1365)
|
||||
|
||||
Check out the [updated keyboard map](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png)
|
||||
|
||||
<a href="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png" width="100%" alt="Keyboard map"/></a>
|
||||
|
||||
Stay creative and productive with Excalidraw!
|
||||
`,
|
||||
"1.9.23":`
|
||||
## Fixed
|
||||
- Link navigation error in view mode introduced with 1.9.21 [#7120](https://github.com/excalidraw/excalidraw/pull/7120)
|
||||
`,
|
||||
"1.9.21":`
|
||||
## Fixed:
|
||||
- When moving a group of objects on the grid, each object snapped separately resulting in a jumbled-up image [#7082](https://github.com/excalidraw/excalidraw/issues/7082)
|
||||
|
||||
## New from Excalidraw.com:
|
||||
- 🎉 Laser Pointer. Press "K" to activate the laser pointer, or find it under more tools. In View-Mode double click/tap the canvas to toggle the laser pointer
|
||||
|
||||

|
||||
`,
|
||||
"1.9.20":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/QB2rKRxxYlg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Fourth Font displays correctly in SVG embeds mode
|
||||
- The re-colorMap map (see [1.9.19](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.9.19) for more info) did not work when either of the fill or stroke color properties of the image was missing.
|
||||
- Excalidraw Pasting with middle mouse button on Linux [#1338](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1338) 🙏@Aeases
|
||||
|
||||
### Fixed by excalidraw.com
|
||||
- Excalidraw's native eyedropper fixes [#7019](https://github.com/excalidraw/excalidraw/pull/7019)
|
||||
|
||||
## New
|
||||
- Now you can insert [Mermaid](https://mermaid.live/) diagrams as Excalidraw elements into your drawings (currently only the [Flowchart](https://mermaid.js.org/syntax/flowchart.html) type is supported, [other diagram types](https://mermaid.js.org/intro/#diagram-types) are inserted as Mermaid native images.
|
||||
- ⚠️**This feature requires Obsidian API v1.4.14 (the latest desktop version). On Obsidian mobile API v1.4.14 is only available to Obsidian insiders currently**
|
||||
- If you want to contribute to the project please head over to [mermaid-to-excalidraw](https://github.com/excalidraw/mermaid-to-excalidraw) and help create the converters for the other diagram types.
|
||||
- The Fourth Font now also supports the OTF format
|
||||
- Disable snap-to-grid in grid mode by holding down the CTRL/CMD while drawing or moving an element [#6983](https://github.com/excalidraw/excalidraw/pull/6983)
|
||||
- I updated the Excalidraw logo in Obsidian. This affects the logo on the tab and the ribbon.
|
||||
|
||||
### New from excalidraw.com
|
||||
- Elements alignment snapping. Hold down the CTRL/CMD button while moving an element to snap it to other objects. [#6256](https://github.com/excalidraw/excalidraw/pull/6256)
|
||||
|
||||
### New in the script library
|
||||
- The amazing shape [Boolean Operations](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Boolean%20Operations.md) script created by 🙏@GColoy is available in the script library.
|
||||
|
||||
### New in Excalidraw Automate
|
||||
- ${String.fromCharCode(96)}getPolyBool()${String.fromCharCode(96)} returns a [PolyBool](https://github.com/velipso/polybooljs) object
|
||||
- sample mermaid code:
|
||||
${String.fromCharCode(96,96,96)}js
|
||||
ea = ExcalidrawAutomate();
|
||||
ea.setView();
|
||||
await ea.addMermaid(
|
||||
${String.fromCharCode(96)}flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]${String.fromCharCode(96)}
|
||||
);
|
||||
ea.addElementsToView();
|
||||
${String.fromCharCode(96,96,96)}`,
|
||||
"1.9.19":`
|
||||
## New
|
||||
- I added new features to the [Deconstruct Selected Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md) script
|
||||
|
||||
97
src/dialogs/ModifierKeySettings.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Setting } from "obsidian";
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ModifierKeySet, ModifierSetType, modifierKeyTooltipMessages } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
type ModifierKeyCategories = Partial<{
|
||||
[modifierSetType in ModifierSetType]: string;
|
||||
}>;
|
||||
|
||||
const CATEGORIES: ModifierKeyCategories = {
|
||||
WebBrowserDragAction: t("WEB_BROWSER_DRAG_ACTION"),
|
||||
LocalFileDragAction: t("LOCAL_FILE_DRAG_ACTION"),
|
||||
InternalDragAction: t("INTERNAL_DRAG_ACTION"),
|
||||
LinkClickAction: t("PANE_TARGET"),
|
||||
};
|
||||
|
||||
export class ModifierKeySettingsComponent {
|
||||
private isMacOS: boolean;
|
||||
|
||||
constructor(
|
||||
private contentEl: HTMLElement,
|
||||
private modifierKeyConfig: {
|
||||
Mac: Record<string, ModifierKeySet>;
|
||||
Win: Record<string, ModifierKeySet>;
|
||||
},
|
||||
private update?: Function,
|
||||
) {
|
||||
this.isMacOS = (DEVICE.isMacOS || DEVICE.isIOS);
|
||||
}
|
||||
|
||||
render() {
|
||||
const platform = this.isMacOS ? "Mac" : "Win";
|
||||
const modifierKeysConfig = this.modifierKeyConfig[platform];
|
||||
|
||||
Object.entries(CATEGORIES).forEach(([modifierSetType, label]) => {
|
||||
const detailsEl = this.contentEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: label,
|
||||
cls: "excalidraw-setting-h4",
|
||||
});
|
||||
|
||||
const modifierKeys = modifierKeysConfig[modifierSetType];
|
||||
detailsEl.createDiv({
|
||||
//@ts-ignore
|
||||
text: t("DEFAULT_ACTION_DESC") + modifierKeyTooltipMessages()[modifierSetType][modifierKeys.defaultAction],
|
||||
cls: "setting-item-description"
|
||||
});
|
||||
Object.entries(modifierKeys.rules).forEach(([action, rule]) => {
|
||||
const setting = new Setting(detailsEl)
|
||||
//@ts-ignore
|
||||
.setName(modifierKeyTooltipMessages()[modifierSetType][rule.result]);
|
||||
|
||||
setting.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(rule.shift)
|
||||
.setTooltip("SHIFT")
|
||||
.onChange((value) => {
|
||||
rule.shift = value;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
setting.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(rule.ctrl_cmd)
|
||||
.setTooltip(this.isMacOS ? "CMD" : "CTRL")
|
||||
.onChange((value) => {
|
||||
rule.ctrl_cmd = value;
|
||||
this.update();
|
||||
})
|
||||
if(this.isMacOS && modifierSetType !== "LinkClickAction") {
|
||||
toggle.setDisabled(true);
|
||||
toggle.toggleEl.style.opacity = "0.5";
|
||||
}
|
||||
});
|
||||
|
||||
setting.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(rule.alt_opt)
|
||||
.setTooltip(this.isMacOS ? "OPT" : "ALT")
|
||||
.onChange((value) => {
|
||||
rule.alt_opt = value;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
setting.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(rule.meta_ctrl)
|
||||
.setTooltip(this.isMacOS ? "CTRL" : "META")
|
||||
.onChange((value) => {
|
||||
rule.meta_ctrl = value;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { EMPTY_MESSAGE } from "../constants";
|
||||
import { EMPTY_MESSAGE } from "../constants/constants";
|
||||
import { t } from "../lang/helpers";
|
||||
|
||||
export enum openDialogAction {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { ColorComponent, Modal, Setting, SliderComponent, TextComponent, ToggleComponent } from "obsidian";
|
||||
import { COLOR_NAMES, VIEW_TYPE_EXCALIDRAW } from "src/constants";
|
||||
import { COLOR_NAMES, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { setPen } from "src/menu/ObsidianMenu";
|
||||
|
||||
@@ -11,11 +11,14 @@ import {
|
||||
} from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { sleep } from "../utils/Utils";
|
||||
import { escapeRegExp, sleep } from "../utils/Utils";
|
||||
import { getLeaf } from "../utils/ObsidianUtils";
|
||||
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
|
||||
import { KeyEvent, isCTRL } from "src/utils/ModifierkeyHelper";
|
||||
import { KeyEvent, isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ExcalidrawElement, getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { MAX_IMAGE_SIZE } from "src/constants/constants";
|
||||
|
||||
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
|
||||
|
||||
@@ -339,11 +342,11 @@ export class GenericInputPrompt extends Modal {
|
||||
private cancelClickCallback = () => this.cancel();
|
||||
|
||||
private keyDownCallback = (evt: KeyboardEvent) => {
|
||||
if ((evt.key === "Enter" && this.lines === 1) || (isCTRL(evt) && evt.key === "Enter")) {
|
||||
if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) {
|
||||
evt.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
if (this.displayEditorButtons && evt.key === "k" && isCTRL(evt)) {
|
||||
if (this.displayEditorButtons && evt.key === "k" && isWinCTRLorMacCMD(evt)) {
|
||||
evt.preventDefault();
|
||||
this.linkBtnClickCallback();
|
||||
}
|
||||
@@ -461,22 +464,44 @@ export class NewFileActions extends Modal {
|
||||
private resolvePromise: (file: TFile|null) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
private newFile: TFile = null;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private path: string;
|
||||
private keys: KeyEvent;
|
||||
private view: ExcalidrawView;
|
||||
private openNewFile: boolean;
|
||||
private parentFile: TFile;
|
||||
private sourceElement: ExcalidrawElement;
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private path: string,
|
||||
private keys: KeyEvent,
|
||||
private view: ExcalidrawView,
|
||||
private openNewFile: boolean = true,
|
||||
private parentFile?: TFile,
|
||||
) {
|
||||
constructor({
|
||||
plugin,
|
||||
path,
|
||||
keys,
|
||||
view,
|
||||
openNewFile = true,
|
||||
parentFile,
|
||||
sourceElement,
|
||||
}: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
path: string;
|
||||
keys: KeyEvent;
|
||||
view: ExcalidrawView;
|
||||
openNewFile?: boolean;
|
||||
parentFile?: TFile;
|
||||
sourceElement?: ExcalidrawElement;
|
||||
}) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.path = path;
|
||||
this.keys = keys;
|
||||
this.view = view;
|
||||
this.openNewFile = openNewFile;
|
||||
this.sourceElement = sourceElement;
|
||||
if(!parentFile) this.parentFile = view.file;
|
||||
this.waitForClose = new Promise<TFile|null>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.createForm();
|
||||
@@ -528,7 +553,7 @@ export class NewFileActions extends Modal {
|
||||
|
||||
const createFile = async (data: string): Promise<TFile> => {
|
||||
if (!this.path.includes("/")) {
|
||||
const re = new RegExp(`${this.parentFile.name}$`, "g");
|
||||
const re = new RegExp(`${escapeRegExp(this.parentFile.name)}$`, "g");
|
||||
this.path = this.parentFile.path.replace(re, this.path);
|
||||
}
|
||||
if (!this.path.match(/\.md$/)) {
|
||||
@@ -540,7 +565,31 @@ export class NewFileActions extends Modal {
|
||||
return f;
|
||||
};
|
||||
|
||||
const bMd = el.createEl("button", { text: t("PROMPT_BUTTON_CREATE_MARKDOWN") });
|
||||
if(this.sourceElement) {
|
||||
const bEmbedMd = el.createEl("button", {
|
||||
text: t("PROMPT_BUTTON_EMBED_MARKDOWN"),
|
||||
attr: {"aria-label": t("PROMPT_BUTTON_EMBED_MARKDOWN_ARIA")},
|
||||
});
|
||||
bEmbedMd.onclick = async () => {
|
||||
if (!checks) {
|
||||
return;
|
||||
}
|
||||
const f = await createFile("");
|
||||
if(f) {
|
||||
const ea:ExcalidrawAutomate = getEA(this.view);
|
||||
ea.copyViewElementsToEAforEditing([this.sourceElement]);
|
||||
ea.getElement(this.sourceElement.id).isDeleted = true;
|
||||
ea.addEmbeddable(this.sourceElement.x, this.sourceElement.y,MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, undefined,f);
|
||||
ea.addElementsToView();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
const bMd = el.createEl("button", {
|
||||
text: t("PROMPT_BUTTON_CREATE_MARKDOWN"),
|
||||
attr: {"aria-label": t("PROMPT_BUTTON_CREATE_MARKDOWN_ARIA")},
|
||||
});
|
||||
bMd.onclick = async () => {
|
||||
if (!checks) {
|
||||
return;
|
||||
@@ -550,7 +599,10 @@ export class NewFileActions extends Modal {
|
||||
this.close();
|
||||
};
|
||||
|
||||
const bEx = el.createEl("button", { text: t("PROMPT_BUTTON_CREATE_EXCALIDRAW") });
|
||||
const bEx = el.createEl("button", {
|
||||
text: t("PROMPT_BUTTON_CREATE_EXCALIDRAW"),
|
||||
attr: {"aria-label": t("PROMPT_BUTTON_CREATE_EXCALIDRAW_ARIA")},
|
||||
});
|
||||
bEx.onclick = async () => {
|
||||
if (!checks) {
|
||||
return;
|
||||
|
||||
129
src/dialogs/PublishOutOfDateFiles.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Modal, Setting, TFile } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { getIMGFilename } from "src/utils/FileUtils";
|
||||
import { addIframe } from "src/utils/Utils";
|
||||
|
||||
const haveLinkedFilesChanged = (depth: number, mtime: number, path: string, sourceList: Set<string>, plugin: ExcalidrawPlugin):boolean => {
|
||||
if(depth++ > 5) return false;
|
||||
sourceList.add(path);
|
||||
const links = plugin.app.metadataCache.resolvedLinks[path];
|
||||
if(!links) return false;
|
||||
for(const link of Object.keys(links)) {
|
||||
if(sourceList.has(link)) continue;
|
||||
const file = plugin.app.vault.getAbstractFileByPath(link);
|
||||
if(!file || !(file instanceof TFile)) continue;
|
||||
console.log(path, {mtimeLinked: file.stat.mtime, mtimeSource: mtime, path: file.path});
|
||||
if(file.stat.mtime > mtime) return true;
|
||||
if(plugin.isExcalidrawFile(file)) {
|
||||
if(haveLinkedFilesChanged(depth, mtime, file.path, sourceList, plugin)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const listOfOutOfSyncImgExports = async(plugin: ExcalidrawPlugin, recursive: boolean, statusEl: HTMLParagraphElement):Promise<TFile[]> => {
|
||||
const app = plugin.app;
|
||||
|
||||
const publish = app.internalPlugins.plugins["publish"].instance;
|
||||
if(!publish) return;
|
||||
const list = await app.internalPlugins.plugins["publish"].instance.apiList();
|
||||
if(!list || !list.files) return;
|
||||
const outOfSyncFiles = new Set<TFile>();
|
||||
const allFiles = list.files.filter((f:any)=>(f.path.endsWith(".svg") || f.path.endsWith(".png")))
|
||||
const totalCount = allFiles.length;
|
||||
allFiles.forEach((f:any, idx:number)=>{
|
||||
const maybeExcalidraFilePath = getIMGFilename(f.path,"md");
|
||||
const imgFile = app.vault.getAbstractFileByPath(f.path);
|
||||
const excalidrawFile = app.vault.getAbstractFileByPath(maybeExcalidraFilePath);
|
||||
statusEl.innerText = `Status: ${idx+1}/${totalCount} ${imgFile ? imgFile.name : f.path}`;
|
||||
if(!excalidrawFile || !imgFile || !(excalidrawFile instanceof TFile) || !(imgFile instanceof TFile)) return;
|
||||
if(excalidrawFile.stat.mtime <= imgFile.stat.mtime) {
|
||||
if(!recursive) return;
|
||||
if(!haveLinkedFilesChanged(0, excalidrawFile.stat.mtime, excalidrawFile.path, new Set<string>(), plugin)) return;
|
||||
}
|
||||
outOfSyncFiles.add(excalidrawFile);
|
||||
});
|
||||
return Array.from(outOfSyncFiles);
|
||||
}
|
||||
|
||||
export class PublishOutOfDateFilesDialog extends Modal {
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
) {
|
||||
super(plugin.app);
|
||||
}
|
||||
|
||||
async onClose() {}
|
||||
|
||||
onOpen() {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Out of Date SVG Files`);
|
||||
this.createForm(false);
|
||||
}
|
||||
|
||||
async createForm(recursive: boolean) {
|
||||
const detailsEl = this.contentEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: "Video about Obsidian Publish support",
|
||||
});
|
||||
detailsEl.createEl("br");
|
||||
addIframe(detailsEl, "JC1E-jeiWhI");
|
||||
const p = this.contentEl.createEl("p",{text: "Collecting data..."});
|
||||
const statusEl = this.contentEl.createEl("p", {text: "Status: "});
|
||||
const files = await listOfOutOfSyncImgExports(this.plugin, recursive, statusEl);
|
||||
statusEl.style.display = "none";
|
||||
|
||||
if(!files || files.length === 0) {
|
||||
p.innerText = "No out of date files found.";
|
||||
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
const bClose = div.createEl("button", { text: "Close", cls: "excalidraw-prompt-button"});
|
||||
bClose.onclick = () => {
|
||||
this.close();
|
||||
};
|
||||
if(!recursive) {
|
||||
const bRecursive = div.createEl("button", { text: "Check Recursive", cls: "excalidraw-prompt-button"});
|
||||
bRecursive.onclick = () => {
|
||||
this.contentEl.empty();
|
||||
this.createForm(true);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const filesMap = new Map<TFile,boolean>();
|
||||
p.innerText = "Select files to open.";
|
||||
files.forEach((f:TFile) => {
|
||||
filesMap.set(f,true);
|
||||
new Setting(this.contentEl)
|
||||
.setName(f.path)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(true)
|
||||
.onChange(value => {
|
||||
filesMap.set(f,value);
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
const bClose = div.createEl("button", { text: "Close", cls: "excalidraw-prompt-button"});
|
||||
bClose.onclick = () => {
|
||||
this.close();
|
||||
};
|
||||
if(!recursive) {
|
||||
const bRecursive = div.createEl("button", { text: "Check Recursive", cls: "excalidraw-prompt-button"});
|
||||
bRecursive.onclick = () => {
|
||||
this.contentEl.empty();
|
||||
this.createForm(true);
|
||||
};
|
||||
}
|
||||
const bOpen = div.createEl("button", { text: "Open Selected", cls: "excalidraw-prompt-button" });
|
||||
bOpen.onclick = () => {
|
||||
filesMap.forEach((value:boolean,key:TFile) => {
|
||||
if(value) {
|
||||
this.plugin.openDrawing(key,"new-tab",true);
|
||||
}
|
||||
});
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { errorlog, log } from "../utils/Utils";
|
||||
import { errorlog, escapeRegExp, log } from "../utils/Utils";
|
||||
|
||||
const URL =
|
||||
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
|
||||
@@ -88,7 +88,8 @@ export class ScriptInstallPrompt extends Modal {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
await MarkdownRenderer.renderMarkdown(
|
||||
await MarkdownRenderer.render(
|
||||
this.plugin.app,
|
||||
source,
|
||||
this.contentDiv,
|
||||
"",
|
||||
@@ -134,7 +135,7 @@ export class ScriptInstallPrompt extends Modal {
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
const regex = new RegExp(searchTerm, 'gi');
|
||||
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
|
||||
|
||||
// Iterate over all matches in the text node
|
||||
while ((match = regex.exec(nodeContent)) !== null) {
|
||||
|
||||
@@ -5,7 +5,17 @@ type SuggesterInfo = {
|
||||
after: string;
|
||||
};
|
||||
|
||||
const hyperlink = (url: string, text: string) => {
|
||||
return `<a onclick='window.open("${url}")'>${text}</a>`;
|
||||
}
|
||||
|
||||
export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "help",
|
||||
code: "help(target: Function | string)",
|
||||
desc: "Utility function that provides help about ExcalidrawAutomate functions and properties. I recommend calling this function from Developer Console to print out help to the console.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "plugin",
|
||||
code: null,
|
||||
@@ -27,13 +37,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "style.strokeColor",
|
||||
code: "[string]",
|
||||
desc: "A valid css color. See <a onclick='window.open(\"https://www.w3schools.com/colors/default.asp\")'>W3 School Colors</a> for more.",
|
||||
desc: `A valid css color. See ${hyperlink("https://www.w3schools.com/colors/default.asp", "W3 School Colors")} for more.`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "style.backgroundColor",
|
||||
code: "[string]",
|
||||
desc: "A valid css color. See <a onclick='window.open(\"https://www.w3schools.com/colors/default.asp\")'>W3 School Colors</a> for more.",
|
||||
desc: `A valid css color. See ${hyperlink("https://www.w3schools.com/colors/default.asp","W3 School Colors")} for more.`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -123,7 +133,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "canvas.viewBackgroundColor",
|
||||
code: "[string]",
|
||||
desc: "A valid css color.\nSee <a onclick='window.open(\"https://www.w3schools.com/colors/default.asp\")'>W3 School Colors</a> for more.",
|
||||
desc: `A valid css color.\nSee ${hyperlink("https://www.w3schools.com/colors/default.asp","W3 School Colors")} for more.`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -170,20 +180,26 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "create",
|
||||
code: 'create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
|
||||
code: 'async create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
|
||||
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\nReturns the path to the created file",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "createSVG",
|
||||
code: "createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<SVGSVGElement>;",
|
||||
code: "async createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<SVGSVGElement>;",
|
||||
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "createPNG",
|
||||
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<any>;",
|
||||
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
|
||||
code: "async createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",
|
||||
desc: "Create an image based on the objects in ea.getElements(). The elements in ea will be merged with the elements from the provided template file - if any. Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "createPNGBase64",
|
||||
code: "async craetePNGBase64(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<string>;",
|
||||
desc: "The same as createPNG but returns a base64 encoded string instead of a file.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -237,31 +253,40 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "addArrow",
|
||||
code: "addArrow(points: [[x: number, y: number]], formatting?: { startArrowHead?: string; endArrowHead?: string; startObjectId?: string; endObjectId?: string;},): string;",
|
||||
desc: null,
|
||||
desc: `valid values for startArrowHead and endArrowHead are: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addImage",
|
||||
code: "addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
|
||||
code: "async addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
|
||||
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image. anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened. ",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addEmbeddable",
|
||||
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
|
||||
desc: "Adds an iframe to the drawing. If url is not null then the iframe will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe will be loaded from the file. Both the url and the file may not be null.",
|
||||
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe/webview will be loaded from the file. Both the url and the file may not be null.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addMermaid",
|
||||
code: "async addMermaid(diagram: string, groupElements: boolean = true,): Promise<string[]|string>;",
|
||||
desc: "Creates a mermaid diagram and returns the ids of the created elements as a string[]. " +
|
||||
"The elements will be added to ea. To add them to the canvas you'll need to use addElementsToView. " +
|
||||
"Depending on the diagram type the result will be either a single SVG image, or a number of excalidraw elements.<br>" +
|
||||
"If there is an error, the function returns a string with the error message.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addLaTex",
|
||||
code: "addLaTex(topX: number, topY: number, tex: string): Promise<string>;",
|
||||
desc: null,
|
||||
code: "async addLaTex(topX: number, topY: number, tex: string): Promise<string>;",
|
||||
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "connectObjects",
|
||||
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
|
||||
desc: 'type ConnectionPoint = "top" | "bottom" | "left" | "right" | null\nWhen null is passed as ConnectionPoint then Excalidraw will automatically decide\nnumberOfPoints is the number of points on the line. Default is 0 i.e. line will only have a start and end point.\nArrowHead: "triangle"|"dot"|"arrow"|"bar"|null',
|
||||
desc: 'type ConnectionPoint = "top" | "bottom" | "left" | "right" | null\nWhen null is passed as ConnectionPoint then Excalidraw will automatically decide\nnumberOfPoints is the number of points on the line. Default is 0 i.e. line will only have a start and end point.\nArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null',
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -303,7 +328,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "getExcalidrawAPI",
|
||||
code: "getExcalidrawAPI(): any;",
|
||||
desc: "<a onclick='window.open(\"https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref\")'>Excalidraw API</a>",
|
||||
desc: `${hyperlink("https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref","Excalidraw API")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -338,8 +363,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "copyViewElementsToEAforEditing",
|
||||
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;",
|
||||
desc: "Copies elements from view to elementsDict for editing",
|
||||
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void;",
|
||||
desc: "Copies elements from view to elementsDict for editing. If copyImages is true, then relevant entries from scene.files will also be copied. This is required if you want to generate a PNG for a subset of the elements in the drawing (e.g. for AI generation)",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -356,7 +381,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "addElementsToView",
|
||||
code: "addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,shouldRestoreElements?: boolean,): Promise<boolean>;",
|
||||
code: "async addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,shouldRestoreElements?: boolean,): Promise<boolean>;",
|
||||
desc: "Adds elements from elementsDict to the current view\nrepositionToCursor: default is false\nsave: default is true\nnewElementsOnTop: default is false, i.e. the new elements get to the bottom of the stack\nnewElementsOnTop controls whether elements created with ExcalidrawAutomate are added at the bottom of the stack or the top of the stack of elements already in the view\nNote that elements copied to the view with copyViewElementsToEAforEditing retain their position in the stack of elements in the view even if modified using EA",
|
||||
after: "",
|
||||
},
|
||||
@@ -423,19 +448,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "activeScript",
|
||||
code: "activeScript: string;",
|
||||
desc: "Mandatory to set before calling the get and set ScriptSettings functions. Set automatically by the ScriptEngine\nSee for more details: <a onclick='window.open(\"https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html\")'>Script Engine Help</a>",
|
||||
desc: `Mandatory to set before calling the get and set ScriptSettings functions. Set automatically by the ScriptEngine\nSee for more details: ${hyperlink("https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html","Script Engine Help")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getScriptSettings",
|
||||
code: "getScriptSettings(): {};",
|
||||
desc: "Returns script settings. Saves settings in plugin settings, under the activeScript key. See for more details: <a onclick='window.open(\"https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html\")'>Script Engine Help</a>",
|
||||
desc: `Returns script settings. Saves settings in plugin settings, under the activeScript key. See for more details: ${hyperlink("https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html","Script Engine Help")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "setScriptSettings",
|
||||
code: "setScriptSettings(settings: any): Promise<void>;",
|
||||
desc: "Sets script settings.\nSee for more details: <a onclick='window.open(\"https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html\")'>Script Engine Help</a>",
|
||||
code: "async setScriptSettings(settings: any): Promise<void>;",
|
||||
desc: `Sets script settings.\nSee for more details: ${hyperlink("https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html","Script Engine Help")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -513,13 +538,107 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "obsidian",
|
||||
code: "obsidian",
|
||||
desc: "Access functions and objects available on the <a onclick='window.open(\"https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts\")'>Obsidian Module</a>",
|
||||
desc: `Access functions and objects available on the ${hyperlink("https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts","Obsidian Module")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getAttachmentFilepath",
|
||||
code: "async getAttachmentFilepath(filename: string): Promise<string>",
|
||||
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.",
|
||||
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename." +
|
||||
"Prompts the user with a dialog to select new file action.<br>" +
|
||||
" - create markdown file<br>" +
|
||||
" - create excalidraw file<br>" +
|
||||
" - cancel action<br>" +
|
||||
"The new file will be relative to this.targetView.file.path, unless parentFile is provided. " +
|
||||
"If shouldOpenNewFile is true, the new file will be opened in a workspace leaf. " +
|
||||
"targetPane controls which leaf will be used for the new file.<br>" +
|
||||
"Returns the TFile for the new file or null if the user cancelled the action.<br>" +
|
||||
'<code>type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";</code>',
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getActiveEmbeddableViewOrEditor",
|
||||
code: "getActiveEmbeddableViewOrEditor(view?: ExcalidrawView);",
|
||||
desc: "Returns the editor or leaf.view of the currently active embedded obsidian file.<br>" +
|
||||
"If view is not provided, ea.targetView is used.<br>" +
|
||||
"If the embedded file is a markdown document the function will return<br>" +
|
||||
"<code>{file:TFile, editor:Editor}</code> otherwise it will return {view:any}. You can check view type with view.getViewType();",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getViewLastPointerPosition",
|
||||
code: "getViewLastPointerPosition(): {x: number, y: number};",
|
||||
desc: "@returns the last recorded pointer position on the Excalidraw canvas",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getleaf",
|
||||
code: "getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;",
|
||||
desc: "Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.<br>" +
|
||||
"@param origo: the currently active leaf, the origin of the new leaf<br>" +
|
||||
'@param targetPane: <code>type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";',
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "newFilePrompt",
|
||||
code: "async newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;",
|
||||
desc: "",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "DEVICE",
|
||||
code: "get DEVICE(): DeviceType;",
|
||||
desc: "Returns the current device type. Possible values are: <br>" +
|
||||
"<code>type DeviceType = {<br>" +
|
||||
" isDesktop: boolean,<br>" +
|
||||
" isPhone: boolean,<br>" +
|
||||
" isTablet: boolean,<br>" +
|
||||
" isMobile: boolean,<br>" +
|
||||
" isLinux: boolean,<br>" +
|
||||
" isMacOS: boolean,<br>" +
|
||||
" isWindows: boolean,<br>" +
|
||||
" isIOS: boolean,<br>" +
|
||||
" isAndroid: boolean<br>" +
|
||||
"};",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "checkAndCreateFolder",
|
||||
code: "async checkAndCreateFolder(folderpath: string): Promise<TFolder>",
|
||||
desc: "Checks if the folder exists, if not, creates it.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getNewUniqueFilepath",
|
||||
code: "getNewUniqueFilepath(filename: string, folderpath: string): string",
|
||||
desc: "Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename else returns the filepath as provided.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "extractCodeBlocks",
|
||||
code: "extractCodeBlocks(markdown: string): { data: string, type: string }[]",
|
||||
desc: "Grabs the codeblock content from the supplied markdown string. Returns an array of dictionaries with the codeblock content and type",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "postOpenAI",
|
||||
code: "async postOpenAI(requst: AIRequest): Promise<RequestUrlResponse>",
|
||||
desc:
|
||||
"This asynchronous function should be awaited. It posts the supplied request to the OpenAI API and returns the response.<br>" +
|
||||
"The response is a dictionary with the following keys:<br><code>{image, text, instruction, systemPrompt, responseType}</code><br>"+
|
||||
"<b>image</b> should be a dataURL - use ea.createPNGBase64()<br>"+
|
||||
"<b>systemPrompt</b>: if <code>undefined</code> the message to OpenAI will not include a system prompt<br>"+
|
||||
"<b>text</b> is the actual user prompt, a request must have either an image or a text<br>"+
|
||||
"<b>instruction</b> is a user prompt sent as a separate element in the message - I use it to reinforce the type of response I am seeing (e.g. mermaid in a codeblock)<br>"+
|
||||
`<b>imageGenerationProperties</b> if provided then the dall-e model will be used. <code> imageGenerationProperties?: {size?: string, quality?: "standard" | "hd"; n?: number; mask?: string; }</code><br>` +
|
||||
"Different openAI models accept different parameters fr size, quality, n and mask. Consult the API documenation for more information.<br>" +
|
||||
`RequestUrlResponse is defined in the ${hyperlink("https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts","Obsidian API")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "convertStringToDataURL",
|
||||
code: 'async convertStringToDataURL (data:string, type: string = "text/html"):Promise<string>',
|
||||
desc: "Converts a string to a DataURL.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -540,6 +659,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Zoom tarteView to fit elements provided as input. elements === [] will zoom to fit the entire scene. SelectElements toggles whether the elements should be in a selected state at the end of the operation.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "compressToBase64",
|
||||
code: "compressToBase64(str: string):string",
|
||||
desc: "Compresses String to a Base64 string using LZString",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "decompressFromBase64",
|
||||
code: "decompressFromBase64(str: string):string",
|
||||
desc: "Decompresses a base 64 compressed string using LZString",
|
||||
after: "",
|
||||
},
|
||||
];
|
||||
|
||||
export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
|
||||
|
||||
@@ -3,11 +3,11 @@ import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE } from "src/constants";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
|
||||
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { InsertPDFModal } from "./InsertPDFModal";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
|
||||
|
||||
@@ -80,6 +80,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
const ea = this.plugin.ea;
|
||||
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
|
||||
const isImage = file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file));
|
||||
const isAnimatedImage = file && ANIMATED_IMAGE_TYPES.contains(file.extension);
|
||||
const isIFrame = file && !isImage;
|
||||
const isPDF = file && file.extension === "pdf";
|
||||
const isExcalidraw = file && ea.isExcalidrawFile(file);
|
||||
@@ -116,7 +117,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
actionImage.buttonEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isIFrame) {
|
||||
if (isIFrame || isAnimatedImage) {
|
||||
actionIFrame.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionIFrame.buttonEl.style.display = "none";
|
||||
@@ -158,7 +159,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
new Setting(ce)
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText("as iFrame")
|
||||
.setButtonText("as Embeddable")
|
||||
.onClick(async () => {
|
||||
const path = app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
|
||||
@@ -2,8 +2,8 @@ import "obsidian";
|
||||
//import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
//export ExcalidrawAutomate from "./ExcalidrawAutomate";
|
||||
//export {ExcalidrawAutomate} from "./ExcaildrawAutomate";
|
||||
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
export type { Point } from "@zsviczian/excalidraw/types/types";
|
||||
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
export type { Point } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
export const getEA = (view?:any): any => {
|
||||
try {
|
||||
return window.ExcalidrawAutomate.getAPI(view);
|
||||
|
||||
@@ -25,6 +25,7 @@ import ru from "./locale/ru";
|
||||
import tr from "./locale/tr";
|
||||
import zhCN from "./locale/zh-cn";
|
||||
import zhTW from "./locale/zh-tw";
|
||||
import { LOCALE } from "src/constants/constants";
|
||||
|
||||
const localeMap: { [k: string]: Partial<typeof en> } = {
|
||||
ar,
|
||||
@@ -52,16 +53,9 @@ const localeMap: { [k: string]: Partial<typeof en> } = {
|
||||
"zh-tw": zhTW,
|
||||
};
|
||||
|
||||
const locale = localeMap[moment.locale()];
|
||||
const locale = localeMap[LOCALE];
|
||||
|
||||
export function t(str: keyof typeof en): string {
|
||||
if (!locale) {
|
||||
errorlog({
|
||||
where: "helpers.t",
|
||||
message: "Error: Excalidraw locale not found",
|
||||
locale: moment.locale(),
|
||||
});
|
||||
}
|
||||
|
||||
return (locale && locale[str]) || en[str];
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@ import {
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
} from "src/constants";
|
||||
} from "src/constants/constants";
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
// English
|
||||
export default {
|
||||
// main.ts
|
||||
CONVERT_URL_TO_FILE: "Save image from URL to local file",
|
||||
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
|
||||
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
|
||||
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
|
||||
OPEN_IMAGE_SOURCE: "Open Excalidraw drawing",
|
||||
INSTALL_SCRIPT: "Install the script",
|
||||
UPDATE_SCRIPT: "Update available - Click to install",
|
||||
CHECKING_SCRIPT:
|
||||
@@ -57,11 +63,12 @@ export default {
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_COMMAND: "Insert Obsidian Command as a link",
|
||||
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
|
||||
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_PDF: "Insert PDF file from vault",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file from your Vault to the active drawing",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file",
|
||||
INSERT_LATEX:
|
||||
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
@@ -80,7 +87,7 @@ export default {
|
||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
LINK_BUTTON_CLICK_NO_TEXT:
|
||||
"Select a ImageElement, or select a TextElement that contains an internal or external link.\n",
|
||||
"Select an ImageElement, or select a TextElement that contains an internal or external link.\n",
|
||||
FILENAME_INVALID_CHARS:
|
||||
'File name cannot contain any of the following characters: * " \\ < > : | ? #',
|
||||
FORCE_SAVE:
|
||||
@@ -99,6 +106,7 @@ export default {
|
||||
ERROR_SAVING_IMAGE: "Unknown error occured while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
|
||||
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
|
||||
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
|
||||
CONVERT_TO_MARKDOWN: "Convert to file...",
|
||||
|
||||
//settings.ts
|
||||
RELEASE_NOTES_NAME: "Display Release Notes after update",
|
||||
@@ -110,6 +118,8 @@ export default {
|
||||
"<b><u>Toggle ON:</u></b> Show a notification when a new version of the plugin is available.<br>" +
|
||||
"<b><u>Toggle OFF:</u></b> Silent mode. You need to check for plugin updates in Community Plugins.",
|
||||
|
||||
BASIC_HEAD: "Basic",
|
||||
BASIC_DESC: `In the "Basic" settings, you can configure options such as displaying release notes after updates, receiving plugin update notifications, setting the default location for new drawings, specifying the Excalidraw folder for embedding drawings into active documents, defining an Excalidraw template file, and designating an Excalidraw Automate script folder for managing automation scripts.`,
|
||||
FOLDER_NAME: "Excalidraw folder",
|
||||
FOLDER_DESC:
|
||||
"Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
||||
@@ -132,7 +142,39 @@ export default {
|
||||
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
|
||||
"hotkeys to your favorite scripts just like to any other Obsidian command. " +
|
||||
"The folder may not be the root folder of your Vault. ",
|
||||
AI_HEAD: "AI Settings - Experimental",
|
||||
AI_DESC: `In the "AI" settings, you can configure options for using OpenAI's GPT API. ` +
|
||||
`While the OpenAI API is in beta, its use is strictly limited — as such we require you use your own API key. ` +
|
||||
`You can create an OpenAI account, add a small credit (5 USD minimum), and generate your own API key. ` +
|
||||
`Once API key is set, you can use the AI tools in Excalidraw.`,
|
||||
AI_OPENAI_TOKEN_NAME: "OpenAI API key",
|
||||
AI_OPENAI_TOKEN_DESC:
|
||||
"You can get your OpenAI API key from your <a href='https://platform.openai.com/api-keys'>OpenAI account</a>.",
|
||||
AI_OPENAI_TOKEN_PLACEHOLDER: "Enter your OpenAI API key here",
|
||||
AI_OPENAI_DEFAULT_MODEL_NAME: "Default AI model",
|
||||
AI_OPENAI_DEFAULT_MODEL_DESC:
|
||||
"The default AI model to use when generating text. This is a freetext field, so you can enter any valid OpenAI model name. " +
|
||||
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
|
||||
AI_OPENAI_DEFAULT_MODEL_PLACEHOLDER: "Enter your default AI model here. e.g.: gpt-3.5-turbo-1106",
|
||||
AI_OPENAI_DEFAULT_IMAGE_MODEL_NAME: "Default Image Generation AI model",
|
||||
AI_OPENAI_DEFAULT_IMAGE_MODEL_DESC:
|
||||
"The default AI model to use when generating images. Image editing and variations are only supported by dall-e-2 at this time by OpenAI, " +
|
||||
"for this reason dall-e-2 will automatically be used in such cases regardless of this setting.<br>" +
|
||||
"This is a freetext field, so you can enter any valid OpenAI model name. " +
|
||||
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
|
||||
AI_OPENAI_DEFAULT_IMAGE_MODEL_PLACEHOLDER: "Enter your default Image Generation AI model here e.g.: dall-e-3",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_NAME: "Default AI vision model",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_DESC:
|
||||
"The default AI vision model to use when generating text from images. This is a freetext field, so you can enter any valid OpenAI model name. " +
|
||||
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
|
||||
AI_OPENAI_DEFAULT_API_URL_NAME: "OpenAI API URL",
|
||||
AI_OPENAI_DEFAULT_API_URL_DESC:
|
||||
"The default OpenAI API URL. This is a freetext field, so you can enter any valid OpenAI API compatible URL. " +
|
||||
"Excalidraw will use this URL when posting API requests to OpenAI. I am not doing any error handling on this field, so make sure you enter a valid URL and only change this if you know what you are doing. ",
|
||||
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4-vision-preview",
|
||||
SAVING_HEAD: "Saving",
|
||||
SAVING_DESC: "In the 'Saving' section of Excalidraw Settings, you can configure how your drawings are saved. This includes options for compressing Excalidraw JSON in Markdown, setting autosave intervals for both desktop and mobile, defining filename formats, and choosing whether to use the .excalidraw.md or .md file extension. ",
|
||||
COMPRESS_NAME: "Compress Excalidraw JSON in Markdown",
|
||||
COMPRESS_DESC:
|
||||
"By enabling this feature Excalidraw will store the drawing JSON in a Base64 compressed " +
|
||||
@@ -180,7 +222,8 @@ FILENAME_HEAD: "Filename",
|
||||
FILENAME_EXCALIDRAW_EXTENSION_DESC:
|
||||
"This setting does not apply if you use Excalidraw in compatibility mode, " +
|
||||
"i.e. you are not using Excalidraw markdown files.<br><b><u>Toggle ON:</u></b> filename ends with .excalidraw.md<br><b><u>Toggle OFF:</u></b> filename ends with .md",
|
||||
DISPLAY_HEAD: "Display",
|
||||
DISPLAY_HEAD: "Excalidraw appearance and behavior",
|
||||
DISPLAY_DESC: "In the 'appearance and behavior' section of Excalidraw Settings, you can fine-tune how Excalidraw appears and behaves. This includes options for dynamic styling, left-handed mode, matching Excalidraw and Obsidian themes, default modes, and more.",
|
||||
DYNAMICSTYLE_NAME: "Dynamic styling",
|
||||
DYNAMICSTYLE_DESC:
|
||||
"Change Excalidraw UI colors to match the canvas color",
|
||||
@@ -213,7 +256,8 @@ FILENAME_HEAD: "Filename",
|
||||
DEFAULT_PEN_MODE_NAME: "Pen mode",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"Should pen mode be automatically enabled when opening Excalidraw?",
|
||||
|
||||
THEME_HEAD: "Theme and styling",
|
||||
ZOOM_HEAD: "Zoom",
|
||||
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
|
||||
DEFAULT_PINCHZOOM_DESC:
|
||||
"Pinch zoom in pen mode when using the freedraw tool is disabled by default to prevent unwanted accidental zooming with your palm.<br>" +
|
||||
@@ -232,7 +276,14 @@ FILENAME_HEAD: "Filename",
|
||||
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
|
||||
ZOOM_TO_FIT_MAX_LEVEL_DESC:
|
||||
"Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
|
||||
LINKS_HEAD: "Links and transclusion",
|
||||
LASER_HEAD: "Laser pointer",
|
||||
LASER_COLOR: "Laser pointer color",
|
||||
LASER_DECAY_TIME_NAME: "Laser pointer decay time",
|
||||
LASER_DECAY_TIME_DESC: "Laser pointer decay time in milliseconds. Default is 1000 (i.e. 1 second).",
|
||||
LASER_DECAY_LENGTH_NAME: "Laser pointer decay length.",
|
||||
LASER_DECAY_LENGTH_DESC: "Laser pointer decay length in line points. Default is 50.",
|
||||
LINKS_HEAD: "Links, transclusion and TODOs",
|
||||
LINKS_HEAD_DESC: "In the 'Links, transclusion and TODOs' section of Excalidraw Settings, you can configure how Excalidraw handles links, transclusions, and TODO items. This includes options for opening links, managing panes, displaying links with brackets, customizing link prefixes, handling TODO items, and more. ",
|
||||
LINKS_DESC:
|
||||
`${labelCTRL()}+CLICK on <code>[[Text Elements]]</code> to open them as links. ` +
|
||||
"If the selected text has more than one <code>[[valid Obsidian links]]</code>, only the first will be opened. " +
|
||||
@@ -240,15 +291,21 @@ FILENAME_HEAD: "Filename",
|
||||
"the plugin will open it in a browser. " +
|
||||
"When Obsidian files change, the matching <code>[[link]]</code> in your drawings will also change. " +
|
||||
"If you don't want text accidentally changing in your drawings use <code>[[links|with aliases]]</code>.",
|
||||
DRAG_MODIFIER_NAME: "Link Click and Drag&Drop Modifier Keys",
|
||||
DRAG_MODIFIER_DESC: "Modifier key behavior when clicking links and dragging and dropping elements. " +
|
||||
"Excalidraw will not validate your configuration... pay attention to avoid conflicting settings. " +
|
||||
"These settings are different for Apple and non-Apple. If you use Obsidian on multiple platforms, you'll need to make the settings separately. "+
|
||||
"The toggles follow the order of " +
|
||||
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Windows key)."),
|
||||
ADJACENT_PANE_NAME: "Reuse adjacent pane",
|
||||
ADJACENT_PANE_DESC:
|
||||
`When ${labelCTRL()}+${labelSHIFT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
|
||||
`When ${labelCTRL()}+${labelALT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
|
||||
"Turning this setting on, Excalidraw will first look for an existing pane, and try to open the link there. " +
|
||||
"Excalidraw will look for the other workspace pane based on your focus/navigation history, i.e. the workpane that was active before you " +
|
||||
"activated Excalidraw.",
|
||||
MAINWORKSPACE_PANE_NAME: "Open in main workspace",
|
||||
MAINWORKSPACE_PANE_DESC:
|
||||
`When ${labelCTRL()}+${labelSHIFT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane in the current active window. ` +
|
||||
`When ${labelCTRL()}+${labelALT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane in the current active window. ` +
|
||||
"Turning this setting on, Excalidraw will open the link in an existing or new pane in the main workspace. ",
|
||||
LINK_BRACKETS_NAME: "Show <code>[[brackets]]</code> around links",
|
||||
LINK_BRACKETS_DESC: `${
|
||||
@@ -266,7 +323,7 @@ FILENAME_HEAD: "Filename",
|
||||
"You can override this setting for a specific drawing by adding <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_URL_PREFIX}: "🌐 "</code> to the file's frontmatter.`,
|
||||
PARSE_TODO_NAME: "Parse todo",
|
||||
PARSE_TODO_DESC: "Convert '- [ ] ' and '- [x] ' to checkpox and tick in the box.",
|
||||
PARSE_TODO_DESC: "Convert '- [ ] ' and '- [x] ' to checkbox and tick in the box.",
|
||||
TODO_NAME: "Open TODO icon",
|
||||
TODO_DESC: "Icon to use for open TODO items",
|
||||
DONE_NAME: "Completed TODO icon",
|
||||
@@ -304,11 +361,15 @@ FILENAME_HEAD: "Filename",
|
||||
GET_URL_TITLE_NAME: "Use iframely to resolve page title",
|
||||
GET_URL_TITLE_DESC:
|
||||
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
|
||||
MD_HEAD: "Markdown-embed settings",
|
||||
MD_HEAD_DESC:
|
||||
`You can transclude formatted markdown documents into drawings as images ${labelSHIFT()} drop from the file explorer or using ` +
|
||||
"the command palette action.",
|
||||
|
||||
PDF_TO_IMAGE: "PDF to Image",
|
||||
PDF_TO_IMAGE_SCALE_NAME: "PDF to Image conversion scale",
|
||||
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper imagee. " +
|
||||
"Additionally, if you want to copy PDF pages (as images) to Excalidraw.com, the bigger image size may result in exceeding the 2MB limit on Excalidraw.com.",
|
||||
EMBED_TOEXCALIDRAW_HEAD: "Embed files into Excalidraw",
|
||||
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
|
||||
MD_HEAD: "Embed markdown into Excalidraw as image",
|
||||
MD_EMBED_CUSTOMDATA_HEAD_NAME: "Interactive Markdown Files",
|
||||
MD_EMBED_CUSTOMDATA_HEAD_DESC: `These settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
|
||||
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
|
||||
MD_TRANSCLUDE_WIDTH_DESC:
|
||||
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
|
||||
@@ -344,8 +405,16 @@ FILENAME_HEAD: "Filename",
|
||||
"Setting the font-family in the css is has limitations. By default only your operating system's standard fonts are available (see README for details). " +
|
||||
"You can add one custom font beyond that using the setting above. " +
|
||||
'You can override this css setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-css: css_file_in_vault|css-snippet".',
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_HEAD: "Embedding Excalidraw into your Notes and Exporting",
|
||||
EMBED_DESC: `In the "Embed & Export" settings, you can configure how images and Excalidraw drawings are embedded and exported within your documents. Key settings include choosing the image type for markdown preview (such as Native SVG or PNG), specifying the type of file to insert into the document (original Excalidraw, PNG, or SVG), and managing image caching for embedding in markdown. You can also control image sizing, whether to embed drawings using wiki links or markdown links, and adjust settings related to image themes, background colors, and Obsidian integration.
|
||||
Additionally, there are settings for auto-export, which automatically generates SVG and/or PNG files to match the title of your Excalidraw drawings, keeping them in sync with file renames and deletions.`,
|
||||
EMBED_CANVAS: "Obsidian Canvas support",
|
||||
EMBED_CANVAS_NAME: "Immersive embedding",
|
||||
EMBED_CANVAS_DESC:
|
||||
"Hide canvas node border and background when embedding an Excalidraw drawing to Canvas. " +
|
||||
"Note that for a full transparent background for your image, you will still need to configure Excalidraw to export images with transparent background.",
|
||||
EMBED_CACHING: "Image caching",
|
||||
EXPORT_SUBHEAD: "Export Settings",
|
||||
EMBED_SIZING: "Image sizing",
|
||||
EMBED_THEME_BACKGROUND: "Image theme and background color",
|
||||
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
|
||||
@@ -386,6 +455,11 @@ FILENAME_HEAD: "Filename",
|
||||
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
||||
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
||||
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
|
||||
EMBED_MARKDOWN_COMMENT_DESC:
|
||||
"Embed the link to the original Excalidraw file as a markdown link under the image, e.g.:<code>%%[[drawing.excalidraw]]%%</code>.<br>" +
|
||||
"Instead of adding a markdown comment you may also select the embedded SVG or PNG line and use the command palette action: " +
|
||||
"'<code>Excalidraw: Open Excalidraw drawing</code>' to open the drawing.",
|
||||
EMBED_WIKILINK_NAME: "Embed Drawing using Wiki link",
|
||||
EMBED_WIKILINK_DESC:
|
||||
"<b><u>Toggle ON:</u></b> Excalidraw will embed a [[wiki link]].<br><b><u>Toggle OFF:</u></b> Excalidraw will embed a [markdown](link).",
|
||||
@@ -416,13 +490,21 @@ FILENAME_HEAD: "Filename",
|
||||
"Embed the .svg file into your documents instead of Excalidraw making you embeds platform independent. " +
|
||||
"While the auto-export switch is on, this file will get updated every time you edit the Excalidraw drawing with the matching name. " +
|
||||
"You can override this setting on a file level by adding the <code>excalidraw-autoexport</code> frontmatter key. Valid values for this key are " +
|
||||
"<code>none</code>,<code>both</code>,<code>svg</code>, and <code>png</code>",
|
||||
"<code>none</code>,<code>both</code>,<code>svg</code>, and <code>png</code>.",
|
||||
EXPORT_PNG_NAME: "Auto-export PNG",
|
||||
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
||||
EXPORT_BOTH_DARK_AND_LIGHT_NAME: "Export both dark- and light-themed image",
|
||||
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "When enabled, Excalidraw will export two files instead of one: filename.dark.png, filename.light.png and/or filename.dark.svg and filename.light.svg<br>"+
|
||||
"Double files will be exported both if auto-export SVG or PNG (or both) are enabled, as well as when clicking export on a single image.",
|
||||
COMPATIBILITY_HEAD: "Compatibility features",
|
||||
COMPATIBILITY_DESC: "You should only enable these features if you have a strong reason for wanting to work with excalidraw.com files instead of markdown files. Many of the plugin features are not supported on legacy files. Typical usecase would be if you use set your vault up on top of a Visual Studio Code project folder and you have .excalidraw drawings you want to access from Visual Studio Code as well. Another usecase might be using Excalidraw in Logseq and Obsidian in parallel.",
|
||||
SLIDING_PANES_NAME: "Sliding panes plugin support",
|
||||
SLIDING_PANES_DESC:
|
||||
"Need to restart Obsidian for this change to take effect.<br>" +
|
||||
"If you use the <a href='https://github.com/deathau/sliding-panes-obsidian' target='_blank'>Sliding Panes plugin</a> " +
|
||||
"you can enable this setting to make Excalidraw drawings work with the Sliding Panes plugin.<br>" +
|
||||
"Note, that Excalidraw Sliding Panes support causes compatibility issues with Obsidian Workspaces.<br>" +
|
||||
"Note also, that the 'Stack Tabs' feature is now available in Obsidian, providing native support for most of the Sliding Panes functionality.",
|
||||
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
||||
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
||||
SYNC_EXCALIDRAW_NAME:
|
||||
@@ -432,26 +514,42 @@ FILENAME_HEAD: "Filename",
|
||||
"then update the drawing in the .md file based on the .excalidraw file",
|
||||
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
||||
COMPATIBILITY_MODE_DESC:
|
||||
"⚠️ Enable this only if you know what you are doing. In 99.9% of the cases you DO NOT want this on. " +
|
||||
"By enabling this feature drawings you create with the ribbon icon, the command palette actions, " +
|
||||
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
||||
"when you open a legacy file for editing.",
|
||||
MATHJAX_NAME: "MathJax (LaTeX) javascript library host",
|
||||
MATHJAX_DESC: "If you are using LaTeX equiations in Excalidraw then the plugin needs to load a javascript library for that. " +
|
||||
"Some users are unable to access certain host servers. If you are experiencing issues try changing the host here. You may need to "+
|
||||
MATHJAX_DESC: "If you are using LaTeX equations in Excalidraw, then the plugin needs to load a javascript library for that. " +
|
||||
"Some users are unable to access certain host servers. If you are experiencing issues, try changing the host here. You may need to "+
|
||||
"restart Obsidian after closing settings, for this change to take effect.",
|
||||
LATEX_DEFAULT_NAME: "Default LaTeX formual for new equations",
|
||||
LATEX_DEFAULT_NAME: "Default LaTeX formula for new equations",
|
||||
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
|
||||
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
|
||||
NONSTANDARD_DESC: "These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.",
|
||||
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
|
||||
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a fourth font option, which adds a fourth font button to the properties panel for text elements. `,
|
||||
CUSTOM_PEN_HEAD: "Custom pens",
|
||||
CUSTOM_PEN_NAME: "Number of custom pens",
|
||||
CUSTOM_PEN_DESC: "You will see these pens next to the Obsidian Menu on the canvas. You can customize the pens on the canvas by long-pressing the pen button.",
|
||||
EXPERIMENTAL_HEAD: "Experimental features",
|
||||
EXPERIMENTAL_DESC:
|
||||
"Some of these setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
||||
EXPERIMENTAL_HEAD: "Miscellaneous features",
|
||||
EXPERIMENTAL_DESC: `These miscellaneous features in Excalidraw include options for setting default LaTeX formulas for new equations, enabling a Field Suggester for autocompletion, displaying type indicators for Excalidraw files, enabling immersive image embedding in live preview editing mode, and experimenting with Taskbone Optical Character Recognition for text extraction from images and drawings. Users can also enter a Taskbone API key for extended usage of the OCR service.`,
|
||||
EA_HEAD: "Excalidraw Automate",
|
||||
EA_DESC:
|
||||
"ExcalidrawAutomate is a scripting and automation API for Excalidraw. Unfortunately, the documentation of the API is sparse. " +
|
||||
"I recommend reading the <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> file, " +
|
||||
"visiting the <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> page - though the information " +
|
||||
"here has not been updated for a long while -, and finally to enable the field suggester below. The field suggester will show you the available " +
|
||||
"functions, their parameters and short description as you type. The field suggester is the most up-to-date documentation of the API.",
|
||||
FIELD_SUGGESTER_NAME: "Enable Field Suggester",
|
||||
FIELD_SUGGESTER_DESC:
|
||||
"Field Suggester borrowed from Breadcrumbs and Templater plugins. The Field Suggester will show an autocomplete menu " +
|
||||
"when you type <code>excalidraw-</code> or <code>ea.</code> with function description as hints on the individual items in the list.",
|
||||
STARTUP_SCRIPT_NAME: "Startup script",
|
||||
STARTUP_SCRIPT_DESC:
|
||||
"If set, excalidraw will execute the script at plugin startup. This is useful if you want to set any of the Excalidraw Automate hooks. The startup script is a markdown file " +
|
||||
"that should contain the javascript code you want to execute when Excalidraw starts.",
|
||||
STARTUP_SCRIPT_BUTTON_CREATE: "Create startup script",
|
||||
STARTUP_SCRIPT_BUTTON_OPEN: "Open startup script",
|
||||
STARTUP_SCRIPT_EXISTS: "Startup script file already exists",
|
||||
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
||||
FILETYPE_DESC:
|
||||
"Excalidraw files will receive an indicator using the emoji or text defined in the next setting.",
|
||||
@@ -463,6 +561,7 @@ FILENAME_HEAD: "Filename",
|
||||
"Turn this on to support image embedding styles such as ![[drawing|width|style]] in live preview editing mode. " +
|
||||
"The setting will not affect the currently open documents. You need close the open documents and re-open them for the change " +
|
||||
"to take effect.",
|
||||
CUSTOM_FONT_HEAD: "Fourth font",
|
||||
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
"By turning this on, you will see a fourth font button on the properties panel for text elements. " +
|
||||
@@ -474,6 +573,7 @@ FILENAME_HEAD: "Filename",
|
||||
"Select a .ttf, .woff or .woff2 font file from your vault to use as the fourth font. " +
|
||||
"If no file is selected, Excalidraw will use the Virgil font by default.",
|
||||
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
|
||||
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
|
||||
TASKBONE_HEAD: "Taskbone Optical Character Recogntion",
|
||||
TASKBONE_DESC: "This is an experimental integration of optical character recognition into Excalidraw. Please note, that taskbone is an independent external service not provided by Excalidraw, nor the Excalidraw-Obsidian plugin project. " +
|
||||
"The OCR service will grab legible text from freedraw lines and embedded pictures on your canvas and place the recognized text in the frontmatter of your drawing as well as onto clipboard. " +
|
||||
@@ -489,9 +589,12 @@ FILENAME_HEAD: "Filename",
|
||||
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "Select a file then press enter.",
|
||||
SELECT_COMMAND: "Select a command then press enter.",
|
||||
SELECT_FILE_WITH_OPTION_TO_SCALE: `Select a file then press ENTER, or ${labelSHIFT()}+${labelMETA()}+ENTER to insert at 100% scale.`,
|
||||
NO_MATCH: "No file matches your query.",
|
||||
NO_MATCHING_COMMAND: "No command matches your query.",
|
||||
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
||||
SELECT_COMMAND_PLACEHOLDER: "Select the command you want to insert the link for.",
|
||||
SELECT_DRAWING: "Select the image or drawing you want to insert",
|
||||
TYPE_FILENAME: "Type name of drawing to select.",
|
||||
SELECT_FILE_OR_TYPE_NEW:
|
||||
@@ -520,7 +623,7 @@ FILENAME_HEAD: "Filename",
|
||||
TOGGLE_DISABLEBINDING: "Toggle to invert default binding behavior",
|
||||
TOGGLE_FRAME_RENDERING: "Toggle frame rendering",
|
||||
TOGGLE_FRAME_CLIPPING: "Toggle frame clipping",
|
||||
OPEN_LINK_CLICK: "Navigate to selected element link",
|
||||
OPEN_LINK_CLICK: "Open Link",
|
||||
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
|
||||
|
||||
//IFrameActionsMenu.tsx
|
||||
@@ -530,6 +633,30 @@ FILENAME_HEAD: "Filename",
|
||||
ZOOM_TO_FIT: "Zoom to fit",
|
||||
RELOAD: "Reload original link",
|
||||
OPEN_IN_BROWSER: "Open current link in browser",
|
||||
PROPERTIES: "Properties",
|
||||
COPYCODE: "Copy source to clipboard",
|
||||
|
||||
//EmbeddableSettings.tsx
|
||||
ES_TITLE: "Embeddable Element Settings",
|
||||
ES_RENAME: "Rename File",
|
||||
ES_ZOOM: "Embedded Content Scaling",
|
||||
ES_YOUTUBE_START: "YouTube Start Time",
|
||||
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
|
||||
ES_YOUTUBE_START_INVALID: "The YouTube Start Time is invalid. Please check the format and try again",
|
||||
ES_FILENAME_VISIBLE: "Filename Visible",
|
||||
ES_BACKGROUND_HEAD: "Embedded note background color",
|
||||
ES_BACKGROUND_MATCH_ELEMENT: "Match Element Background Color",
|
||||
ES_BACKGROUND_MATCH_CANVAS: "Match Canvas Background Color",
|
||||
ES_BACKGROUND_COLOR: "Background Color",
|
||||
ES_BORDER_HEAD: "Embedded note border color",
|
||||
ES_BORDER_COLOR: "Border Color",
|
||||
ES_BORDER_MATCH_ELEMENT: "Match Element Border Color",
|
||||
ES_BACKGROUND_OPACITY: "Background Opacity",
|
||||
ES_BORDER_OPACITY: "Border Opacity",
|
||||
ES_EMBEDDABLE_SETTINGS: "Embeddable Markdown Settings",
|
||||
ES_USE_OBSIDIAN_DEFAULTS: "Use Obsidian Defaults",
|
||||
ES_ZOOM_100_RELATIVE_DESC: "The button will adjust the element scale so it will show the content at 100% relative to the current zoom level of your canvas",
|
||||
ES_ZOOM_100: "Relative 100%",
|
||||
|
||||
//Prompts.ts
|
||||
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",
|
||||
@@ -537,8 +664,12 @@ FILENAME_HEAD: "Filename",
|
||||
PROMPT_ERROR_DRAWING_CLOSED: "Unknown error. It seems as if your drawing was closed or the drawing file is missing",
|
||||
PROMPT_TITLE_NEW_FILE: "New File",
|
||||
PROMPT_TITLE_CONFIRMATION: "Confirmation",
|
||||
PROMPT_BUTTON_CREATE_EXCALIDRAW: "Create Excalidraw",
|
||||
PROMPT_BUTTON_CREATE_MARKDOWN: "Create Markdown",
|
||||
PROMPT_BUTTON_CREATE_EXCALIDRAW: "Create EX",
|
||||
PROMPT_BUTTON_CREATE_EXCALIDRAW_ARIA: "Create Excalidraw drawing and open in new tab",
|
||||
PROMPT_BUTTON_CREATE_MARKDOWN: "Create MD",
|
||||
PROMPT_BUTTON_CREATE_MARKDOWN_ARIA: "Create markdown document and open in new tab",
|
||||
PROMPT_BUTTON_EMBED_MARKDOWN: "Embed MD",
|
||||
PROMPT_BUTTON_EMBED_MARKDOWN_ARIA: "Replace selected element with embedded markdown document",
|
||||
PROMPT_BUTTON_NEVERMIND: "Nevermind",
|
||||
PROMPT_BUTTON_OK: "OK",
|
||||
PROMPT_BUTTON_CANCEL: "Cancel",
|
||||
@@ -546,5 +677,11 @@ FILENAME_HEAD: "Filename",
|
||||
PROMPT_BUTTON_INSERT_SPACE: "Insert space",
|
||||
PROMPT_BUTTON_INSERT_LINK: "Insert markdown link to file",
|
||||
PROMPT_BUTTON_UPPERCASE: "Uppercase",
|
||||
|
||||
|
||||
//ModifierKeySettings
|
||||
WEB_BROWSER_DRAG_ACTION: "Web Browser Drag Action",
|
||||
LOCAL_FILE_DRAG_ACTION: "OS Local File Drag Action",
|
||||
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
|
||||
PANE_TARGET: "Link click behavior",
|
||||
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
} from "src/constants";
|
||||
} from "src/constants/constants";
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
// 简体中文
|
||||
@@ -61,7 +61,7 @@ export default {
|
||||
IMPORT_SVG: "从 SVG 文件导入图形元素到当前绘图中(暂不支持文本元素)",
|
||||
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以 iFrame 形式嵌入)到当前绘图中",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以 Embeddable 形式嵌入)到当前绘图中",
|
||||
INSERT_LATEX:
|
||||
`插入 LaTeX 公式到当前绘图。按住 ${labelALT()} 可观看视频演示。`,
|
||||
ENTER_LATEX: "输入 LaTeX 表达式",
|
||||
|
||||
728
src/main.ts
@@ -1,4 +1,4 @@
|
||||
import { Globe, RotateCcw, Scan } from "lucide-react";
|
||||
import { Copy, Globe, RotateCcw, Scan, Settings } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PenStyle } from "src/PenTypes";
|
||||
|
||||
@@ -27,8 +27,10 @@ export const ICONS = {
|
||||
</svg>
|
||||
),
|
||||
Reload: (<RotateCcw />),
|
||||
Copy: (<Copy /> ),
|
||||
Globe: (<Globe />),
|
||||
ZoomToSelectedElement: (<Scan />),
|
||||
Properties: (<Settings />),
|
||||
ZoomToSection: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { TFile } from "obsidian";
|
||||
import * as React from "react";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { ExcalidrawElement, ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import { ICONS } from "./ActionIcons";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants";
|
||||
import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
|
||||
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
|
||||
import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings";
|
||||
|
||||
export class EmbeddableMenu {
|
||||
|
||||
@@ -23,6 +24,9 @@ export class EmbeddableMenu {
|
||||
private updateElement = (subpath: string, element: ExcalidrawEmbeddableElement, file: TFile) => {
|
||||
if(!element) return;
|
||||
const view = this.view;
|
||||
const app = view.app;
|
||||
element = view.excalidrawAPI.getSceneElements().find((e:ExcalidrawElement) => e.id === element.id);
|
||||
if(!element) return;
|
||||
const path = app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
view.file.path,
|
||||
@@ -52,6 +56,7 @@ export class EmbeddableMenu {
|
||||
|
||||
renderButtons(appState: AppState) {
|
||||
const view = this.view;
|
||||
const app = view.app;
|
||||
const api = view?.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if(!api) return null;
|
||||
if(!view.file) return null;
|
||||
@@ -73,21 +78,26 @@ export class EmbeddableMenu {
|
||||
if(!link) return null;
|
||||
|
||||
const isExcalidrawiFrame = useDefaultExcalidrawFrame(element);
|
||||
let isObsidianiFrame = element.link?.match(REG_LINKINDEX_HYPERLINK);
|
||||
let isObsidianiFrame = Boolean(element.link?.match(REG_LINKINDEX_HYPERLINK));
|
||||
|
||||
if(!isExcalidrawiFrame && !isObsidianiFrame) {
|
||||
const res = REGEX_LINK.getRes(element.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
return null;
|
||||
if(link.startsWith("data:text/html")) {
|
||||
isObsidianiFrame = true;
|
||||
} else {
|
||||
const res = REGEX_LINK.getRes(element.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
link = REGEX_LINK.getLink(res);
|
||||
|
||||
isObsidianiFrame = Boolean(link.match(REG_LINKINDEX_HYPERLINK));
|
||||
}
|
||||
|
||||
link = REGEX_LINK.getLink(res);
|
||||
|
||||
isObsidianiFrame = link.match(REG_LINKINDEX_HYPERLINK);
|
||||
|
||||
if(!isObsidianiFrame) {
|
||||
const { subpath, file } = processLinkText(link, view);
|
||||
if(!file || file.extension!=="md") return null;
|
||||
if(!file) return;
|
||||
const isMD = file.extension==="md";
|
||||
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
|
||||
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
|
||||
const left = `${x-appState.offsetLeft}px`;
|
||||
@@ -112,79 +122,93 @@ export class EmbeddableMenu {
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
title={t("NARROW_TO_HEADING")}
|
||||
action={async () => {
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
|
||||
const values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}}
|
||||
icon={ICONS.ZoomToSection}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"MarkdownBlock"}
|
||||
title={t("NARROW_TO_BLOCK")}
|
||||
action={async () => {
|
||||
if(!file) return;
|
||||
const paragrphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
|
||||
const values = ["entire-file"].concat(paragrphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
{isMD && (
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
title={t("NARROW_TO_HEADING")}
|
||||
action={async () => {
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
|
||||
const values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}}
|
||||
icon={ICONS.ZoomToSection}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
{isMD && (
|
||||
<ActionButton
|
||||
key={"MarkdownBlock"}
|
||||
title={t("NARROW_TO_BLOCK")}
|
||||
action={async () => {
|
||||
if(!file) return;
|
||||
const paragrphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
|
||||
const values = ["entire-file"].concat(paragrphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
|
||||
if(selectedBlock==="entire-file") {
|
||||
if(subpath==="") return;
|
||||
this.updateElement("", element, file);
|
||||
return;
|
||||
}
|
||||
|
||||
let blockID = selectedBlock.node.id;
|
||||
if(blockID && (`#^${blockID}` === subpath)) return;
|
||||
if (!blockID) {
|
||||
const offset = selectedBlock.node?.position?.end?.offset;
|
||||
if(!offset) return;
|
||||
blockID = nanoid();
|
||||
const fileContents = await app.vault.cachedRead(file);
|
||||
if(!fileContents) return;
|
||||
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
|
||||
await sleep(200); //wait for cache to update
|
||||
}
|
||||
this.updateElement(`#^${blockID}`, element, file);
|
||||
}}
|
||||
icon={ICONS.ZoomToBlock}
|
||||
view={view}
|
||||
/>
|
||||
if(selectedBlock==="entire-file") {
|
||||
if(subpath==="") return;
|
||||
this.updateElement("", element, file);
|
||||
return;
|
||||
}
|
||||
|
||||
let blockID = selectedBlock.node.id;
|
||||
if(blockID && (`#^${blockID}` === subpath)) return;
|
||||
if (!blockID) {
|
||||
const offset = selectedBlock.node?.position?.end?.offset;
|
||||
if(!offset) return;
|
||||
blockID = nanoid();
|
||||
const fileContents = await app.vault.cachedRead(file);
|
||||
if(!fileContents) return;
|
||||
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
|
||||
await sleep(200); //wait for cache to update
|
||||
}
|
||||
this.updateElement(`#^${blockID}`, element, file);
|
||||
}}
|
||||
icon={ICONS.ZoomToBlock}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
api.zoomToFit([element], 30, 0.1);
|
||||
}}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"Properties"}
|
||||
title={t("PROPERTIES")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(view.plugin,view,file,element).open();
|
||||
}}
|
||||
icon={ICONS.Properties}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -252,6 +276,28 @@ export class EmbeddableMenu {
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"Properties"}
|
||||
title={t("PROPERTIES")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(view.plugin,view,null,element).open();
|
||||
}}
|
||||
icon={ICONS.Properties}
|
||||
view={view}
|
||||
/>
|
||||
{link?.startsWith("data:text/html") && (
|
||||
<ActionButton
|
||||
key={"CopyCode"}
|
||||
title={t("COPYCODE")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
navigator.clipboard.writeText(atob(link.split(",")[1]));
|
||||
}}
|
||||
icon={ICONS.Copy}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import clsx from "clsx";
|
||||
import { TFile } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { VIEW_TYPE_EXCALIDRAW } from "src/constants";
|
||||
import { DEVICE, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
|
||||
import { PenSettingsModal } from "src/dialogs/PenSettingsModal";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { PenStyle } from "src/PenTypes";
|
||||
@@ -142,7 +142,7 @@ export class ObsidianMenu {
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
aria-label={pen.type}
|
||||
aria-label={DEVICE.isDesktop ? pen.type : undefined}
|
||||
style={{
|
||||
...appState.activeTool.type === "freedraw" && appState.currentStrokeOptions === pen.penOptions
|
||||
? {background: "var(--color-primary)"}
|
||||
@@ -225,7 +225,10 @@ export class ObsidianMenu {
|
||||
prevClickTimestamp = now;
|
||||
}}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={name}>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
aria-label={DEVICE.isDesktop ? name : undefined}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</label>
|
||||
@@ -265,6 +268,7 @@ export class ObsidianMenu {
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
this.view.setCurrentPositionToCenter();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}}
|
||||
|
||||
@@ -3,16 +3,15 @@ import { Notice, TFile } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import { ICONS, saveIcon, stringToSVG } from "./ActionIcons";
|
||||
import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../constants";
|
||||
import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../constants/constants";
|
||||
import { insertLaTeXToView, search } from "../ExcalidrawAutomate";
|
||||
import ExcalidrawView, { TextMode } from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import { ReleaseNotes } from "../dialogs/ReleaseNotes";
|
||||
import { ScriptIconMap } from "../Scripts";
|
||||
import { getIMGFilename } from "../utils/FileUtils";
|
||||
import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { isALT, isCTRL, isSHIFT, mdPropModifier } from "src/utils/ModifierkeyHelper";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
|
||||
import { ExportDialog } from "src/dialogs/ExportDialog";
|
||||
|
||||
@@ -380,7 +379,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
|
||||
return;
|
||||
}
|
||||
this.props.view.plugin.taskbone.getTextForView(this.props.view, isCTRL(e));
|
||||
this.props.view.plugin.taskbone.getTextForView(this.props.view, isWinCTRLorMacCMD(e));
|
||||
}}
|
||||
icon={ICONS.ocr}
|
||||
view={this.props.view}
|
||||
@@ -505,7 +504,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
key={"latex"}
|
||||
title={t("INSERT_LATEX")}
|
||||
action={(e) => {
|
||||
if(isALT(e)) {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
this.props.view.openExternalLink("https://youtu.be/r08wk-58DPk");
|
||||
return;
|
||||
}
|
||||
@@ -522,7 +521,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
this.props.centerPointer();
|
||||
this.props.view.plugin.insertLinkDialog.start(
|
||||
this.props.view.file.path,
|
||||
this.props.view.addText,
|
||||
(text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.props.view.addText (text, fontFamily, save),
|
||||
);
|
||||
}}
|
||||
icon={ICONS.insertLink}
|
||||
@@ -532,12 +531,12 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
key={"link-to-element"}
|
||||
title={t("INSERT_LINK_TO_ELEMENT")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if(isALT(e)) {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
this.props.view.openExternalLink("https://youtu.be/yZQoJg2RCKI");
|
||||
return;
|
||||
}
|
||||
this.props.view.copyLinkToSelectedElementToClipboard(
|
||||
isCTRL(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
);
|
||||
}}
|
||||
icon={ICONS.copyElementLink}
|
||||
|
||||
@@ -4,7 +4,7 @@ import ExcalidrawPlugin from "../main"
|
||||
import {log} from "../utils/Utils"
|
||||
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
|
||||
import FrontmatterEditor from "src/utils/Frontmatter";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { blobToBase64 } from "src/utils/FileUtils";
|
||||
|
||||
|
||||
1265
src/settings.ts
@@ -1,6 +1,6 @@
|
||||
import { randomId, randomInteger } from "../utils";
|
||||
|
||||
import { ExcalidrawLinearElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExcalidrawLinearElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
|
||||
export type Point = [number, number];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GITHUB_RELEASES } from "src/constants";
|
||||
import { GITHUB_RELEASES } from "src/constants/constants";
|
||||
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
|
||||
export type PathCommand = {
|
||||
type: string;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { getTransformMatrix, transformPoints } from "./transform";
|
||||
import { pointsOnPath } from "points-on-path";
|
||||
import { randomId, getWindingOrder } from "./utils";
|
||||
import { ROUNDNESS } from "../constants";
|
||||
import { ROUNDNESS } from "../constants/constants";
|
||||
|
||||
const SUPPORTED_TAGS = [
|
||||
"svg",
|
||||
|
||||
13
src/types.d.ts
vendored
@@ -32,6 +32,7 @@ declare global {
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
internalPlugins: any;
|
||||
isMobile(): boolean;
|
||||
getObsidianUrl(file:TFile): string;
|
||||
}
|
||||
@@ -48,4 +49,16 @@ declare module "obsidian" {
|
||||
ctx?: any,
|
||||
): EventRef;
|
||||
}
|
||||
interface DataAdapter {
|
||||
url: {
|
||||
pathToFileURL(path: string): URL;
|
||||
},
|
||||
basePath: string;
|
||||
}
|
||||
interface Editor {
|
||||
insertText(data: string): void;
|
||||
}
|
||||
interface MetadataCache {
|
||||
getBacklinksForFile(file: TFile): any;
|
||||
}
|
||||
}
|
||||
252
src/utils/AIUtils.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { DEVICE } from "../constants/constants";
|
||||
import { Notice, RequestUrlResponse, requestUrl } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
|
||||
type MessageContent =
|
||||
| string
|
||||
| (string | { type: "image_url"; image_url: string })[];
|
||||
|
||||
export type GPTCompletionRequest = {
|
||||
model: string;
|
||||
messages?: {
|
||||
role?: "system" | "user" | "assistant" | "function";
|
||||
content?: MessageContent;
|
||||
name?: string | undefined;
|
||||
}[];
|
||||
functions?: any[] | undefined;
|
||||
function_call?: any | undefined;
|
||||
stream?: boolean | undefined;
|
||||
temperature?: number | undefined;
|
||||
top_p?: number | undefined;
|
||||
max_tokens?: number | undefined;
|
||||
n?: number | undefined;
|
||||
best_of?: number | undefined;
|
||||
frequency_penalty?: number | undefined;
|
||||
presence_penalty?: number | undefined;
|
||||
logit_bias?:
|
||||
| {
|
||||
[x: string]: number;
|
||||
}
|
||||
| undefined;
|
||||
stop?: (string[] | string) | undefined;
|
||||
size?: string;
|
||||
quality?: "standard" | "hd";
|
||||
prompt?: string;
|
||||
image?: string;
|
||||
mask?: string;
|
||||
};
|
||||
|
||||
export type AIRequest = {
|
||||
image?: string;
|
||||
text?: string;
|
||||
instruction?: string;
|
||||
systemPrompt?: string;
|
||||
imageGenerationProperties?: {
|
||||
size?: string; //depends on model
|
||||
quality?: "standard" | "hd"; //depends on model
|
||||
n?: number; //dall-e-3 only accepts 1
|
||||
mask?: string; //dall-e-2 only (image editing)
|
||||
};
|
||||
};
|
||||
|
||||
const handleImageEditPrompt = async (request: AIRequest) : Promise<RequestUrlResponse> => {
|
||||
const plugin: ExcalidrawPlugin = window.ExcalidrawAutomate.plugin;
|
||||
const {
|
||||
openAIAPIToken,
|
||||
openAIImageEditsURL,
|
||||
} = plugin.settings;
|
||||
const { image, text, imageGenerationProperties} = request;
|
||||
|
||||
const body = new FormData();
|
||||
body.append("model", "dall-e-2");
|
||||
text.trim() !== "" && body.append("prompt", text);
|
||||
|
||||
if (image) {
|
||||
const imageBlob = await fetch(image).then((res) => res.blob());
|
||||
body.append('image', imageBlob, 'image.png');
|
||||
}
|
||||
|
||||
if (imageGenerationProperties.mask) {
|
||||
const maskBlob = await fetch(imageGenerationProperties.mask).then((res) => res.blob());
|
||||
body.append('mask', maskBlob, 'masik.png');
|
||||
}
|
||||
|
||||
imageGenerationProperties.size && body.append("size", imageGenerationProperties.size);
|
||||
imageGenerationProperties.n && body.append("n", String(imageGenerationProperties.n));
|
||||
|
||||
try {
|
||||
//https://platform.openai.com/docs/api-reference/images
|
||||
const resp = await fetch(
|
||||
openAIImageEditsURL,
|
||||
{
|
||||
method: "post",
|
||||
body,
|
||||
headers: {
|
||||
Authorization: `Bearer ${openAIAPIToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if(!resp) return null;
|
||||
return {
|
||||
status: resp.status,
|
||||
headers: resp.headers as any,
|
||||
text: null,
|
||||
json: await resp.json(),
|
||||
arrayBuffer: null,
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleGenericPrompt = async (request: AIRequest) : Promise<RequestUrlResponse> => {
|
||||
const plugin: ExcalidrawPlugin = window.ExcalidrawAutomate.plugin;
|
||||
const {
|
||||
openAIAPIToken,
|
||||
openAIDefaultTextModel,
|
||||
openAIDefaultVisionModel,
|
||||
openAIURL,
|
||||
openAIImageGenerationURL,
|
||||
openAIDefaultImageGenerationModel,
|
||||
} = plugin.settings;
|
||||
const { image, text, instruction, systemPrompt, imageGenerationProperties} = request;
|
||||
const isImageGeneration = Boolean(imageGenerationProperties);
|
||||
const requestType = isImageGeneration ? "dall-e" : (image ? "image" : "text");
|
||||
let body: GPTCompletionRequest;
|
||||
|
||||
switch (requestType) {
|
||||
case "text":
|
||||
body = {
|
||||
model: openAIDefaultTextModel,
|
||||
max_tokens: 4096,
|
||||
messages: [
|
||||
...(systemPrompt && systemPrompt.trim() !=="" ? [{role: "system" as const,content: systemPrompt}] : []),
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
...(instruction && instruction.trim() !=="" ? [{role: "user" as const,content: instruction}] : []),
|
||||
],
|
||||
};
|
||||
break;
|
||||
case "image":
|
||||
body = {
|
||||
model: openAIDefaultVisionModel,
|
||||
max_tokens: 4096,
|
||||
messages: [
|
||||
...(systemPrompt && systemPrompt.trim() !=="" ? [{role: "system" as const,content: systemPrompt}] : []),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: image,
|
||||
},
|
||||
...(text ? [text] : []),
|
||||
...(instruction && instruction.trim() !== "" ? [instruction] : []),
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
break;
|
||||
case "dall-e":
|
||||
body = {
|
||||
model: openAIDefaultImageGenerationModel,
|
||||
prompt: text,
|
||||
...imageGenerationProperties
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
//https://platform.openai.com/docs/api-reference/images
|
||||
const resp = await fetch (isImageGeneration ? openAIImageGenerationURL : openAIURL, {
|
||||
method: "post",
|
||||
//@ts-ignore
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${openAIAPIToken}`,
|
||||
}
|
||||
});
|
||||
if(!resp) return null;
|
||||
return {
|
||||
status: resp.status,
|
||||
headers: resp.headers as any,
|
||||
text: null,
|
||||
json: await resp.json(),
|
||||
arrayBuffer: null,
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return null;
|
||||
|
||||
/*
|
||||
//does not seem to work on Android :(
|
||||
try {
|
||||
//https://platform.openai.com/docs/api-reference/images
|
||||
const resp = await requestUrl ({
|
||||
url: isImageGeneration ? openAIImageGenerationURL : openAIURL,
|
||||
method: "post",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${openAIAPIToken}`,
|
||||
},
|
||||
throw: false
|
||||
});
|
||||
return resp;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return null;*/
|
||||
}
|
||||
|
||||
|
||||
export const postOpenAI = async (request: AIRequest) : Promise<RequestUrlResponse> => {
|
||||
const plugin: ExcalidrawPlugin = window.ExcalidrawAutomate.plugin;
|
||||
const { openAIAPIToken } = plugin.settings;
|
||||
const { image, imageGenerationProperties} = request;
|
||||
const isImageGeneration = Boolean(imageGenerationProperties);
|
||||
const isImageVariationOrEditing = isImageGeneration && (Boolean(imageGenerationProperties.mask) || Boolean(image));
|
||||
|
||||
if(openAIAPIToken === "") {
|
||||
new Notice("OpenAI API Token is not set. Please set it in plugin settings.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(isImageVariationOrEditing) {
|
||||
return await handleImageEditPrompt(request);
|
||||
}
|
||||
return await handleGenericPrompt(request);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the codeblock contents from the supplied markdown string.
|
||||
* @param markdown
|
||||
* @param codeblockType
|
||||
* @returns an array of dictionaries with the codeblock contents and type
|
||||
*/
|
||||
export const extractCodeBlocks = (markdown: string): { data: string, type: string }[] => {
|
||||
if (!markdown) return [];
|
||||
|
||||
markdown = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
||||
const result: { data: string, type: string }[] = [];
|
||||
const regex = /```([a-zA-Z0-9]*)\n([\s\S]+?)```/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const codeblockType = match[1]??"";
|
||||
const codeblockString = match[2].trim();
|
||||
result.push({ data: codeblockString, type: codeblockType });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ container.appendChild(node.contentEl)
|
||||
import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils";
|
||||
import { CustomMutationObserver, isDebugMode } from "./DebugHelper";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
@@ -72,8 +73,9 @@ export class CanvasNodeFactory {
|
||||
const node = this.canvas.createFileNode({pos: {x:0,y:0}, file, subpath, save: false});
|
||||
node.setFilePath(file.path,subpath);
|
||||
node.render();
|
||||
containerEl.style.background = "var(--background-primary)";
|
||||
containerEl.appendChild(node.contentEl)
|
||||
//containerEl.style.background = "var(--background-primary)";
|
||||
node.containerEl.querySelector(".canvas-node-content-blocker")?.remove();
|
||||
containerEl.appendChild(node.containerEl)
|
||||
this.nodes.set(elementId, node);
|
||||
return node;
|
||||
}
|
||||
@@ -93,8 +95,8 @@ export class CanvasNodeFactory {
|
||||
if (!node.child.editor?.containerEl?.parentElement?.parentElement) return;
|
||||
node.child.editor.containerEl.parentElement.parentElement.classList.remove(obsidianTheme);
|
||||
node.child.editor.containerEl.parentElement.parentElement.classList.add(theme);
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
|
||||
const nodeObserverFn: MutationCallback = (mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
const targetElement = mutation.target as HTMLElement;
|
||||
@@ -104,7 +106,10 @@ export class CanvasNodeFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const observer = isDebugMode
|
||||
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
|
||||
: new MutationObserver(nodeObserverFn);
|
||||
|
||||
observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
|
||||
})();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants";
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
|
||||
import { getParentOfClass } from "./ObsidianUtils";
|
||||
import { TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { getLinkParts } from "./Utils";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
|
||||
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
|
||||
return !element.link.startsWith("["); // && !element.link.match(TWITTER_REG);
|
||||
return !(element.link.startsWith("[") || element.link.startsWith("file:") || element.link.startsWith("data:")); // && !element.link.match(TWITTER_REG);
|
||||
}
|
||||
|
||||
export const leafMap = new Map<string, WorkspaceLeaf>();
|
||||
|
||||
38
src/utils/DebugHelper.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export const isDebugMode = false;
|
||||
export const durationTreshold = 0; //0.05; //ms
|
||||
|
||||
export class CustomMutationObserver {
|
||||
private originalCallback: MutationCallback;
|
||||
private observer: MutationObserver | null;
|
||||
private name: string;
|
||||
|
||||
constructor(callback: MutationCallback, name: string) {
|
||||
this.originalCallback = callback;
|
||||
this.observer = null;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
observe(target: Node, options: MutationObserverInit) {
|
||||
const wrappedCallback: MutationCallback = async (mutationsList, observer) => {
|
||||
const startTime = performance.now(); // Get start time
|
||||
await this.originalCallback(mutationsList, observer); // Invoke the original callback
|
||||
const endTime = performance.now(); // Get end time
|
||||
const executionTime = endTime - startTime;
|
||||
if (executionTime > durationTreshold) {
|
||||
console.log(`Excalidraw ${this.name} MutationObserver callback took ${executionTime}ms to execute`);
|
||||
}
|
||||
};
|
||||
|
||||
this.observer = new MutationObserver(wrappedCallback);
|
||||
|
||||
// Start observing with the modified callback
|
||||
this.observer.observe(target, options);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { DynamicStyle } from "src/types";
|
||||
import { cloneElement } from "src/ExcalidrawAutomate";
|
||||
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { addAppendUpdateCustomData } from "./Utils";
|
||||
import { mutateElement } from "src/constants/constants";
|
||||
|
||||
export const setDynamicStyle = (
|
||||
ea: ExcalidrawAutomate,
|
||||
@@ -30,7 +35,7 @@ export const setDynamicStyle = (
|
||||
const darker = "#101010";
|
||||
const lighter = "#f0f0f0";
|
||||
const step = 10;
|
||||
const mixRatio = 0.8;
|
||||
const mixRatio = 0.9;
|
||||
|
||||
const invertColor = (c:string) => {
|
||||
const cm = ea.getCM(c);
|
||||
@@ -43,68 +48,116 @@ export const setDynamicStyle = (
|
||||
: invertColor(color);
|
||||
|
||||
const bgLightness = cmBG().lightness;
|
||||
const isDark = cmBG().isDark();
|
||||
|
||||
const isDark = cmBG().darkerBy(step).isDark();
|
||||
const isGray = dynamicStyle === "gray";
|
||||
|
||||
//@ts-ignore
|
||||
const accentColorString = app.getAccentColor();
|
||||
const accent = () => ea.getCM(accentColorString);
|
||||
const accentColorString = view.app.getAccentColor();
|
||||
const accent = () => isGray
|
||||
? ea.getCM(accentColorString)
|
||||
: ea.getCM(accentColorString).mix({color:cmBG(),ratio:0.2});
|
||||
|
||||
const cmBlack = () => ea.getCM("#000000").lightnessTo(bgLightness);
|
||||
|
||||
const isGray = dynamicStyle === "gray";
|
||||
const gray1 = isGray
|
||||
? isDark ? cmBlack().lighterBy(15) : cmBlack().darkerBy(15)
|
||||
: isDark ? cmBG().lighterBy(15).mix({color:cmBlack(),ratio:0.6}) : cmBG().darkerBy(15).mix({color:cmBlack(),ratio:0.6});
|
||||
const gray2 = isGray
|
||||
? isDark ? cmBlack().lighterBy(5) : cmBlack().darkerBy(5)
|
||||
: isDark ? cmBG().lighterBy(5).mix({color:cmBlack(),ratio:0.6}) : cmBG().darkerBy(5).mix({color:cmBlack(),ratio:0.6});
|
||||
|
||||
const gray1 = () => isGray
|
||||
? isDark ? cmBlack().lighterBy(10) : cmBlack().darkerBy(10)
|
||||
: isDark ? cmBG().lighterBy(10).mix({color:cmBlack(),ratio:0.5}) : cmBG().darkerBy(10).mix({color:cmBlack(),ratio:0.5});
|
||||
const gray2 = () => isGray
|
||||
? isDark ? cmBlack().lighterBy(4) : cmBlack().darkerBy(4)
|
||||
: isDark ? cmBG().lighterBy(4).mix({color:cmBlack(),ratio:0.5}) : cmBG().darkerBy(4).mix({color:cmBlack(),ratio:0.5});
|
||||
|
||||
|
||||
const text = cmBG().mix({color:isDark?lighter:darker, ratio:mixRatio});
|
||||
|
||||
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
|
||||
const style = `--color-primary: ${str(accent())};` +
|
||||
`--color-primary-darker: ${str(accent().darkerBy(step))};` +
|
||||
`--color-primary-darkest: ${str(accent().darkerBy(step))};` +
|
||||
`--button-gray-1: ${str(gray1)};` +
|
||||
`--button-gray-2: ${str(gray2)};` +
|
||||
`--input-border-color: ${str(gray1)};` +
|
||||
`--input-bg-color: ${str(gray2)};` +
|
||||
`--input-label-color: ${str(text)};` +
|
||||
`--island-bg-color: ${gray2.alphaTo(0.93).stringHEX()};` +
|
||||
`--popup-secondary-bg-color: ${gray2.alphaTo(0.93).stringHEX()};` +
|
||||
`--icon-fill-color: ${str(text)};` +
|
||||
`--text-primary-color: ${str(text)};` +
|
||||
`--overlay-bg-color: ${gray2.alphaTo(0.6).stringHEX()};` +
|
||||
`--popup-bg-color: ${str(gray1)};` +
|
||||
`--color-gray-100: ${str(text)};` +
|
||||
`--color-gray-40: ${str(text)};` +
|
||||
`--color-gray-30: ${str(gray1)};` +
|
||||
`--color-gray-80: ${str(gray1)};` +
|
||||
`--sidebar-border-color: ${str(gray1)};` +
|
||||
`--color-primary-light: ${str(accent().lighterBy(step))};` +
|
||||
`--button-hover-bg: ${str(gray1)};` +
|
||||
`--sidebar-bg-color: ${gray2.alphaTo(0.93).stringHEX()};` +
|
||||
`--sidebar-shadow: ${str(gray1)};` +
|
||||
`--popup-text-color: ${str(text)};` +
|
||||
`--code-normal: ${str(text)};` +
|
||||
`--code-background: ${str(gray2)};` +
|
||||
`--h1-color: ${str(text)};` +
|
||||
`--h2-color: ${str(text)};` +
|
||||
`--h3-color: ${str(text)};` +
|
||||
`--h4-color: ${str(text)};` +
|
||||
`color: ${str(text)};` +
|
||||
`--select-highlight-color: ${str(gray1)};`;
|
||||
const styleObject:{[x: string]: string;} = {
|
||||
[`--color-primary`]: str(accent()),
|
||||
[`--color-surface-low`]: str(gray1()),
|
||||
[`--color-surface-mid`]: str(gray1()),
|
||||
[`--color-surface-lowest`]: str(gray2()),
|
||||
[`--color-surface-high`]: str(gray1().lighterBy(step)),
|
||||
[`--color-on-primary-container`]: str(!isDark?accent().darkerBy(15):accent().lighterBy(15)),
|
||||
[`--color-surface-primary-container`]: str(isDark?accent().darkerBy(step):accent().lighterBy(step)),
|
||||
//[`--color-primary-darker`]: str(accent().darkerBy(step)),
|
||||
//[`--color-primary-darkest`]: str(accent().darkerBy(step)),
|
||||
[`--button-gray-1`]: str(gray1()),
|
||||
[`--button-gray-2`]: str(gray2()),
|
||||
[`--input-border-color`]: str(gray1()),
|
||||
[`--input-bg-color`]: str(gray2()),
|
||||
[`--input-label-color`]: str(text),
|
||||
[`--island-bg-color`]: gray2().alphaTo(0.93).stringHEX(),
|
||||
[`--popup-secondary-bg-color`]: gray2().alphaTo(0.93).stringHEX(),
|
||||
[`--icon-fill-color`]: str(text),
|
||||
[`--text-primary-color`]: str(text),
|
||||
[`--overlay-bg-color`]: gray2().alphaTo(0.6).stringHEX(),
|
||||
[`--popup-bg-color`]: str(gray1()),
|
||||
[`--color-on-surface`]: str(text),
|
||||
//[`--color-gray-100`]: str(text),
|
||||
[`--color-gray-40`]: str(text), //frame
|
||||
[`--color-gray-50`]: str(text), //frame
|
||||
[`--color-surface-highlight`]: str(gray1()),
|
||||
//[`--color-gray-30`]: str(gray1),
|
||||
[`--color-gray-80`]: str(isDark?text.lighterBy(15):text.darkerBy(15)), //frame
|
||||
[`--sidebar-border-color`]: str(gray1()),
|
||||
[`--color-primary-light`]: str(accent().lighterBy(step)),
|
||||
[`--button-hover-bg`]: str(gray1()),
|
||||
[`--sidebar-bg-color`]: gray2().alphaTo(0.93).stringHEX(),
|
||||
[`--sidebar-shadow`]: str(gray1()),
|
||||
[`--popup-text-color`]: str(text),
|
||||
[`--code-normal`]: str(text),
|
||||
[`--code-background`]: str(gray2()),
|
||||
[`--h1-color`]: str(text),
|
||||
[`--h2-color`]: str(text),
|
||||
[`--h3-color`]: str(text),
|
||||
[`--h4-color`]: str(text),
|
||||
[`color`]: str(text),
|
||||
[`--select-highlight-color`]: str(gray1()),
|
||||
};
|
||||
|
||||
view.excalidrawContainer?.setAttribute(
|
||||
"style",
|
||||
style
|
||||
)
|
||||
const styleString = Object.keys(styleObject)
|
||||
.map((property) => `${property}: ${styleObject[property]}`)
|
||||
.join("; ");
|
||||
|
||||
setTimeout(()=>view.updateScene({appState:{dynamicStyle: style}}));
|
||||
/*view.excalidrawContainer?.setAttribute(
|
||||
"style",
|
||||
styleString
|
||||
)*/
|
||||
|
||||
setTimeout(()=>{
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if(!api) return;
|
||||
const frameColor = {
|
||||
stroke: str(isDark?gray2().lighterBy(15):gray2().darkerBy(15)),
|
||||
fill: str((isDark?gray2().lighterBy(30):gray2().darkerBy(30)).alphaTo(0.2)),
|
||||
nameColor: str(isDark?gray2().lighterBy(40):gray2().darkerBy(40)),
|
||||
}
|
||||
const scene = api.getSceneElements();
|
||||
scene.filter(el=>el.type==="frame").forEach((e:ExcalidrawFrameElement)=>{
|
||||
const f = cloneElement(e);
|
||||
addAppendUpdateCustomData(f,{frameColor});
|
||||
if(
|
||||
e.customData && e.customData.frameColor &&
|
||||
e.customData.frameColor.stroke === frameColor.stroke &&
|
||||
e.customData.frameColor.fill === frameColor.fill &&
|
||||
e.customData.frameColor.nameColor === frameColor.nameColor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
mutateElement(e,{customData: f.customData});
|
||||
});
|
||||
|
||||
view.updateScene({
|
||||
appState:{
|
||||
frameColor,
|
||||
dynamicStyle: styleObject
|
||||
}
|
||||
});
|
||||
});
|
||||
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
|
||||
if(toolspanel) {
|
||||
let toolsStyle = toolspanel.getAttribute("style");
|
||||
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
|
||||
toolspanel.setAttribute("style",toolsStyle+style);
|
||||
toolspanel.setAttribute("style",toolsStyle+styleString);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
import { MAX_IMAGE_SIZE, IMAGE_TYPES } from "src/constants";
|
||||
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
|
||||
import { TFile } from "obsidian";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
|
||||
export const insertImageToView = async (
|
||||
ea: ExcalidrawAutomate,
|
||||
@@ -33,7 +34,7 @@ export const insertEmbeddableToView = async (
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
if(file && IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) {
|
||||
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
|
||||
return await insertImageToView(ea, position, file);
|
||||
} else {
|
||||
const id = ea.addEmbeddable(
|
||||
@@ -47,4 +48,17 @@ export const insertEmbeddableToView = async (
|
||||
await ea.addElementsToView(false, true, true);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
export const getLinkTextFromLink = (text: string): string => {
|
||||
if (!text) return;
|
||||
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
const parts = REGEX_LINK.getRes(text).next();
|
||||
if (!parts.value) return;
|
||||
|
||||
const linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
||||
if (linktext.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
return linktext;
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { URLFETCHTIMEOUT } from "src/constants";
|
||||
import { MimeType } from "src/EmbeddedFileLoader";
|
||||
import { DEVICE, URLFETCHTIMEOUT } from "src/constants/constants";
|
||||
import { IMAGE_MIME_TYPES, MimeType } from "src/EmbeddedFileLoader";
|
||||
import { ExcalidrawSettings } from "src/settings";
|
||||
import { errorlog, getDataURL } from "./Utils";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
* @param filepath
|
||||
*/
|
||||
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;
|
||||
|
||||
export function splitFolderAndFilename(filepath: string): {
|
||||
folderpath: string;
|
||||
filename: string;
|
||||
basename: string;
|
||||
extension: string;
|
||||
} {
|
||||
const lastIndex = filepath.lastIndexOf("/");
|
||||
const filename = lastIndex == -1 ? filepath : filepath.substring(lastIndex + 1);
|
||||
@@ -21,6 +24,7 @@ export function splitFolderAndFilename(filepath: string): {
|
||||
folderpath: normalizePath(filepath.substring(0, lastIndex)),
|
||||
filename,
|
||||
basename: filename.replace(/\.[^/.]+$/, ""),
|
||||
extension: filename.substring(filename.lastIndexOf(".") + 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,7 +138,7 @@ export function getEmbedFilename(
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
export async function checkAndCreateFolder(folderpath: string) {
|
||||
export async function checkAndCreateFolder(folderpath: string):Promise<TFolder> {
|
||||
const vault = app.vault;
|
||||
folderpath = normalizePath(folderpath);
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/658
|
||||
@@ -146,7 +150,7 @@ export async function checkAndCreateFolder(folderpath: string) {
|
||||
if (folder && folder instanceof TFile) {
|
||||
new Notice(`The folder cannot be created because it already exists as a file: ${folderpath}.`)
|
||||
}
|
||||
await vault.createFolder(folderpath);
|
||||
return await vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
export const getURLImageExtension = (url: string):string => {
|
||||
@@ -155,15 +159,10 @@ export const getURLImageExtension = (url: string):string => {
|
||||
}
|
||||
|
||||
export const getMimeType = (extension: string):MimeType => {
|
||||
if(IMAGE_MIME_TYPES.hasOwnProperty(extension)) {
|
||||
return IMAGE_MIME_TYPES[extension as ImageExtension];
|
||||
};
|
||||
switch (extension) {
|
||||
case "png": return "image/png";
|
||||
case "jpeg": return "image/jpeg";
|
||||
case "jpg": return "image/jpeg";
|
||||
case "gif": return "image/gif";
|
||||
case "webp": return "image/webp";
|
||||
case "bmp": return "image/bmp";
|
||||
case "ico": return "image/x-icon";
|
||||
case "svg": return "image/svg+xml";
|
||||
case "md": return "image/svg+xml";
|
||||
default: return "application/octet-stream";
|
||||
}
|
||||
@@ -172,14 +171,18 @@ export const getMimeType = (extension: string):MimeType => {
|
||||
// using fetch API
|
||||
const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT): Promise<RequestUrlResponse> => {
|
||||
try {
|
||||
const timeoutPromise = new Promise<Response>((resolve) =>
|
||||
setTimeout(() => resolve(null), timeout)
|
||||
);
|
||||
|
||||
const response = await Promise.race([
|
||||
fetch(url),
|
||||
new Promise<Response>((resolve) => setTimeout(() => resolve(null), timeout))
|
||||
fetch(url, { mode: 'no-cors' }), //cors error cannot be caught
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
if (!response) {
|
||||
new Notice(`URL did not load within the timeout period of ${timeout}ms.\n\nTry force-saving again in a few seconds.\n\n${url}`,8000);
|
||||
throw new Error(`URL did not load within the timeout period of ${timeout}ms`);
|
||||
errorlog({ where: getFileFromURL, message: `URL did not load within the timeout period of ${timeout}ms.\n\nTry force-saving again in a few seconds.\n\n${url}`, url: url });
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -192,8 +195,8 @@ const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number =
|
||||
text: null,
|
||||
};
|
||||
} catch (e) {
|
||||
errorlog({ where: getFileFromURL, message: e.message, url: url });
|
||||
return undefined;
|
||||
//errorlog({ where: getFileFromURL, message: e.message, url: url });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,19 +204,23 @@ const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number =
|
||||
// https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FJSG%2FfTMP6WGQRC.png?alt=media&token=6d2993b4-e629-46b6-98d1-133af7448c49
|
||||
const getFileFromURLFallback = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT):Promise<RequestUrlResponse> => {
|
||||
try {
|
||||
const timeoutPromise = new Promise<RequestUrlResponse | null>((resolve) =>
|
||||
setTimeout(() => resolve(null), timeout)
|
||||
);
|
||||
|
||||
return await Promise.race([
|
||||
(async () => new Promise<RequestUrlResponse>((resolve) => setTimeout(()=>resolve(null), timeout)))(),
|
||||
requestUrl({url: url, method: "get", contentType: mimeType, throw: false })
|
||||
timeoutPromise,
|
||||
requestUrl({url: url, throw: false }), //if method: "get" is added it won't load images on Android, contentType: mimeType,
|
||||
])
|
||||
} catch (e) {
|
||||
errorlog({where: getFileFromURL, message: `URL did not load within timeout period of ${timeout}ms`, url: url});
|
||||
return undefined;
|
||||
errorlog({where: getFileFromURLFallback, message: `URL did not load within timeout period of ${timeout}ms`, url: url});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getDataURLFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT): Promise<DataURL> => {
|
||||
let response = await getFileFromURL(url, mimeType, timeout);
|
||||
if(response && response.status !== 200) {
|
||||
if(!response || response?.status !== 200) {
|
||||
response = await getFileFromURLFallback(url, mimeType, timeout);
|
||||
}
|
||||
return response && response.status === 200
|
||||
@@ -272,9 +279,9 @@ export const getDataURLFromURL = async (
|
||||
export const blobToBase64 = async (blob: Blob): Promise<string> => {
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const bytes = new Uint8Array(arrayBuffer)
|
||||
var binary = '';
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
let binary = '';
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
@@ -285,4 +292,82 @@ export const getPDFDoc = async (f: TFile): Promise<any> => {
|
||||
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
|
||||
//@ts-ignore
|
||||
return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
|
||||
}
|
||||
|
||||
export const readLocalFile = async (filePath:string): Promise<string> => {
|
||||
if (!DEVICE.isDesktop) return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
//@ts-ignore
|
||||
app.vault.adapter.fs.readFile(filePath, 'utf8', (err:any, data:any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const readLocalFileBinary = async (filePath:string): Promise<ArrayBuffer> => {
|
||||
if (!DEVICE.isDesktop) return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const path = decodeURI(filePath);
|
||||
//@ts-ignore
|
||||
app.vault.adapter.fs.readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||||
resolve(arrayBuffer);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const getPathWithoutExtension = (f:TFile): string => {
|
||||
if(!f) return null;
|
||||
return f.path.substring(0, f.path.lastIndexOf("."));
|
||||
}
|
||||
|
||||
const VAULT_BASE_URL = DEVICE.isDesktop
|
||||
? app.vault.adapter.url.pathToFileURL(app.vault.adapter.basePath).toString()
|
||||
: "";
|
||||
export const getInternalLinkOrFileURLLink = (
|
||||
path: string, plugin:ExcalidrawPlugin, alias?: string, sourceFile?: TFile
|
||||
):{link: string, isInternal: boolean, file?: TFile, url?: string} => {
|
||||
if(!DEVICE.isDesktop) {
|
||||
//I've not tested this... don't even know if external drag and drop works on mobile
|
||||
//Added this for safety
|
||||
return {link: `[${alias??""}](${path})`, isInternal: false, url: path};
|
||||
}
|
||||
const vault = plugin.app.vault;
|
||||
const fileURLString = vault.adapter.url.pathToFileURL(path).toString();
|
||||
if (fileURLString.startsWith(VAULT_BASE_URL)) {
|
||||
const internalPath = normalizePath(fileURLString.substring(VAULT_BASE_URL.length));
|
||||
const file = vault.getAbstractFileByPath(internalPath);
|
||||
if(file && file instanceof TFile) {
|
||||
const link = plugin.app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
sourceFile?.path,
|
||||
true,
|
||||
);
|
||||
return {link: getLink(plugin, { embed: false, path: link, alias}), isInternal: true, file};
|
||||
};
|
||||
}
|
||||
return {link: `[${alias??""}](${fileURLString})`, isInternal: false, url: fileURLString};
|
||||
}
|
||||
|
||||
/**
|
||||
* get markdown or wiki link
|
||||
* @param plugin
|
||||
* @param param1: { embed = true, path, alias }
|
||||
* @returns
|
||||
*/
|
||||
export const getLink = (
|
||||
plugin: ExcalidrawPlugin,
|
||||
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string }
|
||||
):string => {
|
||||
return plugin.settings.embedWikiLink
|
||||
? `${embed ? "!" : ""}[[${path}${alias ? `|${alias}` : ""}]]`
|
||||
: `${embed ? "!" : ""}[${alias ?? ""}](${encodeURI(path)})`
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import ExcalidrawView, { TextMode } from "src/ExcalidrawView";
|
||||
import { rotatedDimensions } from "./Utils";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import { App, Notice, TFile } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { convertSVGStringToElement } from "./Utils";
|
||||
import { PreviewImageType } from "./UtilTypes";
|
||||
import { FILENAMEPARTS, PreviewImageType } from "./UtilTypes";
|
||||
|
||||
//@ts-ignore
|
||||
const DB_NAME = "Excalidraw " + app.appId;
|
||||
@@ -19,10 +19,11 @@ export type ImageKey = {
|
||||
isDark: boolean;
|
||||
previewImageType: PreviewImageType;
|
||||
scale: number;
|
||||
};
|
||||
} & FILENAMEPARTS;
|
||||
|
||||
const getKey = (key: ImageKey): string =>
|
||||
`${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark ? 1 : 0}#${
|
||||
`${key.filepath}#${key.blockref??""}#${key.sectionref??""}#${key.isDark ? 1 : 0}#${
|
||||
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasSectionref}#${
|
||||
key.previewImageType === PreviewImageType.SVGIMG
|
||||
? 1
|
||||
: key.previewImageType === PreviewImageType.PNG
|
||||
@@ -36,7 +37,8 @@ class ImageCache {
|
||||
private backupStoreName: string;
|
||||
private db: IDBDatabase | null;
|
||||
private isInitializing: boolean;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private app: App;
|
||||
public initializationNotice: boolean = false;
|
||||
private obsidanURLCache = new Map<string, string>();
|
||||
|
||||
@@ -47,10 +49,11 @@ class ImageCache {
|
||||
this.db = null;
|
||||
this.isInitializing = false;
|
||||
this.plugin = null;
|
||||
app.workspace.onLayoutReady(() => this.initializeDB());
|
||||
}
|
||||
|
||||
private async initializeDB(): Promise<void> {
|
||||
public async initializeDB(plugin: ExcalidrawPlugin): Promise<void> {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
if (this.isInitializing || this.db !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -124,8 +127,8 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
await this.purgeInvalidCacheFiles();
|
||||
await this.purgeInvalidBackupFiles();
|
||||
setTimeout(async ()=>this.purgeInvalidCacheFiles(), 60000);
|
||||
setTimeout(async ()=>this.purgeInvalidBackupFiles(), 120000);
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
if(this.initializationNotice) {
|
||||
@@ -137,41 +140,49 @@ class ImageCache {
|
||||
}
|
||||
|
||||
private async purgeInvalidCacheFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
|
||||
const request = store.openCursor();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const transaction = this.db!.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const files = this.app.vault.getFiles();
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
const request = store.openCursor();
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
if(cursor) {
|
||||
const key = cursor.key as string;
|
||||
const isLegacyKey = key.replaceAll(/[^#]/g,"").length < 9; // introduced hasGroupref, etc. in 1.9.28
|
||||
const filepath = key.split("#")[0];
|
||||
const fileExists = files.some((f: TFile) => f.path === filepath);
|
||||
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
|
||||
if (!file || (file && file.stat.mtime > cursor.value.mtime) || !cursor.value.blob) {
|
||||
if (isLegacyKey || !file || (file && file.stat.mtime > cursor.value.mtime) || (!cursor.value.blob && !cursor.value.svg)) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
new Promise<void>((innerResolve, innerReject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
deleteRequest.onsuccess = () => resolve();
|
||||
deleteRequest.onerror = () =>
|
||||
reject(new Error(`Failed to delete file with key: ${key}`));
|
||||
deleteRequest.onsuccess = () => innerResolve();
|
||||
deleteRequest.onerror = (ev: Event) => {
|
||||
const error = deleteRequest.error;
|
||||
const errorMsg = `Failed to delete file with key: ${key}. Error: ${error.message}`
|
||||
innerReject(new Error(errorMsg));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
Promise.all(deletePromises)
|
||||
.then(() => resolve())
|
||||
.then(() => {
|
||||
transaction.commit();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to purge invalid files from IndexedDB."));
|
||||
const error = request.error;
|
||||
console.log(error);
|
||||
const errorMsg = `Failed to purge invalid files from IndexedDB. Error: ${error.message}`
|
||||
reject(new Error(errorMsg));
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -179,12 +190,10 @@ class ImageCache {
|
||||
private async purgeInvalidBackupFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.backupStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.backupStoreName);
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
const files = this.app.vault.getFiles();
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
|
||||
const request = store.openCursor();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
@@ -203,13 +212,19 @@ class ImageCache {
|
||||
cursor.continue();
|
||||
} else {
|
||||
Promise.all(deletePromises)
|
||||
.then(() => resolve())
|
||||
.then(() => {
|
||||
transaction.commit();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to purge invalid backup files from IndexedDB."));
|
||||
const error = request.error;
|
||||
const errorMsg = `Failed to purge invalid backup files from IndexedDB. Error: ${error.message}`
|
||||
console.log(error);
|
||||
reject(new Error(errorMsg));
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -255,27 +270,42 @@ class ImageCache {
|
||||
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
|
||||
}
|
||||
|
||||
private fullyInitialized = false;
|
||||
|
||||
public async getImageFromCache(key_: ImageKey): Promise<string | SVGSVGElement | undefined> {
|
||||
if (!this.isReady()) {
|
||||
return null; // Database not initialized yet
|
||||
}
|
||||
|
||||
const key = getKey(key_);
|
||||
const cachedData = await this.getCacheData(key);
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return undefined;
|
||||
if (cachedData && cachedData.mtime === file.stat.mtime) {
|
||||
if(cachedData.svg) {
|
||||
return convertSVGStringToElement(cachedData.svg);
|
||||
|
||||
try {
|
||||
const cachedData = this.fullyInitialized
|
||||
? await this.getCacheData(key)
|
||||
: await Promise.race([
|
||||
this.getCacheData(key),
|
||||
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
|
||||
]);
|
||||
this.fullyInitialized = true;
|
||||
if(!cachedData) return undefined;
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return undefined;
|
||||
if (cachedData && cachedData.mtime === file.stat.mtime) {
|
||||
if(cachedData.svg) {
|
||||
return convertSVGStringToElement(cachedData.svg);
|
||||
}
|
||||
if(this.obsidanURLCache.has(key)) {
|
||||
return this.obsidanURLCache.get(key);
|
||||
}
|
||||
const obsidianURL = URL.createObjectURL(cachedData.blob);
|
||||
this.obsidanURLCache.set(key, obsidianURL);
|
||||
return obsidianURL;
|
||||
}
|
||||
if(this.obsidanURLCache.has(key)) {
|
||||
return this.obsidanURLCache.get(key);
|
||||
}
|
||||
const obsidianURL = URL.createObjectURL(cachedData.blob);
|
||||
this.obsidanURLCache.set(key, obsidianURL);
|
||||
return obsidianURL;
|
||||
return undefined;
|
||||
} catch(e) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async getBAKFromCache(filepath: string): Promise<BackupData | null> {
|
||||
@@ -291,7 +321,7 @@ class ImageCache {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return;
|
||||
|
||||
|
||||
|
||||
14
src/utils/MermaidUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { requireApiVersion } from "obsidian";
|
||||
|
||||
export const getMermaidImageElements = (elements: ExcalidrawElement[]):ExcalidrawImageElement[] =>
|
||||
elements
|
||||
? elements.filter((element) =>
|
||||
element.type === "image" && element.customData?.mermaidText
|
||||
) as ExcalidrawImageElement[]
|
||||
: [];
|
||||
|
||||
export const getMermaidText = (element: ExcalidrawElement):string =>
|
||||
element.customData?.mermaidText;
|
||||
|
||||
export const shouldRenderMermaid = ():boolean => requireApiVersion("1.4.14");
|
||||
@@ -1,18 +1,88 @@
|
||||
import { DEVICE, isDarwin } from "src/constants";
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import { ExcalidrawSettings } from "src/settings";
|
||||
export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: boolean};
|
||||
export type KeyEvent = PointerEvent | MouseEvent | KeyboardEvent | React.DragEvent | React.PointerEvent | React.MouseEvent | ModifierKeys;
|
||||
export type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
export type ExternalDragAction = "insert-link"|"image-url"|"image-import"|"embeddable";
|
||||
export type WebBrowserDragAction = "link"|"image-url"|"image-import"|"embeddable";
|
||||
export type LocalFileDragAction = "link"|"image-url"|"image-import"|"embeddable";
|
||||
export type InternalDragAction = "link"|"image"|"image-fullsize"|"embeddable";
|
||||
export type ModifierSetType = "WebBrowserDragAction" | "LocalFileDragAction" | "InternalDragAction" | "LinkClickAction";
|
||||
|
||||
type ModifierKey = {
|
||||
shift: boolean;
|
||||
ctrl_cmd: boolean;
|
||||
alt_opt: boolean;
|
||||
meta_ctrl: boolean;
|
||||
result: WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget;
|
||||
};
|
||||
|
||||
export type ModifierKeySet = {
|
||||
defaultAction: WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget;
|
||||
rules: ModifierKey[];
|
||||
};
|
||||
|
||||
export type ModifierKeyTooltipMessages = Partial<{
|
||||
[modifierSetType in ModifierSetType]: Partial<{
|
||||
[action in WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget]: string;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export const modifierKeyTooltipMessages = ():ModifierKeyTooltipMessages => {
|
||||
return {
|
||||
WebBrowserDragAction: {
|
||||
"image-import": "Import Image to Vault",
|
||||
"image-url": `Insert Image or YouTube Thumbnail with URL`,
|
||||
"link": "Insert Link",
|
||||
"embeddable": "Insert Interactive-Frame",
|
||||
// Add more messages for WebBrowserDragAction as needed
|
||||
},
|
||||
LocalFileDragAction: {
|
||||
"image-import": "Insert Image: import external or reuse existing if path in Vault",
|
||||
"image-url": `Insert Image: with local URI or internal-link if from Vault`,
|
||||
"link": "Insert Link: local URI or internal-link if from Vault",
|
||||
"embeddable": "Insert Interactive-Frame: local URI or internal-link if from Vault",
|
||||
},
|
||||
InternalDragAction: {
|
||||
"image": "Insert Image",
|
||||
"image-fullsize": "Insert Image @100%",
|
||||
"link": `Insert Link`,
|
||||
"embeddable": "Insert Interactive-Frame",
|
||||
},
|
||||
LinkClickAction: {
|
||||
"active-pane": "Open in current active window",
|
||||
"new-pane": "Open in a new adjacent window",
|
||||
"popout-window": "Open in a popout window",
|
||||
"new-tab": "Open in a new tab",
|
||||
"md-properties": "Show the Markdown image-properties dialog (only relevant if you have embedded a markdown document as an image)",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const processModifiers = (ev: KeyEvent, modifierType: ModifierSetType): WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget => {
|
||||
const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings;
|
||||
const keySet = ((DEVICE.isMacOS || DEVICE.isIOS) ? settings.modifierKeyConfig.Mac : settings.modifierKeyConfig.Win)[modifierType];
|
||||
for (const rule of keySet.rules) {
|
||||
const { shift, ctrl_cmd, alt_opt, meta_ctrl, result } = rule;
|
||||
if (
|
||||
(isSHIFT(ev) === shift) &&
|
||||
(isWinCTRLorMacCMD(ev) === ctrl_cmd) &&
|
||||
(isWinALTorMacOPT(ev) === alt_opt) &&
|
||||
(isWinMETAorMacCTRL(ev) === meta_ctrl)
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return keySet.defaultAction;
|
||||
}
|
||||
|
||||
export const labelCTRL = () => DEVICE.isIOS || DEVICE.isMacOS ? "CMD" : "CTRL";
|
||||
export const labelALT = () => DEVICE.isIOS || DEVICE.isMacOS ? "OPT" : "ALT";
|
||||
export const labelMETA = () => DEVICE.isIOS || DEVICE.isMacOS ? "CTRL" : (DEVICE.isWindows ? "WIN" : "META");
|
||||
export const labelSHIFT = () => "SHIFT";
|
||||
|
||||
export const isCTRL = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.metaKey : e.ctrlKey;
|
||||
export const isALT = (e:KeyEvent) => e.altKey;
|
||||
export const isMETA = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.ctrlKey : e.metaKey;
|
||||
export const isWinCTRLorMacCMD = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.metaKey : e.ctrlKey;
|
||||
export const isWinALTorMacOPT = (e:KeyEvent) => e.altKey;
|
||||
export const isWinMETAorMacCTRL = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.ctrlKey : e.metaKey;
|
||||
export const isSHIFT = (e:KeyEvent) => e.shiftKey;
|
||||
|
||||
export const setCTRL = (e:ModifierKeys, value: boolean): ModifierKeys => {
|
||||
@@ -38,39 +108,38 @@ export const setSHIFT = (e:ModifierKeys, value: boolean): ModifierKeys => {
|
||||
return e;
|
||||
}
|
||||
|
||||
export const mdPropModifier = (ev: KeyEvent): boolean => !isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && isMETA(ev);
|
||||
export const scaleToFullsizeModifier = (ev: KeyEvent) =>
|
||||
( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) ||
|
||||
(!isSHIFT(ev) && isCTRL(ev) && isALT(ev) && !isMETA(ev));
|
||||
|
||||
export const linkClickModifierType = (ev: KeyEvent):PaneTarget => {
|
||||
if(isCTRL(ev) && !isALT(ev) && isSHIFT(ev) && !isMETA(ev)) return "active-pane";
|
||||
if(isCTRL(ev) && !isALT(ev) && !isSHIFT(ev) && !isMETA(ev)) return "new-tab";
|
||||
if(isCTRL(ev) && isALT(ev) && !isSHIFT(ev) && !isMETA(ev)) return "new-pane";
|
||||
if(DEVICE.isDesktop && isCTRL(ev) && isALT(ev) && isSHIFT(ev) && !isMETA(ev) ) return "popout-window";
|
||||
if(isCTRL(ev) && isALT(ev) && isSHIFT(ev) && !isMETA(ev)) return "new-tab";
|
||||
if(mdPropModifier(ev)) return "md-properties";
|
||||
return "active-pane";
|
||||
export const mdPropModifier = (ev: KeyEvent): boolean => !isSHIFT(ev) && isWinCTRLorMacCMD(ev) && !isWinALTorMacOPT(ev) && isWinMETAorMacCTRL(ev);
|
||||
export const scaleToFullsizeModifier = (ev: KeyEvent) => {
|
||||
const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings;
|
||||
const keySet = ((DEVICE.isMacOS || DEVICE.isIOS) ? settings.modifierKeyConfig.Mac : settings.modifierKeyConfig.Win )["InternalDragAction"];
|
||||
const rule = keySet.rules.find(r => r.result === "image-fullsize");
|
||||
if(!rule) return false;
|
||||
const { shift, ctrl_cmd, alt_opt, meta_ctrl, result } = rule;
|
||||
return (
|
||||
(isSHIFT(ev) === shift) &&
|
||||
(isWinCTRLorMacCMD(ev) === ctrl_cmd) &&
|
||||
(isWinALTorMacOPT(ev) === alt_opt) &&
|
||||
(isWinMETAorMacCTRL(ev) === meta_ctrl)
|
||||
);
|
||||
}
|
||||
|
||||
export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => {
|
||||
if(DEVICE.isWindows && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "embeddable";
|
||||
if(DEVICE.isMacOS && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "embeddable";
|
||||
if(DEVICE.isWindows && !isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "insert-link";
|
||||
if(DEVICE.isMacOS && isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "insert-link";
|
||||
if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image-import";
|
||||
if(DEVICE.isWindows && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "image-import";
|
||||
return "image-url";
|
||||
export const linkClickModifierType = (ev: KeyEvent):PaneTarget => {
|
||||
const action = processModifiers(ev, "LinkClickAction") as PaneTarget;
|
||||
if(!DEVICE.isDesktop && action === "popout-window") return "active-pane";
|
||||
return action;
|
||||
}
|
||||
|
||||
export const webbrowserDragModifierType = (ev: KeyEvent):WebBrowserDragAction => {
|
||||
return processModifiers(ev, "WebBrowserDragAction") as WebBrowserDragAction;
|
||||
}
|
||||
|
||||
export const localFileDragModifierType = (ev: KeyEvent):LocalFileDragAction => {
|
||||
return processModifiers(ev, "LocalFileDragAction") as LocalFileDragAction;
|
||||
}
|
||||
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468
|
||||
export const internalDragModifierType = (ev: KeyEvent):InternalDragAction => {
|
||||
if( !(DEVICE.isIOS || DEVICE.isMacOS) && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "embeddable";
|
||||
if( (DEVICE.isIOS || DEVICE.isMacOS) && !isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "embeddable";
|
||||
if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image";
|
||||
if(!isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image";
|
||||
if(scaleToFullsizeModifier(ev)) return "image-fullsize";
|
||||
return "link";
|
||||
return processModifiers(ev, "InternalDragAction") as InternalDragAction;
|
||||
}
|
||||
|
||||
export const emulateCTRLClickForLinks = (e:KeyEvent) => {
|
||||
@@ -85,27 +154,23 @@ export const emulateCTRLClickForLinks = (e:KeyEvent) => {
|
||||
export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => {
|
||||
const ev = {shiftKey: false, ctrlKey: false, metaKey: false, altKey: false};
|
||||
if(!action) return ev;
|
||||
switch(action) {
|
||||
case "active-pane":
|
||||
setCTRL(ev, true);
|
||||
setSHIFT(ev, true);
|
||||
break;
|
||||
case "new-pane":
|
||||
setCTRL(ev, true);
|
||||
setALT(ev, true);
|
||||
break;
|
||||
case "popout-window":
|
||||
setCTRL(ev, true);
|
||||
setALT(ev, true);
|
||||
setSHIFT(ev, true);
|
||||
break;
|
||||
case "new-tab":
|
||||
setCTRL(ev, true);
|
||||
break;
|
||||
case "md-properties":
|
||||
setCTRL(ev, true);
|
||||
setMETA(ev, true);
|
||||
break;
|
||||
const platform = DEVICE.isMacOS || DEVICE.isIOS ? "Mac" : "Win";
|
||||
const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings;
|
||||
const modifierKeyConfig = settings.modifierKeyConfig;
|
||||
|
||||
const config = modifierKeyConfig[platform]?.LinkClickAction;
|
||||
|
||||
if (config) {
|
||||
const rule = config.rules.find(rule => rule.result === action);
|
||||
if (rule) {
|
||||
setCTRL(ev, rule.ctrl_cmd);
|
||||
setALT(ev, rule.alt_opt);
|
||||
setMETA(ev, rule.meta_ctrl);
|
||||
setSHIFT(ev, rule.shift);
|
||||
} else {
|
||||
const defaultAction = config.defaultAction as PaneTarget;
|
||||
return emulateKeysForLinkClick(defaultAction);
|
||||
}
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import {
|
||||
App,
|
||||
normalizePath, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
normalizePath, parseFrontMatterEntry, TFile, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
} from "obsidian";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
|
||||
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
|
||||
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants";
|
||||
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
|
||||
|
||||
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
|
||||
let parent = element.parentElement;
|
||||
while (
|
||||
parent &&
|
||||
!(parent instanceof window.HTMLBodyElement) &&
|
||||
!parent.classList.contains(cssClass)
|
||||
!parent.classList.contains(cssClass) &&
|
||||
!(parent instanceof window.HTMLBodyElement)
|
||||
) {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
@@ -233,3 +233,25 @@ export const obsidianPDFQuoteWithRef = (text:string):{quote: string, link: strin
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const extractSVGPNGFileName = (text:string) => {
|
||||
const regex = /\[\[([^\]|#^]+\.(?:svg|png))(?:[^\]]+)?\]\]|\[[^\]]+\]\(([^\)]+\.(?:svg|png))\)/;
|
||||
const match = text.match(regex);
|
||||
return match ? (match[1] || match[2]) : null;
|
||||
}
|
||||
|
||||
export const getFileCSSClasses = (
|
||||
file: TFile,
|
||||
): string[] => {
|
||||
if (file) {
|
||||
const plugin = window?.ExcalidrawAutomate?.plugin;
|
||||
if(!plugin) return [];
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if(!fileCache?.frontmatter) return [];
|
||||
const x = parseFrontMatterEntry(fileCache.frontmatter, "cssclasses");
|
||||
if (Array.isArray(x)) return x
|
||||
if (typeof x === "string") return Array.from(new Set(x.split(/[, ]+/).filter(Boolean)));
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export class StylesManager {
|
||||
private styleDark: string;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
plugin.app.workspace.onLayoutReady(async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
//import Excalidraw from "@zsviczian/excalidraw";
|
||||
import {
|
||||
App,
|
||||
Notice,
|
||||
@@ -7,10 +6,13 @@ import {
|
||||
TFile,
|
||||
} from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/types";
|
||||
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import {
|
||||
ASSISTANT_FONT,
|
||||
CASCADIA_FONT,
|
||||
VIRGIL_FONT,
|
||||
} from "src/constants/constFonts";
|
||||
import {
|
||||
FRONTMATTER_KEY_EXPORT_DARK,
|
||||
FRONTMATTER_KEY_EXPORT_TRANSPARENT,
|
||||
FRONTMATTER_KEY_EXPORT_SVGPADDING,
|
||||
@@ -19,19 +21,21 @@ import {
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
IMAGE_TYPES
|
||||
} from "../constants";
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { compressToBase64, decompressFromBase64 } from "lz-string";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
|
||||
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
|
||||
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
|
||||
import { FILENAMEPARTS } from "./UtilTypes";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
|
||||
import { cleanBlockRef, cleanSectionHeading } from "./ObsidianUtils";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
|
||||
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
|
||||
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
@@ -211,9 +215,8 @@ export const getFontDataURL = async (
|
||||
: "font/truetype";
|
||||
fontName = name ?? f.basename;
|
||||
dataURL = await getDataURL(ab, mimeType);
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}") format("${
|
||||
f.extension === "ttf" ? "truetype" : f.extension
|
||||
}");}`;
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
|
||||
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
|
||||
const split = fontDef.split(";base64,", 2);
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;
|
||||
}
|
||||
@@ -258,6 +261,7 @@ export const getSVG = async (
|
||||
scene: any,
|
||||
exportSettings: ExportSettings,
|
||||
padding: number,
|
||||
srcFile: TFile|null, //if set, will replace markdown links with obsidian links
|
||||
): Promise<SVGSVGElement> => {
|
||||
let elements:ExcalidrawElement[] = scene.elements;
|
||||
if(elements.some(el => el.type === "embeddable")) {
|
||||
@@ -268,8 +272,13 @@ export const getSVG = async (
|
||||
}
|
||||
|
||||
try {
|
||||
return await exportToSvg({
|
||||
elements,
|
||||
const svg = await exportToSvg({
|
||||
elements: srcFile
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements,
|
||||
hostFile: srcFile,
|
||||
})
|
||||
: elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
@@ -279,7 +288,17 @@ export const getSVG = async (
|
||||
},
|
||||
files: scene.files,
|
||||
exportPadding: padding,
|
||||
exportingFrame: null,
|
||||
renderEmbeddables: true,
|
||||
});
|
||||
if(svg) {
|
||||
svg.addClass("excalidraw-svg");
|
||||
if(srcFile instanceof TFile) {
|
||||
const cssClasses = getFileCSSClasses(srcFile);
|
||||
cssClasses.forEach((cssClass) => svg.addClass(cssClass));
|
||||
}
|
||||
}
|
||||
return svg;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
@@ -352,18 +371,22 @@ export const getQuickImagePreview = async (
|
||||
export const embedFontsInSVG = (
|
||||
svg: SVGSVGElement,
|
||||
plugin: ExcalidrawPlugin,
|
||||
localOnly: boolean = false,
|
||||
): SVGSVGElement => {
|
||||
//replace font references with base64 fonts
|
||||
const includesVirgil =
|
||||
//replace font references with base64 fonts)
|
||||
const includesVirgil = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Virgil']") != null;
|
||||
const includesCascadia =
|
||||
const includesCascadia = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Cascadia']") != null;
|
||||
const includesAssistant = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Assistant']") != null;
|
||||
const includesLocalFont =
|
||||
svg.querySelector("text[font-family^='LocalFont']") != null;
|
||||
const defs = svg.querySelector("defs");
|
||||
if (defs && (includesCascadia || includesVirgil || includesLocalFont)) {
|
||||
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
|
||||
defs.innerHTML = `<style>${includesVirgil ? VIRGIL_FONT : ""}${
|
||||
includesCascadia ? CASCADIA_FONT : ""
|
||||
includesCascadia ? CASCADIA_FONT : ""}${
|
||||
includesAssistant ? ASSISTANT_FONT : ""
|
||||
}${includesLocalFont ? plugin.fourthFontDef : ""}</style>`;
|
||||
}
|
||||
return svg;
|
||||
@@ -490,11 +513,11 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
|
||||
};
|
||||
|
||||
export const compress = (data: string): string => {
|
||||
return compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
|
||||
return LZString.compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
|
||||
};
|
||||
|
||||
export const decompress = (data: string): string => {
|
||||
return decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
|
||||
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
|
||||
};
|
||||
|
||||
export const hasExportTheme = (
|
||||
@@ -707,18 +730,18 @@ export const updateFrontmatterInString = (data:string, keyValuePairs: [string,st
|
||||
return data;
|
||||
}
|
||||
|
||||
const isHyperlink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
|
||||
const isHyperLink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
|
||||
|
||||
export const isContainer = (el: ExcalidrawElement) => el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text");
|
||||
|
||||
export const hyperlinkIsImage = (data: string):boolean => {
|
||||
if(!isHyperlink(data)) false;
|
||||
if(!isHyperLink(data)) false;
|
||||
const corelink = data.split("?")[0];
|
||||
return IMAGE_TYPES.contains(corelink.substring(corelink.lastIndexOf(".")+1));
|
||||
}
|
||||
|
||||
export const hyperlinkIsYouTubeLink = (link:string): boolean =>
|
||||
isHyperlink(link) &&
|
||||
isHyperLink(link) &&
|
||||
(link.startsWith("https://youtu.be") || link.startsWith("https://www.youtube.com") || link.startsWith("https://youtube.com") || link.startsWith("https//www.youtu.be")) &&
|
||||
link.match(/(youtu.be\/|v=)([^?\/\&]*)/)!==null
|
||||
|
||||
@@ -764,4 +787,20 @@ export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
|
||||
return firstChild;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export const escapeRegExp = (str:string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
|
||||
export const addIframe = (containerEl: HTMLElement, link:string, startAt?: number, style:string = "settings") => {
|
||||
const wrapper = containerEl.createDiv({cls: `excalidraw-videoWrapper ${style}`})
|
||||
wrapper.createEl("iframe", {
|
||||
attr: {
|
||||
allowfullscreen: true,
|
||||
allow: "encrypted-media;picture-in-picture",
|
||||
frameborder: "0",
|
||||
title: "YouTube video player",
|
||||
src: "https://www.youtube.com/embed/" + link + (startAt ? "?start=" + startAt : ""),
|
||||
sandbox: "allow-forms allow-presentation allow-same-origin allow-scripts allow-modals",
|
||||
},
|
||||
});
|
||||
}
|
||||
62
src/utils/YoutTubeUtils.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const REG_YOUTUBE = /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|.*&t=|\?start=|.*&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||
export const isYouTube = (url: string): boolean => {
|
||||
return Boolean(
|
||||
url.match(REG_YOUTUBE)
|
||||
);
|
||||
}
|
||||
|
||||
export const getYouTubeStartAt = (url: string): string => {
|
||||
const ytLink = url.match(REG_YOUTUBE);
|
||||
if (ytLink?.[2]) {
|
||||
const time = ytLink[3] ? parseInt(ytLink[3]) : 0;
|
||||
const hours = Math.floor(time / 3600);
|
||||
const minutes = Math.floor((time - hours * 3600) / 60);
|
||||
const seconds = time - hours * 3600 - minutes * 60;
|
||||
if(hours === 0 && minutes === 0 && seconds === 0) return "";
|
||||
if(hours === 0 && minutes === 0) return `${String(seconds).padStart(2, '0')}`;
|
||||
if(hours === 0) return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const isValidYouTubeStart = (value: string): boolean => {
|
||||
if(/^[0-9]+$/.test(value)) return true; // Matches only numbers (seconds)
|
||||
if(/^[0-9]+:[0-9]+$/.test(value)) return true; // Matches only numbers (minutes and seconds)
|
||||
if(/^[0-9]+:[0-9]+:[0-9]+$/.test(value)) return true; // Matches only numbers (hours, minutes, and seconds
|
||||
};
|
||||
|
||||
export const updateYouTubeStartTime = (link: string, startTime: string): string => {
|
||||
const match = link.match(REG_YOUTUBE);
|
||||
if (match?.[2]) {
|
||||
const startTimeParam = startTime === ""
|
||||
? ``
|
||||
: `t=${timeStringToSeconds(startTime)}`;
|
||||
let updatedLink = link;
|
||||
if (match[3]) {
|
||||
// If start time already exists, update it
|
||||
updatedLink = link.replace(/([?&])t=[a-zA-Z0-9_-]+/, `$1${startTimeParam}`);
|
||||
updatedLink = updatedLink.replace(/([?&])start=[a-zA-Z0-9_-]+/, `$1${startTimeParam}`);
|
||||
} else {
|
||||
// If no start time exists, add it to the link
|
||||
updatedLink += (link.includes('?') ? '&' : '?') + startTimeParam;
|
||||
}
|
||||
return updatedLink;
|
||||
}
|
||||
return link;
|
||||
};
|
||||
|
||||
const timeStringToSeconds = (time: string): number => {
|
||||
const timeParts = time.split(':').map(Number);
|
||||
const totalParts = timeParts.length;
|
||||
|
||||
if (totalParts === 1) {
|
||||
return timeParts[0]; // Only seconds provided (ss)
|
||||
} else if (totalParts === 2) {
|
||||
return timeParts[0] * 60 + timeParts[1]; // Minutes and seconds provided (mm:ss)
|
||||
} else if (totalParts === 3) {
|
||||
return timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]; // Hours, minutes, and seconds provided (hh:mm:ss)
|
||||
}
|
||||
|
||||
return 0; // Invalid format, return 0 or handle accordingly
|
||||
};
|
||||
132
styles.css
@@ -7,6 +7,7 @@
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
background-color: white;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.context-menu-option__shortcut {
|
||||
@@ -183,15 +184,24 @@ li[data-testid] {
|
||||
}
|
||||
|
||||
.excalidraw-videoWrapper {
|
||||
max-width:600px
|
||||
max-width:600px;
|
||||
}
|
||||
.excalidraw-videoWrapper div {
|
||||
.excalidraw-videoWrapper.settings {
|
||||
max-width:340px;
|
||||
}
|
||||
|
||||
.excalidraw-videoWrapper div{
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
height: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.excalidraw-videoWrapper.settings iframe {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.excalidraw-videoWrapper iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -336,7 +346,7 @@ label.color-input-container > input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.excalidraw-settings input {
|
||||
.excalidraw-settings input:not([type="color"]) {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
@@ -362,10 +372,6 @@ div.excalidraw-draginfo {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.excalidraw .HelpDialog__key {
|
||||
background-color: var(--color-gray-80) !important;
|
||||
}
|
||||
|
||||
.excalidraw .embeddable-menu {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
@@ -414,4 +420,116 @@ div.excalidraw-draginfo {
|
||||
|
||||
.excalidraw-svg svg a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.excalidraw .Modal {
|
||||
background-color: initial;
|
||||
border: initial;
|
||||
max-width: initial;
|
||||
max-height: initial;
|
||||
width: initial;
|
||||
height: initial;
|
||||
}
|
||||
|
||||
summary.excalidraw-setting-h1 {
|
||||
font-variant: var(--h1-variant);
|
||||
letter-spacing: -0.015em;
|
||||
line-height: var(--h1-line-height);
|
||||
font-size: var(--h1-size);
|
||||
color: var(--h1-color);
|
||||
font-weight: var(--h1-weight);
|
||||
font-style: var(--h1-style);
|
||||
font-family: var(--h1-font);
|
||||
/*margin-block-start: var(--p-spacing);*/
|
||||
margin-block-end: var(--p-spacing);
|
||||
}
|
||||
|
||||
summary.excalidraw-setting-h3 {
|
||||
font-variant: var(--h3-variant);
|
||||
letter-spacing: -0.015em;
|
||||
line-height: var(--h3-line-height);
|
||||
font-size: var(--h3-size);
|
||||
color: var(--h3-color);
|
||||
font-weight: var(--h3-weight);
|
||||
font-style: var(--h3-style);
|
||||
font-family: var(--h3-font);
|
||||
margin-block-start: var(--p-spacing);
|
||||
margin-block-end: var(--p-spacing);
|
||||
}
|
||||
|
||||
summary.excalidraw-setting-h4 {
|
||||
font-variant: var(--h4-variant);
|
||||
letter-spacing: -0.015em;
|
||||
line-height: var(--h4-line-height);
|
||||
font-size: var(--h4-size);
|
||||
color: var(--h4-color);
|
||||
font-weight: var(--h4-weight);
|
||||
font-style: var(--h4-style);
|
||||
font-family: var(--h4-font);
|
||||
margin-block-start: var(--p-spacing);
|
||||
margin-block-end: var(--p-spacing);
|
||||
}
|
||||
|
||||
hr.excalidraw-setting-hr {
|
||||
margin: 1rem 0rem 0rem 0rem;
|
||||
}
|
||||
|
||||
.excalidraw-mdEmbed-hideFilename .mod-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .canvas-node:not(.is-editing).transparent {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
|
||||
border: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.excalidraw .canvas-node .ex-md-font-hand-drawn {
|
||||
--font-text: "Virgil";
|
||||
}
|
||||
|
||||
.excalidraw .canvas-node .ex-md-font-code {
|
||||
--font-text: "Cascadia";
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .workspace-leaf,
|
||||
.excalidraw__embeddable-container .workspace-leaf .view-content {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .workspace-leaf-content .view-content {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .workspace-leaf .view-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .workspace-leaf-content .image-container,
|
||||
.excalidraw__embeddable-container .workspace-leaf-content .audio-container,
|
||||
.excalidraw__embeddable-container .workspace-leaf-content .video-container {
|
||||
display: flex;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"sourceMap": true,
|
||||
"module": "es2015",
|
||||
"sourceMap": false,
|
||||
"module": "ES2015",
|
||||
"target": "es2017", //es2017 because script engine requires for async execution
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
@@ -17,7 +17,8 @@
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"inlineSourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||