Initial commit

This commit is contained in:
MassiveBox 2025-09-19 19:16:32 +00:00
commit 04f54e248a
42 changed files with 4538 additions and 0 deletions

478
src/api.ts Normal file
View file

@ -0,0 +1,478 @@
/**
* Copyright (c) 2023 frostime. All rights reserved.
* https://github.com/frostime/sy-plugin-template-vite
*
* See API Document in [API.md](https://github.com/siyuan-note/siyuan/blob/master/API.md)
* API [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md)
*/
import { fetchPost, fetchSyncPost, IWebSocketData } from "siyuan";
export async function request(url: string, data: any) {
let response: IWebSocketData = await fetchSyncPost(url, data);
let res = response.code === 0 ? response.data : null;
return res;
}
// **************************************** Noteboook ****************************************
export async function lsNotebooks(): Promise<IReslsNotebooks> {
let url = '/api/notebook/lsNotebooks';
return request(url, '');
}
export async function openNotebook(notebook: NotebookId) {
let url = '/api/notebook/openNotebook';
return request(url, { notebook: notebook });
}
export async function closeNotebook(notebook: NotebookId) {
let url = '/api/notebook/closeNotebook';
return request(url, { notebook: notebook });
}
export async function renameNotebook(notebook: NotebookId, name: string) {
let url = '/api/notebook/renameNotebook';
return request(url, { notebook: notebook, name: name });
}
export async function createNotebook(name: string): Promise<Notebook> {
let url = '/api/notebook/createNotebook';
return request(url, { name: name });
}
export async function removeNotebook(notebook: NotebookId) {
let url = '/api/notebook/removeNotebook';
return request(url, { notebook: notebook });
}
export async function getNotebookConf(notebook: NotebookId): Promise<IResGetNotebookConf> {
let data = { notebook: notebook };
let url = '/api/notebook/getNotebookConf';
return request(url, data);
}
export async function setNotebookConf(notebook: NotebookId, conf: NotebookConf): Promise<NotebookConf> {
let data = { notebook: notebook, conf: conf };
let url = '/api/notebook/setNotebookConf';
return request(url, data);
}
// **************************************** File Tree ****************************************
export async function createDocWithMd(notebook: NotebookId, path: string, markdown: string): Promise<DocumentId> {
let data = {
notebook: notebook,
path: path,
markdown: markdown,
};
let url = '/api/filetree/createDocWithMd';
return request(url, data);
}
export async function renameDoc(notebook: NotebookId, path: string, title: string): Promise<DocumentId> {
let data = {
doc: notebook,
path: path,
title: title
};
let url = '/api/filetree/renameDoc';
return request(url, data);
}
export async function removeDoc(notebook: NotebookId, path: string) {
let data = {
notebook: notebook,
path: path,
};
let url = '/api/filetree/removeDoc';
return request(url, data);
}
export async function moveDocs(fromPaths: string[], toNotebook: NotebookId, toPath: string) {
let data = {
fromPaths: fromPaths,
toNotebook: toNotebook,
toPath: toPath
};
let url = '/api/filetree/moveDocs';
return request(url, data);
}
export async function getHPathByPath(notebook: NotebookId, path: string): Promise<string> {
let data = {
notebook: notebook,
path: path
};
let url = '/api/filetree/getHPathByPath';
return request(url, data);
}
export async function getHPathByID(id: BlockId): Promise<string> {
let data = {
id: id
};
let url = '/api/filetree/getHPathByID';
return request(url, data);
}
export async function getIDsByHPath(notebook: NotebookId, path: string): Promise<BlockId[]> {
let data = {
notebook: notebook,
path: path
};
let url = '/api/filetree/getIDsByHPath';
return request(url, data);
}
// **************************************** Asset Files ****************************************
export async function upload(assetsDirPath: string, files: any[]): Promise<IResUpload> {
let form = new FormData();
form.append('assetsDirPath', assetsDirPath);
for (let file of files) {
form.append('file[]', file);
}
let url = '/api/asset/upload';
return request(url, form);
}
// **************************************** Block ****************************************
type DataType = "markdown" | "dom";
export async function insertBlock(
dataType: DataType, data: string,
nextID?: BlockId, previousID?: BlockId, parentID?: BlockId
): Promise<IResdoOperations[]> {
let payload = {
dataType: dataType,
data: data,
nextID: nextID,
previousID: previousID,
parentID: parentID
}
let url = '/api/block/insertBlock';
return request(url, payload);
}
export async function prependBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise<IResdoOperations[]> {
let payload = {
dataType: dataType,
data: data,
parentID: parentID
}
let url = '/api/block/prependBlock';
return request(url, payload);
}
export async function appendBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise<IResdoOperations[]> {
let payload = {
dataType: dataType,
data: data,
parentID: parentID
}
let url = '/api/block/appendBlock';
return request(url, payload);
}
export async function updateBlock(dataType: DataType, data: string, id: BlockId): Promise<IResdoOperations[]> {
let payload = {
dataType: dataType,
data: data,
id: id
}
let url = '/api/block/updateBlock';
return request(url, payload);
}
export async function deleteBlock(id: BlockId): Promise<IResdoOperations[]> {
let data = {
id: id
}
let url = '/api/block/deleteBlock';
return request(url, data);
}
export async function moveBlock(id: BlockId, previousID?: PreviousID, parentID?: ParentID): Promise<IResdoOperations[]> {
let data = {
id: id,
previousID: previousID,
parentID: parentID
}
let url = '/api/block/moveBlock';
return request(url, data);
}
export async function foldBlock(id: BlockId) {
let data = {
id: id
}
let url = '/api/block/foldBlock';
return request(url, data);
}
export async function unfoldBlock(id: BlockId) {
let data = {
id: id
}
let url = '/api/block/unfoldBlock';
return request(url, data);
}
export async function getBlockKramdown(id: BlockId): Promise<IResGetBlockKramdown> {
let data = {
id: id
}
let url = '/api/block/getBlockKramdown';
return request(url, data);
}
export async function getChildBlocks(id: BlockId): Promise<IResGetChildBlock[]> {
let data = {
id: id
}
let url = '/api/block/getChildBlocks';
return request(url, data);
}
export async function transferBlockRef(fromID: BlockId, toID: BlockId, refIDs: BlockId[]) {
let data = {
fromID: fromID,
toID: toID,
refIDs: refIDs
}
let url = '/api/block/transferBlockRef';
return request(url, data);
}
// **************************************** Attributes ****************************************
export async function setBlockAttrs(id: BlockId, attrs: { [key: string]: string }) {
let data = {
id: id,
attrs: attrs
}
let url = '/api/attr/setBlockAttrs';
return request(url, data);
}
export async function getBlockAttrs(id: BlockId): Promise<{ [key: string]: string }> {
let data = {
id: id
}
let url = '/api/attr/getBlockAttrs';
return request(url, data);
}
// **************************************** SQL ****************************************
export async function sql(sql: string): Promise<any[]> {
let sqldata = {
stmt: sql,
};
let url = '/api/query/sql';
return request(url, sqldata);
}
export async function getBlockByID(blockId: string): Promise<Block> {
let sqlScript = `select * from blocks where id ='${blockId}'`;
let data = await sql(sqlScript);
return data[0];
}
// **************************************** Template ****************************************
export async function render(id: DocumentId, path: string): Promise<IResGetTemplates> {
let data = {
id: id,
path: path
}
let url = '/api/template/render';
return request(url, data);
}
export async function renderSprig(template: string): Promise<string> {
let url = '/api/template/renderSprig';
return request(url, { template: template });
}
// **************************************** File ****************************************
export async function getFile(path: string): Promise<any> {
let data = {
path: path
}
let url = '/api/file/getFile';
return new Promise((resolve, _) => {
fetchPost(url, data, (content: any) => {
resolve(content)
});
});
}
/**
* fetchPost will secretly convert data into json, this func merely return Blob
* @param endpoint
* @returns
*/
export const getFileBlob = async (path: string): Promise<Blob | null> => {
const endpoint = '/api/file/getFile'
let response = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({
path: path
})
});
if (!response.ok) {
return null;
}
let data = await response.blob();
return data;
}
export async function putFile(path: string, isDir: boolean, file: any) {
let form = new FormData();
form.append('path', path);
form.append('isDir', isDir.toString());
// Copyright (c) 2023, terwer.
// https://github.com/terwer/siyuan-plugin-importer/blob/v1.4.1/src/api/kernel-api.ts
form.append('modTime', Math.floor(Date.now() / 1000).toString());
form.append('file', file);
let url = '/api/file/putFile';
return request(url, form);
}
export async function removeFile(path: string) {
let data = {
path: path
}
let url = '/api/file/removeFile';
return request(url, data);
}
export async function readDir(path: string): Promise<IResReadDir> {
let data = {
path: path
}
let url = '/api/file/readDir';
return request(url, data);
}
// **************************************** Export ****************************************
export async function exportMdContent(id: DocumentId): Promise<IResExportMdContent> {
let data = {
id: id
}
let url = '/api/export/exportMdContent';
return request(url, data);
}
export async function exportResources(paths: string[], name: string): Promise<IResExportResources> {
let data = {
paths: paths,
name: name
}
let url = '/api/export/exportResources';
return request(url, data);
}
// **************************************** Convert ****************************************
export type PandocArgs = string;
export async function pandoc(args: PandocArgs[]) {
let data = {
args: args
}
let url = '/api/convert/pandoc';
return request(url, data);
}
// **************************************** Notification ****************************************
// /api/notification/pushMsg
// {
// "msg": "test",
// "timeout": 7000
// }
export async function pushMsg(msg: string, timeout: number = 7000) {
let payload = {
msg: msg,
timeout: timeout
};
let url = "/api/notification/pushMsg";
return request(url, payload);
}
export async function pushErrMsg(msg: string, timeout: number = 7000) {
let payload = {
msg: msg,
timeout: timeout
};
let url = "/api/notification/pushErrMsg";
return request(url, payload);
}
// **************************************** Network ****************************************
export async function forwardProxy(
url: string, method: string = 'GET', payload: any = {},
headers: any[] = [], timeout: number = 7000, contentType: string = "text/html"
): Promise<IResForwardProxy> {
let data = {
url: url,
method: method,
timeout: timeout,
contentType: contentType,
headers: headers,
payload: payload
}
let url1 = '/api/network/forwardProxy';
return request(url1, data);
}
// **************************************** System ****************************************
export async function bootProgress(): Promise<IResBootProgress> {
return request('/api/system/bootProgress', {});
}
export async function version(): Promise<string> {
return request('/api/system/version', {});
}
export async function currentTime(): Promise<number> {
return request('/api/system/currentTime', {});
}

63
src/hello.svelte Normal file
View file

@ -0,0 +1,63 @@
<!--
Copyright (c) 2024 by frostime. All Rights Reserved.
Author : frostime
Date : 2023-11-19 12:30:45
FilePath : /src/hello.svelte
LastEditTime : 2024-10-16 14:37:50
Description :
-->
<script lang="ts">
import { onDestroy, onMount } from "svelte";
// import { version } from "@/api";
import { showMessage, fetchPost, Protyle } from "siyuan";
export let app;
export let blockID: string;
let time: string = "";
let divProtyle: HTMLDivElement;
let protyle: any;
onMount(async () => {
// ver = await version();
fetchPost("/api/system/currentTime", {}, (response) => {
time = new Date(response.data).toString();
});
if (blockID) {
protyle = await initProtyle();
} else {
divProtyle.innerHTML = "Please open a document first";
}
});
onDestroy(() => {
showMessage("Hello panel closed");
protyle?.destroy();
});
async function initProtyle() {
return new Protyle(app, divProtyle, {
blockId: blockID
});
}
</script>
<div class="b3-dialog__content">
<div>appId:</div>
<div class="fn__hr"></div>
<div class="plugin-sample__time">${app?.appId}</div>
<div class="fn__hr"></div>
<div class="fn__hr"></div>
<div>API demo:</div>
<div class="fn__hr" />
<div class="plugin-sample__time">
System current time: <span id="time">{time}</span>
</div>
<div class="fn__hr" />
<div class="fn__hr" />
<div>Protyle demo: id = {blockID}</div>
<div class="fn__hr" />
<div id="protyle" style="height: 360px;" bind:this={divProtyle}/>
</div>

0
src/index.scss Normal file
View file

1011
src/index.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,118 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let type: string; // Setting Type
export let key: string;
export let value: any;
// Optional parameters
export let placeholder: string = "";
export let options: { [key: string | number]: string } = {};
export let slider: {
min: number;
max: number;
step: number;
} = { min: 0, max: 100, step: 1 };
export let button: {
label: string;
callback?: () => void;
} = { label: value, callback: () => {} };
export let fnSize: boolean = true; // If the form input is used within setting panel context, it is usually given a fixed width by a class named "fn__size200".
export let style: string = ""; // Custom style
const dispatch = createEventDispatcher();
function click() {
button?.callback();
dispatch("click", { key: key });
}
function changed() {
dispatch("changed", { key: key, value: value });
}
</script>
{#if type === "checkbox"}
<!-- Checkbox -->
<input
class="b3-switch fn__flex-center"
id={key}
type="checkbox"
bind:checked={value}
on:change={changed}
style={style}
/>
{:else if type === "textinput"}
<!-- Text Input -->
<input
class:b3-text-field={true}
class:fn__flex-center={true}
class:fn__size200={fnSize}
id={key}
{placeholder}
bind:value={value}
on:change={changed}
style={style}
/>
{:else if type === "textarea"}
<textarea
class="b3-text-field fn__block"
style={`resize: vertical; height: 10em; white-space: nowrap; ${style}`}
bind:value={value}
on:change={changed}
/>
{:else if type === "number"}
<input
class:b3-text-field={true}
class:fn__flex-center={true}
class:fn__size200={fnSize}
id={key}
type="number"
bind:value={value}
on:change={changed}
style={style}
/>
{:else if type === "button"}
<!-- Button Input -->
<button
class:b3-button={true}
class:b3-button--outline={true}
class:fn__flex-center={true}
class:fn__size200={fnSize}
id={key}
on:click={click}
style={style}
>
{button.label}
</button>
{:else if type === "select"}
<!-- Dropdown select -->
<select
class:b3-select={true}
class:fn__flex-center={true}
class:fn__size200={fnSize}
id="iconPosition"
bind:value={value}
on:change={changed}
style={style}
>
{#each Object.entries(options) as [value, text]}
<option {value}>{text}</option>
{/each}
</select>
{:else if type == "slider"}
<!-- Slider -->
<div class="b3-tooltips b3-tooltips__n" aria-label={value}>
<input
class:b3-slider={true}
class:fn__size200={fnSize}
id="fontSize"
min={slider.min}
max={slider.max}
step={slider.step}
type="range"
bind:value={value}
on:change={changed}
style={style}
/>
</div>
{/if}

View file

@ -0,0 +1,53 @@
<!--
Copyright (c) 2024 by frostime. All Rights Reserved.
Author : frostime
Date : 2024-06-01 20:03:50
FilePath : /src/libs/components/item-wrap.svelte
LastEditTime : 2024-07-19 15:28:57
Description : The setting item container
-->
<script lang="ts">
export let title: string; // Displayint Setting Title
export let description: string; // Displaying Setting Text
export let direction: 'row' | 'column' = 'column';
</script>
{#if direction === "row"}
<div class="item-wrap b3-label" data-key="CustomCSS">
<div class="fn__block">
<span class="title">{title}</span>
<div class="b3-label__text">{@html description}</div>
<div class="fn__hr"></div>
<div style="display: flex; flex-direction: column; gap: 5px; position: relative;">
<slot />
</div>
</div>
</div>
{:else}
<div class="item-wrap fn__flex b3-label config__item">
<div class="fn__flex-1">
<span class="title">{title}</span>
<div class="b3-label__text">
{@html description}
</div>
</div>
<span class="fn__space" />
<slot />
</div>
{/if}
<style>
span.title {
font-weight: bold;
color: var(--b3-theme-primary)
}
.item-wrap.b3-label {
box-shadow: none !important;
padding-bottom: 16px;
margin-bottom: 16px;
}
.item-wrap.b3-label:not(:last-child) {
border-bottom: 1px solid var(--b3-border-color);
}
</style>

View file

@ -0,0 +1,6 @@
import FormInput from './form-input.svelte';
import FormWrap from './form-wrap.svelte';
const Form = { Wrap: FormWrap, Input: FormInput };
export default Form;
export { FormInput, FormWrap };

View file

@ -0,0 +1,3 @@
<div class="item__readme b3-typography">
<slot/>
</div>

View file

@ -0,0 +1,51 @@
<!--
Copyright (c) 2023 by frostime All Rights Reserved.
Author : frostime
Date : 2023-07-01 19:23:50
FilePath : /src/libs/components/setting-panel.svelte
LastEditTime : 2024-08-09 21:41:07
Description :
-->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Form from './Form';
export let group: string;
export let settingItems: ISettingItem[];
export let display: boolean = true;
const dispatch = createEventDispatcher();
function onClick( {detail}) {
dispatch("click", { key: detail.key });
}
function onChanged( {detail}) {
dispatch("changed", {group: group, ...detail});
}
$: fn__none = display ? "" : "fn__none";
</script>
<div class="config__tab-container {fn__none}" data-name={group}>
<slot />
{#each settingItems as item (item.key)}
<Form.Wrap
title={item.title}
description={item.description}
direction={item?.direction}
>
<Form.Input
type={item.type}
key={item.key}
bind:value={item.value}
placeholder={item?.placeholder}
options={item?.options}
slider={item?.slider}
button={item?.button}
on:click={onClick}
on:changed={onChanged}
/>
</Form.Wrap>
{/each}
</div>

99
src/libs/const.ts Normal file
View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2024 by frostime. All Rights Reserved.
* @Author : frostime
* @Date : 2024-06-08 20:36:30
* @FilePath : /src/libs/const.ts
* @LastEditTime : 2024-06-08 20:48:06
* @Description :
*/
export const BlockType2NodeType: {[key in BlockType]: string} = {
d: 'NodeDocument',
p: 'NodeParagraph',
query_embed: 'NodeBlockQueryEmbed',
l: 'NodeList',
i: 'NodeListItem',
h: 'NodeHeading',
iframe: 'NodeIFrame',
tb: 'NodeThematicBreak',
b: 'NodeBlockquote',
s: 'NodeSuperBlock',
c: 'NodeCodeBlock',
widget: 'NodeWidget',
t: 'NodeTable',
html: 'NodeHTMLBlock',
m: 'NodeMathBlock',
av: 'NodeAttributeView',
audio: 'NodeAudio'
}
export const NodeIcons = {
NodeAttributeView: {
icon: "iconDatabase"
},
NodeAudio: {
icon: "iconRecord"
},
NodeBlockQueryEmbed: {
icon: "iconSQL"
},
NodeBlockquote: {
icon: "iconQuote"
},
NodeCodeBlock: {
icon: "iconCode"
},
NodeDocument: {
icon: "iconFile"
},
NodeHTMLBlock: {
icon: "iconHTML5"
},
NodeHeading: {
icon: "iconHeadings",
subtypes: {
h1: { icon: "iconH1" },
h2: { icon: "iconH2" },
h3: { icon: "iconH3" },
h4: { icon: "iconH4" },
h5: { icon: "iconH5" },
h6: { icon: "iconH6" }
}
},
NodeIFrame: {
icon: "iconLanguage"
},
NodeList: {
subtypes: {
o: { icon: "iconOrderedList" },
t: { icon: "iconCheck" },
u: { icon: "iconList" }
}
},
NodeListItem: {
icon: "iconListItem"
},
NodeMathBlock: {
icon: "iconMath"
},
NodeParagraph: {
icon: "iconParagraph"
},
NodeSuperBlock: {
icon: "iconSuper"
},
NodeTable: {
icon: "iconTable"
},
NodeThematicBreak: {
icon: "iconLine"
},
NodeVideo: {
icon: "iconVideo"
},
NodeWidget: {
icon: "iconBoth"
}
};

164
src/libs/dialog.ts Normal file
View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2024 by frostime. All Rights Reserved.
* @Author : frostime
* @Date : 2024-03-23 21:37:33
* @FilePath : /src/libs/dialog.ts
* @LastEditTime : 2024-10-16 14:31:04
* @Description : Kits about dialogs
*/
import { Dialog } from "siyuan";
import { type SvelteComponent } from "svelte";
export const inputDialog = (args: {
title: string, placeholder?: string, defaultText?: string,
confirm?: (text: string) => void, cancel?: () => void,
width?: string, height?: string
}) => {
const dialog = new Dialog({
title: args.title,
content: `<div class="b3-dialog__content">
<div class="ft__breakword"><textarea class="b3-text-field fn__block" style="height: 100%;" placeholder=${args?.placeholder ?? ''}>${args?.defaultText ?? ''}</textarea></div>
</div>
<div class="b3-dialog__action">
<button class="b3-button b3-button--cancel">${window.siyuan.languages.cancel}</button><div class="fn__space"></div>
<button class="b3-button b3-button--text" id="confirmDialogConfirmBtn">${window.siyuan.languages.confirm}</button>
</div>`,
width: args.width ?? "520px",
height: args.height
});
const target: HTMLTextAreaElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword>textarea");
const btnsElement = dialog.element.querySelectorAll(".b3-button");
btnsElement[0].addEventListener("click", () => {
if (args?.cancel) {
args.cancel();
}
dialog.destroy();
});
btnsElement[1].addEventListener("click", () => {
if (args?.confirm) {
args.confirm(target.value);
}
dialog.destroy();
});
};
export const inputDialogSync = async (args: {
title: string, placeholder?: string, defaultText?: string,
width?: string, height?: string
}) => {
return new Promise<string>((resolve) => {
let newargs = {
...args, confirm: (text) => {
resolve(text);
}, cancel: () => {
resolve(null);
}
};
inputDialog(newargs);
});
}
interface IConfirmDialogArgs {
title: string;
content: string | HTMLElement;
confirm?: (ele?: HTMLElement) => void;
cancel?: (ele?: HTMLElement) => void;
width?: string;
height?: string;
}
export const confirmDialog = (args: IConfirmDialogArgs) => {
const { title, content, confirm, cancel, width, height } = args;
const dialog = new Dialog({
title,
content: `<div class="b3-dialog__content">
<div class="ft__breakword">
</div>
</div>
<div class="b3-dialog__action">
<button class="b3-button b3-button--cancel">${window.siyuan.languages.cancel}</button><div class="fn__space"></div>
<button class="b3-button b3-button--text" id="confirmDialogConfirmBtn">${window.siyuan.languages.confirm}</button>
</div>`,
width: width,
height: height
});
const target: HTMLElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword");
if (typeof content === "string") {
target.innerHTML = content;
} else {
target.appendChild(content);
}
const btnsElement = dialog.element.querySelectorAll(".b3-button");
btnsElement[0].addEventListener("click", () => {
if (cancel) {
cancel(target);
}
dialog.destroy();
});
btnsElement[1].addEventListener("click", () => {
if (confirm) {
confirm(target);
}
dialog.destroy();
});
};
export const confirmDialogSync = async (args: IConfirmDialogArgs) => {
return new Promise<HTMLElement>((resolve) => {
let newargs = {
...args, confirm: (ele: HTMLElement) => {
resolve(ele);
}, cancel: (ele: HTMLElement) => {
resolve(ele);
}
};
confirmDialog(newargs);
});
};
export const simpleDialog = (args: {
title: string, ele: HTMLElement | DocumentFragment,
width?: string, height?: string,
callback?: () => void;
}) => {
const dialog = new Dialog({
title: args.title,
content: `<div class="dialog-content" style="display: flex; height: 100%;"/>`,
width: args.width,
height: args.height,
destroyCallback: args.callback
});
dialog.element.querySelector(".dialog-content").appendChild(args.ele);
return {
dialog,
close: dialog.destroy.bind(dialog)
};
}
export const svelteDialog = (args: {
title: string, constructor: (container: HTMLElement) => SvelteComponent,
width?: string, height?: string,
callback?: () => void;
}) => {
let container = document.createElement('div')
container.style.display = 'contents';
let component = args.constructor(container);
const { dialog, close } = simpleDialog({
...args, ele: container, callback: () => {
component.$destroy();
if (args.callback) args.callback();
}
});
return {
component,
dialog,
close
}
}

43
src/libs/index.d.ts vendored Normal file
View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 by frostime. All Rights Reserved.
* @Author : frostime
* @Date : 2024-04-19 18:30:12
* @FilePath : /src/libs/index.d.ts
* @LastEditTime : 2024-04-30 16:39:54
* @Description :
*/
type TSettingItemType = "checkbox" | "select" | "textinput" | "textarea" | "number" | "slider" | "button" | "hint" | "custom";
interface ISettingItemCore {
type: TSettingItemType;
key: string;
value: any;
placeholder?: string;
slider?: {
min: number;
max: number;
step: number;
};
options?: { [key: string | number]: string };
button?: {
label: string;
callback: () => void;
}
}
interface ISettingItem extends ISettingItemCore {
title: string;
description: string;
direction?: "row" | "column";
}
//Interface for setting-utils
interface ISettingUtilsItem extends ISettingItem {
action?: {
callback: () => void;
}
createElement?: (currentVal: any) => HTMLElement;
getEleVal?: (ele: HTMLElement) => any;
setEleVal?: (ele: HTMLElement, val: any) => void;
}

48
src/libs/promise-pool.ts Normal file
View file

@ -0,0 +1,48 @@
export default class PromiseLimitPool<T> {
private maxConcurrent: number;
private currentRunning = 0;
private queue: (() => void)[] = [];
private promises: Promise<T>[] = [];
constructor(maxConcurrent: number) {
this.maxConcurrent = maxConcurrent;
}
add(fn: () => Promise<T>): void {
const promise = new Promise<T>((resolve, reject) => {
const run = async () => {
try {
this.currentRunning++;
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.currentRunning--;
this.next();
}
};
if (this.currentRunning < this.maxConcurrent) {
run();
} else {
this.queue.push(run);
}
});
this.promises.push(promise);
}
async awaitAll(): Promise<T[]> {
return Promise.all(this.promises);
}
/**
* Handles the next task in the queue.
*/
private next(): void {
if (this.queue.length > 0 && this.currentRunning < this.maxConcurrent) {
const nextRun = this.queue.shift()!;
nextRun();
}
}
}

397
src/libs/setting-utils.ts Normal file
View file

@ -0,0 +1,397 @@
/*
* Copyright (c) 2023 by frostime. All Rights Reserved.
* @Author : frostime
* @Date : 2023-12-17 18:28:19
* @FilePath : /src/libs/setting-utils.ts
* @LastEditTime : 2024-05-01 17:44:16
* @Description :
*/
import { Plugin, Setting } from 'siyuan';
/**
* The default function to get the value of the element
* @param type
* @returns
*/
const createDefaultGetter = (type: TSettingItemType) => {
let getter: (ele: HTMLElement) => any;
switch (type) {
case 'checkbox':
getter = (ele: HTMLInputElement) => {
return ele.checked;
};
break;
case 'select':
case 'slider':
case 'textinput':
case 'textarea':
getter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => {
return ele.value;
};
break;
case 'number':
getter = (ele: HTMLInputElement) => {
return parseInt(ele.value);
}
break;
default:
getter = () => null;
break;
}
return getter;
}
/**
* The default function to set the value of the element
* @param type
* @returns
*/
const createDefaultSetter = (type: TSettingItemType) => {
let setter: (ele: HTMLElement, value: any) => void;
switch (type) {
case 'checkbox':
setter = (ele: HTMLInputElement, value: any) => {
ele.checked = value;
};
break;
case 'select':
case 'slider':
case 'textinput':
case 'textarea':
case 'number':
setter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: any) => {
ele.value = value;
};
break;
default:
setter = () => {};
break;
}
return setter;
}
export class SettingUtils {
plugin: Plugin;
name: string;
file: string;
settings: Map<string, ISettingUtilsItem> = new Map();
elements: Map<string, HTMLElement> = new Map();
constructor(args: {
plugin: Plugin,
name?: string,
callback?: (data: any) => void,
width?: string,
height?: string
}) {
this.name = args.name ?? 'settings';
this.plugin = args.plugin;
this.file = this.name.endsWith('.json') ? this.name : `${this.name}.json`;
this.plugin.setting = new Setting({
width: args.width,
height: args.height,
confirmCallback: () => {
for (let key of this.settings.keys()) {
this.updateValueFromElement(key);
}
let data = this.dump();
if (args.callback !== undefined) {
args.callback(data);
}
this.plugin.data[this.name] = data;
this.save(data);
},
destroyCallback: () => {
//Restore the original value
for (let key of this.settings.keys()) {
this.updateElementFromValue(key);
}
}
});
}
async load() {
let data = await this.plugin.loadData(this.file);
console.debug('Load config:', data);
if (data) {
for (let [key, item] of this.settings) {
item.value = data?.[key] ?? item.value;
}
}
this.plugin.data[this.name] = this.dump();
return data;
}
async save(data?: any) {
data = data ?? this.dump();
await this.plugin.saveData(this.file, this.dump());
console.debug('Save config:', data);
return data;
}
/**
* read the data after saving
* @param key key name
* @returns setting item value
*/
get(key: string) {
return this.settings.get(key)?.value;
}
/**
* Set data to this.settings,
* but do not save it to the configuration file
* @param key key name
* @param value value
*/
set(key: string, value: any) {
let item = this.settings.get(key);
if (item) {
item.value = value;
this.updateElementFromValue(key);
}
}
/**
* Set and save setting item value
* If you want to set and save immediately you can use this method
* @param key key name
* @param value value
*/
async setAndSave(key: string, value: any) {
let item = this.settings.get(key);
if (item) {
item.value = value;
this.updateElementFromValue(key);
await this.save();
}
}
/**
* Read in the value of element instead of setting obj in real time
* @param key key name
* @param apply whether to apply the value to the setting object
* if true, the value will be applied to the setting object
* @returns value in html
*/
take(key: string, apply: boolean = false) {
let item = this.settings.get(key);
let element = this.elements.get(key) as any;
if (!element) {
return
}
if (apply) {
this.updateValueFromElement(key);
}
return item.getEleVal(element);
}
/**
* Read data from html and save it
* @param key key name
* @param value value
* @return value in html
*/
async takeAndSave(key: string) {
let value = this.take(key, true);
await this.save();
return value;
}
/**
* Disable setting item
* @param key key name
*/
disable(key: string) {
let element = this.elements.get(key) as any;
if (element) {
element.disabled = true;
}
}
/**
* Enable setting item
* @param key key name
*/
enable(key: string) {
let element = this.elements.get(key) as any;
if (element) {
element.disabled = false;
}
}
/**
* JSON
* @returns object
*/
dump(): Object {
let data: any = {};
for (let [key, item] of this.settings) {
if (item.type === 'button') continue;
data[key] = item.value;
}
return data;
}
addItem(item: ISettingUtilsItem) {
this.settings.set(item.key, item);
const IsCustom = item.type === 'custom';
let error = IsCustom && (item.createElement === undefined || item.getEleVal === undefined || item.setEleVal === undefined);
if (error) {
console.error('The custom setting item must have createElement, getEleVal and setEleVal methods');
return;
}
if (item.getEleVal === undefined) {
item.getEleVal = createDefaultGetter(item.type);
}
if (item.setEleVal === undefined) {
item.setEleVal = createDefaultSetter(item.type);
}
if (item.createElement === undefined) {
let itemElement = this.createDefaultElement(item);
this.elements.set(item.key, itemElement);
this.plugin.setting.addItem({
title: item.title,
description: item?.description,
direction: item?.direction,
createActionElement: () => {
this.updateElementFromValue(item.key);
let element = this.getElement(item.key);
return element;
}
});
} else {
this.plugin.setting.addItem({
title: item.title,
description: item?.description,
direction: item?.direction,
createActionElement: () => {
let val = this.get(item.key);
let element = item.createElement(val);
this.elements.set(item.key, element);
return element;
}
});
}
}
createDefaultElement(item: ISettingUtilsItem) {
let itemElement: HTMLElement;
//阻止思源内置的回车键确认
const preventEnterConfirm = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopImmediatePropagation();
}
}
switch (item.type) {
case 'checkbox':
let element: HTMLInputElement = document.createElement('input');
element.type = 'checkbox';
element.checked = item.value;
element.className = "b3-switch fn__flex-center";
itemElement = element;
element.onchange = item.action?.callback ?? (() => { });
break;
case 'select':
let selectElement: HTMLSelectElement = document.createElement('select');
selectElement.className = "b3-select fn__flex-center fn__size200";
let options = item?.options ?? {};
for (let val in options) {
let optionElement = document.createElement('option');
let text = options[val];
optionElement.value = val;
optionElement.text = text;
selectElement.appendChild(optionElement);
}
selectElement.value = item.value;
selectElement.onchange = item.action?.callback ?? (() => { });
itemElement = selectElement;
break;
case 'slider':
let sliderElement: HTMLInputElement = document.createElement('input');
sliderElement.type = 'range';
sliderElement.className = 'b3-slider fn__size200 b3-tooltips b3-tooltips__n';
sliderElement.ariaLabel = item.value;
sliderElement.min = item.slider?.min.toString() ?? '0';
sliderElement.max = item.slider?.max.toString() ?? '100';
sliderElement.step = item.slider?.step.toString() ?? '1';
sliderElement.value = item.value;
sliderElement.onchange = () => {
sliderElement.ariaLabel = sliderElement.value;
item.action?.callback();
}
itemElement = sliderElement;
break;
case 'textinput':
let textInputElement: HTMLInputElement = document.createElement('input');
textInputElement.className = 'b3-text-field fn__flex-center fn__size200';
textInputElement.value = item.value;
textInputElement.onchange = item.action?.callback ?? (() => { });
itemElement = textInputElement;
textInputElement.addEventListener('keydown', preventEnterConfirm);
break;
case 'textarea':
let textareaElement: HTMLTextAreaElement = document.createElement('textarea');
textareaElement.className = "b3-text-field fn__block";
textareaElement.value = item.value;
textareaElement.onchange = item.action?.callback ?? (() => { });
itemElement = textareaElement;
break;
case 'number':
let numberElement: HTMLInputElement = document.createElement('input');
numberElement.type = 'number';
numberElement.className = 'b3-text-field fn__flex-center fn__size200';
numberElement.value = item.value;
itemElement = numberElement;
numberElement.addEventListener('keydown', preventEnterConfirm);
break;
case 'button':
let buttonElement: HTMLButtonElement = document.createElement('button');
buttonElement.className = "b3-button b3-button--outline fn__flex-center fn__size200";
buttonElement.innerText = item.button?.label ?? 'Button';
buttonElement.onclick = item.button?.callback ?? (() => { });
itemElement = buttonElement;
break;
case 'hint':
let hintElement: HTMLElement = document.createElement('div');
hintElement.className = 'b3-label fn__flex-center';
itemElement = hintElement;
break;
}
return itemElement;
}
/**
* return the setting element
* @param key key name
* @returns element
*/
getElement(key: string) {
// let item = this.settings.get(key);
let element = this.elements.get(key) as any;
return element;
}
private updateValueFromElement(key: string) {
let item = this.settings.get(key);
if (item.type === 'button') return;
let element = this.elements.get(key) as any;
item.value = item.getEleVal(element);
}
private updateElementFromValue(key: string) {
let item = this.settings.get(key);
if (item.type === 'button') return;
let element = this.elements.get(key) as any;
item.setEleVal(element, item.value);
}
}

139
src/setting-example.svelte Normal file
View file

@ -0,0 +1,139 @@
<script lang="ts">
import { showMessage } from "siyuan";
import SettingPanel from "./libs/components/setting-panel.svelte";
let groups: string[] = ["🌈 Group 1", "✨ Group 2"];
let focusGroup = groups[0];
const group1Items: ISettingItem[] = [
{
type: 'checkbox',
title: 'checkbox',
description: 'checkbox',
key: 'a',
value: true
},
{
type: 'textinput',
title: 'text',
description: 'This is a text',
key: 'b',
value: 'This is a text',
placeholder: 'placeholder'
},
{
type: 'textarea',
title: 'textarea',
description: 'This is a textarea',
key: 'b2',
value: 'This is a textarea',
placeholder: 'placeholder',
direction: 'row'
},
{
type: 'select',
title: 'select',
description: 'select',
key: 'c',
value: 'x',
options: {
x: 'x',
y: 'y',
z: 'z'
}
}
];
const group2Items: ISettingItem[] = [
{
type: 'button',
title: 'button',
description: 'This is a button',
key: 'e',
value: 'Click Button',
button: {
label: 'Click Me',
callback: () => {
showMessage('Hello, world!');
}
}
},
{
type: 'slider',
title: 'slider',
description: 'slider',
key: 'd',
value: 50,
slider: {
min: 0,
max: 100,
step: 1
}
}
];
/********** Events **********/
interface ChangeEvent {
group: string;
key: string;
value: any;
}
const onChanged = ({ detail }: CustomEvent<ChangeEvent>) => {
if (detail.group === groups[0]) {
// setting.set(detail.key, detail.value);
//Please add your code here
//Udpate the plugins setting data, don't forget to call plugin.save() for data persistence
}
};
</script>
<div class="fn__flex-1 fn__flex config__panel">
<ul class="b3-tab-bar b3-list b3-list--background">
{#each groups as group}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<li
data-name="editor"
class:b3-list-item--focus={group === focusGroup}
class="b3-list-item"
on:click={() => {
focusGroup = group;
}}
on:keydown={() => {}}
>
<span class="b3-list-item__text">{group}</span>
</li>
{/each}
</ul>
<div class="config__tab-wrap">
<SettingPanel
group={groups[0]}
settingItems={group1Items}
display={focusGroup === groups[0]}
on:changed={onChanged}
on:click={({ detail }) => { console.debug("Click:", detail.key); }}
>
<div class="fn__flex b3-label">
💡 This is our default settings.
</div>
</SettingPanel>
<SettingPanel
group={groups[1]}
settingItems={group2Items}
display={focusGroup === groups[1]}
on:changed={onChanged}
on:click={({ detail }) => { console.debug("Click:", detail.key); }}
>
</SettingPanel>
</div>
</div>
<style lang="scss">
.config__panel {
height: 100%;
}
.config__panel > ul > li {
padding-left: 1rem;
}
</style>

65
src/types/api.d.ts vendored Normal file
View file

@ -0,0 +1,65 @@
interface IResGetNotebookConf {
box: string;
conf: NotebookConf;
name: string;
}
interface IReslsNotebooks {
notebooks: Notebook[];
}
interface IResUpload {
errFiles: string[];
succMap: { [key: string]: string };
}
interface IResdoOperations {
doOperations: doOperation[];
undoOperations: doOperation[] | null;
}
interface IResGetBlockKramdown {
id: BlockId;
kramdown: string;
}
interface IResGetChildBlock {
id: BlockId;
type: BlockType;
subtype?: BlockSubType;
}
interface IResGetTemplates {
content: string;
path: string;
}
interface IResReadDir {
isDir: boolean;
isSymlink: boolean;
name: string;
}
interface IResExportMdContent {
hPath: string;
content: string;
}
interface IResBootProgress {
progress: number;
details: string;
}
interface IResForwardProxy {
body: string;
contentType: string;
elapsed: number;
headers: { [key: string]: string };
status: number;
url: string;
}
interface IResExportResources {
path: string;
}

106
src/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2024 by frostime. All Rights Reserved.
* @Author : frostime
* @Date : 2023-08-15 10:28:10
* @FilePath : /src/types/index.d.ts
* @LastEditTime : 2024-06-08 20:50:53
* @Description : Frequently used data structures in SiYuan
*/
type DocumentId = string;
type BlockId = string;
type NotebookId = string;
type PreviousID = BlockId;
type ParentID = BlockId | DocumentId;
type Notebook = {
id: NotebookId;
name: string;
icon: string;
sort: number;
closed: boolean;
}
type NotebookConf = {
name: string;
closed: boolean;
refCreateSavePath: string;
createDocNameTemplate: string;
dailyNoteSavePath: string;
dailyNoteTemplatePath: string;
}
type BlockType =
| 'd'
| 'p'
| 'query_embed'
| 'l'
| 'i'
| 'h'
| 'iframe'
| 'tb'
| 'b'
| 's'
| 'c'
| 'widget'
| 't'
| 'html'
| 'm'
| 'av'
| 'audio';
type BlockSubType = "d1" | "d2" | "s1" | "s2" | "s3" | "t1" | "t2" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "table" | "task" | "toggle" | "latex" | "quote" | "html" | "code" | "footnote" | "cite" | "collection" | "bookmark" | "attachment" | "comment" | "mindmap" | "spreadsheet" | "calendar" | "image" | "audio" | "video" | "other";
type Block = {
id: BlockId;
parent_id?: BlockId;
root_id: DocumentId;
hash: string;
box: string;
path: string;
hpath: string;
name: string;
alias: string;
memo: string;
tag: string;
content: string;
fcontent?: string;
markdown: string;
length: number;
type: BlockType;
subtype: BlockSubType;
/** string of { [key: string]: string }
* For instance: "{: custom-type=\"query-code\" id=\"20230613234017-zkw3pr0\" updated=\"20230613234509\"}"
*/
ial?: string;
sort: number;
created: string;
updated: string;
}
type doOperation = {
action: string;
data: string;
id: BlockId;
parentID: BlockId | DocumentId;
previousID: BlockId;
retData: null;
}
interface Window {
siyuan: {
config: any;
notebooks: any;
menus: any;
dialogs: any;
blockPanels: any;
storage: any;
user: any;
ws: any;
languages: any;
emojis: any;
};
Lute: any;
}