├── .eslintignore
├── CHANGELOG.md
├── scripts
├── .gitignore
└── make_dev_link.js
├── icon.png
├── preview.png
├── asset
└── action.png
├── src
├── libs
│ ├── b3-typography.svelte
│ ├── index.d.ts
│ ├── setting-panel.svelte
│ ├── setting-item.svelte
│ └── setting-utils.ts
├── index.scss
├── types
│ ├── api.d.ts
│ └── index.d.ts
├── hello.svelte
├── i18n
│ ├── zh_CN.json
│ └── en_US.json
├── setting-example.svelte
├── api.ts
└── index.ts
├── tools
├── orginal_logo.jpeg
└── orginal_header.jpeg
├── .gitignore
├── svelte.config.js
├── tsconfig.node.json
├── plugin.json
├── package.json
├── LICENSE
├── .eslintrc.cjs
├── README_zh_CN.md
├── tsconfig.json
├── .github
└── workflows
│ └── release.yml
├── README.md
└── vite.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.0.1 2024.01.13
2 | - init
--------------------------------------------------------------------------------
/scripts/.gitignore:
--------------------------------------------------------------------------------
1 | .venv
2 | build
3 | dist
4 | *.exe
5 | *.spec
6 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zxkmm/siyuan_streamer_mode/HEAD/icon.png
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zxkmm/siyuan_streamer_mode/HEAD/preview.png
--------------------------------------------------------------------------------
/asset/action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zxkmm/siyuan_streamer_mode/HEAD/asset/action.png
--------------------------------------------------------------------------------
/src/libs/b3-typography.svelte:
--------------------------------------------------------------------------------
1 |
39 |
appId:
40 |
41 |
${app?.appId}
42 |
43 |
44 |
API demo:
45 |
46 |
47 | System current time: {time}
48 |
49 |
50 |
51 |
Protyle demo: id = {blockID}
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release on Tag Push
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | # Checkout
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 |
16 | # Install Node.js
17 | - name: Install Node.js
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: 20
21 | registry-url: "https://registry.npmjs.org"
22 |
23 | # Install pnpm
24 | - name: Install pnpm
25 | uses: pnpm/action-setup@v4
26 | id: pnpm-install
27 | with:
28 | version: 8
29 | run_install: false
30 |
31 | # Get pnpm store directory
32 | - name: Get pnpm store directory
33 | id: pnpm-cache
34 | shell: bash
35 | run: |
36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
37 |
38 | # Setup pnpm cache
39 | - name: Setup pnpm cache
40 | uses: actions/cache@v3
41 | with:
42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
44 | restore-keys: |
45 | ${{ runner.os }}-pnpm-store-
46 |
47 | # Install dependencies
48 | - name: Install dependencies
49 | run: pnpm install
50 |
51 | # Build for production, 这一步会生成一个 package.zip
52 | - name: Build for production
53 | run: pnpm build
54 |
55 | - name: Release
56 | uses: ncipollo/release-action@v1
57 | with:
58 | allowUpdates: true
59 | artifactErrorsFailBuild: true
60 | artifacts: "package.zip"
61 | token: ${{ secrets.GITHUB_TOKEN }}
62 | prerelease: true
63 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2023 frostime. All rights reserved.
3 | */
4 |
5 | /**
6 | * Frequently used data structures in SiYuan
7 | */
8 | type DocumentId = string;
9 | type BlockId = string;
10 | type NotebookId = string;
11 | type PreviousID = BlockId;
12 | type ParentID = BlockId | DocumentId;
13 |
14 | type Notebook = {
15 | id: NotebookId;
16 | name: string;
17 | icon: string;
18 | sort: number;
19 | closed: boolean;
20 | }
21 |
22 | type NotebookConf = {
23 | name: string;
24 | closed: boolean;
25 | refCreateSavePath: string;
26 | createDocNameTemplate: string;
27 | dailyNoteSavePath: string;
28 | dailyNoteTemplatePath: string;
29 | }
30 |
31 | type BlockType = "d" | "s" | "h" | "t" | "i" | "p" | "f" | "audio" | "video" | "other";
32 |
33 | 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";
34 |
35 | type Block = {
36 | id: BlockId;
37 | parent_id?: BlockId;
38 | root_id: DocumentId;
39 | hash: string;
40 | box: string;
41 | path: string;
42 | hpath: string;
43 | name: string;
44 | alias: string;
45 | memo: string;
46 | tag: string;
47 | content: string;
48 | fcontent?: string;
49 | markdown: string;
50 | length: number;
51 | type: BlockType;
52 | subtype: BlockSubType;
53 | /** string of { [key: string]: string }
54 | * For instance: "{: custom-type=\"query-code\" id=\"20230613234017-zkw3pr0\" updated=\"20230613234509\"}"
55 | */
56 | ial?: string;
57 | sort: number;
58 | created: string;
59 | updated: string;
60 | }
61 |
62 | type doOperation = {
63 | action: string;
64 | data: string;
65 | id: BlockId;
66 | parentID: BlockId | DocumentId;
67 | previousID: BlockId;
68 | retData: null;
69 | }
70 |
71 | interface Window {
72 | siyuan: {
73 | notebooks: any;
74 | menus: any;
75 | dialogs: any;
76 | blockPanels: any;
77 | storage: any;
78 | user: any;
79 | ws: any;
80 | languages: any;
81 | };
82 | }
83 |
--------------------------------------------------------------------------------
/src/i18n/zh_CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "warnTitle": "免责申明",
3 | "warnDesc": "我努力确保本功能在保护您的隐私方面尽可能有效,但由于计算机系统的复杂性和不确定性,我无法保证此功能能提供绝对的隐私保护。
例如(不限于):技术故障、系统错误。
本插件本身只能避免视觉上的隐私泄漏,您的所有数据依然(而且从来都)是明文保存(所以这不是该插件带来的结果),所以如下情况依然可以获取到你的数据。
例如(不限于):直接读取DOM、直接读取硬盘、屏幕阅读器、滥用系统提供的无障碍接口获取屏幕文字(例如划词查询词典软件)。
请您在使用本功能时,采取额外的预防措施以保护您的个人信息安全。继续使用本功能即表示您已充分理解并接受上述风险(且我拥有并保留所有的追加和修改此协定的权力),您同意自行承担因隐私泄露可能产生的所有后果。我建议您定期检查和更新您的隐私保护措施,并采取一切必要措施以防止您的隐私信息被未经授权的访问或泄露。",
4 | "totalSwitch": "主开关",
5 | "totalSwitchDesc": "此插件的内部开关",
6 | "eventBusSwitchProtyleSwitch": "事件总线 switch-protyle",
7 | "eventBusSwitchProtyleSwitchDesc": "监听编辑器文件变更(静态)",
8 | "eventBusClickEditorcontentSwitch": "事件总线 click-editorcontent",
9 | "eventBusClickEditorcontentSwitchDesc": "监听编辑器内容点击(半动态)",
10 | "eventBusWsMainSwitch": "事件总线 ws-main",
11 | "eventBusWsMainSwitchDesc": "监听编辑操作(动态,注意性能)",
12 | "eventBusLoadedProtyleStatic":"事件总线 loaded-protyle-static",
13 | "eventBusLoadedProtyleStaticDesc":"编辑器加载 (静态)",
14 | "eventBusLoadedProtyleDynamic":"事件总线 loaded-protyle-dynamic",
15 | "eventBusLoadedProtyleDynamicDesc":"编辑器加载(动态)",
16 | "doubleBlock":"二次屏蔽",
17 | "doubleBlockDesc":"延迟一定时间后再屏蔽一次。避免屏蔽后又有内容加载。耗双倍性能,若电脑不好(或者太卡)请禁用。",
18 | "keywordsBlacklistTitle": "关键词黑名单",
19 | "keywordsBlacklistDesc": "输入您不想在直播中暴露的关键词。用英文或中文半角逗号分隔,即“,”或“,”。",
20 | "keywordsBlacklistNoteTitle": "备注",
21 | "keywordsBlacklistNoteDesc": "您可以在此处做笔记,例如写下要记录的关键词。",
22 | "hintTitle": "关于",
23 | "hintDesc": "
",
24 | "uninstall_hint": "siyuan_streamer_mode: 卸载成功",
25 | "streamerModeReveal":"主播模式:点击以切换揭露掩盖",
26 | "streamerModeRevealMobile":"切换揭露掩盖",
27 | "streamerModeRevealMobileNoti":"已切换,请二次确认已不再直播。",
28 | "streamerModeMenu":"主播模式",
29 | "revealDoubleCheck":"确定揭露掩盖吗?请二次确认你已经停止直播!",
30 | "revealDoubleCheckMobile":"揭露掩盖前,请二次确认你已经停止直播!",
31 | "forbidFirefoxAlert": "主播模式插件:很抱歉,因为火狐浏览器不支持该插件所用的CSS API, 所以该插件不支持火狐浏览器。如果你在直播,请关闭浏览器(不要点击确认),并换用Chromium内核的浏览器或者思源本体,以避免信息泄露。点击确认将禁用主播模式并加载,如果你在直播,请直接关闭浏览器或者标签。",
32 | "listeningBreadcrumb":"监听面包屑",
33 | "listeningBreadcrumbDesc":"请注意,此选项会添加一个监听器,请注意性能。",
34 | "settings":"设置"
35 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Streamer Mode: Avoiding the Leakage of Sensitive Information During Live Streaming
2 |
3 | # What is Streamer Mode?
4 | Streamer Mode is a special feature provided by some software, particularly gaming and live streaming applications, designed to protect users' privacy and security during live streaming or video recording. When Streamer Mode is activated, the software automatically hides or blocks certain sensitive information, such as:
5 |
6 | 1. **Personal Information**: Automatically blocks usernames, email addresses, real names that you had choosen.
7 | 2. **Passwords**: Automatically blocks passwords that you had choosen.
8 | 3. **IP Address**: Automatically blocks IPs that you had choosen.
9 | 4. **Links**: Automatically blocks links that you had choosen.
10 |
11 | This allows streamers to focus on their content without worrying about privacy leaks or harassment, enhancing the security and privacy protection of live streaming.
12 |
13 | # Credits
14 | - https://github.com/mdzz2048/siyuan-plugin-hsr by mdzz2048 for the highlight code
15 | - https://github.com/TCOTC/siyuan-plugin-hsr-mdzz2048-fork for the nice idea and highlight code
16 | - https://github.com/Misuzu2027/syplugin-document-search Misuzu2027 offered code that helped me to port highlight code from Vue to classic TS.
17 | - https://github.com/leolee9086 leolee9086 offered the methods to fix breadcrum listener
18 | - https://github.com/frostime Frostime offered the methods to fix breadcrum listener
19 |
20 | # Disclaimer
21 |
22 | - If you find this plugin useful, please consider giving my GitHub repository a free star⭐️. Thank you!
23 | - Link: https://github.com/zxkmm/siyuan_streamer_mode
24 |
25 | # Links
26 |
27 | - Repository/Source Code: https://github.com/zxkmm/siyuan_streamer_mode
28 | - Download: Search "siyuan_marketpace_blacklist" in the marketplace or visit https://github.com/zxkmm/siyuan_streamer_mode/releases
29 | - Report bugs/ Submit feature requests: https://github.com/zxkmm/siyuan_streamer_mode/issues
30 |
31 | # Additional Attachment to MIT License
32 |
33 | You are free to use the code in this repository, regardless of whether it's closed source or not, or whether it's part of paid software or not. However, I have incorporated these additional requests into the license of this repository. If you use the code, design, text, algorithms, or anything else from this repository, you must include my username "zxkmm" and the link to this repository in three places:
34 |
35 | 1. In the code comments.
36 | 2. In the settings interface related to my code.
37 | 3. On the 'About' page of your software/website/and or any other format of computer production.
38 |
--------------------------------------------------------------------------------
/src/setting-example.svelte:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 | {#each groups as group}
66 | {
71 | focusGroup = group;
72 | }}
73 | on:keydown={() => {}}
74 | >
75 | {group}
76 |
77 | {/each}
78 |
79 |
80 |
86 |
87 | 💡 This is our default settings.
88 |
89 |
90 |
91 |
92 |
93 |
101 |
102 |
--------------------------------------------------------------------------------
/src/libs/setting-item.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 | {title}
32 |
33 | {@html description}
34 |
35 |
36 |
37 |
38 | {#if type === "checkbox"}
39 |
40 |
47 | {:else if type === "textinput"}
48 |
49 |
56 | {:else if type === "number"}
57 |
64 | {:else if type === "button"}
65 |
66 |
71 | {settingValue}
72 |
73 | {:else if type === "select"}
74 |
75 |
81 | {#each Object.entries(options) as [value, text]}
82 | {text}
83 | {/each}
84 |
85 | {:else if type == "slider"}
86 |
87 |
88 |
98 |
99 | {/if}
100 |
101 |
--------------------------------------------------------------------------------
/src/i18n/en_US.json:
--------------------------------------------------------------------------------
1 | {
2 | "warnTitle": "Disclaimer",
3 | "warnDesc": "Although I strive to ensure that this feature is as effective as possible in protecting your privacy, due to the complexity and uncertainty of computer systems, we cannot guarantee absolute privacy protection. Specifically, we cannot completely rule out the risk of privacy content leakage due to technical failures, system errors, or other unforeseen factors, including but not limited to hard drive data reading, DOM operation errors, or delays in implementing privacy protection measures. This plugin can only aim to minimize visual privacy leaks. Please take additional precautions to protect your personal information security when using this feature. Continuing to use this feature indicates that you fully understand and accept the above risks (and I reserve the right to add and modify this agreement), and you agree to bear all consequences that may arise from privacy leakage. I advise you to regularly check and update your privacy protection measures and take all necessary steps to prevent your privacy information from being accessed or leaked without authorization.",
4 | "totalSwitch": "Main Switch",
5 | "totalSwitchDesc": "Internal switch for this plugin",
6 | "eventBusSwitchProtyleSwitch": "Event Bus switch-protyle",
7 | "eventBusSwitchProtyleSwitchDesc": "Listen for file changes in the editor (static)",
8 | "eventBusClickEditorcontentSwitch": "Event Bus click-editorcontent",
9 | "eventBusClickEditorcontentSwitchDesc": "Listen for content clicks in the editor (semi-dynamic)",
10 | "eventBusWsMainSwitch": "Event Bus ws-main",
11 | "eventBusLoadedProtyleStatic":"Event Bus loaded-protyle-static",
12 | "eventBusLoadedProtyleStaticDesc":"Editor loaded (static)",
13 | "eventBusLoadedProtyleDynamic":"Event Bus loaded-protyle-dynamic",
14 | "eventBusLoadedProtyleDynamicDesc":"Editor loaded (dynamic, be mindful of performance)",
15 | "eventBusWsMainSwitchDesc": "Listen for editing operations (dynamic, be mindful of performance)",
16 | "doubleBlock":"Double Block",
17 | "doubleBlockDesc":"Block content twice with a slighly delay to cover the situations that some element didn't finished loading when apply blocking, this cost double performance, disable it if you have a low performance computer",
18 | "keywordsBlacklistTitle": "Keyword Blacklist",
19 | "keywordsBlacklistDesc": "Enter keywords you do not want to expose in your live stream. Separate them with a comma in English or Chinese, i.e., ',' or ','.",
20 | "keywordsBlacklistNoteTitle": "Note",
21 | "keywordsBlacklistNoteDesc": "You can take notes here, for example, write down keywords you want to record.",
22 | "hintTitle": "About",
23 | "hintDesc": "
● Developed by zxkmm , open-sourced under the MIT License. ● If you like this plugin, please star my GitHub repository⭐. ● Link: https://github.com/zxkmm/siyuan_streamer_mode ● Please note that all keywords are stored in plain text in the configuration file, so be cautious about your privacy security. ",
24 | "uninstall_hint": "siyuan_streamer_mode: Uninstall successful",
25 | "streamerModeReveal":"Streamer Mode: Click to swap reveal",
26 | "streamerModeRevealMobile":"Swap reveal",
27 | "streamerModeRevealMobileNoti":"Swapped, double check you have stopped streaming.",
28 | "streamerModeMenu":"Streamer Mode",
29 | "revealDoubleCheck":"Are you sure you want to reveal? Please double check that you have stopped streaming!",
30 | "revealDoubleCheckMobile":"Before, reveal, please double check that you have stopped streaming!",
31 | "forbidFirefoxAlert":"Streamer Mode Plugin: Sorry, this plugin does not support Firefox browser because the CSS API used by the plugin is not supported by Firefox. If you are streaming, please close the browser (do not click confirm aka OK or something) and switch to a browser with a Chromium kernel or SiYuan to avoid information leakage. Click confirm will disable the streamer mode and load the content, if you are streaming, please close the browser or tab directly.",
32 | "listeningBreadcrumb":"Listening Breadcrumb",
33 | "listeningBreadcrumbDesc":"Please note that this option will add a listener, be mindful of performance.",
34 | "settings":"Settings"
35 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path"
2 | import { defineConfig, loadEnv } from "vite"
3 | import minimist from "minimist"
4 | import { viteStaticCopy } from "vite-plugin-static-copy"
5 | import livereload from "rollup-plugin-livereload"
6 | import { svelte } from "@sveltejs/vite-plugin-svelte"
7 | import zipPack from "vite-plugin-zip-pack";
8 | import fg from 'fast-glob';
9 |
10 | const args = minimist(process.argv.slice(2))
11 | const isWatch = args.watch || args.w || false
12 | const devDistDir = "./dev"
13 | const distDir = isWatch ? devDistDir : "./dist"
14 |
15 | console.log("isWatch=>", isWatch)
16 | console.log("distDir=>", distDir)
17 |
18 | export default defineConfig({
19 | resolve: {
20 | alias: {
21 | "@": resolve(__dirname, "src"),
22 | }
23 | },
24 |
25 | plugins: [
26 | svelte(),
27 |
28 | viteStaticCopy({
29 | targets: [
30 | {
31 | src: "./README*.md",
32 | dest: "./",
33 | },
34 | {
35 | src: "./icon.png",
36 | dest: "./",
37 | },
38 | {
39 | src: "./preview.png",
40 | dest: "./",
41 | },
42 | {
43 | src: "./plugin.json",
44 | dest: "./",
45 | },
46 | {
47 | src: "./src/i18n/**",
48 | dest: "./i18n/",
49 | },
50 | ],
51 | }),
52 | ],
53 |
54 | // https://github.com/vitejs/vite/issues/1930
55 | // https://vitejs.dev/guide/env-and-mode.html#env-files
56 | // https://github.com/vitejs/vite/discussions/3058#discussioncomment-2115319
57 | // 在这里自定义变量
58 | define: {
59 | "process.env.DEV_MODE": `"${isWatch}"`,
60 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
61 | },
62 |
63 | build: {
64 | // 输出路径
65 | outDir: distDir,
66 | emptyOutDir: false,
67 |
68 | // 构建后是否生成 source map 文件
69 | sourcemap: false,
70 |
71 | // 设置为 false 可以禁用最小化混淆
72 | // 或是用来指定是应用哪种混淆器
73 | // boolean | 'terser' | 'esbuild'
74 | // 不压缩,用于调试
75 | minify: !isWatch,
76 |
77 | lib: {
78 | // Could also be a dictionary or array of multiple entry points
79 | entry: resolve(__dirname, "src/index.ts"),
80 | // the proper extensions will be added
81 | fileName: "index",
82 | formats: ["cjs"],
83 | },
84 | rollupOptions: {
85 | plugins: [
86 | ...(
87 | isWatch ? [
88 | livereload(devDistDir),
89 | {
90 | //监听静态资源文件
91 | name: 'watch-external',
92 | async buildStart() {
93 | const files = await fg([
94 | 'src/i18n/*.json',
95 | './README*.md',
96 | './plugin.json'
97 | ]);
98 | for (let file of files) {
99 | this.addWatchFile(file);
100 | }
101 | }
102 | }
103 | ] : [
104 | zipPack({
105 | inDir: './dist',
106 | outDir: './',
107 | outFileName: 'package.zip'
108 | })
109 | ]
110 | )
111 | ],
112 |
113 | // make sure to externalize deps that shouldn't be bundled
114 | // into your library
115 | external: ["siyuan", "process"],
116 |
117 | output: {
118 | entryFileNames: "[name].js",
119 | assetFileNames: (assetInfo) => {
120 | if (assetInfo.name === "style.css") {
121 | return "index.css"
122 | }
123 | return assetInfo.name
124 | },
125 | },
126 | },
127 | }
128 | })
129 |
--------------------------------------------------------------------------------
/scripts/make_dev_link.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import http from 'node:http';
3 | import readline from 'node:readline';
4 |
5 |
6 | //************************************ Write you dir here ************************************
7 |
8 | //Please write the "workspace/data/plugins" directory here
9 | //请在这里填写你的 "workspace/data/plugins" 目录
10 | let targetDir = '';
11 | //Like this
12 | // let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`;
13 | //********************************************************************************************
14 |
15 | const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info);
16 | const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info);
17 |
18 | let POST_HEADER = {
19 | // "Authorization": `Token ${token}`,
20 | "Content-Type": "application/json",
21 | }
22 |
23 | async function myfetch(url, options) {
24 | //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本
25 | return new Promise((resolve, reject) => {
26 | let req = http.request(url, options, (res) => {
27 | let data = '';
28 | res.on('data', (chunk) => {
29 | data += chunk;
30 | });
31 | res.on('end', () => {
32 | resolve({
33 | ok: true,
34 | status: res.statusCode,
35 | json: () => JSON.parse(data)
36 | });
37 | });
38 | });
39 | req.on('error', (e) => {
40 | reject(e);
41 | });
42 | req.end();
43 | });
44 | }
45 |
46 | async function getSiYuanDir() {
47 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces';
48 | let conf = {};
49 | try {
50 | let response = await myfetch(url, {
51 | method: 'POST',
52 | headers: POST_HEADER
53 | });
54 | if (response.ok) {
55 | conf = await response.json();
56 | } else {
57 | error(`\tHTTP-Error: ${response.status}`);
58 | return null;
59 | }
60 | } catch (e) {
61 | error(`\tError: ${e}`);
62 | error("\tPlease make sure SiYuan is running!!!");
63 | return null;
64 | }
65 | return conf.data;
66 | }
67 |
68 | async function chooseTarget(workspaces) {
69 | let count = workspaces.length;
70 | log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`)
71 | for (let i = 0; i < workspaces.length; i++) {
72 | log(`\t[${i}] ${workspaces[i].path}`);
73 | }
74 |
75 | if (count == 1) {
76 | return `${workspaces[0].path}/data/plugins`;
77 | } else {
78 | const rl = readline.createInterface({
79 | input: process.stdin,
80 | output: process.stdout
81 | });
82 | let index = await new Promise((resolve, reject) => {
83 | rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => {
84 | resolve(answer);
85 | });
86 | });
87 | rl.close();
88 | return `${workspaces[index].path}/data/plugins`;
89 | }
90 | }
91 |
92 | log('>>> Try to visit constant "targetDir" in make_dev_link.js...')
93 |
94 | if (targetDir === '') {
95 | log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....')
96 | let res = await getSiYuanDir();
97 |
98 | if (res === null || res === undefined || res.length === 0) {
99 | log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....');
100 |
101 | // console.log(process.env)
102 | let env = process.env?.SIYUAN_PLUGIN_DIR;
103 | if (env !== undefined && env !== null && env !== '') {
104 | targetDir = env;
105 | log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`);
106 | } else {
107 | error('\tCan not get SiYuan directory from environment variable "SIYUAN_PLUGIN_DIR", failed!');
108 | process.exit(1);
109 | }
110 | } else {
111 | targetDir = await chooseTarget(res);
112 | }
113 |
114 |
115 | log(`>>> Successfully got target directory: ${targetDir}`);
116 | }
117 |
118 | //Check
119 | if (!fs.existsSync(targetDir)) {
120 | error(`Failed! plugin directory not exists: "${targetDir}"`);
121 | error(`Please set the plugin directory in scripts/make_dev_link.js`);
122 | process.exit(1);
123 | }
124 |
125 |
126 | //check if plugin.json exists
127 | if (!fs.existsSync('./plugin.json')) {
128 | //change dir to parent
129 | process.chdir('../');
130 | if (!fs.existsSync('./plugin.json')) {
131 | error('Failed! plugin.json not found');
132 | process.exit(1);
133 | }
134 | }
135 |
136 | //load plugin.json
137 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8'));
138 | const name = plugin?.name;
139 | if (!name || name === '') {
140 | error('Failed! Please set plugin name in plugin.json');
141 | process.exit(1);
142 | }
143 |
144 | //dev directory
145 | const devDir = `${process.cwd()}/dev`;
146 | //mkdir if not exists
147 | if (!fs.existsSync(devDir)) {
148 | fs.mkdirSync(devDir);
149 | }
150 |
151 | function cmpPath(path1, path2) {
152 | path1 = path1.replace(/\\/g, '/');
153 | path2 = path2.replace(/\\/g, '/');
154 | // sepertor at tail
155 | if (path1[path1.length - 1] !== '/') {
156 | path1 += '/';
157 | }
158 | if (path2[path2.length - 1] !== '/') {
159 | path2 += '/';
160 | }
161 | return path1 === path2;
162 | }
163 |
164 | const targetPath = `${targetDir}/${name}`;
165 | //如果已经存在,就退出
166 | if (fs.existsSync(targetPath)) {
167 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink();
168 |
169 | if (isSymbol) {
170 | let srcPath = fs.readlinkSync(targetPath);
171 |
172 | if (cmpPath(srcPath, devDir)) {
173 | log(`Good! ${targetPath} is already linked to ${devDir}`);
174 | } else {
175 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`);
176 | }
177 | } else {
178 | error(`Failed! ${targetPath} already exists and is not a symbolic link`);
179 | }
180 |
181 | } else {
182 | //创建软链接
183 | fs.symlinkSync(devDir, targetPath, 'junction');
184 | log(`Done! Created symlink ${targetPath}`);
185 | }
186 |
187 |
--------------------------------------------------------------------------------
/src/libs/setting-utils.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2023 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2023-09-16 18:05:00
5 | * @FilePath : /src/libs/setting-utils.ts
6 | * @LastEditTime : 2023-12-28 18:10:12
7 | * @Description : A utility for siyuan plugin settings
8 | */
9 |
10 | import { Plugin, Setting } from 'siyuan';
11 |
12 | export class SettingUtils {
13 | plugin: Plugin;
14 | name: string;
15 | file: string;
16 |
17 | settings: Map
= new Map();
18 | elements: Map = new Map();
19 |
20 | constructor(plugin: Plugin, name?: string, callback?: (data: any) => void, width?: string, height?: string) {
21 | this.name = name ?? 'settings';
22 | this.plugin = plugin;
23 | this.file = this.name.endsWith('.json') ? this.name : `${this.name}.json`;
24 | this.plugin.setting = new Setting({
25 | width: width,
26 | height: height,
27 | confirmCallback: () => {
28 | for (let key of this.settings.keys()) {
29 | this.updateValue(key);
30 | }
31 | let data = this.dump();
32 | if (callback !== undefined) {
33 | callback(data);
34 | } else {
35 | this.plugin.data[this.name] = data;
36 | this.save();
37 | }
38 | window.location.reload();
39 | }
40 | });
41 | }
42 |
43 | async load() {
44 | let data = await this.plugin.loadData(this.file);
45 | // console.debug('Load config:', data);
46 | if (data) {
47 | for (let [key, item] of this.settings) {
48 | item.value = data?.[key] ?? item.value;
49 | }
50 | }
51 | this.plugin.data[this.name] = this.dump();
52 | return data;
53 | }
54 |
55 | async save() {
56 | let data = this.dump();
57 | await this.plugin.saveData(this.file, this.dump());
58 | return data;
59 | }
60 |
61 | /**
62 | * Get setting item value
63 | * @param key key name
64 | * @returns setting item value
65 | */
66 | get(key: string) {
67 | return this.settings.get(key)?.value;
68 | }
69 |
70 | /**
71 | * 将设置项目导出为 JSON 对象
72 | * @returns object
73 | */
74 | dump(): Object {
75 | let data: any = {};
76 | for (let [key, item] of this.settings) {
77 | if (item.type === 'button') continue;
78 | data[key] = item.value;
79 | }
80 | return data;
81 | }
82 |
83 | addItem(item: ISettingItem) {
84 | this.settings.set(item.key, item);
85 | let itemElement: HTMLElement;
86 | switch (item.type) {
87 | case 'checkbox':
88 | let element: HTMLInputElement = document.createElement('input');
89 | element.type = 'checkbox';
90 | element.checked = item.value;
91 | element.className = "b3-switch fn__flex-center";
92 | itemElement = element;
93 | break;
94 | case 'select':
95 | let selectElement: HTMLSelectElement = document.createElement('select');
96 | selectElement.className = "b3-select fn__flex-center fn__size200";
97 | let options = item?.options ?? {};
98 | for (let val in options) {
99 | let optionElement = document.createElement('option');
100 | let text = options[val];
101 | optionElement.value = val;
102 | optionElement.text = text;
103 | selectElement.appendChild(optionElement);
104 | }
105 | selectElement.value = item.value;
106 | itemElement = selectElement;
107 | break;
108 | case 'slider':
109 | let sliderElement: HTMLInputElement = document.createElement('input');
110 | sliderElement.type = 'range';
111 | sliderElement.className = 'b3-slider fn__size200 b3-tooltips b3-tooltips__n';
112 | sliderElement.ariaLabel = item.value;
113 | sliderElement.min = item.slider?.min.toString() ?? '0';
114 | sliderElement.max = item.slider?.max.toString() ?? '100';
115 | sliderElement.step = item.slider?.step.toString() ?? '1';
116 | sliderElement.value = item.value;
117 | sliderElement.onchange = () => {
118 | sliderElement.ariaLabel = sliderElement.value;
119 | }
120 | itemElement = sliderElement;
121 | break;
122 | case 'textinput':
123 | let textInputElement: HTMLInputElement = document.createElement('input');
124 | textInputElement.className = 'b3-text-field fn__flex-center fn__size200';
125 | textInputElement.value = item.value;
126 | itemElement = textInputElement;
127 | break;
128 | case 'textarea':
129 | let textareaElement: HTMLTextAreaElement = document.createElement('textarea');
130 | textareaElement.className = "b3-text-field fn__block";
131 | textareaElement.value = item.value;
132 | itemElement = textareaElement;
133 | break;
134 | case 'number':
135 | let numberElement: HTMLInputElement = document.createElement('input');
136 | numberElement.type = 'number';
137 | numberElement.className = 'b3-text-field fn__flex-center fn__size200';
138 | numberElement.value = item.value;
139 | itemElement = numberElement;
140 | break;
141 | case 'button':
142 | let buttonElement: HTMLButtonElement = document.createElement('button');
143 | buttonElement.className = "b3-button b3-button--outline fn__flex-center fn__size200";
144 | buttonElement.innerText = item.button?.label ?? 'Button';
145 | buttonElement.onclick = item.button?.callback ?? (() => {});
146 | itemElement = buttonElement;
147 | break;
148 | case 'hint':
149 | let hintElement: HTMLElement = document.createElement('div');
150 | hintElement.className = 'b3-label fn__flex-center';
151 | itemElement = hintElement;
152 | break;
153 | }
154 | this.elements.set(item.key, itemElement);
155 | this.plugin.setting.addItem({
156 | title: item.title,
157 | description: item?.description,
158 | createActionElement: () => {
159 | let element = this.getElement(item.key);
160 | return element;
161 | }
162 | })
163 | }
164 |
165 | private getElement(key: string) {
166 | let item = this.settings.get(key);
167 | let element = this.elements.get(key) as any;
168 | switch (item.type) {
169 | case 'checkbox':
170 | element.checked = item.value;
171 | break;
172 | case 'select':
173 | element.value = item.value;
174 | break;
175 | case 'slider':
176 | element.value = item.value;
177 | element.ariaLabel = item.value;
178 | break;
179 | case 'textinput':
180 | element.value = item.value;
181 | break;
182 | case 'textarea':
183 | element.value = item.value;
184 | break;
185 | }
186 | return element;
187 | }
188 |
189 | private updateValue(key: string) {
190 | let item = this.settings.get(key);
191 | let element = this.elements.get(key) as any;
192 | // console.debug(element, element?.value);
193 | switch (item.type) {
194 | case 'checkbox':
195 | item.value = element.checked;
196 | break;
197 | case 'select':
198 | item.value = element.value;
199 | break;
200 | case 'slider':
201 | item.value = element.value;
202 | break;
203 | case 'textinput':
204 | item.value = element.value;
205 | break;
206 | case 'textarea':
207 | item.value = element.value;
208 | break;
209 | }
210 | }
211 |
212 | }
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2023 frostime. All rights reserved.
3 | * https://github.com/frostime/sy-plugin-template-vite
4 | *
5 | * See API Document in [API.md](https://github.com/siyuan-note/siyuan/blob/master/API.md)
6 | * API 文档见 [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md)
7 | */
8 |
9 | import { fetchSyncPost, IWebSocketData } from "siyuan";
10 |
11 |
12 | async function request(url: string, data: any) {
13 | let response: IWebSocketData = await fetchSyncPost(url, data);
14 | let res = response.code === 0 ? response.data : null;
15 | return res;
16 | }
17 |
18 |
19 | // **************************************** Noteboook ****************************************
20 |
21 |
22 | export async function lsNotebooks(): Promise {
23 | let url = '/api/notebook/lsNotebooks';
24 | return request(url, '');
25 | }
26 |
27 |
28 | export async function openNotebook(notebook: NotebookId) {
29 | let url = '/api/notebook/openNotebook';
30 | return request(url, { notebook: notebook });
31 | }
32 |
33 |
34 | export async function closeNotebook(notebook: NotebookId) {
35 | let url = '/api/notebook/closeNotebook';
36 | return request(url, { notebook: notebook });
37 | }
38 |
39 |
40 | export async function renameNotebook(notebook: NotebookId, name: string) {
41 | let url = '/api/notebook/renameNotebook';
42 | return request(url, { notebook: notebook, name: name });
43 | }
44 |
45 |
46 | export async function createNotebook(name: string): Promise {
47 | let url = '/api/notebook/createNotebook';
48 | return request(url, { name: name });
49 | }
50 |
51 |
52 | export async function removeNotebook(notebook: NotebookId) {
53 | let url = '/api/notebook/removeNotebook';
54 | return request(url, { notebook: notebook });
55 | }
56 |
57 |
58 | export async function getNotebookConf(notebook: NotebookId): Promise {
59 | let data = { notebook: notebook };
60 | let url = '/api/notebook/getNotebookConf';
61 | return request(url, data);
62 | }
63 |
64 |
65 | export async function setNotebookConf(notebook: NotebookId, conf: NotebookConf): Promise {
66 | let data = { notebook: notebook, conf: conf };
67 | let url = '/api/notebook/setNotebookConf';
68 | return request(url, data);
69 | }
70 |
71 |
72 | // **************************************** File Tree ****************************************
73 | export async function createDocWithMd(notebook: NotebookId, path: string, markdown: string): Promise {
74 | let data = {
75 | notebook: notebook,
76 | path: path,
77 | markdown: markdown,
78 | };
79 | let url = '/api/filetree/createDocWithMd';
80 | return request(url, data);
81 | }
82 |
83 |
84 | export async function renameDoc(notebook: NotebookId, path: string, title: string): Promise {
85 | let data = {
86 | doc: notebook,
87 | path: path,
88 | title: title
89 | };
90 | let url = '/api/filetree/renameDoc';
91 | return request(url, data);
92 | }
93 |
94 |
95 | export async function removeDoc(notebook: NotebookId, path: string) {
96 | let data = {
97 | notebook: notebook,
98 | path: path,
99 | };
100 | let url = '/api/filetree/removeDoc';
101 | return request(url, data);
102 | }
103 |
104 |
105 | export async function moveDocs(fromPaths: string[], toNotebook: NotebookId, toPath: string) {
106 | let data = {
107 | fromPaths: fromPaths,
108 | toNotebook: toNotebook,
109 | toPath: toPath
110 | };
111 | let url = '/api/filetree/moveDocs';
112 | return request(url, data);
113 | }
114 |
115 |
116 | export async function getHPathByPath(notebook: NotebookId, path: string): Promise {
117 | let data = {
118 | notebook: notebook,
119 | path: path
120 | };
121 | let url = '/api/filetree/getHPathByPath';
122 | return request(url, data);
123 | }
124 |
125 |
126 | export async function getHPathByID(id: BlockId): Promise {
127 | let data = {
128 | id: id
129 | };
130 | let url = '/api/filetree/getHPathByID';
131 | return request(url, data);
132 | }
133 |
134 |
135 | export async function getIDsByHPath(notebook: NotebookId, path: string): Promise {
136 | let data = {
137 | notebook: notebook,
138 | path: path
139 | };
140 | let url = '/api/filetree/getIDsByHPath';
141 | return request(url, data);
142 | }
143 |
144 | // **************************************** Asset Files ****************************************
145 |
146 | export async function upload(assetsDirPath: string, files: any[]): Promise {
147 | let form = new FormData();
148 | form.append('assetsDirPath', assetsDirPath);
149 | for (let file of files) {
150 | form.append('file[]', file);
151 | }
152 | let url = '/api/asset/upload';
153 | return request(url, form);
154 | }
155 |
156 | // **************************************** Block ****************************************
157 | type DataType = "markdown" | "dom";
158 | export async function insertBlock(
159 | dataType: DataType, data: string,
160 | nextID?: BlockId, previousID?: BlockId, parentID?: BlockId
161 | ): Promise {
162 | let payload = {
163 | dataType: dataType,
164 | data: data,
165 | nextID: nextID,
166 | previousID: previousID,
167 | parentID: parentID
168 | }
169 | let url = '/api/block/insertBlock';
170 | return request(url, payload);
171 | }
172 |
173 |
174 | export async function prependBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise {
175 | let payload = {
176 | dataType: dataType,
177 | data: data,
178 | parentID: parentID
179 | }
180 | let url = '/api/block/prependBlock';
181 | return request(url, payload);
182 | }
183 |
184 |
185 | export async function appendBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise {
186 | let payload = {
187 | dataType: dataType,
188 | data: data,
189 | parentID: parentID
190 | }
191 | let url = '/api/block/appendBlock';
192 | return request(url, payload);
193 | }
194 |
195 |
196 | export async function updateBlock(dataType: DataType, data: string, id: BlockId): Promise {
197 | let payload = {
198 | dataType: dataType,
199 | data: data,
200 | id: id
201 | }
202 | let url = '/api/block/updateBlock';
203 | return request(url, payload);
204 | }
205 |
206 |
207 | export async function deleteBlock(id: BlockId): Promise {
208 | let data = {
209 | id: id
210 | }
211 | let url = '/api/block/deleteBlock';
212 | return request(url, data);
213 | }
214 |
215 |
216 | export async function moveBlock(id: BlockId, previousID?: PreviousID, parentID?: ParentID): Promise {
217 | let data = {
218 | id: id,
219 | previousID: previousID,
220 | parentID: parentID
221 | }
222 | let url = '/api/block/moveBlock';
223 | return request(url, data);
224 | }
225 |
226 |
227 | export async function foldBlock(id: BlockId) {
228 | let data = {
229 | id: id
230 | }
231 | let url = '/api/block/foldBlock';
232 | return request(url, data);
233 | }
234 |
235 |
236 | export async function unfoldBlock(id: BlockId) {
237 | let data = {
238 | id: id
239 | }
240 | let url = '/api/block/unfoldBlock';
241 | return request(url, data);
242 | }
243 |
244 |
245 | export async function getBlockKramdown(id: BlockId): Promise {
246 | let data = {
247 | id: id
248 | }
249 | let url = '/api/block/getBlockKramdown';
250 | return request(url, data);
251 | }
252 |
253 |
254 | export async function getChildBlocks(id: BlockId): Promise {
255 | let data = {
256 | id: id
257 | }
258 | let url = '/api/block/getChildBlocks';
259 | return request(url, data);
260 | }
261 |
262 | export async function transferBlockRef(fromID: BlockId, toID: BlockId, refIDs: BlockId[]) {
263 | let data = {
264 | fromID: fromID,
265 | toID: toID,
266 | refIDs: refIDs
267 | }
268 | let url = '/api/block/transferBlockRef';
269 | return request(url, data);
270 | }
271 |
272 | // **************************************** Attributes ****************************************
273 | export async function setBlockAttrs(id: BlockId, attrs: { [key: string]: string }) {
274 | let data = {
275 | id: id,
276 | attrs: attrs
277 | }
278 | let url = '/api/attr/setBlockAttrs';
279 | return request(url, data);
280 | }
281 |
282 |
283 | export async function getBlockAttrs(id: BlockId): Promise<{ [key: string]: string }> {
284 | let data = {
285 | id: id
286 | }
287 | let url = '/api/attr/getBlockAttrs';
288 | return request(url, data);
289 | }
290 |
291 | // **************************************** SQL ****************************************
292 |
293 | export async function sql(sql: string): Promise {
294 | let sqldata = {
295 | stmt: sql,
296 | };
297 | let url = '/api/query/sql';
298 | return request(url, sqldata);
299 | }
300 |
301 | export async function getBlockByID(blockId: string): Promise {
302 | let sqlScript = `select * from blocks where id ='${blockId}'`;
303 | let data = await sql(sqlScript);
304 | return data[0];
305 | }
306 |
307 | // **************************************** Template ****************************************
308 |
309 | export async function render(id: DocumentId, path: string): Promise {
310 | let data = {
311 | id: id,
312 | path: path
313 | }
314 | let url = '/api/template/render';
315 | return request(url, data);
316 | }
317 |
318 |
319 | export async function renderSprig(template: string): Promise {
320 | let url = '/api/template/renderSprig';
321 | return request(url, { template: template });
322 | }
323 |
324 | // **************************************** File ****************************************
325 |
326 | export async function getFile(path: string): Promise {
327 | let data = {
328 | path: path
329 | }
330 | let url = '/api/file/getFile';
331 | try {
332 | let file = await fetchSyncPost(url, data);
333 | return file;
334 | } catch (error_msg) {
335 | return null;
336 | }
337 | }
338 |
339 | export async function putFile(path: string, isDir: boolean, file: any) {
340 | let form = new FormData();
341 | form.append('path', path);
342 | form.append('isDir', isDir.toString());
343 | // Copyright (c) 2023, terwer.
344 | // https://github.com/terwer/siyuan-plugin-importer/blob/v1.4.1/src/api/kernel-api.ts
345 | form.append('modTime', Math.floor(Date.now() / 1000).toString());
346 | form.append('file', file);
347 | let url = '/api/file/putFile';
348 | return request(url, form);
349 | }
350 |
351 | export async function removeFile(path: string) {
352 | let data = {
353 | path: path
354 | }
355 | let url = '/api/file/removeFile';
356 | return request(url, data);
357 | }
358 |
359 |
360 |
361 | export async function readDir(path: string): Promise {
362 | let data = {
363 | path: path
364 | }
365 | let url = '/api/file/readDir';
366 | return request(url, data);
367 | }
368 |
369 |
370 | // **************************************** Export ****************************************
371 |
372 | export async function exportMdContent(id: DocumentId): Promise {
373 | let data = {
374 | id: id
375 | }
376 | let url = '/api/export/exportMdContent';
377 | return request(url, data);
378 | }
379 |
380 | export async function exportResources(paths: string[], name: string): Promise {
381 | let data = {
382 | paths: paths,
383 | name: name
384 | }
385 | let url = '/api/export/exportResources';
386 | return request(url, data);
387 | }
388 |
389 | // **************************************** Convert ****************************************
390 |
391 | export type PandocArgs = string;
392 | export async function pandoc(args: PandocArgs[]) {
393 | let data = {
394 | args: args
395 | }
396 | let url = '/api/convert/pandoc';
397 | return request(url, data);
398 | }
399 |
400 | // **************************************** Notification ****************************************
401 |
402 | // /api/notification/pushMsg
403 | // {
404 | // "msg": "test",
405 | // "timeout": 7000
406 | // }
407 | export async function pushMsg(msg: string, timeout: number = 7000) {
408 | let payload = {
409 | msg: msg,
410 | timeout: timeout
411 | };
412 | let url = "/api/notification/pushMsg";
413 | return request(url, payload);
414 | }
415 |
416 | export async function pushErrMsg(msg: string, timeout: number = 7000) {
417 | let payload = {
418 | msg: msg,
419 | timeout: timeout
420 | };
421 | let url = "/api/notification/pushErrMsg";
422 | return request(url, payload);
423 | }
424 |
425 | // **************************************** Network ****************************************
426 | export async function forwardProxy(
427 | url: string, method: string = 'GET', payload: any = {},
428 | headers: any[] = [], timeout: number = 7000, contentType: string = "text/html"
429 | ): Promise {
430 | let data = {
431 | url: url,
432 | method: method,
433 | timeout: timeout,
434 | contentType: contentType,
435 | headers: headers,
436 | payload: payload
437 | }
438 | let url1 = '/api/network/forwardProxy';
439 | return request(url1, data);
440 | }
441 |
442 |
443 | // **************************************** System ****************************************
444 |
445 | export async function bootProgress(): Promise {
446 | return request('/api/system/bootProgress', {});
447 | }
448 |
449 |
450 | export async function version(): Promise {
451 | return request('/api/system/version', {});
452 | }
453 |
454 |
455 | export async function currentTime(): Promise {
456 | return request('/api/system/currentTime', {});
457 | }
458 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, showMessage, getFrontend, Menu } from "siyuan";
2 | import "@/index.scss";
3 |
4 | import { SettingUtils } from "./libs/setting-utils";
5 |
6 | const STORAGE_NAME = "menu-config";
7 |
8 | var packageNameClass = document.getElementsByClassName("ft__on-surface");
9 | var isStreamerModeReveal = false;
10 |
11 | export default class siyuan_streamer_mode extends Plugin {
12 | private settingUtils: SettingUtils;
13 | private isMobile: boolean;
14 | private eventBusHandlersRegistered = false;
15 | private listenerRegistered = false;
16 |
17 | convertStringToArray(userInput) {
18 | if (userInput) {
19 | var inputArray = userInput.split(/[,,]/);
20 | for (let i = 0; i < inputArray.length; i++) {
21 | inputArray[i] = inputArray[i].trim();
22 | }
23 | return inputArray;
24 | } else {
25 | // 处理 undefined
26 | return [];
27 | }
28 | }
29 |
30 | /// copied from google ai, haven't tested yet.... TODO
31 |
32 | // private readonly splitRegex = /[,,]/;
33 | // convertStringToArray(userInput: string): string[] {
34 | // if (userInput) {
35 | // return userInput.split(this.splitRegex).map((str) => str.trim());
36 | // } else {
37 | // return [];
38 | // }
39 | // }
40 |
41 | /// ^^^
42 |
43 | blackOutKeyWords(_keywords_array_) {
44 | //this func were ported from these repos:
45 | //此函数从如下仓库移植
46 | //https://github.com/mdzz2048/siyuan-plugin-hsr by mdzz2048
47 | //idea from https://github.com/TCOTC/siyuan-plugin-hsr-mdzz2048-fork by TCOTC AKA JerffreyChen
48 | //谢谢!!!💓💓💓
49 |
50 | // 创建 createTreeWalker 迭代器,用于遍历文本节点,保存到一个数组
51 |
52 | /// should be same but put it here for the test anyways... TODO
53 |
54 | // if (isStreamerModeReveal) {
55 | // return;
56 | // }
57 |
58 | /// ^^^
59 |
60 | if (!isStreamerModeReveal) {
61 | const treeWalker = document.createTreeWalker(
62 | document,
63 | NodeFilter.SHOW_TEXT
64 | );
65 | const allTextNodes = [];
66 | let currentNode = treeWalker.nextNode();
67 | while (currentNode) {
68 | allTextNodes.push(currentNode);
69 | currentNode = treeWalker.nextNode();
70 | }
71 |
72 | // // 清除上个高亮
73 | CSS.highlights.clear();
74 |
75 | // 存储所有找到的ranges
76 | let allRanges = [];
77 |
78 | // 遍历关键词数组
79 | _keywords_array_.forEach((keyword) => {
80 | // 为空判断
81 | const str = keyword.trim().toLowerCase();
82 | if (!str) return;
83 |
84 | // 查找所有文本节点是否包含搜索词
85 | const ranges = allTextNodes
86 | .map((el) => {
87 | return { el, text: el.textContent.toLowerCase() };
88 | })
89 | .map(({ el, text }) => {
90 | const indices = [];
91 | let startPos = 0;
92 | while (startPos < text.length) {
93 | const index = text.indexOf(str, startPos);
94 | if (index === -1) break;
95 | indices.push(index);
96 | startPos = index + str.length;
97 | }
98 |
99 | // 根据搜索词的位置创建选区
100 | return indices.map((index) => {
101 | const range = new Range();
102 | range.setStart(el, index);
103 | range.setEnd(el, index + str.length);
104 | return range;
105 | });
106 | });
107 |
108 | // 合并ranges
109 | allRanges = allRanges.concat(ranges.flat());
110 | });
111 |
112 | // 创建高亮对象
113 | const keywordsHighlight = new Highlight(...allRanges);
114 | const keywordsCount = allRanges.length;
115 | const keywordsRange = allRanges;
116 |
117 | // 注册高亮
118 | CSS.highlights.set("blocked_text", keywordsHighlight);
119 |
120 | return { kwdCount: keywordsCount, kwdRange: keywordsRange };
121 | }
122 | }
123 |
124 | init_event_bus_handler() {
125 | if (this.eventBusHandlersRegistered) {
126 | return;
127 | }
128 |
129 | isStreamerModeReveal = false;
130 |
131 | if (this.settingUtils.get("totalSwitch")) {
132 | const _blacklist_words_ = this.convertStringToArray(
133 | this.settingUtils.get("keywordsBlacklist")
134 | );
135 |
136 | if (this.settingUtils.get("doubleBlock")) {
137 | if (this.settingUtils.get("eventBusSwitchProtyleSwitch")) {
138 | this.eventBus.on("switch-protyle", () => {
139 | setTimeout(() => {
140 | this.blackOutKeyWords(_blacklist_words_);
141 | setTimeout(() => {
142 | this.blackOutKeyWords(_blacklist_words_);
143 | }, 100); // before 2nd time
144 | //TODO: this shouldnt hard coded...... it's a ok value here on my computer but should not hard coded
145 | }, 0); // b4 1st tm
146 | });
147 | }
148 | } else {
149 | if (this.settingUtils.get("eventBusSwitchProtyleSwitch")) {
150 | this.eventBus.on("switch-protyle", () => {
151 | this.blackOutKeyWords(_blacklist_words_);
152 | });
153 | }
154 | }
155 |
156 | if (this.settingUtils.get("eventBusClickEditorcontentSwitch")) {
157 | this.eventBus.on("click-editorcontent", () =>
158 | this.blackOutKeyWords(_blacklist_words_)
159 | );
160 | }
161 |
162 | if (this.settingUtils.get("eventBusWsMainSwitch")) {
163 | this.eventBus.on("ws-main", () =>
164 | this.blackOutKeyWords(_blacklist_words_)
165 | );
166 | }
167 |
168 | if (this.settingUtils.get("eventBusLoadedProtyleStatic")) {
169 | this.eventBus.on("loaded-protyle-static", () =>
170 | this.blackOutKeyWords(_blacklist_words_)
171 | );
172 | }
173 |
174 | if (this.settingUtils.get("eventBusLoadedProtyleDynamic")) {
175 | this.eventBus.on("loaded-protyle-dynamic", () =>
176 | this.blackOutKeyWords(_blacklist_words_)
177 | );
178 | }
179 | }
180 |
181 | this.eventBusHandlersRegistered = true;
182 | }
183 |
184 | offEventBusHandler() {
185 | isStreamerModeReveal = true;
186 |
187 | if (this.settingUtils.get("totalSwitch")) {
188 | const _blacklist_words_ = this.convertStringToArray(
189 | this.settingUtils.get("keywordsBlacklist")
190 | );
191 |
192 | if (this.settingUtils.get("doubleBlock")) {
193 | if (this.settingUtils.get("eventBusSwitchProtyleSwitch")) {
194 | this.eventBus.off("switch-protyle", () => {
195 | setTimeout(() => {
196 | this.blackOutKeyWords(_blacklist_words_);
197 | setTimeout(() => {
198 | this.blackOutKeyWords(_blacklist_words_);
199 | }, 100); // before 2nd time
200 | //TODO: this shouldnt hard coded...... it's a ok value here on my computer but should not hard coded
201 | }, 0); // b4 1st tm
202 | });
203 | }
204 | } else {
205 | if (this.settingUtils.get("eventBusSwitchProtyleSwitch")) {
206 | this.eventBus.off("switch-protyle", () => {
207 | this.blackOutKeyWords(_blacklist_words_);
208 | });
209 | }
210 | }
211 |
212 | if (this.settingUtils.get("eventBusClickEditorcontentSwitch")) {
213 | this.eventBus.off("click-editorcontent", () =>
214 | this.blackOutKeyWords(_blacklist_words_)
215 | );
216 | }
217 |
218 | if (this.settingUtils.get("eventBusWsMainSwitch")) {
219 | this.eventBus.off("ws-main", () =>
220 | this.blackOutKeyWords(_blacklist_words_)
221 | );
222 | }
223 |
224 | if (this.settingUtils.get("eventBusLoadedProtyleStatic")) {
225 | this.eventBus.off("loaded-protyle-static", () =>
226 | this.blackOutKeyWords(_blacklist_words_)
227 | );
228 | }
229 |
230 | if (this.settingUtils.get("eventBusLoadedProtyleDynamic")) {
231 | this.eventBus.off("loaded-protyle-dynamic", () =>
232 | this.blackOutKeyWords(_blacklist_words_)
233 | );
234 | }
235 | }
236 |
237 | CSS.highlights.clear();
238 | this.eventBusHandlersRegistered = false;
239 | }
240 |
241 | reloadInterface() {
242 | // window.location.reload();
243 | showMessage(this.i18n.reload_hint);
244 | }
245 |
246 | swapStreamerMode() {
247 | console.log("swap");
248 | if (this.isMobile) {
249 | if (isStreamerModeReveal) {
250 | this.init_event_bus_handler();
251 | const _blacklist_words_ = this.convertStringToArray(
252 | this.settingUtils.get("keywordsBlacklist")
253 | );
254 |
255 | this.blackOutKeyWords(_blacklist_words_);
256 | isStreamerModeReveal = false;
257 | } else {
258 | this.offEventBusHandler();
259 | isStreamerModeReveal = true;
260 | }
261 | } else {
262 | if (isStreamerModeReveal) {
263 | this.init_event_bus_handler();
264 | const _blacklist_words_ = this.convertStringToArray(
265 | this.settingUtils.get("keywordsBlacklist")
266 | );
267 |
268 | this.blackOutKeyWords(_blacklist_words_);
269 | isStreamerModeReveal = false;
270 | } else {
271 | const userConfirmed = window.confirm(this.i18n.revealDoubleCheck);
272 | if (userConfirmed) {
273 | this.offEventBusHandler();
274 | isStreamerModeReveal = true;
275 | }
276 | }
277 | }
278 | }
279 |
280 | async protectBreadCrumb() {
281 | if (this.settingUtils.get("totalSwitch")) {
282 | const targetNode = document.querySelector(".protyle-breadcrumb__bar");
283 | // console.log(targetNode);
284 | if (!targetNode) {
285 | // mobile doesnt have breadcrumb
286 | return;
287 | }
288 |
289 | const config = { attributes: true, childList: true, subtree: true };
290 |
291 | const callback = async (mutationsList, observer) => {
292 | // if (this.listenerRegistered){return;}
293 | for (let mutation of mutationsList) {
294 | if (mutation.type === "childList") {
295 | const _blacklist_words_ = this.convertStringToArray(
296 | await this.settingUtils.get("keywordsBlacklist")
297 | );
298 |
299 | // console.log(this);
300 | this.blackOutKeyWords(_blacklist_words_); //do it once in anyway.
301 | // } else if (mutation.type === "attributes") {
302 | // console.log(mutation.attributeName);
303 | // console.log("did");
304 | }
305 | }
306 | };
307 |
308 | const observer = new MutationObserver(callback);
309 |
310 | observer.observe(targetNode, config);
311 | console.log("Breadcrumb listener registered");
312 | this.listenerRegistered = true;
313 | }
314 | }
315 |
316 | async onload() {
317 | const frontEnd = getFrontend();
318 | this.isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile";
319 |
320 | this.settingUtils = new SettingUtils(this, STORAGE_NAME);
321 | this.settingUtils.load();
322 | this.settingUtils.addItem({
323 | key: "totalSwitch",
324 | value: true,
325 | type: "checkbox",
326 | title: this.i18n.totalSwitch,
327 | description: this.i18n.totalSwitchDesc,
328 | });
329 | this.settingUtils.addItem({
330 | key: "eventBusSwitchProtyleSwitch",
331 | value: true,
332 | type: "checkbox",
333 | title: this.i18n.eventBusSwitchProtyleSwitch,
334 | description: this.i18n.eventBusSwitchProtyleSwitchDesc,
335 | });
336 | this.settingUtils.addItem({
337 | key: "eventBusClickEditorcontentSwitch",
338 | value: false,
339 | type: "checkbox",
340 | title: this.i18n.eventBusClickEditorcontentSwitch,
341 | description: this.i18n.eventBusClickEditorcontentSwitchDesc,
342 | });
343 | this.settingUtils.addItem({
344 | key: "eventBusWsMainSwitch",
345 | value: false,
346 | type: "checkbox",
347 | title: this.i18n.eventBusWsMainSwitch,
348 | description: this.i18n.eventBusWsMainSwitchDesc,
349 | });
350 | this.settingUtils.addItem({
351 | key: "eventBusLoadedProtyleStatic",
352 | value: true,
353 | type: "checkbox",
354 | title: this.i18n.eventBusLoadedProtyleStatic,
355 | description: this.i18n.eventBusLoadedProtyleStaticDesc,
356 | });
357 | this.settingUtils.addItem({
358 | key: "eventBusLoadedProtyleDynamic",
359 | value: false,
360 | type: "checkbox",
361 | title: this.i18n.eventBusLoadedProtyleDynamic,
362 | description: this.i18n.eventBusLoadedProtyleDynamicDesc,
363 | });
364 | this.settingUtils.addItem({
365 | key: "doubleBlock",
366 | value: true,
367 | type: "checkbox",
368 | title: this.i18n.doubleBlock,
369 | description: this.i18n.doubleBlockDesc,
370 | });
371 | this.settingUtils.addItem({
372 | key: "listeningBreadcrumb",
373 | value: true,
374 | type: "checkbox",
375 | title: this.i18n.listeningBreadcrumb,
376 | description: this.i18n.listeningBreadcrumbDesc,
377 | });
378 | this.settingUtils.addItem({
379 | key: "keywordsBlacklist",
380 | value: "",
381 | type: "textarea",
382 | title: this.i18n.keywordsBlacklistTitle,
383 | description: this.i18n.keywordsBlacklistDesc,
384 | });
385 | this.settingUtils.addItem({
386 | key: "keywordsBlacklistNotes",
387 | value: "",
388 | type: "textarea",
389 | title: this.i18n.keywordsBlacklistNoteTitle,
390 | description: this.i18n.keywordsBlacklistNoteDesc,
391 | });
392 | this.settingUtils.addItem({
393 | key: "warn",
394 | value: "",
395 | type: "hint",
396 | title: this.i18n.warnTitle,
397 | description: this.i18n.warnDesc,
398 | });
399 | this.settingUtils.addItem({
400 | key: "hint",
401 | value: "",
402 | type: "hint",
403 | title: this.i18n.hintTitle,
404 | description: this.i18n.hintDesc,
405 | });
406 |
407 | this.addIcons(`
408 |
409 |
410 |
411 | `);
412 |
413 | }
414 |
415 | onLayoutReady() {
416 | // Check if the browser is Firefox
417 | if (navigator.userAgent.includes("Firefox")) {
418 | alert(this.i18n.forbidFirefoxAlert);
419 | return;
420 | }
421 |
422 | this.loadData(STORAGE_NAME);
423 | this.settingUtils.load();
424 |
425 |
426 |
427 | if (this.settingUtils.get("totalSwitch")) {
428 |
429 | const topBarElement = this.addTopBar({
430 | icon: "iconStreamer",
431 | title: this.isMobile
432 | ? this.i18n.streamerModeMenu
433 | : this.i18n.streamerModeReveal,
434 | position: "right",
435 | callback: () => {
436 | if (this.isMobile) {
437 | this.addMenu();
438 | // console.log("mobile");
439 | } else {
440 | let rect = topBarElement.getBoundingClientRect();
441 | // 如果被隐藏,则使用更多按钮
442 | if (rect.width === 0) {
443 | rect = document.querySelector("#barMore").getBoundingClientRect();
444 | }
445 | if (rect.width === 0) {
446 | rect = document
447 | .querySelector("#barPlugins")
448 | .getBoundingClientRect();
449 | }
450 | this.swapStreamerMode();
451 | }
452 | },
453 | });
454 |
455 |
456 | const _blacklist_words_ = this.convertStringToArray(
457 | this.settingUtils.get("keywordsBlacklist")
458 | );
459 |
460 | this.blackOutKeyWords(_blacklist_words_); //do it once in anyway.
461 |
462 | this.init_event_bus_handler();
463 | }
464 |
465 | if (
466 | this.settingUtils.get("listeningBreadcrumb") &&
467 | !this.listenerRegistered
468 | ) {
469 | this.protectBreadCrumb();
470 | }
471 | }
472 |
473 | async onunload() {
474 | CSS.highlights.clear();
475 | // await this.settingUtils.save(); //this could probably cause load old settings
476 | // this.reloadInterface();
477 | }
478 |
479 | uninstall() {
480 | CSS.highlights.clear();
481 | this.removeData(STORAGE_NAME);
482 | showMessage(this.i18n.uninstall_hint);
483 | }
484 |
485 | private addMenu(rect?: DOMRect) {
486 | const menu = new Menu("topBarSample", () => {
487 | console.log(this.i18n.byeMenu);
488 | });
489 |
490 | if (!this.isMobile) {
491 | menu.addItem({
492 | icon: "iconLayout",
493 | label: "Open Float Layer(open help first)",
494 | click: () => {
495 | this.addFloatLayer({
496 | ids: ["20210428212840-8rqwn5o", "20201225220955-l154bn4"],
497 | defIds: ["20230415111858-vgohvf3", "20200813131152-0wk5akh"],
498 | x: window.innerWidth - 768 - 120,
499 | y: 32,
500 | });
501 | },
502 | });
503 | } else {
504 | menu.addItem({
505 | icon: "iconStreamer",
506 | label: this.i18n.streamerModeRevealMobile,
507 | click: () => {
508 | this.swapStreamerMode();
509 | showMessage(this.i18n.streamerModeRevealMobileNoti);
510 | },
511 | });
512 | }
513 | menu.addItem({
514 | icon: "iconInfo",
515 | label: this.i18n.revealDoubleCheckMobile,
516 | type: "readonly",
517 | });
518 |
519 | menu.addSeparator();
520 | menu.addItem({
521 | icon: "iconSettings",
522 | label: "Official Setting Dialog",
523 | click: () => {
524 | this.openSetting();
525 | },
526 | });
527 |
528 | if (this.isMobile) {
529 | menu.fullscreen();
530 | } else {
531 | menu.open({
532 | x: rect.right,
533 | y: rect.bottom,
534 | isLeft: true,
535 | });
536 | }
537 | }
538 | }
539 |
--------------------------------------------------------------------------------