A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 449 lines 17 kB view raw
1import {App} from "../index"; 2import {EventBus} from "./EventBus"; 3import {fetchPost} from "../util/fetch"; 4import {isMobile, isWindow} from "../util/functions"; 5/// #if !MOBILE 6import {Custom} from "../layout/dock/Custom"; 7import {getAllModels} from "../layout/getAll"; 8import {Tab} from "../layout/Tab"; 9import {resizeTopBar, setPanelFocus} from "../layout/util"; 10import {getDockByType} from "../layout/tabUtil"; 11///#else 12import {MobileCustom} from "../mobile/dock/MobileCustom"; 13/// #endif 14import {hasClosestByAttribute} from "../protyle/util/hasClosest"; 15import {BlockPanel} from "../block/Panel"; 16import {Setting} from "./Setting"; 17import {clearOBG} from "../layout/dock/util"; 18import {Constants} from "../constants"; 19 20export class Plugin { 21 private app: App; 22 public i18n: IObject; 23 public eventBus: EventBus; 24 public data: any = {}; 25 public displayName: string; 26 public readonly name: string; 27 public protyleSlash: { 28 filter: string[], 29 html: string, 30 id: string, 31 callback: (protyle: import("../protyle").Protyle, nodeElement: HTMLElement) => void 32 }[] = []; 33 // TODO 34 public customBlockRenders: { 35 [key: string]: { 36 icon: string, 37 action: "edit" | "more"[], 38 genCursor: boolean, 39 render: (options: { app: App, element: Element }) => void 40 } 41 } = {}; 42 public topBarIcons: Element[] = []; 43 public setting: Setting; 44 public statusBarIcons: Element[] = []; 45 public commands: ICommand[] = []; 46 public models: { 47 /// #if !MOBILE 48 [key: string]: (options: { tab: Tab, data: any }) => Custom 49 /// #endif 50 } = {}; 51 public docks: { 52 [key: string]: { 53 config: IPluginDockTab, 54 /// #if !MOBILE 55 model: (options: { tab: Tab }) => Custom 56 /// #else 57 mobileModel: (element: Element) => MobileCustom 58 /// #endif 59 } 60 } = {}; 61 private protyleOptionsValue: IProtyleOptions; 62 63 constructor(options: { 64 app: App, 65 name: string, 66 displayName: string, 67 i18n: IObject 68 }) { 69 this.app = options.app; 70 this.i18n = options.i18n; 71 this.displayName = options.displayName; 72 this.eventBus = new EventBus(options.name); 73 74 // https://github.com/siyuan-note/siyuan/issues/9943 75 Object.defineProperty(this, "name", { 76 value: options.name, 77 writable: false, 78 }); 79 80 this.updateProtyleToolbar([]).forEach(toolbarItem => { 81 if (typeof toolbarItem === "string" || Constants.INLINE_TYPE.concat("|").includes(toolbarItem.name) || !toolbarItem.hotkey) { 82 return; 83 } 84 if (!window.siyuan.config.keymap.plugin) { 85 window.siyuan.config.keymap.plugin = {}; 86 } 87 if (!window.siyuan.config.keymap.plugin[options.name]) { 88 window.siyuan.config.keymap.plugin[options.name] = { 89 [toolbarItem.name]: { 90 default: toolbarItem.hotkey, 91 custom: toolbarItem.hotkey, 92 } 93 }; 94 } 95 if (!window.siyuan.config.keymap.plugin[options.name][toolbarItem.name]) { 96 window.siyuan.config.keymap.plugin[options.name][toolbarItem.name] = { 97 default: toolbarItem.hotkey, 98 custom: toolbarItem.hotkey, 99 }; 100 } 101 }); 102 } 103 104 public onload() { 105 // 加载 106 } 107 108 public onunload() { 109 // 禁用/关闭 110 } 111 112 public uninstall() { 113 // 卸载 114 } 115 116 public async updateCards(options: ICardData) { 117 return options; 118 } 119 120 public onLayoutReady() { 121 // 布局加载完成 122 } 123 124 public addCommand(command: ICommand) { 125 if (!window.siyuan.config.keymap.plugin) { 126 window.siyuan.config.keymap.plugin = {}; 127 } 128 if (!window.siyuan.config.keymap.plugin[this.name]) { 129 command.customHotkey = command.hotkey; 130 window.siyuan.config.keymap.plugin[this.name] = { 131 [command.langKey]: { 132 default: command.hotkey, 133 custom: command.hotkey, 134 } 135 }; 136 } else if (!window.siyuan.config.keymap.plugin[this.name][command.langKey]) { 137 command.customHotkey = command.hotkey; 138 window.siyuan.config.keymap.plugin[this.name][command.langKey] = { 139 default: command.hotkey, 140 custom: command.hotkey, 141 }; 142 } else if (window.siyuan.config.keymap.plugin[this.name][command.langKey]) { 143 if (typeof window.siyuan.config.keymap.plugin[this.name][command.langKey].custom === "string") { 144 command.customHotkey = window.siyuan.config.keymap.plugin[this.name][command.langKey].custom; 145 } else { 146 command.customHotkey = command.hotkey; 147 } 148 window.siyuan.config.keymap.plugin[this.name][command.langKey]["default"] = command.hotkey; 149 } 150 if (typeof command.customHotkey !== "string") { 151 console.error(`${this.name} - commands data is error and has been removed.`); 152 } else { 153 this.commands.push(command); 154 } 155 } 156 157 public addIcons(svg: string) { 158 const svgElement = document.querySelector(`svg[data-name="${this.name}"] defs`); 159 if (svgElement) { 160 svgElement.insertAdjacentHTML("afterbegin", svg); 161 } else { 162 const lastSvgElement = document.querySelector("body > svg:last-of-type"); 163 if (lastSvgElement) { 164 lastSvgElement.insertAdjacentHTML("afterend", `<svg data-name="${this.name}" style="position: absolute; width: 0; height: 0; overflow: hidden;" xmlns="http://www.w3.org/2000/svg"> 165<defs>${svg}</defs></svg>`); 166 } else { 167 document.body.insertAdjacentHTML("afterbegin", `<svg data-name="${this.name}" style="position: absolute; width: 0; height: 0; overflow: hidden;" xmlns="http://www.w3.org/2000/svg"> 168<defs>${svg}</defs></svg>`); 169 } 170 } 171 } 172 173 public addTopBar(options: { 174 icon: string, 175 title: string, 176 position?: "south" | "left", 177 callback: (evt: MouseEvent) => void 178 }) { 179 if (!options.icon.startsWith("icon") && !options.icon.startsWith("<svg")) { 180 console.error(`plugin ${this.name} addTopBar error: icon must be svg id or svg tag`); 181 return; 182 } 183 const iconElement = document.createElement("div"); 184 iconElement.setAttribute("data-menu", "true"); 185 iconElement.addEventListener("click", options.callback); 186 iconElement.id = `plugin_${this.name}_${this.topBarIcons.length}`; 187 if (isMobile()) { 188 iconElement.className = "b3-menu__item"; 189 iconElement.innerHTML = (options.icon.startsWith("icon") ? `<svg class="b3-menu__icon"><use xlink:href="#${options.icon}"></use></svg>` : options.icon) + 190 `<span class="b3-menu__label">${options.title}</span>`; 191 } else if (!isWindow()) { 192 iconElement.className = "toolbar__item ariaLabel"; 193 iconElement.setAttribute("aria-label", options.title); 194 iconElement.innerHTML = options.icon.startsWith("icon") ? `<svg><use xlink:href="#${options.icon}"></use></svg>` : options.icon; 195 iconElement.addEventListener("click", options.callback); 196 iconElement.setAttribute("data-location", options.position || "right"); 197 resizeTopBar(); 198 } 199 if (isMobile() && window.siyuan.storage) { 200 if (!window.siyuan.storage[Constants.LOCAL_PLUGINTOPUNPIN].includes(iconElement.id)) { 201 document.querySelector("#menuAbout")?.after(iconElement); 202 } 203 } else if (!isWindow() && window.siyuan.storage) { 204 if (window.siyuan.storage[Constants.LOCAL_PLUGINTOPUNPIN].includes(iconElement.id)) { 205 iconElement.classList.add("fn__none"); 206 } 207 document.querySelector("#" + (iconElement.getAttribute("data-location") === "right" ? "barPlugins" : "drag"))?.before(iconElement); 208 } 209 this.topBarIcons.push(iconElement); 210 return iconElement; 211 } 212 213 public addStatusBar(options: { 214 element: HTMLElement, 215 position?: "right" | "left", 216 }) { 217 /// #if !MOBILE 218 options.element.setAttribute("data-location", options.position || "right"); 219 this.statusBarIcons.push(options.element); 220 const statusElement = document.getElementById("status"); 221 if (statusElement) { 222 if (options.element.getAttribute("data-location") === "right") { 223 statusElement.insertAdjacentElement("beforeend", options.element); 224 } else { 225 statusElement.insertAdjacentElement("afterbegin", options.element); 226 } 227 } 228 return options.element; 229 /// #endif 230 } 231 232 public openSetting() { 233 if (!this.setting) { 234 return; 235 } 236 this.setting.open(this.displayName || this.name); 237 } 238 239 public loadData(storageName: string) { 240 if (typeof this.data[storageName] === "undefined") { 241 this.data[storageName] = ""; 242 } 243 return new Promise((resolve) => { 244 fetchPost("/api/file/getFile", {path: `/data/storage/petal/${this.name}/${storageName}`}, (response) => { 245 if (response.code !== 404) { 246 this.data[storageName] = response; 247 } 248 resolve(this.data[storageName]); 249 }); 250 }); 251 } 252 253 public saveData(storageName: string, data: any) { 254 if (window.siyuan.config.readonly || window.siyuan.isPublish) { 255 return; 256 } 257 258 return new Promise((resolve) => { 259 const pathString = `/data/storage/petal/${this.name}/${storageName}`; 260 let file: File; 261 if (typeof data === "object") { 262 file = new File([new Blob([JSON.stringify(data)], { 263 type: "application/json" 264 })], pathString.split("/").pop()); 265 } else { 266 file = new File([new Blob([data])], pathString.split("/").pop()); 267 } 268 const formData = new FormData(); 269 formData.append("path", pathString); 270 formData.append("file", file); 271 formData.append("isDir", "false"); 272 fetchPost("/api/file/putFile", formData, (response) => { 273 this.data[storageName] = data; 274 resolve(response); 275 }); 276 }); 277 } 278 279 public removeData(storageName: string) { 280 if (window.siyuan.config.readonly || window.siyuan.isPublish) { 281 return; 282 } 283 284 return new Promise((resolve) => { 285 if (!this.data) { 286 this.data = {}; 287 } 288 fetchPost("/api/file/removeFile", {path: `/data/storage/petal/${this.name}/${storageName}`}, (response) => { 289 delete this.data[storageName]; 290 resolve(response); 291 }); 292 }); 293 } 294 295 public getOpenedTab() { 296 const tabs: { [key: string]: Custom[] } = {}; 297 const modelKeys = Object.keys(this.models); 298 modelKeys.forEach(item => { 299 tabs[item.replace(this.name, "")] = []; 300 }); 301 /// #if !MOBILE 302 getAllModels().custom.find(item => { 303 if (modelKeys.includes(item.type)) { 304 tabs[item.type.replace(this.name, "")].push(item); 305 } 306 }); 307 /// #endif 308 return tabs; 309 } 310 311 public addTab(options: { 312 type: string, 313 destroy?: () => void, 314 beforeDestroy?: () => void, 315 resize?: () => void, 316 update?: () => void, 317 init: () => void 318 }) { 319 /// #if !MOBILE 320 const type2 = this.name + options.type; 321 this.models[type2] = (arg: { data: any, tab: Tab }) => { 322 const customObj = new Custom({ 323 app: this.app, 324 tab: arg.tab, 325 type: type2, 326 data: arg.data, 327 init: options.init, 328 beforeDestroy: options.beforeDestroy, 329 destroy: options.destroy, 330 resize: options.resize, 331 update: options.update, 332 }); 333 customObj.element.addEventListener("click", () => { 334 clearOBG(); 335 setPanelFocus(customObj.element.parentElement.parentElement); 336 }); 337 return customObj; 338 }; 339 return this.models[type2]; 340 /// #endif 341 } 342 343 public addDock(options: { 344 config: IPluginDockTab, 345 data: any, 346 type: string, 347 destroy?: () => void, 348 resize?: () => void, 349 update?: () => void, 350 init: () => void 351 }) { 352 const type2 = this.name + options.type; 353 if (typeof options.config.index === "undefined") { 354 options.config.index = 1000; 355 } 356 this.docks[type2] = { 357 config: options.config, 358 /// #if MOBILE 359 mobileModel: (element) => { 360 const customObj = new MobileCustom({ 361 element, 362 type: type2, 363 data: options.data, 364 init: options.init, 365 update: options.update, 366 destroy: options.destroy, 367 }); 368 return customObj; 369 }, 370 /// #else 371 model: (arg: { tab: Tab }) => { 372 const customObj = new Custom({ 373 app: this.app, 374 tab: arg.tab, 375 type: type2, 376 data: options.data, 377 init: options.init, 378 destroy: options.destroy, 379 resize: options.resize, 380 update: options.update, 381 }); 382 customObj.element.addEventListener("click", (event: MouseEvent) => { 383 setPanelFocus(customObj.element); 384 if (hasClosestByAttribute(event.target as HTMLElement, "data-type", "min")) { 385 getDockByType(type2).toggleModel(type2); 386 } 387 }); 388 customObj.element.classList.add("sy__" + type2); 389 return customObj; 390 } 391 /// #endif 392 }; 393 if (!window.siyuan.config.keymap.plugin) { 394 window.siyuan.config.keymap.plugin = {}; 395 } 396 if (options.config.hotkey) { 397 if (!window.siyuan.config.keymap.plugin[this.name]) { 398 window.siyuan.config.keymap.plugin[this.name] = { 399 [type2]: { 400 default: options.config.hotkey, 401 custom: options.config.hotkey, 402 } 403 }; 404 } else if (!window.siyuan.config.keymap.plugin[this.name][type2]) { 405 window.siyuan.config.keymap.plugin[this.name][type2] = { 406 default: options.config.hotkey, 407 custom: options.config.hotkey, 408 }; 409 } else if (window.siyuan.config.keymap.plugin[this.name][type2]) { 410 if (typeof window.siyuan.config.keymap.plugin[this.name][type2].custom !== "string") { 411 window.siyuan.config.keymap.plugin[this.name][type2].custom = options.config.hotkey; 412 } 413 window.siyuan.config.keymap.plugin[this.name][type2]["default"] = options.config.hotkey; 414 } 415 } 416 return this.docks[type2]; 417 } 418 419 public addFloatLayer = (options: { 420 refDefs: IRefDefs[], 421 x?: number, 422 y?: number, 423 targetElement?: HTMLElement, 424 originalRefBlockIDs?: IObject, 425 isBacklink: boolean, 426 }) => { 427 window.siyuan.blockPanels.push(new BlockPanel({ 428 app: this.app, 429 originalRefBlockIDs: options.originalRefBlockIDs, 430 targetElement: options.targetElement, 431 isBacklink: options.isBacklink, 432 x: options.x, 433 y: options.y, 434 refDefs: options.refDefs, 435 })); 436 }; 437 438 public updateProtyleToolbar(toolbar: Array<string | IMenuItem>) { 439 return toolbar; 440 } 441 442 set protyleOptions(options: IProtyleOptions) { 443 this.protyleOptionsValue = options; 444 } 445 446 get protyleOptions() { 447 return this.protyleOptionsValue; 448 } 449}