├── .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 |
2 | 3 |
-------------------------------------------------------------------------------- /tools/orginal_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxkmm/siyuan_streamer_mode/HEAD/tools/orginal_logo.jpeg -------------------------------------------------------------------------------- /tools/orginal_header.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxkmm/siyuan_streamer_mode/HEAD/tools/orginal_header.jpeg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | pnpm-lock.yaml 5 | package.zip 6 | node_modules 7 | dev 8 | dist 9 | build 10 | tmp 11 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/libs/index.d.ts: -------------------------------------------------------------------------------- 1 | type TSettingItemType = "checkbox" | "select" | "textinput" | "textarea" | "number" | "slider" | "button" | "hint"; 2 | interface ISettingItem { 3 | key: string; 4 | value: any; 5 | type: TSettingItemType; 6 | title: string; 7 | description?: string; 8 | placeholder?: string; 9 | slider?: { 10 | min: number; 11 | max: number; 12 | step: number; 13 | }; 14 | options?: { [key: string | number]: string }; 15 | button?: { 16 | label: string; 17 | callback: () => void; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siyuan_streamer_mode", 3 | "author": "zxkmm", 4 | "url": "https://github.com/zxkmm/siyuan_streamer_mode", 5 | "version": "0.2.2", 6 | "minAppVersion": "2.11.4", 7 | "backends": [ 8 | "windows", 9 | "linux", 10 | "darwin", 11 | "docker", 12 | "android" 13 | ], 14 | "frontends": [ 15 | "desktop", 16 | "browser-desktop", 17 | "desktop-window", 18 | "mobile" 19 | ], 20 | "displayName": { 21 | "en_US": "siyuan_streamer_mode", 22 | "zh_CN": "隐私模式" 23 | }, 24 | "description": { 25 | "en_US": "Streamer mode for SiYuan.", 26 | "zh_CN": "掩盖敏感信息" 27 | }, 28 | "readme": { 29 | "en_US": "README.md", 30 | "zh_CN": "README_zh_CN.md" 31 | }, 32 | "funding": { 33 | "custom": [ 34 | "https://github.com/zxkmm/zxkmm" 35 | ] 36 | }, 37 | "keywords": [ 38 | "集市", "黑名单", "主播模式", "隐藏", "移除", "streamer", "blacklist", "blocklist", "remove", "hide","隐私模式" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siyuan_streamer_mode", 3 | "version": "0.2.2", 4 | "type": "module", 5 | "description": "Streamer mode for SiYuan.", 6 | "repository": "https://github.com/zxkmm/siyuan_streamer_mode", 7 | "homepage": "https://github.com/zxkmm/siyuan_streamer_mode", 8 | "author": "zxkmm", 9 | "license": "MIT", 10 | "scripts": { 11 | "make-link": "node --no-warnings ./scripts/make_dev_link.js", 12 | "dev": "vite build --watch", 13 | "build": "vite build" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/vite-plugin-svelte": "^2.0.3", 17 | "@tsconfig/svelte": "^4.0.1", 18 | "@types/node": "^20.2.0", 19 | "eslint": "^8.42.0", 20 | "fast-glob": "^3.2.12", 21 | "glob": "^7.2.3", 22 | "minimist": "^1.2.8", 23 | "rollup-plugin-livereload": "^2.0.5", 24 | "sass": "^1.62.1", 25 | "siyuan": "0.9.1", 26 | "svelte": "^3.57.0", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^5.0.4", 29 | "vite": "^4.3.7", 30 | "vite-plugin-static-copy": "^0.15.0", 31 | "vite-plugin-zip-pack": "^1.0.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SiYuan 思源笔记 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:svelte/recommended", 6 | "turbo", 7 | "prettier", 8 | ], 9 | 10 | parser: "@typescript-eslint/parser", 11 | 12 | overrides: [ 13 | { 14 | files: ["*.svelte"], 15 | parser: "svelte-eslint-parser", 16 | // Parse the script in `.svelte` as TypeScript by adding the following configuration. 17 | parserOptions: { 18 | parser: "@typescript-eslint/parser", 19 | }, 20 | }, 21 | ], 22 | 23 | plugins: ["@typescript-eslint", "prettier"], 24 | 25 | rules: { 26 | // Note: you must disable the base rule as it can report incorrect errors 27 | semi: "off", 28 | quotes: "off", 29 | "no-undef": "off", 30 | "@typescript-eslint/no-var-requires": "off", 31 | "@typescript-eslint/no-this-alias": "off", 32 | "@typescript-eslint/no-non-null-assertion": "off", 33 | "@typescript-eslint/no-unused-vars": "off", 34 | "@typescript-eslint/no-explicit-any": "off", 35 | "turbo/no-undeclared-env-vars": "off", 36 | "prettier/prettier": "error", 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | #helloPanel { 2 | border: 1px rgb(189, 119, 119) dashed; 3 | } 4 | 5 | .plugin-sample { 6 | &__custom-tab { 7 | background-color: var(--b3-theme-background); 8 | height: 100%; 9 | width: 100%; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | &__custom-dock { 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | &__time { 22 | background: var(--b3-card-info-background); 23 | border-radius: 4px; 24 | padding: 2px 8px; 25 | } 26 | } 27 | 28 | 29 | 30 | //this css were ported from these repos: 31 | //此css从如下仓库移植 32 | //https://github.com/mdzz2048/siyuan-plugin-hsr by mdzz2048 33 | //idea from https://github.com/TCOTC/siyuan-plugin-hsr-mdzz2048-fork by TCOTC AKA JerffreyChen 34 | //谢谢!!!💓💓💓 35 | 36 | // .highlight-search-result { 37 | // position: abslute; 38 | // top: 135px; 39 | // right: 10px; 40 | // } 41 | 42 | ::highlight(blocked_text) { 43 | background-color: black; 44 | color: transparent; 45 | } 46 | 47 | // ::highlight(search-focus) { 48 | // background-color: black; 49 | // color: transparent; 50 | // } -------------------------------------------------------------------------------- /src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | interface IResGetNotebookConf { 2 | box: string; 3 | conf: NotebookConf; 4 | name: string; 5 | } 6 | 7 | interface IReslsNotebooks { 8 | notebooks: Notebook[]; 9 | } 10 | 11 | interface IResUpload { 12 | errFiles: string[]; 13 | succMap: { [key: string]: string }; 14 | } 15 | 16 | interface IResdoOperations { 17 | doOperations: doOperation[]; 18 | undoOperations: doOperation[] | null; 19 | } 20 | 21 | interface IResGetBlockKramdown { 22 | id: BlockId; 23 | kramdown: string; 24 | } 25 | 26 | interface IResGetChildBlock { 27 | id: BlockId; 28 | type: BlockType; 29 | subtype?: BlockSubType; 30 | } 31 | 32 | interface IResGetTemplates { 33 | content: string; 34 | path: string; 35 | } 36 | 37 | interface IResReadDir { 38 | isDir: boolean; 39 | isSymlink: boolean; 40 | name: string; 41 | } 42 | 43 | interface IResExportMdContent { 44 | hPath: string; 45 | content: string; 46 | } 47 | 48 | interface IResBootProgress { 49 | progress: number; 50 | details: string; 51 | } 52 | 53 | interface IResForwardProxy { 54 | body: string; 55 | contentType: string; 56 | elapsed: number; 57 | headers: { [key: string]: string }; 58 | status: number; 59 | url: string; 60 | } 61 | 62 | interface IResExportResources { 63 | path: string; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/libs/setting-panel.svelte: -------------------------------------------------------------------------------- 1 | 9 | 29 | 30 |
31 | 32 | {#each settingItems as item (item.key)} 33 | 45 | {/each} 46 |
-------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | # 思源主播模式 2 | 3 | ## 恳请: 4 | ## 如果您有空的话,恳求您给我的GitHub仓库点一个**免费**的Star(星星)。 5 | ## 本插件是完全开源免费的,我相信任何经营模式都有可取之处,本插件的开源免费经营模式,需要的就是您的鼓励。 6 | ## 我是毕业大学生,在艰难的就业市场中正在寻找工作。 7 | ## 您给我点的免费Star将很大程度帮助我解决温饱问题,再次感谢! 8 | ## 链接:https://github.com/zxkmm/siyuan_streamer_mode 9 | ## 10 | 11 | 主播模式:直播时避免泄漏敏感信息 (灵感来源于Discord app的主播模式) 12 | 13 | # 什么是主播模式 14 | Streamer Mode 是一些软件(尤其是游戏和直播软件)提供的一种特殊模式,用于在直播或录制视频时保护用户的隐私和安全。开启 Streamer Mode 后,软件会屏蔽某些敏感信息,例如: 15 | 16 | 1. **个人信息**:用户名、电子邮件地址、真实姓名等。 17 | 2. **密码**:屏蔽你定义的敏感的密码。 18 | 3. **IP地址**:屏蔽你定义的敏感的IP。 19 | 4. **链接**:屏蔽你定义的敏感的链接。 20 | 21 | 这样可以让主播在直播过程中不必担心隐私泄露或受到骚扰,提高直播的安全性和隐私保护。 22 | 23 | # 备注 24 | 如果您喜欢这个插件,请给我的 GitHub 仓库点亮免费的星星⭐(Star), 谢谢! 25 | 链接:https://github.com/zxkmm/siyuan_streamer_mode 26 | 27 | # 致谢 28 | - https://github.com/mdzz2048/siyuan-plugin-hsr 由 mdzz2048 提供的高亮代码 29 | - https://github.com/TCOTC/siyuan-plugin-hsr-mdzz2048-fork 由 TCOTC 提供的优秀想法和高亮代码 30 | - https://github.com/Misuzu2027/syplugin-document-search 由 Misuzu2027 提供的高亮代码让我从 Vue 移植到经典 TS 有参考 31 | - https://github.com/leolee9086 由 leolee9086 提供的修复面包屑监听器的方案 32 | - https://github.com/frostime 由 Frostime 提供的修复面包屑监听器的方案 33 | 34 | # 链接 35 | repo/源码:https://github.com/zxkmm/siyuan_streamer_mode 36 | 下载:集市搜索 “集市黑名单” 或者 访问 https://github.com/zxkmm/siyuan_streamer_mode/releases 37 | 汇报 bug / 提交功能请求:https://github.com/zxkmm/siyuan_streamer_mode/issues 38 | 39 | 40 | # 对MIT许可证的额外附加 41 | 42 | 您可以自由使用/分发此存储库中的代码,无论您打算闭源还是开源,或者无论您打算用在付费或免费软件的一部分,您都可以自由使用。然而,我已将这些额外的请求纳入了此存储库的许可证。如果您使用了来自此存储库的代码、设计、文本、算法或任何其他东西,您必须在以下三个地方包含我的用户名 "zxkmm" 和此存储库的链接: 43 | 44 | 1. 在代码注释中。 45 | 2. 在与我的代码相关的设置界面中。 46 | 3. 在您的软件/网站的 '关于' 页面以及或任何其他形式的计算机产品的中。 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": [ 7 | "ES2020", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "Node", 14 | // "allowImportingTsExtensions": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | /* Linting */ 21 | "strict": false, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | /* Svelte */ 26 | /** 27 | * Typecheck JS in `.svelte` and `.js` files by default. 28 | * Disable checkJs if you'd like to use dynamic types in JS. 29 | * Note that setting allowJs false does not prevent the use 30 | * of JS in `.svelte` files. 31 | */ 32 | "allowJs": true, 33 | "checkJs": true, 34 | "types": [ 35 | "node", 36 | "vite/client", 37 | "svelte" 38 | ], 39 | // "baseUrl": "./src", 40 | "paths": { 41 | "@/*": ["./src/*"], 42 | "@/libs/*": ["./src/libs/*"], 43 | } 44 | }, 45 | "include": [ 46 | "tools/**/*.ts", 47 | "src/**/*.ts", 48 | "src/**/*.d.ts", 49 | "src/**/*.tsx", 50 | "src/**/*.vue" 51 | ], 52 | "references": [ 53 | { 54 | "path": "./tsconfig.node.json" 55 | } 56 | ], 57 | "root": "." 58 | } -------------------------------------------------------------------------------- /src/hello.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
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": "
  • ● 由 zxkmm 开发,基于 MIT 许可证开源。
  • ● 如果您喜欢这个插件,请给我的 GitHub 仓库点个星⭐。
  • ● 链接: https://github.com/zxkmm/siyuan_streamer_mode
  • ● 请注意,所有关键词都以明文形式保存在配置文件中,请注意您的隐私安全。
", 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 | 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 | --------------------------------------------------------------------------------