Compare commits

...

9 Commits

Author SHA1 Message Date
zsviczian
ee7fc3eddd 2.7.0-beta-5 Cleaned up FileManager, ObserverManager and PackageManager carveout 2024-12-14 23:04:16 +01:00
zsviczian
639ccdf83e Package Manager 2024-12-14 15:38:48 +01:00
zsviczian
2b901c473b Moved observers to OberverManager 2024-12-14 15:04:07 +01:00
zsviczian
b419079734 refactoring: filemanager, types moved to types 2024-12-14 14:30:08 +01:00
zsviczian
5c4d37cce4 2.7.0-beta-4 2024-12-14 13:13:42 +01:00
zsviczian
7b5f701f8f Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2024-12-14 09:55:03 +01:00
zsviczian
0eca97bf18 fixed scene reload on embeddable edit causing edit mode to be interrupted. fixed LaTeX.ts race condition. 2024-12-14 09:54:58 +01:00
zsviczian
f620263fc6 Merge pull request #2155 from dmscode/master
Update zh-cn.ts to b8655cf
2024-12-14 07:20:47 +01:00
dmscode
4e299677bd Update zh-cn.ts to b8655cf 2024-12-14 07:40:38 +08:00
21 changed files with 882 additions and 698 deletions

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.0-beta-3",
"version": "2.7.0-beta-5",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -23,7 +23,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-20",
"@zsviczian/excalidraw": "0.17.6-21",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",

View File

@@ -676,7 +676,7 @@ export class EmbeddedFilesLoader {
}
if (!excalidrawData.getEquation(id).isLoaded) {
const latex = equation.latex;
const data = await tex2dataURL(latex);
const data = await tex2dataURL(latex, 4, this.plugin.app);
if (data) {
const fileData = {
mimeType: data.mimeType,

View File

@@ -1605,7 +1605,7 @@ export class ExcalidrawAutomate {
*/
async addLaTex(topX: number, topY: number, tex: string): Promise<string> {
const id = nanoid();
const image = await tex2dataURL(tex);
const image = await tex2dataURL(tex, 4, this.plugin.app);
if (!image) {
return null;
}
@@ -1648,7 +1648,7 @@ export class ExcalidrawAutomate {
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
return await tex2dataURL(tex,scale, this.plugin.app);
};
/**

View File

@@ -703,24 +703,24 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
}
public async setEmbeddableIsEditingSelf() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableIsEditingSelf, "ExcalidrawView.setEmbeddableIsEditingSelf");
this.clearEmbeddableIsEditingSelfTimer();
public async setEmbeddableNodeIsEditing() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableNodeIsEditing, "ExcalidrawView.setEmbeddableNodeIsEditing");
this.clearEmbeddableNodeIsEditingTimer();
await this.forceSave(true);
this.semaphores.embeddableIsEditingSelf = true;
}
public clearEmbeddableIsEditingSelfTimer () {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableIsEditingSelfTimer, "ExcalidrawView.clearEmbeddableIsEditingSelfTimer");
public clearEmbeddableNodeIsEditingTimer () {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditingTimer, "ExcalidrawView.clearEmbeddableNodeIsEditingTimer");
if(this.editingSelfResetTimer) {
window.clearTimeout(this.editingSelfResetTimer);
this.editingSelfResetTimer = null;
}
}
public clearEmbeddableIsEditingSelf() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableIsEditingSelf, "ExcalidrawView.clearEmbeddableIsEditingSelf");
this.clearEmbeddableIsEditingSelfTimer();
public clearEmbeddableNodeIsEditing() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditing, "ExcalidrawView.clearEmbeddableNodeIsEditing");
this.clearEmbeddableNodeIsEditingTimer();
this.editingSelfResetTimer = window.setTimeout(()=>this.semaphores.embeddableIsEditingSelf = false,EMBEDDABLE_SEMAPHORE_TIMEOUT);
}
@@ -1938,7 +1938,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
this.clearPreventReloadTimer();
this.clearEmbeddableIsEditingSelfTimer();
this.clearEmbeddableNodeIsEditingTimer();
this.plugin.scriptEngine?.removeViewEAs(this);
this.excalidrawAPI = null;
if(this.draginfoDiv) {
@@ -2048,7 +2048,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (this.semaphores.embeddableIsEditingSelf) {
//console.log("reload - embeddable is editing")
if(this.editingSelfResetTimer) {
this.clearEmbeddableIsEditingSelfTimer();
this.clearEmbeddableNodeIsEditingTimer();
this.semaphores.embeddableIsEditingSelf = false;
}
if(loadOnModifyTrigger) {
@@ -2657,9 +2657,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.clearDirty();
const om = this.excalidrawData.getOpenMode();
this.semaphores.preventReload = false;
const penEnabled =
this.plugin.settings.defaultPenMode === "always" ||
(this.plugin.settings.defaultPenMode === "mobile" && DEVICE.isMobile);
const penEnabled = this.plugin.isPenMode();
const api = this.excalidrawAPI;
if (api) {
//isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes
@@ -3700,6 +3698,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (selectedElementWithLink?.id) {
linktext = getLinkTextFromLink(selectedElementWithLink.text);
if(!linktext) return;
if(this.app.metadataCache.getFirstLinkpathDest(linktext.split("#")[0],this.file.path) === this.file) return;
}
} else {
const {linkText, selectedElement} = this.getLinkTextForElement(selectedEl, selectedEl);
@@ -3964,6 +3963,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(st.newElement?.type === "freedraw") {
this.freedrawLastActiveTimestamp = Date.now();
}
if (st.newElement || st.editingTextElement || st.editingLinearElement) {
this.plugin.wasPenModeActivePreviously = st.penMode;
}
this.viewModeEnabled = st.viewModeEnabled;
if (this.semaphores.justLoaded) {
const elcount = this.excalidrawData?.scene?.elements?.length ?? 0;

View File

@@ -3,19 +3,27 @@ import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import ExcalidrawView from "./ExcalidrawView";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { App } from "obsidian";
declare const loadMathjaxToSVG: Function;
let mathjaxLoaded = false;
let tex2dataURLExternal: Function;
let clearVariables: Function;
let loadMathJaxPromise: Promise<void> | null = null;
const loadMathJax = async () => {
if (!mathjaxLoaded) {
const module = await loadMathjaxToSVG();
tex2dataURLExternal = module.tex2dataURL;
clearVariables = module.clearMathJaxVariables;
mathjaxLoaded = true;
if (!loadMathJaxPromise) {
loadMathJaxPromise = (async () => {
if (!mathjaxLoaded) {
const module = await loadMathjaxToSVG();
tex2dataURLExternal = module.tex2dataURL;
clearVariables = module.clearMathJaxVariables;
mathjaxLoaded = true;
}
})();
}
return loadMathJaxPromise;
};
export const updateEquation = async (
@@ -43,7 +51,8 @@ export const updateEquation = async (
export async function tex2dataURL(
tex: string,
scale: number = 4
scale: number = 4,
app: App,
): Promise<{
mimeType: MimeType;
fileId: FileId;

465
src/Managers/FileManager.ts Normal file
View File

@@ -0,0 +1,465 @@
import { debug } from "src/utils/DebugHelper";
import { App, FrontMatterCache, MarkdownView, normalizePath, Notice, TAbstractFile, TFile, WorkspaceLeaf } from "obsidian";
import { BLANK_DRAWING, DARK_BLANK_DRAWING, DEVICE, EXPORT_TYPES, FRONTMATTER, FRONTMATTER_KEYS, JSON_parse, nanoid, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import { Prompt, templatePromt } from "src/dialogs/Prompt";
import { changeThemeOfExcalidrawMD, ExcalidrawData, getMarkdownDrawingSection } from "src/ExcalidrawData";
import ExcalidrawView, { getTextMode } from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { DEBUGGING } from "src/utils/DebugHelper";
import { checkAndCreateFolder, download, getIMGFilename, getLink, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/FileUtils";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
import { getExcalidrawViews, getNewOrAdjacentLeaf, isObsidianThemeDark, openLeaf } from "src/utils/ObsidianUtils";
import { errorlog, getExportTheme } from "src/utils/Utils";
export class PluginFileManager {
private plugin: ExcalidrawPlugin;
private app: App;
private excalidrawFiles: Set<TFile> = new Set<TFile>();
get settings() {
return this.plugin.settings;
}
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
}
public isExcalidrawFile(f: TFile): boolean {
if(!f) return false;
if (f.extension === "excalidraw") {
return true;
}
const fileCache = f ? this.plugin.app.metadataCache.getFileCache(f) : null;
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEYS["plugin"].name];
}
//managing my own list of Excalidraw files because in the onDelete event handler
//the file object is already gone from metadataCache, thus I can't check if it was an Excalidraw file
public updateFileCache(
file: TFile,
frontmatter?: FrontMatterCache,
deleted: boolean = false,
) {
if (frontmatter && typeof frontmatter[FRONTMATTER_KEYS["plugin"].name] !== "undefined") {
this.excalidrawFiles.add(file);
return;
}
if (!deleted && file.extension === "excalidraw") {
this.excalidrawFiles.add(file);
return;
}
this.excalidrawFiles.delete(file);
}
public getExcalidrawFiles(): Set<TFile> {
return this.excalidrawFiles;
}
public destroy() {
this.excalidrawFiles.clear();
}
public async createDrawing(
filename: string,
foldername?: string,
initData?: string,
): Promise<TFile> {
const folderpath = normalizePath(
foldername ? foldername : this.settings.folder,
);
await checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(this.app.vault, filename, folderpath);
const file = await this.app.vault.create(
fname,
initData ?? (await this.plugin.getBlankDrawing()),
);
//wait for metadata cache
let counter = 0;
while(file instanceof TFile && !this.isExcalidrawFile(file) && counter++<10) {
await sleep(50);
}
if(counter > 10) {
errorlog({file, error: "new drawing not recognized as an excalidraw file", fn: this.createDrawing});
}
return file;
}
public async getBlankDrawing(): Promise<string> {
const templates = getListOfTemplateFiles(this.plugin);
if(templates) {
const template = await templatePromt(templates, this.app);
if (template && template instanceof TFile) {
if (
(template.extension == "md" && !this.settings.compatibilityMode) ||
(template.extension == "excalidraw" && this.settings.compatibilityMode)
) {
const data = await this.app.vault.read(template);
if (data) {
return this.settings.matchTheme
? changeThemeOfExcalidrawMD(data)
: data;
}
}
}
}
if (this.settings.compatibilityMode) {
return this.settings.matchTheme && isObsidianThemeDark()
? DARK_BLANK_DRAWING
: BLANK_DRAWING;
}
const blank =
this.settings.matchTheme && isObsidianThemeDark()
? DARK_BLANK_DRAWING
: BLANK_DRAWING;
return `${FRONTMATTER}\n${getMarkdownDrawingSection(
blank,
this.settings.compress,
)}`;
}
public async embedDrawing(file: TFile) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView && activeView.file) {
const excalidrawRelativePath = this.app.metadataCache.fileToLinktext(
file,
activeView.file.path,
this.settings.embedType === "excalidraw",
);
const editor = activeView.editor;
//embed Excalidraw
if (this.settings.embedType === "excalidraw") {
editor.replaceSelection(
getLink(this.plugin, {path: excalidrawRelativePath}),
);
editor.focus();
return;
}
//embed image
let theme = this.settings.autoExportLightAndDark
? getExportTheme (
this.plugin,
file,
this.settings.exportWithTheme
? isObsidianThemeDark() ? "dark":"light"
: "light"
)
: "";
theme = (theme === "")
? ""
: theme + ".";
const imageRelativePath = getIMGFilename(
excalidrawRelativePath,
theme+this.settings.embedType.toLowerCase(),
);
const imageFullpath = getIMGFilename(
file.path,
theme+this.settings.embedType.toLowerCase(),
);
//will hold incorrect value if theme==="", however in that case it won't be used
const otherTheme = theme === "dark." ? "light." : "dark.";
const otherImageRelativePath = theme === ""
? null
: getIMGFilename(
excalidrawRelativePath,
otherTheme+this.settings.embedType.toLowerCase(),
);
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
if (!imgFile) {
await this.app.vault.create(imageFullpath, "");
await sleep(200); //wait for metadata cache to update
}
const inclCom = this.settings.embedMarkdownCommentLinks;
editor.replaceSelection(
this.settings.embedWikiLink
? `![[${imageRelativePath}]]\n` +
(inclCom
? `%%[[${excalidrawRelativePath}|🖋 Edit in Excalidraw]]${
otherImageRelativePath
? ", and the [["+otherImageRelativePath+"|"+otherTheme.split(".")[0]+" exported image]]"
: ""
}%%`
: "")
: `![](${encodeURI(imageRelativePath)})\n` +
(inclCom ? `%%[🖋 Edit in Excalidraw](${encodeURI(excalidrawRelativePath,
)})${otherImageRelativePath?", and the ["+otherTheme.split(".")[0]+" exported image]("+encodeURI(otherImageRelativePath)+")":""}%%` : ""),
);
editor.focus();
}
}
public async exportLibrary() {
if (DEVICE.isMobile) {
const prompt = new Prompt(
this.app,
"Please provide a filename",
"my-library",
"filename, leave blank to cancel action",
);
prompt.openAndGetValue(async (filename: string) => {
if (!filename) {
return;
}
filename = `${filename}.excalidrawlib`;
const folderpath = normalizePath(this.settings.folder);
await checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(
this.app.vault,
filename,
folderpath,
);
this.app.vault.create(fname, this.settings.library);
new Notice(`Exported library to ${fname}`, 6000);
});
return;
}
download(
"data:text/plain;charset=utf-8",
encodeURIComponent(JSON.stringify(this.settings.library2, null, "\t")),
"my-obsidian-library.excalidrawlib",
);
}
/**
* Opens a drawing file
* @param drawingFile
* @param location
* @param active
* @param subpath
* @param justCreated
* @param popoutLocation
*/
public openDrawing(
drawingFile: TFile,
location: PaneTarget,
active: boolean = false,
subpath?: string,
justCreated: boolean = false,
popoutLocation?: {x?: number, y?: number, width?: number, height?: number},
) {
const fnGetLeaf = ():WorkspaceLeaf => {
if(location === "md-properties") {
location = "new-tab";
}
let leaf: WorkspaceLeaf;
if(location === "popout-window") {
//@ts-ignore (the api does not include x,y)
leaf = this.app.workspace.openPopoutLeaf(popoutLocation);
}
if(location === "new-tab") {
leaf = this.app.workspace.getLeaf('tab');
}
if(!leaf) {
leaf = this.app.workspace.getLeaf(false);
if ((leaf.view.getViewType() !== 'empty') && (location === "new-pane")) {
leaf = getNewOrAdjacentLeaf(this.plugin, leaf)
}
}
return leaf;
}
const {leaf, promise} = openLeaf({
plugin: this.plugin,
fnGetLeaf: () => fnGetLeaf(),
file: drawingFile,
openState:!subpath || subpath === ""
? {active}
: { active, eState: { subpath } }
});
promise.then(()=>{
const ea = this.plugin.ea;
if(justCreated && ea.onFileCreateHook) {
try {
ea.onFileCreateHook({
ea,
excalidrawFile: drawingFile,
view: leaf.view as ExcalidrawView,
});
} catch(e) {
console.error(e);
}
}
})
}
/**
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
* @param {string} data - Excalidraw scene JSON string
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
*/
public async exportSceneToMD(data: string, compressOverride?: boolean): Promise<string> {
if (!data) {
return "";
}
const excalidrawData = JSON_parse(data);
const textElements = excalidrawData.elements?.filter(
(el: any) => el.type == "text",
);
let outString = `# Excalidraw Data\n## Text Elements\n`;
let id: string;
for (const te of textElements) {
id = te.id;
//replacing Excalidraw text IDs with my own, because default IDs may contain
//characters not recognized by Obsidian block references
//also Excalidraw IDs are inconveniently long
if (te.id.length > 8) {
id = nanoid();
data = data.replaceAll(te.id, id); //brute force approach to replace all occurrences.
}
outString += `${te.originalText ?? te.text} ^${id}\n\n`;
}
return (
outString +
getMarkdownDrawingSection(
JSON.stringify(JSON_parse(data), null, "\t"),
typeof compressOverride === "undefined"
? this.settings.compress
: compressOverride,
)
);
}
// -------------------------------------------------------
// ------------------ Event Handlers ---------------------
// -------------------------------------------------------
/**
* watch filename change to rename .svg, .png; to sync to .md; to update links
* @param file
* @param oldPath
* @returns
*/
public async renameEventHandler (file: TAbstractFile, oldPath: string) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renameEventHandler, `ExcalidrawPlugin.renameEventHandler`, file, oldPath);
if (!(file instanceof TFile)) {
return;
}
if (!this.isExcalidrawFile(file)) {
return;
}
if (!this.settings.keepInSync) {
return;
}
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
const oldIMGpath = getIMGFilename(oldPath, ext);
const imgFile = app.vault.getAbstractFileByPath(
normalizePath(oldIMGpath),
);
if (imgFile && imgFile instanceof TFile) {
const newIMGpath = getIMGFilename(file.path, ext);
await this.app.fileManager.renameFile(imgFile, newIMGpath);
}
});
}
public async modifyEventHandler (file: TFile) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.modifyEventHandler,`ExcalidrawPlugin.modifyEventHandler`, file);
const excalidrawViews = getExcalidrawViews(this.app);
excalidrawViews.forEach(async (excalidrawView) => {
if(excalidrawView.semaphores?.viewunload) {
return;
}
if (
excalidrawView.file &&
(excalidrawView.file.path === file.path ||
(file.extension === "excalidraw" &&
`${file.path.substring(
0,
file.path.lastIndexOf(".excalidraw"),
)}.md` === excalidrawView.file.path))
) {
if(excalidrawView.semaphores?.preventReload) {
excalidrawView.semaphores.preventReload = false;
return;
}
//if the user hasn't touched the file for 5 minutes, don't synchronize, reload.
//this is to avoid complex sync scenarios of multiple remote changes outside an active collaboration session
const activeView = this.app.workspace.activeLeaf.view;
const isEditingMarkdownSideInSplitView = (activeView !== excalidrawView) &&
activeView instanceof MarkdownView && activeView.file === excalidrawView.file;
if(!isEditingMarkdownSideInSplitView && (excalidrawView.lastSaveTimestamp + 300000 < Date.now())) {
excalidrawView.reload(true, excalidrawView.file);
return;
}
if(file.extension==="md") {
if(excalidrawView.semaphores?.embeddableIsEditingSelf) return;
const inData = new ExcalidrawData(this.plugin);
const data = await this.app.vault.read(file);
await inData.loadData(data,file,getTextMode(data));
excalidrawView.synchronizeWithData(inData);
inData.destroy();
if(excalidrawView?.isDirty()) {
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
clearTimeout(excalidrawView.autosaveTimer);
}
if(excalidrawView.autosaveFunction) {
excalidrawView.autosaveFunction();
}
}
} else {
excalidrawView.reload(true, excalidrawView.file);
}
}
});
}
/**
* watch file delete and delete corresponding .svg and .png
* @param file
* @returns
*/
public async deleteEventHandler (file: TFile) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.deleteEventHandler,`ExcalidrawPlugin.deleteEventHandler`, file);
if (!(file instanceof TFile)) {
return;
}
const isExcalidarwFile = this.getExcalidrawFiles().has(file);
this.updateFileCache(file, undefined, true);
if (!isExcalidarwFile) {
return;
}
//close excalidraw view where this file is open
const excalidrawViews = getExcalidrawViews(this.app);
for (const excalidrawView of excalidrawViews) {
if (excalidrawView.file.path === file.path) {
await excalidrawView.leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: { file: null },
});
}
}
//delete PNG and SVG files as well
if (this.settings.keepInSync) {
window.setTimeout(() => {
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
const imgPath = getIMGFilename(file.path, ext);
const imgFile = this.app.vault.getAbstractFileByPath(
normalizePath(imgPath),
);
if (imgFile && imgFile instanceof TFile) {
await this.app.vault.delete(imgFile);
}
});
}, 500);
}
};
}

View File

@@ -0,0 +1,199 @@
import { debug, DEBUGGING } from "src/utils/DebugHelper";
import ExcalidrawPlugin from "src/main";
import { CustomMutationObserver } from "src/utils/DebugHelper";
import { getExcalidrawViews, isObsidianThemeDark } from "src/utils/ObsidianUtils";
import { App, TFile } from "obsidian";
export class ObserverManager {
private plugin: ExcalidrawPlugin;
private app: App;
private themeObserver: MutationObserver | CustomMutationObserver;
private fileExplorerObserver: MutationObserver | CustomMutationObserver;
private modalContainerObserver: MutationObserver | CustomMutationObserver;
private workspaceDrawerLeftObserver: MutationObserver | CustomMutationObserver;
private workspaceDrawerRightObserver: MutationObserver | CustomMutationObserver;
private activeViewDoc: Document;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
if(this.settings.matchThemeTrigger) this.addThemeObserver();
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
this.addModalContainerObserver();
}
get settings() {
return this.plugin.settings;
}
public addThemeObserver() {
if(this.themeObserver) return;
const { matchThemeTrigger } = this.settings;
if (!matchThemeTrigger) return;
const themeObserverFn:MutationCallback = async (mutations: MutationRecord[]) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(themeObserverFn, `ExcalidrawPlugin.addThemeObserver`, mutations);
const { matchThemeTrigger } = this.settings;
if (!matchThemeTrigger) return;
const bodyClassList = document.body.classList;
const mutation = mutations[0];
if (mutation?.oldValue === bodyClassList.value) return;
const darkClass = bodyClassList.contains('theme-dark');
if (mutation?.oldValue?.includes('theme-dark') === darkClass) return;
setTimeout(()=>{ //run async to avoid blocking the UI
const theme = isObsidianThemeDark() ? "dark" : "light";
const excalidrawViews = getExcalidrawViews(this.app);
excalidrawViews.forEach(excalidrawView => {
if (excalidrawView.file && excalidrawView.excalidrawAPI) {
excalidrawView.setTheme(theme);
}
});
});
};
this.themeObserver = DEBUGGING
? new CustomMutationObserver(themeObserverFn, "themeObserver")
: new MutationObserver(themeObserverFn);
this.themeObserver.observe(document.body, {
attributeOldValue: true,
attributeFilter: ["class"],
});
}
public removeThemeObserver() {
if(!this.themeObserver) return;
this.themeObserver.disconnect();
this.themeObserver = null;
}
public experimentalFileTypeDisplayToggle(enabled: boolean) {
if (enabled) {
this.experimentalFileTypeDisplay();
return;
}
if (this.fileExplorerObserver) {
this.fileExplorerObserver.disconnect();
}
this.fileExplorerObserver = null;
}
/**
* Display characters configured in settings, in front of the filename, if the markdown file is an excalidraw drawing
* Must be called after the workspace is ready
* The function is called from onload()
*/
private async experimentalFileTypeDisplay() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay`);
const insertFiletype = (el: HTMLElement) => {
if (el.childElementCount !== 1) {
return;
}
const filename = el.getAttribute("data-path");
if (!filename) {
return;
}
const f = this.app.vault.getAbstractFileByPath(filename);
if (!f || !(f instanceof TFile)) {
return;
}
if (this.plugin.isExcalidrawFile(f)) {
el.insertBefore(
createDiv({
cls: "nav-file-tag",
text: this.settings.experimentalFileTag,
}),
el.firstChild,
);
}
};
const fileExplorerObserverFn:MutationCallback = (mutationsList) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(fileExplorerObserverFn, `ExcalidrawPlugin.experimentalFileTypeDisplay > fileExplorerObserverFn`, mutationsList);
const mutationsWithNodes = mutationsList.filter((mutation) => mutation.addedNodes.length > 0);
mutationsWithNodes.forEach((mutationNode) => {
mutationNode.addedNodes.forEach((node) => {
if (!(node instanceof Element)) {
return;
}
node.querySelectorAll(".nav-file-title").forEach(insertFiletype);
});
});
};
this.fileExplorerObserver = DEBUGGING
? new CustomMutationObserver(fileExplorerObserverFn, "fileExplorerObserver")
: new MutationObserver(fileExplorerObserverFn);
//the part that should only run after onLayoutReady
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
const container = document.querySelector(".nav-files-container");
if (container) {
this.fileExplorerObserver.observe(container, {
childList: true,
subtree: true,
});
}
}
/**
* Monitors if the user clicks outside the Excalidraw view, and saves the drawing if it's dirty
* @returns
*/
public addModalContainerObserver() {
if(!this.plugin.activeExcalidrawView) return;
if(this.modalContainerObserver) {
if(this.activeViewDoc === this.plugin.activeExcalidrawView.ownerDocument) {
return;
}
this.removeModalContainerObserver();
}
//The user clicks settings, or "open another vault", or the command palette
const modalContainerObserverFn: MutationCallback = async (m: MutationRecord[]) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(modalContainerObserverFn,`ExcalidrawPlugin.modalContainerObserverFn`, m);
if (
(m.length !== 1) ||
(m[0].type !== "childList") ||
(m[0].addedNodes.length !== 1) ||
(!this.plugin.activeExcalidrawView) ||
this.plugin.activeExcalidrawView?.semaphores?.viewunload ||
(!this.plugin.activeExcalidrawView?.isDirty())
) {
return;
}
this.plugin.activeExcalidrawView.save();
};
this.modalContainerObserver = DEBUGGING
? new CustomMutationObserver(modalContainerObserverFn, "modalContainerObserver")
: new MutationObserver(modalContainerObserverFn);
this.activeViewDoc = this.plugin.activeExcalidrawView.ownerDocument;
this.modalContainerObserver.observe(this.activeViewDoc.body, {
childList: true,
});
}
public removeModalContainerObserver() {
if(!this.modalContainerObserver) return;
this.modalContainerObserver.disconnect();
this.activeViewDoc = null;
this.modalContainerObserver = null;
}
public destroy() {
this.removeThemeObserver();
this.removeModalContainerObserver();
if (this.workspaceDrawerLeftObserver) {
this.workspaceDrawerLeftObserver.disconnect();
}
if (this.workspaceDrawerRightObserver) {
this.workspaceDrawerRightObserver.disconnect();
}
if (this.fileExplorerObserver) {
this.fileExplorerObserver.disconnect();
}
}
}

View File

@@ -0,0 +1,80 @@
import { ExcalidrawLib } from "../ExcalidrawLib";
import { Packages } from "../types/types";
import { debug, DEBUGGING } from "../utils/DebugHelper";
declare let REACT_PACKAGES:string;
declare let react:any;
declare let reactDOM:any;
declare let excalidrawLib: typeof ExcalidrawLib;
export class PackageManager {
private packageMap: Map<Window, Packages> = new Map<Window, Packages>();
private EXCALIDRAW_PACKAGE: string;
constructor() {
this.packageMap.set(window,{react, reactDOM, excalidrawLib});
}
public setPackage(window: Window, pkg: Packages) {
this.packageMap.set(window, pkg);
}
public getPackageMap() {
return this.packageMap;
}
public getPackage(win:Window):Packages {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getPackage, `ExcalidrawPlugin.getPackage`, win);
if(this.packageMap.has(win)) {
return this.packageMap.get(win);
}
const {react:r, reactDOM:rd, excalidrawLib:e} = win.eval.call(win,
`(function() {
${REACT_PACKAGES + this.EXCALIDRAW_PACKAGE};
return {react:React,reactDOM:ReactDOM,excalidrawLib:ExcalidrawLib};
})()`);
this.packageMap.set(win,{react:r, reactDOM:rd, excalidrawLib:e});
return {react:r, reactDOM:rd, excalidrawLib:e};
}
public deletePackage(win: Window) {
const { react, reactDOM, excalidrawLib } = this.getPackage(win);
if (win.ExcalidrawLib === excalidrawLib) {
excalidrawLib.destroyObsidianUtils();
delete win.ExcalidrawLib;
}
if (win.React === react) {
Object.keys(win.React).forEach((key) => {
delete win.React[key];
});
delete win.React;
}
if (win.ReactDOM === reactDOM) {
Object.keys(win.ReactDOM).forEach((key) => {
delete win.ReactDOM[key];
});
delete win.ReactDOM;
}
this.packageMap.delete(win);
}
public setExcalidrawPackage(pkg: string) {
this.EXCALIDRAW_PACKAGE = pkg;
}
public destroy() {
REACT_PACKAGES = "";
Object.values(this.packageMap).forEach((p: Packages) => {
delete p.excalidrawLib;
delete p.reactDOM;
delete p.react;
});
this.packageMap.clear();
}
}

View File

@@ -4,7 +4,7 @@ import { COLOR_NAMES } from "src/constants/constants";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { setPen } from "src/menu/ObsidianMenu";
import { ExtendedFillStyle, PenType } from "src/PenTypes";
import { ExtendedFillStyle, PenType } from "src/types/PenTypes";
import { getExcalidrawViews } from "src/utils/ObsidianUtils";
import { PENS } from "src/utils/Pens";
import { fragWithHTML } from "src/utils/Utils";

View File

@@ -910,6 +910,8 @@ FILENAME_HEAD: "文件名",
ES_YOUTUBE_START_INVALID: "YouTube 起始时间无效。请检查格式并重试",
ES_FILENAME_VISIBLE: "显示文件名",
ES_BACKGROUND_HEAD: "背景色",
ES_BACKGROUND_DESC_INFO : "点击此处了解更多颜色信息" ,
ES_BACKGROUND_DESC_DETAIL : "背景颜色仅影响 Markdown 嵌入预览模式。在编辑模式下,它会根据场景(通过文档属性设置)或插件设置,遵循 Obsidian 的浅色/深色主题。背景颜色有两层:元素背景颜色(下层)和上层颜色。选择“匹配元素背景”意味着两层都遵循元素颜色。选择“匹配画布”或特定背景颜色时,保留元素背景层。设置透明度(例如 50%)会将画布或选定的颜色与元素背景颜色混合。要移除元素背景层,可以在 Excalidraw 的元素属性编辑器中将元素颜色设置为透明,这样只有上层颜色生效。" ,
ES_BACKGROUND_MATCH_ELEMENT: "匹配元素背景色",
ES_BACKGROUND_MATCH_CANVAS: "匹配画布背景色",
ES_BACKGROUND_COLOR: "背景色",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { Copy, Crop, Globe, RotateCcw, Scan, Settings, TextSelect } from "lucide-react";
import * as React from "react";
import { PenStyle } from "src/PenTypes";
import { PenStyle } from "src/types/PenTypes";
export const ICONS = {
ExportImage: (

View File

@@ -5,7 +5,7 @@ import * as React from "react";
import { DEVICE } from "src/constants/constants";
import { PenSettingsModal } from "src/dialogs/PenSettingsModal";
import ExcalidrawView from "src/ExcalidrawView";
import { PenStyle } from "src/PenTypes";
import { PenStyle } from "src/types/PenTypes";
import { PENS } from "src/utils/Pens";
import ExcalidrawPlugin from "../main";
import { ICONS, penIcon, stringToSVG } from "./ActionIcons";

View File

@@ -1,4 +1,4 @@
import { ExcalidrawAutomate, createPNG } from "../ExcalidrawAutomate";
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
import {Notice, requestUrl} from "obsidian"
import ExcalidrawPlugin from "../main"
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"

View File

@@ -13,7 +13,7 @@ import {
import { GITHUB_RELEASES } from "./constants/constants";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
import { PenStyle } from "./PenTypes";
import { PenStyle } from "./types/PenTypes";
import { DynamicStyle, GridSettings } from "./types/types";
import { PreviewImageType } from "./utils/UtilTypes";
import { setDynamicStyle } from "./utils/DynamicStyling";

View File

@@ -44,6 +44,10 @@ declare global {
interface Window {
ExcalidrawAutomate: ExcalidrawAutomate;
pdfjsLib: any;
eval: (x: string) => any;
React?: any;
ReactDOM?: any;
ExcalidrawLib?: any;
}
}

View File

@@ -115,9 +115,9 @@ export class CanvasNodeFactory {
if (!this.initialized || !node) return;
try {
if (node.file === this.view.file) {
await this.view.setEmbeddableIsEditingSelf();
}
//if (node.file === this.view.file) {
await this.view.setEmbeddableNodeIsEditing();
//}
node.startEditing();
node.isEditing = true;
@@ -141,9 +141,9 @@ export class CanvasNodeFactory {
if (!this.initialized || !node || !node.isEditing) return;
try {
if (node.file === this.view.file) {
this.view.clearEmbeddableIsEditingSelf();
}
//if (node.file === this.view.file) {
this.view.clearEmbeddableNodeIsEditing();
//}
node.child.showPreview();
node.isEditing = false;
this.observer?.disconnect();

View File

@@ -1,4 +1,4 @@
import { PenStyle, PenType } from "src/PenTypes";
import { PenStyle, PenType } from "src/types/PenTypes";
export const PENS:Record<PenType,PenStyle> = {
"default": {