File refactoring
This commit is contained in:
parent
e23cc424f8
commit
6bca12c934
5 changed files with 142 additions and 72 deletions
|
@ -2,8 +2,8 @@ export const SVG_MIME = "image/svg+xml";
|
|||
export const JSON_MIME = "application/json";
|
||||
export const DATA_PATH = "/data/";
|
||||
export const ASSETS_PATH = "assets/";
|
||||
export const STORAGE_PATH = DATA_PATH + "storage/petal/siyuan-jsdraw-plugin";
|
||||
export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json";
|
||||
export const CONFIG_PATH = STORAGE_PATH + "/conf.json";
|
||||
export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin/";
|
||||
export const TOOLBAR_FILENAME = "toolbar.json";
|
||||
export const CONFIG_FILENAME = "conf.json";
|
||||
export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path=";
|
||||
export const DUMMY_HOST = "https://dummy.host/";
|
|
@ -1,17 +1,18 @@
|
|||
import {MaterialIconProvider} from "@js-draw/material-icons";
|
||||
import {getFile, saveFile, uploadAsset} from "@/file";
|
||||
import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const";
|
||||
import {IDsToAssetPath} from "@/helper";
|
||||
import {PluginAsset, PluginFile} from "@/file";
|
||||
import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const";
|
||||
import Editor, {BaseWidget, EditorEventType} from "js-draw";
|
||||
import {Dialog, Plugin, openTab, getFrontend} from "siyuan";
|
||||
import {replaceSyncID} from "@/protyle";
|
||||
import {removeFile} from "@/api";
|
||||
|
||||
export class PluginEditor {
|
||||
|
||||
private readonly element: HTMLElement;
|
||||
private readonly editor: Editor;
|
||||
|
||||
private drawingFile: PluginAsset;
|
||||
private toolbarFile: PluginFile;
|
||||
|
||||
private readonly fileID: string;
|
||||
private syncID: string;
|
||||
private readonly initialSyncID: string;
|
||||
|
@ -34,12 +35,13 @@ export class PluginEditor {
|
|||
this.initialSyncID = initialSyncID;
|
||||
this.syncID = initialSyncID;
|
||||
|
||||
this.genToolbar()
|
||||
this.genToolbar();
|
||||
|
||||
// restore drawing
|
||||
getFile(DATA_PATH +IDsToAssetPath(fileID, initialSyncID)).then(svg => {
|
||||
if(svg != null) {
|
||||
this.editor.loadFromSVG(svg);
|
||||
this.drawingFile = new PluginAsset(fileID, initialSyncID, SVG_MIME);
|
||||
this.drawingFile.loadFromSiYuanFS().then(() => {
|
||||
if(this.drawingFile.getContent() != null) {
|
||||
this.editor.loadFromSVG(this.drawingFile.getContent());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -52,11 +54,13 @@ export class PluginEditor {
|
|||
|
||||
const toolbar = this.editor.addToolbar();
|
||||
|
||||
// restore toolbar state
|
||||
const toolbarState = await getFile(TOOLBAR_PATH);
|
||||
if (toolbarState != null) {
|
||||
toolbar.deserializeState(toolbarState);
|
||||
// restore toolbarFile state
|
||||
this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME);
|
||||
this.toolbarFile.loadFromSiYuanFS().then(() => {
|
||||
if(this.toolbarFile.getContent() != null) {
|
||||
toolbar.deserializeState(this.toolbarFile.getContent());
|
||||
}
|
||||
});
|
||||
|
||||
// save button
|
||||
const saveButton = toolbar.addSaveButton(async () => {
|
||||
|
@ -65,7 +69,8 @@ export class PluginEditor {
|
|||
|
||||
// save toolbar config on tool change (toolbar state is not saved in SVGs!)
|
||||
this.editor.notifier.on(EditorEventType.ToolUpdated, () => {
|
||||
saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState());
|
||||
this.toolbarFile.setContent(toolbar.serializeState());
|
||||
this.toolbarFile.save();
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -77,18 +82,20 @@ export class PluginEditor {
|
|||
const oldSyncID = this.syncID;
|
||||
|
||||
try {
|
||||
newSyncID = (await uploadAsset(this.fileID, SVG_MIME, svgElem.outerHTML)).syncID;
|
||||
if(newSyncID != oldSyncID) {
|
||||
const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID);
|
||||
this.drawingFile.setContent(svgElem.outerHTML);
|
||||
await this.drawingFile.save();
|
||||
newSyncID = this.drawingFile.getSyncID();
|
||||
if(newSyncID != oldSyncID) { // supposed to replace protyle
|
||||
const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle
|
||||
if(!changed) {
|
||||
alert(
|
||||
"Error replacing old sync ID with new one! You may need to manually replace the file path." +
|
||||
"\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." +
|
||||
"\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory."
|
||||
);
|
||||
return;
|
||||
return; // don't delete old drawing if protyle unchanged (could cause confusion)
|
||||
}
|
||||
await removeFile(DATA_PATH + IDsToAssetPath(this.fileID, oldSyncID));
|
||||
await this.drawingFile.removeOld(oldSyncID);
|
||||
}
|
||||
saveButton.setDisabled(true);
|
||||
setTimeout(() => { // @todo improve save button feedback
|
||||
|
|
128
src/file.ts
128
src/file.ts
|
@ -1,52 +1,108 @@
|
|||
import {getFileBlob, putFile, upload} from "@/api";
|
||||
import {ASSETS_PATH} from "@/const";
|
||||
import {assetPathToIDs} from "@/helper";
|
||||
import {getFileBlob, putFile, removeFile, upload} from "@/api";
|
||||
import {ASSETS_PATH, DATA_PATH} from "@/const";
|
||||
import {assetPathToIDs, IDsToAssetName} from "@/helper";
|
||||
|
||||
function toFile(title: string, content: string, mimeType: string){
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
return new File([blob], title, { type: mimeType });
|
||||
}
|
||||
abstract class PluginFileBase {
|
||||
|
||||
// upload asset to the assets folder, return fileID and syncID
|
||||
export async function uploadAsset(fileID: string, mimeType: string, content: string) {
|
||||
protected content: string | null;
|
||||
|
||||
const file = toFile(fileID + ".svg", content, mimeType);
|
||||
protected fileName: string;
|
||||
protected folderPath: string;
|
||||
protected mimeType: string;
|
||||
|
||||
let r = await upload('/' + ASSETS_PATH, [file]);
|
||||
if(r.errFiles) {
|
||||
throw new Error("Failed to upload file");
|
||||
getContent() { return this.content; }
|
||||
setContent(content: string) { this.content = content; }
|
||||
setFileName(fileName: string) { this.fileName = fileName; }
|
||||
|
||||
private setFolderPath(folderPath: string) {
|
||||
if(folderPath.startsWith('/') && folderPath.endsWith('/')) {
|
||||
this.folderPath = folderPath;
|
||||
}else{
|
||||
throw new Error("folderPath must start and end with /");
|
||||
}
|
||||
}
|
||||
return assetPathToIDs(r.succMap[file.name]);
|
||||
|
||||
}
|
||||
// folderPath must start and end with /
|
||||
constructor(folderPath: string, fileName: string, mimeType: string) {
|
||||
this.setFolderPath(folderPath);
|
||||
this.fileName = fileName;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
export function saveFile(path: string, mimeType: string, content: string) {
|
||||
|
||||
const file = toFile(path.split('/').pop(), content, mimeType);
|
||||
async loadFromSiYuanFS() {
|
||||
const blob = await getFileBlob(this.folderPath + this.fileName);
|
||||
const text = await blob.text();
|
||||
|
||||
try {
|
||||
putFile(path, false, file);
|
||||
const res = JSON.parse(text);
|
||||
if(res.code == 404) {
|
||||
this.content = null;
|
||||
return;
|
||||
}
|
||||
}catch {}
|
||||
|
||||
this.content = text;
|
||||
}
|
||||
|
||||
async remove(customFilename?: string) {
|
||||
let filename = customFilename || this.fileName;
|
||||
await removeFile(this.folderPath + filename);
|
||||
}
|
||||
|
||||
protected toFile(customFilename?: string): File {
|
||||
let filename = customFilename || this.fileName;
|
||||
const blob = new Blob([this.content], { type: this.mimeType });
|
||||
return new File([blob], filename, { type: this.mimeType });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PluginFile extends PluginFileBase {
|
||||
|
||||
async save() {
|
||||
const file = this.toFile();
|
||||
try {
|
||||
await putFile(this.folderPath + this.fileName, false, file);
|
||||
} catch (error) {
|
||||
console.error("Error saving file:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getFile(path: string) {
|
||||
|
||||
const blob = await getFileBlob(path);
|
||||
const jsonText = await blob.text();
|
||||
|
||||
// if we got a 404 api response, we will return null
|
||||
try {
|
||||
const res = JSON.parse(jsonText);
|
||||
if(res.code == 404) {
|
||||
return null;
|
||||
}
|
||||
}catch {}
|
||||
|
||||
// js-draw expects a string!
|
||||
return jsonText;
|
||||
|
||||
}
|
||||
|
||||
export class PluginAsset extends PluginFileBase {
|
||||
|
||||
private fileID: string
|
||||
private syncID: string
|
||||
|
||||
getFileID() { return this.fileID; }
|
||||
getSyncID() { return this.syncID; }
|
||||
|
||||
constructor(fileID: string, syncID: string, mimeType: string) {
|
||||
super(DATA_PATH + ASSETS_PATH, IDsToAssetName(fileID, syncID), mimeType);
|
||||
this.fileID = fileID;
|
||||
this.syncID = syncID;
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
||||
const file = this.toFile(this.fileID + '.svg');
|
||||
|
||||
let r = await upload('/' + ASSETS_PATH, [file]);
|
||||
if (r.errFiles) {
|
||||
throw new Error("Failed to upload file");
|
||||
}
|
||||
const ids = assetPathToIDs(r.succMap[file.name])
|
||||
|
||||
this.fileID = ids.fileID;
|
||||
this.syncID = ids.syncID;
|
||||
super.setFileName(IDsToAssetName(this.fileID, this.syncID));
|
||||
|
||||
}
|
||||
|
||||
async removeOld(oldSyncID: string) {
|
||||
await super.remove(IDsToAssetName(this.fileID, oldSyncID));
|
||||
}
|
||||
|
||||
}
|
|
@ -47,8 +47,11 @@ export function generateRandomString() {
|
|||
|
||||
}
|
||||
|
||||
export function IDsToAssetName(fileID: string, syncID: string) {
|
||||
return `${fileID}-${syncID}.svg`;
|
||||
}
|
||||
export function IDsToAssetPath(fileID: string, syncID: string) {
|
||||
return `${ASSETS_PATH}${fileID}-${syncID}.svg`
|
||||
return `${ASSETS_PATH}${IDsToAssetName(fileID, syncID)}`
|
||||
}
|
||||
export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null {
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {sql} from "@/api";
|
||||
import {getFile, uploadAsset} from "@/file";
|
||||
import {PluginAsset, PluginFile} from "@/file";
|
||||
import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const";
|
||||
import {replaceBlockContent} from "@/protyle";
|
||||
import {generateRandomString, getMarkdownBlock} from "@/helper";
|
||||
|
@ -13,11 +13,15 @@ export async function migrate() {
|
|||
for(const block of blocks) {
|
||||
const oldFileID = extractID(block.markdown);
|
||||
if(oldFileID) {
|
||||
const newFileID = generateRandomString() + "-" + oldFileID;
|
||||
const file = await getFile(DATA_PATH + ASSETS_PATH + oldFileID + ".svg");
|
||||
const r = await uploadAsset(newFileID, SVG_MIME, file);
|
||||
const newMarkdown = getMarkdownBlock(r.fileID, r.syncID);
|
||||
await replaceBlockContent(block.id, block.markdown, newMarkdown);
|
||||
const oldFile = new PluginFile(DATA_PATH + ASSETS_PATH, oldFileID + '.svg', SVG_MIME);
|
||||
await oldFile.loadFromSiYuanFS();
|
||||
const newFile = new PluginAsset(generateRandomString(), oldFileID, SVG_MIME);
|
||||
newFile.setContent(oldFile.getContent());
|
||||
await newFile.save();
|
||||
const newMarkdown = getMarkdownBlock(newFile.getFileID(), newFile.getSyncID());
|
||||
if(await replaceBlockContent(block.id, block.markdown, newMarkdown)) {
|
||||
await oldFile.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue