├── .npmrc ├── .eslintignore ├── versions.json ├── main.ts ├── img ├── 1.png ├── 2.png ├── 3.png └── index.png ├── .editorconfig ├── manifest.json ├── .gitignore ├── src ├── data │ ├── data.ts │ └── types.ts ├── settings │ ├── base-setting.ts │ ├── data.ts │ ├── index.ts │ └── ui │ │ ├── manager-delay.ts │ │ ├── manager-style.ts │ │ ├── manager-group.ts │ │ ├── manager-tag.ts │ │ └── manager-basis.ts ├── utils.ts ├── modal │ ├── delete-modal.ts │ ├── disable-modal.ts │ ├── note-modal.ts │ ├── share-t-modal.ts │ ├── update-modal.ts │ ├── share-modal.ts │ ├── group-modal.ts │ ├── tags-modal.ts │ └── hide-modal.ts ├── migrations.ts ├── lang │ ├── inxdex.ts │ └── locale │ │ ├── ko.ts │ │ ├── ja.ts │ │ ├── zh_cn.ts │ │ ├── es.ts │ │ ├── ru.ts │ │ ├── fr.ts │ │ └── en.ts ├── repo-resolver.ts ├── agreement.ts ├── command.ts └── github-install.ts ├── version-bump.mjs ├── tsconfig.json ├── .eslintrc ├── package.json ├── .github └── workflows │ └── release.yml ├── esbuild.config.mjs ├── LICENSE ├── docs └── README_CN.md ├── README.md └── styles.css /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0" 3 | } 4 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import Manager from './src/main' 2 | 3 | export default Manager 4 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenozero-dev/obsidian-manager/HEAD/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenozero-dev/obsidian-manager/HEAD/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenozero-dev/obsidian-manager/HEAD/img/3.png -------------------------------------------------------------------------------- /img/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenozero-dev/obsidian-manager/HEAD/img/index.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "better-plugins-manager", 3 | "name": "Better Plugins Manager", 4 | "version": "0.3.6", 5 | "minAppVersion": "1.5.8", 6 | "description": "Plugin Manager: Simplify, Enhance, Personalize | 插件管理器:简化操作、增强功能、个性化设置", 7 | "author": "zero", 8 | "authorUrl": "https://github.com/0011000000110010/", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # 不要将编译后的 `main.js` 文件放入代码库中。应该将其上传到 GitHub 发布版中。 12 | main.js 13 | 14 | # Exclude sourcemaps 15 | *.map 16 | 17 | # obsidian 18 | data.json 19 | 20 | # 排除macOS Finder (System Explorer)视图状态 21 | .DS_Store 22 | 23 | # 无 24 | 笔记.md 25 | repealed.ts 26 | README.md 27 | i18n 28 | 29 | # 参考资料 30 | 参考 -------------------------------------------------------------------------------- /src/data/data.ts: -------------------------------------------------------------------------------- 1 | export const ITEM_STYLE = { 2 | 'alwaysExpand': '始终展开', 3 | 'neverExpand': '永不展开', 4 | 'hoverExpand': '悬浮展开', 5 | 'clickExpand': '单击展开' 6 | } 7 | 8 | export const GROUP_STYLE = { 9 | 'a': '样式一', 10 | 'b': '样式二', 11 | 'c': '样式三', 12 | 'd': '样式四' 13 | } 14 | 15 | export const TAG_STYLE = { 16 | 'a': '样式一', 17 | 'b': '样式二', 18 | 'c': '样式三', 19 | 'd': '样式四' 20 | } -------------------------------------------------------------------------------- /src/data/types.ts: -------------------------------------------------------------------------------- 1 | export interface ManagerPlugin { 2 | id: string; 3 | name: string; 4 | desc: string; 5 | group: string; 6 | tags: string[]; 7 | enabled: boolean; 8 | delay: string; 9 | note:string; 10 | } 11 | 12 | export interface Type { 13 | id: string; 14 | name: string; 15 | color: string; 16 | } 17 | 18 | export interface Tag { 19 | id: string; 20 | name: string; 21 | color: string; 22 | } 23 | 24 | export interface Delay { 25 | id: string; 26 | name: string; 27 | time: number; 28 | } -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 6 | const { minAppVersion } = manifest; 7 | manifest.version = targetVersion; 8 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 9 | 10 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 11 | versions[targetVersion] = minAppVersion; 12 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7", 19 | "ES2021" 20 | ] 21 | }, 22 | "include": [ 23 | "**/*.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "参考/**" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /src/settings/base-setting.ts: -------------------------------------------------------------------------------- 1 | import Manager from 'src/main'; 2 | import { ManagerSettingTab } from '.'; 3 | import { ManagerSettings } from './data'; 4 | import { App } from 'obsidian'; 5 | 6 | export default abstract class BaseSetting { 7 | protected settingTab: ManagerSettingTab; 8 | protected manager: Manager; 9 | protected settings: ManagerSettings; 10 | public containerEl: HTMLElement; 11 | protected app: App; 12 | 13 | constructor(obj: ManagerSettingTab) { 14 | this.settingTab = obj; 15 | this.manager = obj.manager; 16 | this.settings = obj.manager.settings; 17 | this.containerEl = obj.contentEl; 18 | this.app = obj.app; 19 | } 20 | 21 | public abstract main(): void; 22 | public display(): void { this.main() } 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.0", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "builtin-modules": "3.3.0", 19 | "esbuild": "0.17.3", 20 | "obsidian": "latest", 21 | "tslib": "2.4.0", 22 | "typescript": "4.7.4" 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: "18.x" 20 | 21 | - name: Build plugin 22 | run: | 23 | npm install 24 | npm run build 25 | 26 | - name: Create release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | tag="${GITHUB_REF#refs/tags/}" 31 | 32 | gh release create "$tag" \ 33 | --title="$tag" \ 34 | --draft \ 35 | main.js manifest.json styles.css -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = ``; 6 | 7 | const prod = (process.argv[2] === "production"); 8 | 9 | const context = await esbuild.context({ 10 | banner: { 11 | js: banner, 12 | }, 13 | // 修改启动路径 14 | entryPoints: ["main.ts"], 15 | bundle: true, 16 | external: [ 17 | "obsidian", 18 | "electron", 19 | "@codemirror/autocomplete", 20 | "@codemirror/collab", 21 | "@codemirror/commands", 22 | "@codemirror/language", 23 | "@codemirror/lint", 24 | "@codemirror/search", 25 | "@codemirror/state", 26 | "@codemirror/view", 27 | "@lezer/common", 28 | "@lezer/highlight", 29 | "@lezer/lr", 30 | ...builtins], 31 | format: "cjs", 32 | target: "es2018", 33 | logLevel: "info", 34 | sourcemap: prod ? false : "inline", 35 | treeShaking: true, 36 | outfile: "main.js", 37 | }); 38 | 39 | if (prod) { 40 | await context.rebuild(); 41 | process.exit(0); 42 | } else { 43 | await context.watch(); 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 zero 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 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Notice, Platform } from 'obsidian'; 2 | import Manager from 'main'; 3 | 4 | /** 5 | * 打开文件或文件夹的操作系统命令。 6 | * @param i18n - 国际化对象,用于显示操作结果的通知。 7 | * @param dir - 要打开的文件夹路径。 8 | * @description 根据操作系统执行相应的命令来打开文件夹。在Windows上使用'start'命令,在Mac上使用'open'命令。 9 | * 如果操作成功,显示成功通知;如果失败,显示错误通知。 10 | */ 11 | export const managerOpen = (dir: string, manager: Manager) => { 12 | if (Platform.isMobileApp) { 13 | new Notice("移动端暂不支持打开文件夹,请在桌面端操作。"); 14 | return; 15 | } 16 | try { 17 | // 延迟加载避免移动端加载 Node 模块 18 | // eslint-disable-next-line @typescript-eslint/no-var-requires 19 | const { exec } = require('child_process'); 20 | if (Platform.isDesktop || Platform.isWin) { 21 | exec(`start "" "${dir}"`, (error: any) => { 22 | if (error) { new Notice(manager.translator.t('通用_失败_文本')); } else { new Notice(manager.translator.t('通用_成功_文本')); } 23 | }); 24 | return; 25 | } 26 | if (Platform.isMacOS) { 27 | exec(`open ${dir}`, (error: any) => { 28 | if (error) { new Notice(manager.translator.t('通用_失败_文本')); } else { new Notice(manager.translator.t('通用_成功_文本')); } 29 | }); 30 | } 31 | } catch (e) { 32 | console.error("打开目录失败", e); 33 | new Notice(manager.translator.t('通用_失败_文本')); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modal/delete-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, ExtraButtonComponent, Modal, Setting } from 'obsidian'; 2 | import { ManagerSettings } from '../settings/data'; 3 | import Manager from 'main'; 4 | 5 | export class DeleteModal extends Modal { 6 | settings: ManagerSettings; 7 | manager: Manager; 8 | 9 | private deleteCallback: () => void; 10 | 11 | constructor(app: App, manager: Manager, deleteCallback: () => void) { 12 | super(app); 13 | this.manager = manager; 14 | this.deleteCallback = deleteCallback; 15 | } 16 | 17 | private async showHead() { 18 | //@ts-ignore 19 | const modalEl: HTMLElement = this.contentEl.parentElement; 20 | modalEl.addClass('manager-editor__container'); 21 | modalEl.removeChild(modalEl.getElementsByClassName('modal-close-button')[0]); 22 | this.titleEl.parentElement?.addClass('manager-container__header'); 23 | this.contentEl.addClass('manager-item-container'); 24 | 25 | // [标题行] 26 | const titleBar = new Setting(this.titleEl) 27 | titleBar.setClass('manager-delete__title') 28 | titleBar.setName(this.manager.translator.t('卸载_标题')); 29 | 30 | // [标题行] 关闭按钮 31 | const closeButton = new ExtraButtonComponent(titleBar.controlEl) 32 | closeButton.setIcon('circle-x') 33 | closeButton.onClick(() => this.close()); 34 | } 35 | 36 | private async showData() { 37 | const titleBar = new Setting(this.titleEl) 38 | titleBar.setName(this.manager.translator.t('卸载_提示')); 39 | const actionBar = new Setting(this.titleEl) 40 | actionBar.setClass('manager-delete__action') 41 | actionBar.addButton(cb => cb 42 | .setWarning() 43 | .setButtonText(this.manager.translator.t('卸载_卸载')) 44 | .onClick(() => { 45 | this.deleteCallback(); 46 | this.close(); 47 | }) 48 | ); 49 | actionBar.addButton(cb => cb 50 | .setButtonText(this.manager.translator.t('卸载_取消')) 51 | .onClick(() => { this.close(); }) 52 | ); 53 | } 54 | 55 | async onOpen() { 56 | await this.showHead(); 57 | await this.showData(); 58 | } 59 | 60 | async onClose() { 61 | this.contentEl.empty(); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/settings/data.ts: -------------------------------------------------------------------------------- 1 | import { Delay, ManagerPlugin, Tag, Type } from '../data/types'; 2 | 3 | export interface ManagerSettings { 4 | // 持久化 5 | PERSISTENCE: boolean; 6 | // 过滤标签 7 | FILTER_TAG: string; 8 | // 过滤分组 9 | FILTER_GROUP: string; 10 | // 过滤延迟 11 | FILTER_DELAY: string; 12 | 13 | // 语言 14 | LANGUAGE: string; 15 | // 语言是否已按系统初始化过(用于新老版本兼容) 16 | LANGUAGE_INITIALIZED?: boolean; 17 | // 已执行的迁移版本 18 | MIGRATION_VERSION?: string; 19 | // 调试开关 20 | DEBUG: boolean; 21 | // 居中 22 | CENTER: boolean; 23 | // 样式 24 | ITEM_STYLE: string; 25 | // 分组样式 26 | GROUP_STYLE: string; 27 | // 标签样式 28 | TAG_STYLE: string; 29 | 30 | // 延迟 31 | DELAY: boolean; 32 | // 淡出样式 33 | FADE_OUT_DISABLED_PLUGINS: boolean; 34 | // 命令项 35 | COMMAND_ITEM: boolean; 36 | // 命令组 37 | COMMAND_GROUP: boolean; 38 | // 启动时检测更新 39 | STARTUP_CHECK_UPDATES: boolean; 40 | // GitHub 仓库映射 41 | REPO_MAP: Record; 42 | // BPM 安装标记 43 | BPM_INSTALLED: string[]; 44 | // 面板隐藏 BPM 标签 45 | HIDE_BPM_TAG: boolean; 46 | // 插件信息导出目录(相对库路径) 47 | EXPORT_DIR: string; 48 | 49 | // GitHub 令牌 50 | GITHUB_TOKEN: string; 51 | 52 | GROUPS: Type[]; 53 | TAGS: Tag[]; 54 | DELAYS: Delay[]; 55 | Plugins: ManagerPlugin[]; 56 | HIDES: string[], 57 | } 58 | 59 | export const DEFAULT_SETTINGS: ManagerSettings = { 60 | PERSISTENCE: false, 61 | // 筛选 62 | FILTER_TAG: "", 63 | FILTER_GROUP: "", 64 | FILTER_DELAY: "", 65 | 66 | LANGUAGE: "", 67 | LANGUAGE_INITIALIZED: false, 68 | MIGRATION_VERSION: "", 69 | DEBUG: false, 70 | CENTER: false, 71 | ITEM_STYLE: "alwaysExpand", 72 | GROUP_STYLE: "a", 73 | TAG_STYLE: "b", 74 | DELAY: false, 75 | FADE_OUT_DISABLED_PLUGINS: true, 76 | COMMAND_ITEM: false, 77 | COMMAND_GROUP: false, 78 | STARTUP_CHECK_UPDATES: false, 79 | REPO_MAP: {}, 80 | BPM_INSTALLED: [], 81 | HIDE_BPM_TAG: false, 82 | EXPORT_DIR: "", 83 | GITHUB_TOKEN: "", 84 | GROUPS: [], 85 | TAGS: [], 86 | DELAYS: [ 87 | { 88 | "id": "default", 89 | "name": "默认延迟", 90 | "time": 10 91 | }, 92 | ], 93 | Plugins: [], 94 | HIDES: [], 95 | } 96 | -------------------------------------------------------------------------------- /src/modal/disable-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, ExtraButtonComponent, Modal, Setting } from 'obsidian'; 2 | import { ManagerSettings } from '../settings/data'; 3 | import Manager from 'main'; 4 | 5 | export class DisableModal extends Modal { 6 | settings: ManagerSettings; 7 | manager: Manager; 8 | 9 | private deleteCallback: () => void; 10 | 11 | constructor(app: App, manager: Manager, deleteCallback: () => void) { 12 | super(app); 13 | this.manager = manager; 14 | this.deleteCallback = deleteCallback; 15 | } 16 | 17 | private async showHead() { 18 | //@ts-ignore 19 | const modalEl: HTMLElement = this.contentEl.parentElement; 20 | modalEl.addClass('manager-editor__container'); 21 | modalEl.removeChild(modalEl.getElementsByClassName('modal-close-button')[0]); 22 | this.titleEl.parentElement?.addClass('manager-container__header'); 23 | this.contentEl.addClass('manager-item-container'); 24 | 25 | // [标题行] 26 | const titleBar = new Setting(this.titleEl) 27 | titleBar.setClass('manager-delete__title') 28 | titleBar.setName(this.manager.translator.t('一键_标题')); 29 | 30 | // [标题行] 关闭按钮 31 | const closeButton = new ExtraButtonComponent(titleBar.controlEl) 32 | closeButton.setIcon('circle-x') 33 | closeButton.onClick(() => this.close()); 34 | } 35 | 36 | private async showData() { 37 | const titleBar = new Setting(this.titleEl) 38 | titleBar.setName(this.manager.translator.t('一键_提示')); 39 | const actionBar = new Setting(this.titleEl) 40 | actionBar.setClass('manager-delete__action') 41 | actionBar.addButton(cb => cb 42 | .setCta() 43 | .setButtonText(this.manager.translator.t('一键_启禁')) 44 | .onClick(() => { 45 | this.deleteCallback(); 46 | this.close(); 47 | }) 48 | ); 49 | actionBar.addButton(cb => cb 50 | .setButtonText(this.manager.translator.t('一键_取消')) 51 | .onClick(() => { 52 | this.close(); 53 | }) 54 | ); 55 | } 56 | 57 | async onOpen() { 58 | await this.showHead(); 59 | await this.showData(); 60 | } 61 | 62 | async onClose() { 63 | this.contentEl.empty(); 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /docs/README_CN.md: -------------------------------------------------------------------------------- 1 | # Better Plugins Manager 2 | 3 | [English](../README.md) 4 | 5 | ![GitHub Downloads](https://img.shields.io/github/downloads/zenozero-dev/obsidian-manager/total) 6 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/zenozero-dev/obsidian-manager) 7 | ![Last commit](https://img.shields.io/github/last-commit/zenozero-dev/obsidian-manager) 8 | ![Issues](https://img.shields.io/github/issues/zenozero-dev/obsidian-manager) 9 | ![Stars](https://img.shields.io/github/stars/zenozero-dev/obsidian-manager?style=social) 10 | 11 | ![截图](../img/index.png) 12 | 13 | ## BPM 是什么? 14 | 15 | Better Plugins Manager 提供比原生插件管理更强的体验:延迟启动、批量开关、分组/标签、重命名和描述、像 BRAT 一样从 GitHub 选择发行版安装、导出到 Base、以及更友好的移动端界面。 16 | 17 | ## 亮点 18 | 19 | - **快速批量控制**:一键全开/全关,分组启用/禁用,单插件快速开关。 20 | - **整理与注释**:自定义名称/描述/备注,分组、标签(BPM 安装自动打 `bpm-install` 标签),可搜索过滤。 21 | - **GitHub 感知安装**:支持 `user/repo` 或完整 URL,发行版列表选择,缓存仓库映射,卡片可跳转仓库。 22 | - **性能与延迟**:按预设延迟启动插件,缩短启动卡顿。 23 | - **Base 同步**:插件信息写入 Markdown frontmatter,并支持反向同步(分只读/可写字段)。 24 | - **移动端适配**:可折叠操作/搜索区域、长按提示、响应式网格;桌面端布局不受影响。 25 | - **GitHub Token**:可选 PAT,获取发行版更稳定。 26 | 27 | ## 安装 28 | 29 | 本插件已经上架Obsidian官方市场。 30 | 31 | ## 使用 32 | 33 | - 从侧边栏 “Manager” 图标或命令面板(`Manage plugins`)打开。 34 | - 卡片支持重命名、描述/备注编辑、标签/分组、延迟、跳转仓库/文件夹、删除、启用/禁用。 35 | - 筛选:按状态、分组、标签、延迟、关键字搜索。 36 | - 操作:批量开关、分组开关、重载插件、从 GitHub 选发行版安装。 37 | 38 | ## 导出到 Obsidian Base 39 | 40 | 1) 在设置中填写 **插件信息导出目录**(库内文件夹)。 41 | 2) BPM 会为每个插件生成一篇 Markdown 并监听改动。 42 | 3) 规则:`bpm_rw_*` 可编辑;`bpm_ro_*` 只读;`bpm_rwc_repo` 仅在非 BPM 安装且无官方映射时可写。 43 | 44 | Frontmatter 示例如下: 45 | 46 | ```yaml 47 | --- 48 | bpm_ro_id: some-plugin 49 | bpm_rw_name: 自定义名 50 | bpm_rw_desc: 自定义描述 51 | bpm_rw_note: 备注 52 | bpm_rw_enabled: true 53 | bpm_rwc_repo: user/repo 54 | bpm_ro_group: group-id 55 | bpm_ro_tags: 56 | - tag-a 57 | - bpm-install 58 | bpm_ro_delay: delay-id 59 | bpm_ro_installed_via_bpm: true 60 | bpm_ro_updated: 2024-12-12T10:00:00Z 61 | --- 62 | 63 | 正文区:这里的内容可自行编辑或替换。 64 | ``` 65 | 66 | ## 常用设置 67 | 68 | - **延迟预设**:创建延迟配置并分配给插件。 69 | - **隐藏 BPM 标签**:保留自动标签但在界面中隐藏。 70 | - **GitHub API Token**:提升发行版拉取的速率上限。 71 | - **淡化未启用插件**:未启用的卡片视觉弱化。 72 | - **导出提示文案**:可自定义导出文件的正文提示。 73 | 74 | ## 命令 75 | 76 | - 打开管理面板。 77 | - (可选)单插件开关命令。 78 | - (可选)按分组一键启用/禁用。 79 | 80 | ## 兼容性 81 | 82 | - 支持桌面和移动端(Android/iOS),自动按平台切换移动布局,桌面不受影响。 83 | 84 | ## 参与贡献 85 | 86 | 欢迎提交 Issue/PR。反馈问题请附日志与复现步骤;需求建议可先开讨论/Issue。 87 | 88 | ## 许可 89 | 90 | [MIT](../LICENSE) 91 | -------------------------------------------------------------------------------- /src/migrations.ts: -------------------------------------------------------------------------------- 1 | import Manager from "main"; 2 | import { ensureBpmTagExists } from "src/repo-resolver"; 3 | 4 | type Migration = { 5 | version: string; 6 | run: (manager: Manager) => Promise | void; 7 | }; 8 | 9 | const compare = (a: string, b: string): number => { 10 | const pa = (a || "0").split(".").map(Number); 11 | const pb = (b || "0").split(".").map(Number); 12 | const len = Math.max(pa.length, pb.length); 13 | for (let i = 0; i < len; i++) { 14 | const ai = pa[i] || 0; 15 | const bi = pb[i] || 0; 16 | if (ai > bi) return 1; 17 | if (ai < bi) return -1; 18 | } 19 | return 0; 20 | }; 21 | 22 | const migrations: Migration[] = [ 23 | { 24 | version: "0.3.1", 25 | run: async (manager) => { 26 | let changed = false; 27 | // 语言初始化 28 | if (!manager.settings.LANGUAGE_INITIALIZED || !manager.settings.LANGUAGE) { 29 | manager.settings.LANGUAGE = manager.getAppLanguage(); 30 | manager.settings.LANGUAGE_INITIALIZED = true; 31 | changed = true; 32 | } 33 | // 清理默认组/标签 34 | if (manager.settings.GROUPS?.some(g => g.id === "default")) { 35 | manager.settings.GROUPS = manager.settings.GROUPS.filter(g => g.id !== "default"); 36 | changed = true; 37 | } 38 | if (manager.settings.TAGS?.some(t => t.id === "default")) { 39 | manager.settings.TAGS = manager.settings.TAGS.filter(t => t.id !== "default"); 40 | changed = true; 41 | } 42 | // BPM 标签 43 | ensureBpmTagExists(manager); 44 | // 补全缺失 name 45 | if (manager.settings.Plugins && manager.settings.Plugins.length > 0) { 46 | manager.settings.Plugins.forEach(p => { 47 | if (!p.name) { 48 | p.name = p.id; 49 | changed = true; 50 | } 51 | }); 52 | } 53 | if (changed) await manager.saveSettings(); 54 | } 55 | }, 56 | ]; 57 | 58 | export const runMigrations = async (manager: Manager) => { 59 | const currentVersion = manager.manifest.version; 60 | const last = manager.settings.MIGRATION_VERSION || ""; 61 | const pending = migrations 62 | .filter(m => compare(m.version, last) > 0) 63 | .sort((a, b) => compare(a.version, b.version)); 64 | for (const m of pending) { 65 | await m.run(manager); 66 | manager.settings.MIGRATION_VERSION = m.version; 67 | await manager.saveSettings(); 68 | } 69 | // 确保版本记录到位 70 | if (!manager.settings.MIGRATION_VERSION || compare(manager.settings.MIGRATION_VERSION, currentVersion) < 0) { 71 | manager.settings.MIGRATION_VERSION = currentVersion; 72 | await manager.saveSettings(); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/lang/inxdex.ts: -------------------------------------------------------------------------------- 1 | import Manager from "main"; 2 | import zh_cn from './locale/zh_cn'; 3 | import en from "./locale/en"; 4 | import ru from "./locale/ru"; 5 | import ja from "./locale/ja"; 6 | import ko from "./locale/ko"; 7 | import fr from "./locale/fr"; 8 | import es from "./locale/es"; 9 | 10 | export class Translator { 11 | private manager: Manager; 12 | public language = { 13 | 'zh-cn': '简体中文', 14 | 'en': 'English', 15 | 'ru': 'Русский язык', 16 | 'ja': '日本語', 17 | 'ko': '한국어', 18 | 'fr': 'Français', 19 | 'es': 'Español', 20 | }; 21 | 22 | private localeMap: { [k: string]: Partial } = { 23 | 'zh-cn': zh_cn, 24 | 'en': en, 25 | 'ru': ru, 26 | 'ja': ja, 27 | 'ko': ko, 28 | 'fr': fr, 29 | 'es': es, 30 | }; 31 | 32 | constructor(manager: Manager) { 33 | this.manager = manager; 34 | } 35 | 36 | // 方法用于获取翻译后的字符串 37 | public t(str: keyof typeof zh_cn): string { 38 | const language = this.normalizeLang(this.manager.settings.LANGUAGE || 'zh-cn'); // 默认使用 'zh-cn' 39 | const locale = this.localeMap[language] || zh_cn; // 如果 language 不存在,则使用 zh_cn 40 | return locale[str] || zh_cn[str]; // 如果 str 在 locale 中不存在,则使用 zh_cn 中的默认值 41 | } 42 | 43 | private normalizeLang(lang: string): string { 44 | const lower = (lang || '').toLowerCase().replace('_', '-'); 45 | const map: Record = { 46 | // Official mappings we support 47 | 'en': 'en', 48 | 'en-gb': 'en', 49 | 'zh': 'zh-cn', 50 | 'zh-cn': 'zh-cn', 51 | 'zh-tw': 'zh-cn', 52 | 'ru': 'ru', 53 | 'ja': 'ja', 54 | 'ko': 'ko', 55 | 'fr': 'fr', 56 | 'es': 'es', 57 | }; 58 | return map[lower] || map[lower.split('-')[0]] || 'en'; 59 | } 60 | } 61 | 62 | // import { moment } from "obsidian"; 63 | // import zh_cn from './locale/zh_cn'; 64 | // import en from "./locale/en"; 65 | // import ja_jp from "./locale/ja_jp"; 66 | // import ko_kr from "./locale/ko_kr"; 67 | // import ru_ru from "./locale/ru_ru"; 68 | 69 | // export const LANGUAGE = { 70 | // 'zh-cn': '简体中文', 71 | // 'en': '永不展开' 72 | // } 73 | 74 | // const localeMap: { [k: string]: Partial } = { 75 | // 'zh-cn': zh_cn, 76 | // 'en-us': en, 77 | // 'ja-jp': ja_jp, 78 | // 'ko-kr': ko_kr, 79 | // 'ru-ru': ru_ru 80 | // }; 81 | 82 | // // const locales = moment.locales(); 83 | // // console.log(locales); 84 | // // console.log(moment.locale()) 85 | // const locale = localeMap[moment.locale()]; 86 | 87 | // export function t(str: keyof typeof zh_cn): string { 88 | // return (locale && locale[str]) || zh_cn[str]; 89 | // } 90 | -------------------------------------------------------------------------------- /src/modal/note-modal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | ExtraButtonComponent, 4 | Modal, 5 | Setting, 6 | TextAreaComponent, 7 | } from "obsidian"; 8 | import { ManagerSettings } from "../settings/data"; 9 | import Manager from "main"; 10 | import { ManagerPlugin } from "src/data/types"; 11 | import { ManagerModal } from "./manager-modal"; 12 | 13 | export class NoteModal extends Modal { 14 | settings: ManagerSettings; 15 | manager: Manager; 16 | managerPlugin: ManagerPlugin; 17 | managerModal: ManagerModal; 18 | 19 | constructor(app: App, manager: Manager, managerPlugin: ManagerPlugin, managerModal: ManagerModal) { 20 | super(app); 21 | this.settings = manager.settings; 22 | this.manager = manager; 23 | this.managerPlugin = managerPlugin; 24 | this.managerModal = managerModal; 25 | } 26 | 27 | private async showHead() { 28 | //@ts-ignore 29 | const modalEl: HTMLElement = this.contentEl.parentElement; 30 | modalEl.addClass("manager-note__container"); 31 | modalEl.removeChild(modalEl.getElementsByClassName("modal-close-button")[0]); 32 | this.titleEl.parentElement?.addClass("manager-container__header"); 33 | this.contentEl.addClass("manager-item-container"); 34 | // [标题行] 35 | const titleBar = new Setting(this.titleEl).setClass("manager-bar__title").setName(`${this.managerPlugin.name}`); 36 | // [标题行] 关闭按钮 37 | const closeButton = new ExtraButtonComponent(titleBar.controlEl); 38 | closeButton.setIcon("circle-x"); 39 | closeButton.onClick(() => this.close()); 40 | } 41 | 42 | private async showData() { 43 | const textArea = new TextAreaComponent(this.contentEl); 44 | textArea.setValue(this.managerPlugin.note); 45 | textArea.onChange(async (newValue) => { 46 | this.managerPlugin.note = newValue; 47 | await this.manager.savePluginAndExport(this.managerPlugin.id); 48 | this.managerModal.reloadShowData(); 49 | }); 50 | } 51 | 52 | private async reloadShowData() { 53 | let scrollTop = 0; 54 | const modalElement: HTMLElement = this.contentEl; 55 | scrollTop = modalElement.scrollTop; 56 | modalElement.empty(); 57 | await this.showData(); 58 | modalElement.scrollTo(0, scrollTop); 59 | } 60 | 61 | async onOpen() { 62 | await this.showHead(); 63 | await this.showData(); 64 | } 65 | 66 | async onClose() { 67 | this.contentEl.empty(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/modal/share-t-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, ExtraButtonComponent, Modal, Notice, Setting } from 'obsidian'; 2 | import { ManagerSettings } from '../settings/data'; 3 | import Manager from 'main'; 4 | 5 | export class ShareTModal extends Modal { 6 | settings: ManagerSettings; 7 | manager: Manager; 8 | url: string = ''; 9 | private deleteCallback: (type: string, url?: string) => void; 10 | 11 | constructor(app: App, manager: Manager, deleteCallback: (type: string, url?: string) => void) { 12 | super(app); 13 | this.manager = manager; 14 | this.deleteCallback = deleteCallback; 15 | } 16 | 17 | private async showHead() { 18 | //@ts-ignore 19 | const modalEl: HTMLElement = this.contentEl.parentElement; 20 | modalEl.addClass('manager-editor__container'); 21 | modalEl.removeChild(modalEl.getElementsByClassName('modal-close-button')[0]); 22 | this.titleEl.parentElement?.addClass('manager-container__header'); 23 | this.contentEl.addClass('manager-item-container'); 24 | 25 | // [标题行] 26 | const titleBar = new Setting(this.titleEl) 27 | titleBar.setClass('manager-delete__title') 28 | titleBar.setName('共享插件'); 29 | 30 | // [标题行] 关闭按钮 31 | const closeButton = new ExtraButtonComponent(titleBar.controlEl) 32 | closeButton.setIcon('circle-x') 33 | closeButton.onClick(() => this.close()); 34 | } 35 | 36 | private async showData() { 37 | const titleBar = new Setting(this.titleEl) 38 | titleBar.setName('分享链接') 39 | titleBar.addText((cb) => { 40 | cb.setValue('') 41 | cb.setPlaceholder('请输入分享链接') 42 | cb.onChange((value) => { 43 | this.url = value; 44 | }) 45 | }) 46 | 47 | 48 | const actionBar = new Setting(this.titleEl) 49 | actionBar.setClass('manager-delete__action') 50 | actionBar.addButton(cb => cb 51 | .setButtonText('导入') 52 | .onClick(() => { 53 | if (!this.url) { new Notice('请输入分享链接'); return; } 54 | this.deleteCallback('import', this.url); 55 | this.close(); 56 | }) 57 | ); 58 | actionBar.addButton(cb => cb 59 | .setButtonText('导出') 60 | .onClick(() => { 61 | this.deleteCallback('export'); 62 | this.close(); 63 | }) 64 | ); 65 | } 66 | 67 | async onOpen() { 68 | await this.showHead(); 69 | await this.showData(); 70 | } 71 | 72 | async onClose() { 73 | this.contentEl.empty(); 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab } from 'obsidian'; 2 | import Manager from "../main"; 3 | 4 | import ManagerBasis from './ui/manager-basis'; 5 | import ManagerStyle from './ui/manager-style'; 6 | import ManagerDelay from './ui/manager-delay'; 7 | import ManagerTag from './ui/manager-tag'; 8 | import ManagerGroup from './ui/manager-group'; 9 | 10 | 11 | class ManagerSettingTab extends PluginSettingTab { 12 | manager: Manager; 13 | app: App; 14 | contentEl: HTMLDivElement; 15 | 16 | constructor(app: App, manager: Manager) { 17 | super(app, manager); 18 | this.manager = manager; 19 | this.app = app; 20 | } 21 | 22 | display(): void { 23 | const { containerEl } = this; 24 | containerEl.empty(); 25 | containerEl.addClass('manager-setting__container'); 26 | const tabsEl = this.containerEl.createEl('div'); 27 | tabsEl.addClass('manager-setting__tabs'); 28 | this.contentEl = this.containerEl.createEl('div'); 29 | this.contentEl.addClass('manager-setting__content'); 30 | 31 | const tabItems = [ 32 | { text: this.manager.translator.t('设置_基础设置_前缀'), content: () => this.basisDisplay() }, 33 | { text: this.manager.translator.t('设置_样式设置_前缀'), content: () => this.styleDisplay() }, 34 | { text: this.manager.translator.t('设置_分组设置_前缀'), content: () => this.groupDisplay() }, 35 | { text: this.manager.translator.t('设置_标签设置_前缀'), content: () => this.tagDisplay() }, 36 | 37 | ]; 38 | if (this.manager.settings.DELAY) tabItems.push({ text: this.manager.translator.t('设置_延迟设置_前缀'), content: () => this.delayDisplay() }); 39 | 40 | const tabItemsEls: HTMLDivElement[] = []; 41 | 42 | tabItems.forEach((item, index) => { 43 | const itemEl = tabsEl.createEl('div'); 44 | itemEl.addClass('manager-setting__tabs-item'); 45 | itemEl.textContent = item.text; 46 | tabItemsEls.push(itemEl); 47 | if (index === 0) { itemEl.addClass('manager-setting__tabs-item_is-active'); item.content(); } 48 | itemEl.addEventListener('click', () => { 49 | tabItemsEls.forEach(tabEl => { tabEl.removeClass('manager-setting__tabs-item_is-active') }); 50 | itemEl.addClass('manager-setting__tabs-item_is-active'); 51 | item.content(); 52 | }); 53 | }); 54 | } 55 | basisDisplay() { this.contentEl.empty(); new ManagerBasis(this).display(); } 56 | styleDisplay() { this.contentEl.empty(); new ManagerStyle(this).display(); } 57 | delayDisplay() { this.contentEl.empty(); new ManagerDelay(this).display(); } 58 | groupDisplay() { this.contentEl.empty(); new ManagerGroup(this).display(); } 59 | tagDisplay() { this.contentEl.empty(); new ManagerTag(this).display(); } 60 | } 61 | 62 | export { ManagerSettingTab }; 63 | 64 | -------------------------------------------------------------------------------- /src/modal/update-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, DropdownComponent, Modal, Notice, Setting } from "obsidian"; 2 | import Manager from "main"; 3 | import { ReleaseVersion, fetchReleaseVersions } from "src/github-install"; 4 | 5 | export class UpdateModal extends Modal { 6 | private manager: Manager; 7 | private pluginId: string; 8 | private versions: ReleaseVersion[]; 9 | private defaultVersion?: string | null; 10 | private repo?: string; 11 | 12 | constructor(app: App, manager: Manager, pluginId: string, versions: ReleaseVersion[], defaultVersion?: string | null, repo?: string) { 13 | super(app); 14 | this.manager = manager; 15 | this.pluginId = pluginId; 16 | this.versions = versions; 17 | this.defaultVersion = defaultVersion; 18 | this.repo = repo; 19 | } 20 | 21 | async onOpen() { 22 | const { contentEl } = this; 23 | contentEl.empty(); 24 | const title = contentEl.createEl("h3", { text: "选择版本" }); 25 | title.style.marginBottom = "8px"; 26 | 27 | let versionList = [...this.versions]; 28 | if (versionList.length === 0 && this.repo) { 29 | const loading = contentEl.createDiv(); 30 | loading.setText("正在获取可用版本..."); 31 | try { 32 | versionList = await fetchReleaseVersions(this.manager, this.repo); 33 | } catch (e) { 34 | console.error("fetch versions in modal failed", e); 35 | new Notice("获取版本列表失败,请稍后再试"); 36 | } finally { 37 | loading.remove(); 38 | } 39 | this.versions = versionList; 40 | } 41 | 42 | let selected = this.defaultVersion || (versionList[0]?.version ?? ""); 43 | if (versionList.length > 0) { 44 | new Setting(contentEl) 45 | .setName("版本") 46 | .addDropdown((dd: DropdownComponent) => { 47 | versionList.forEach(v => { 48 | dd.addOption(v.version, `${v.version}${v.prerelease ? " (pre)" : ""}`); 49 | }); 50 | dd.setValue(selected); 51 | dd.onChange((v) => { selected = v; }); 52 | }); 53 | } else { 54 | const info = contentEl.createDiv(); 55 | info.setText("未获取到版本列表,将尝试使用检测到的版本或最新版。"); 56 | } 57 | 58 | new Setting(contentEl) 59 | .addButton((btn) => { 60 | btn.setButtonText("下载更新"); 61 | btn.setCta(); 62 | btn.onClick(async () => { 63 | btn.setDisabled(true); 64 | try { 65 | const ok = await this.manager.downloadUpdate(this.pluginId, selected); 66 | if (ok) { 67 | new Notice("已下载并更新插件"); 68 | this.close(); 69 | } 70 | } finally { 71 | btn.setDisabled(false); 72 | } 73 | }); 74 | }) 75 | .addButton((btn) => { 76 | btn.setButtonText("取消"); 77 | btn.onClick(() => this.close()); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/repo-resolver.ts: -------------------------------------------------------------------------------- 1 | import { normalizePath, requestUrl } from "obsidian"; 2 | import Manager from "main"; 3 | 4 | export const BPM_TAG_ID = "bpm-install"; 5 | const CACHE_FILE = "community-plugins-cache.json"; 6 | type RepoMap = Record; 7 | 8 | export class RepoResolver { 9 | private manager: Manager; 10 | private cacheLoaded = false; 11 | private cache: RepoMap = {}; 12 | private bpmTagNameFallback = "bpm install"; 13 | 14 | constructor(manager: Manager) { 15 | this.manager = manager; 16 | } 17 | 18 | private get cachePath() { 19 | return normalizePath(`${this.manager.app.vault.configDir}/plugins/${this.manager.manifest.id}/${CACHE_FILE}`); 20 | } 21 | 22 | private async loadCacheFromFile() { 23 | const adapter = this.manager.app.vault.adapter; 24 | if (await adapter.exists(this.cachePath)) { 25 | try { 26 | const content = await adapter.read(this.cachePath); 27 | this.cache = JSON.parse(content) as RepoMap; 28 | } catch (e) { 29 | console.error("加载仓库缓存失败", e); 30 | this.cache = {}; 31 | } 32 | } 33 | } 34 | 35 | private async writeCache() { 36 | const adapter = this.manager.app.vault.adapter; 37 | try { 38 | await adapter.write(this.cachePath, JSON.stringify(this.cache)); 39 | } catch (e) { 40 | console.error("写入仓库缓存失败", e); 41 | } 42 | } 43 | 44 | private async ensureCacheLoaded() { 45 | if (this.cacheLoaded) return; 46 | await this.loadCacheFromFile(); 47 | // 合并设置中的映射 48 | this.cache = { ...this.cache, ...(this.manager.settings.REPO_MAP || {}) }; 49 | this.cacheLoaded = true; 50 | } 51 | 52 | private async fetchCommunityList(): Promise { 53 | const url = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json"; 54 | try { 55 | const res = await requestUrl({ url }); 56 | const list = res.json as { id: string; repo: string }[]; 57 | const map: RepoMap = {}; 58 | list.forEach((item) => { if (item.id && item.repo) map[item.id] = item.repo; }); 59 | this.cache = { ...this.cache, ...map }; 60 | await this.writeCache(); 61 | return map; 62 | } catch (e) { 63 | console.error("获取社区插件清单失败", e); 64 | return {}; 65 | } 66 | } 67 | 68 | public async resolveRepo(pluginId: string): Promise { 69 | await this.ensureCacheLoaded(); 70 | const fromSettings = this.manager.settings.REPO_MAP?.[pluginId]; 71 | if (fromSettings) return fromSettings; 72 | if (this.cache[pluginId]) return this.cache[pluginId]; 73 | 74 | const remote = await this.fetchCommunityList(); 75 | const found = remote[pluginId]; 76 | if (found) { 77 | this.manager.settings.REPO_MAP[pluginId] = found; 78 | // 仅保存设置,避免导出时递归触发 79 | await this.manager.saveSettings(); 80 | return found; 81 | } 82 | return null; 83 | } 84 | 85 | public async setRepo(pluginId: string, repo: string) { 86 | await this.ensureCacheLoaded(); 87 | this.cache[pluginId] = repo; 88 | this.manager.settings.REPO_MAP[pluginId] = repo; 89 | // 仅保存设置,避免与导出互相递归 90 | await this.manager.saveSettings(); 91 | await this.writeCache(); 92 | } 93 | } 94 | 95 | export const ensureBpmTagExists = (manager: Manager) => { 96 | if (!manager.settings.TAGS.find((t) => t.id === BPM_TAG_ID)) { 97 | manager.settings.TAGS.push({ 98 | id: BPM_TAG_ID, 99 | name: manager.translator ? manager.translator.t("标签_BPM安装_名称") : "bpm install", 100 | color: "#409EFF", 101 | }); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/settings/ui/manager-delay.ts: -------------------------------------------------------------------------------- 1 | import BaseSetting from "../base-setting"; 2 | import { Notice, Setting } from "obsidian"; 3 | 4 | export default class ManagerDelay extends BaseSetting { 5 | main(): void { 6 | let id = ''; 7 | let name = ''; 8 | let time = 0; 9 | new Setting(this.containerEl) 10 | .setHeading() 11 | .setName(this.manager.translator.t('通用_新增_文本')) 12 | .addSlider(cb => cb 13 | .setLimits(0, 100, 1) 14 | .setValue(time) 15 | .setDynamicTooltip() 16 | .onChange((value) => { 17 | time = value; 18 | }) 19 | ) 20 | .addText(cb => cb 21 | .setPlaceholder('ID') 22 | .onChange((value) => { 23 | id = value; 24 | }) 25 | ) 26 | .addText(cb => cb 27 | .setPlaceholder(this.manager.translator.t('通用_名称_文本')) 28 | .onChange((value) => { 29 | name = value; 30 | }) 31 | ) 32 | .addExtraButton(cb => cb 33 | .setIcon('plus') 34 | .onClick(() => { 35 | const containsId = this.manager.settings.DELAYS.some(delay => delay.id === id); 36 | if (!containsId && id !== '') { 37 | this.manager.settings.DELAYS.push({ id, name, time }); 38 | this.manager.saveSettings(); 39 | this.settingTab.delayDisplay(); 40 | new Notice(this.manager.translator.t('设置_延迟设置_通知_一')); 41 | } else { 42 | new Notice(this.manager.translator.t('设置_延迟设置_通知_二')); 43 | } 44 | }) 45 | ) 46 | this.manager.settings.DELAYS.forEach((delay, index) => { 47 | const item = new Setting(this.containerEl) 48 | item.settingEl.addClass('manager-setting-group__item') 49 | item.setName(`[${delay.id}]`) 50 | item.addSlider(cb => cb 51 | .setLimits(0, 100, 1) 52 | .setValue(delay.time) 53 | .setDynamicTooltip() 54 | .onChange((value) => { 55 | delay.time = value 56 | this.manager.saveSettings(); 57 | }) 58 | ) 59 | item.addText(cb => cb 60 | .setValue(delay.name) 61 | .onChange((value) => { 62 | delay.name = value; 63 | this.manager.saveSettings(); 64 | }) 65 | ) 66 | item.addExtraButton(cb => cb 67 | .setIcon('trash-2') 68 | .onClick(() => { 69 | const hasTestGroup = this.settings.Plugins.some(plugin => plugin.delay === delay.id); 70 | if (!hasTestGroup) { 71 | this.manager.settings.DELAYS = this.manager.settings.DELAYS.filter(t => t.id !== delay.id); 72 | this.manager.saveSettings(); 73 | this.settingTab.delayDisplay(); 74 | new Notice(this.manager.translator.t('设置_延迟设置_通知_三')); 75 | } else { 76 | new Notice(this.manager.translator.t('设置_延迟设置_通知_四')); 77 | } 78 | }) 79 | ) 80 | }); 81 | } 82 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Plugins Manager 2 | 3 | [简体中文](docs/README_CN.md) 4 | 5 | ![GitHub Downloads](https://img.shields.io/github/downloads/zenozero-dev/obsidian-manager/total) 6 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/zenozero-dev/obsidian-manager) 7 | ![Last commit](https://img.shields.io/github/last-commit/zenozero-dev/obsidian-manager) 8 | ![Issues](https://img.shields.io/github/issues/zenozero-dev/obsidian-manager) 9 | ![Stars](https://img.shields.io/github/stars/zenozero-dev/obsidian-manager?style=social) 10 | 11 | ![Screenshot](img/index.png) 12 | 13 | ## What is BPM? 14 | 15 | Better Plugins Manager goes beyond Obsidian’s built-in manager: delay-start plugins, batch enable/disable, organize with groups and tags, rename and annotate, install from GitHub with release picker, export data for Base workflows, and keep a mobile-friendly UI. 16 | 17 | ## Highlights 18 | 19 | - **Fast control & batching**: toggle all plugins, enable/disable by group, and per-plugin quick switches. 20 | - **Organize & document**: custom names/descriptions, groups, tags (auto “bpm-install” tag for BPM installs), searchable filters. 21 | - **GitHub-aware install**: paste `user/repo` or full URL, pick releases like BRAT, cache repo mapping, and jump to repo from each card. 22 | - **Delay & performance**: start plugins with per-profile delays. 23 | - **Base-friendly export**: write plugin metadata to Markdown frontmatter and sync back (safe read-only/write tags). 24 | - **Mobile ready**: collapsible action/search bars, long-press tooltips, responsive grid; desktop UI remains unchanged. 25 | - **GitHub token support**: optional PAT to avoid rate limits when fetching releases. 26 | 27 | ## Installation 28 | 29 | This plugin is now available on the Obsidian official marketplace. 30 | 31 | ## Usage 32 | 33 | - Open from the ribbon “Manager” icon or command palette (`Manage plugins`). 34 | - Cards let you rename, edit descriptions/notes, tag/group, set delay, open repo/folder, delete, and toggle enabled. 35 | - Filters: by state, group, tag, delay, and keyword search. 36 | - Actions: batch enable/disable all or by group, reload plugins, install from GitHub with release selection. 37 | 38 | ## Export to Obsidian Base 39 | 40 | 1) In settings, set **Plugin info export directory** (folder inside your vault). 41 | 2) BPM exports one Markdown per plugin and watches the folder for edits. 42 | 3) Read/Write rules: `bpm_rw_*` are editable; `bpm_ro_*` are read-only; `bpm_rwc_repo` is editable only when the plugin is not BPM-installed and has no official repo mapping. 43 | 44 | Frontmatter schema (auto-generated): 45 | 46 | ```yaml 47 | --- 48 | bpm_ro_id: some-plugin 49 | bpm_rw_name: Custom name 50 | bpm_rw_desc: Custom description 51 | bpm_rw_note: Personal note 52 | bpm_rw_enabled: true 53 | bpm_rwc_repo: user/repo 54 | bpm_ro_group: group-id 55 | bpm_ro_tags: 56 | - tag-a 57 | - bpm-install 58 | bpm_ro_delay: delay-id 59 | bpm_ro_installed_via_bpm: true 60 | bpm_ro_updated: 2024-12-12T10:00:00Z 61 | --- 62 | 63 | Body section: you can edit or replace this content. 64 | ``` 65 | 66 | ## Settings you’ll care about 67 | 68 | - **Delay profiles**: create presets and assign per plugin. 69 | - **Hide BPM tag**: keep the auto “bpm-install” tag but hide it in UI. 70 | - **GitHub API token**: raise rate limits for release fetching. 71 | - **Fade inactive plugins**: visually de-emphasize disabled items. 72 | - **Export notices**: configurable hint text for exported files. 73 | 74 | ## Commands 75 | 76 | - Open manager panel. 77 | - (Optional) per-plugin toggle commands. 78 | - (Optional) enable/disable all plugins in a group. 79 | 80 | ## Compatibility 81 | 82 | - Works on desktop and mobile (Android/iOS). 83 | - Uses platform detection to apply the compact mobile layout while keeping the desktop layout unchanged. 84 | 85 | ## Contributing 86 | 87 | Issues and PRs are welcome. For bugs, share console logs and reproduction steps; for features, open a discussion/issue first. 88 | 89 | ## License 90 | 91 | [MIT](LICENSE) 92 | -------------------------------------------------------------------------------- /src/settings/ui/manager-style.ts: -------------------------------------------------------------------------------- 1 | import BaseSetting from "../base-setting"; 2 | import { DropdownComponent, Setting, ToggleComponent } from "obsidian"; 3 | import Commands from "src/command"; 4 | // import { GROUP_STYLE, ITEM_STYLE, TAG_STYLE } from "src/data/data"; 5 | 6 | export default class ManagerBasis extends BaseSetting { 7 | private ITEM_STYLE = { 8 | 'alwaysExpand': this.manager.translator.t('设置_基础设置_目录样式_选项_一'), 9 | 'neverExpand': this.manager.translator.t('设置_基础设置_目录样式_选项_二'), 10 | 'hoverExpand': this.manager.translator.t('设置_基础设置_目录样式_选项_三'), 11 | 'clickExpand': this.manager.translator.t('设置_基础设置_目录样式_选项_四'), 12 | } 13 | private GROUP_STYLE = { 14 | 'a': this.manager.translator.t('设置_基础设置_分组样式_选项_一'), 15 | 'b': this.manager.translator.t('设置_基础设置_分组样式_选项_二'), 16 | 'c': this.manager.translator.t('设置_基础设置_分组样式_选项_三'), 17 | 'd': this.manager.translator.t('设置_基础设置_分组样式_选项_四') 18 | } 19 | private TAG_STYLE = { 20 | 'a': this.manager.translator.t('设置_基础设置_标签样式_选项_一'), 21 | 'b': this.manager.translator.t('设置_基础设置_标签样式_选项_二'), 22 | 'c': this.manager.translator.t('设置_基础设置_标签样式_选项_三'), 23 | 'd': this.manager.translator.t('设置_基础设置_标签样式_选项_四') 24 | } 25 | 26 | 27 | main(): void { 28 | 29 | const itemStyleBar = new Setting(this.containerEl) 30 | .setName(this.manager.translator.t('设置_基础设置_目录样式_标题')) 31 | .setDesc(this.manager.translator.t('设置_基础设置_目录样式_描述')); 32 | const itemStyleDropdown = new DropdownComponent(itemStyleBar.controlEl); 33 | itemStyleDropdown.addOptions(this.ITEM_STYLE); 34 | itemStyleDropdown.setValue(this.settings.ITEM_STYLE); 35 | itemStyleDropdown.onChange((value) => { 36 | this.settings.ITEM_STYLE = value; 37 | this.manager.saveSettings(); 38 | }); 39 | 40 | const groupStyleBar = new Setting(this.containerEl) 41 | .setName(this.manager.translator.t('设置_基础设置_分组样式_标题')) 42 | .setDesc(this.manager.translator.t('设置_基础设置_分组样式_描述')); 43 | const groupStyleDropdown = new DropdownComponent(groupStyleBar.controlEl); 44 | groupStyleDropdown.addOptions(this.GROUP_STYLE); 45 | groupStyleDropdown.setValue(this.settings.GROUP_STYLE); 46 | groupStyleDropdown.onChange((value) => { 47 | this.settings.GROUP_STYLE = value; 48 | this.manager.saveSettings(); 49 | }); 50 | 51 | const tagStyleBar = new Setting(this.containerEl) 52 | .setName(this.manager.translator.t('设置_基础设置_标签样式_标题')) 53 | .setDesc(this.manager.translator.t('设置_基础设置_标签样式_描述')); 54 | const tagStyleDropdown = new DropdownComponent(tagStyleBar.controlEl); 55 | tagStyleDropdown.addOptions(this.TAG_STYLE); 56 | tagStyleDropdown.setValue(this.settings.TAG_STYLE); 57 | tagStyleDropdown.onChange((value) => { 58 | this.settings.TAG_STYLE = value; 59 | this.manager.saveSettings(); 60 | }); 61 | 62 | const topBar = new Setting(this.containerEl) 63 | .setName(this.manager.translator.t('设置_基础设置_界面居中_标题')) 64 | .setDesc(this.manager.translator.t('设置_基础设置_界面居中_描述')); 65 | const topToggle = new ToggleComponent(topBar.controlEl); 66 | topToggle.setValue(this.settings.CENTER); 67 | topToggle.onChange((value) => { 68 | this.settings.CENTER = value; 69 | this.manager.saveSettings(); 70 | }); 71 | 72 | const fadeOutDisabledPluginsBar = new Setting(this.containerEl) 73 | .setName(this.manager.translator.t('设置_基础设置_淡化插件_标题')) 74 | .setDesc(this.manager.translator.t('设置_基础设置_淡化插件_描述')); 75 | const fadeOutDisabledPluginsToggle = new ToggleComponent(fadeOutDisabledPluginsBar.controlEl); 76 | fadeOutDisabledPluginsToggle.setValue(this.settings.FADE_OUT_DISABLED_PLUGINS); 77 | fadeOutDisabledPluginsToggle.onChange((value) => { 78 | this.settings.FADE_OUT_DISABLED_PLUGINS = value; 79 | this.manager.saveSettings(); 80 | }); 81 | 82 | } 83 | } -------------------------------------------------------------------------------- /src/settings/ui/manager-group.ts: -------------------------------------------------------------------------------- 1 | import BaseSetting from "../base-setting"; 2 | import { Notice, Setting } from "obsidian"; 3 | import Commands from "src/command"; 4 | 5 | export default class ManagerGroup extends BaseSetting { 6 | main(): void { 7 | let id = ''; 8 | let name = ''; 9 | let color = this.manager.generateAutoColor(this.manager.settings.GROUPS.map(g => g.color)); 10 | new Setting(this.containerEl) 11 | .setHeading() 12 | .setName(this.manager.translator.t('通用_新增_文本')) 13 | .addColorPicker(cb => cb 14 | .setValue(color) 15 | .onChange((value) => { 16 | color = value; 17 | }) 18 | ) 19 | .addText(cb => cb 20 | .setPlaceholder('ID') 21 | .onChange((value) => { 22 | id = value; 23 | this.manager.saveSettings(); 24 | }) 25 | ) 26 | .addText(cb => cb 27 | .setPlaceholder(this.manager.translator.t('通用_名称_文本')) 28 | .onChange((value) => { 29 | name = value; 30 | }) 31 | ) 32 | .addExtraButton(cb => cb 33 | .setIcon('plus') 34 | .onClick(() => { 35 | const containsId = this.manager.settings.GROUPS.some(tag => tag.id === id); 36 | if (!containsId && id !== '') { 37 | if (color === '') color = this.manager.generateAutoColor(this.manager.settings.GROUPS.map(g => g.color)); 38 | this.manager.settings.GROUPS.push({ id, name, color }); 39 | this.manager.saveSettings(); 40 | this.settingTab.groupDisplay(); 41 | Commands(this.app, this.manager); 42 | new Notice(this.manager.translator.t('设置_分组设置_通知_一')); 43 | } else { 44 | new Notice(this.manager.translator.t('设置_分组设置_通知_二')); 45 | } 46 | }) 47 | ) 48 | 49 | this.manager.settings.GROUPS.forEach((group, index) => { 50 | const item = new Setting(this.containerEl) 51 | item.settingEl.addClass('manager-setting-group__item') 52 | // item.setName(`${index + 1}. `) 53 | item.addColorPicker(cb => cb 54 | .setValue(group.color) 55 | .onChange((value) => { 56 | group.color = value; 57 | this.manager.saveSettings(); 58 | this.settingTab.groupDisplay(); 59 | }) 60 | ) 61 | item.addText(cb => cb 62 | .setValue(group.name) 63 | .onChange((value) => { 64 | group.name = value; 65 | this.manager.saveSettings(); 66 | }).inputEl.addEventListener('blur', () => { 67 | this.settingTab.groupDisplay(); 68 | }) 69 | ) 70 | item.addExtraButton(cb => cb 71 | .setIcon('trash-2') 72 | .onClick(() => { 73 | const hasTestGroup = this.settings.Plugins.some(plugin => plugin.group === group.id); 74 | if (!hasTestGroup) { 75 | this.manager.settings.GROUPS = this.manager.settings.GROUPS.filter(t => t.id !== group.id); 76 | this.manager.saveSettings(); 77 | this.settingTab.groupDisplay(); 78 | Commands(this.app, this.manager); 79 | new Notice(this.manager.translator.t('设置_分组设置_通知_三')); 80 | } else { 81 | new Notice(this.manager.translator.t('设置_分组设置_通知_四')); 82 | } 83 | }) 84 | ) 85 | const tagEl = this.manager.createTag(group.name, group.color, this.settings.GROUP_STYLE); 86 | item.nameEl.appendChild(tagEl); 87 | item.nameEl.appendText(` [${group.id}]`); 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/settings/ui/manager-tag.ts: -------------------------------------------------------------------------------- 1 | import BaseSetting from "../base-setting"; 2 | import { Notice, Setting } from "obsidian"; 3 | import { BPM_TAG_ID } from "src/repo-resolver"; 4 | 5 | export default class ManagerTag extends BaseSetting { 6 | main(): void { 7 | let id = ''; 8 | let name = ''; 9 | let color = this.manager.generateAutoColor(this.manager.settings.TAGS.map(t => t.color)); 10 | new Setting(this.containerEl) 11 | .setHeading() 12 | .setName(this.manager.translator.t('通用_新增_文本')) 13 | .addColorPicker(cb => cb 14 | .setValue(color) 15 | .onChange((value) => { 16 | color = value; 17 | }) 18 | ) 19 | .addText(cb => cb 20 | .setPlaceholder('ID') 21 | .onChange((value) => { 22 | id = value; 23 | this.manager.saveSettings(); 24 | }) 25 | ) 26 | .addText(cb => cb 27 | .setPlaceholder(this.manager.translator.t('通用_名称_文本')) 28 | .onChange((value) => { 29 | name = value; 30 | }) 31 | ) 32 | .addExtraButton(cb => cb 33 | .setIcon('plus') 34 | .onClick(() => { 35 | const containsId = this.manager.settings.TAGS.some(tag => tag.id === id); 36 | if (!containsId && id !== '') { 37 | if (color === '') color = this.manager.generateAutoColor(this.manager.settings.TAGS.map(t => t.color)); 38 | this.manager.settings.TAGS.push({ id, name, color }); 39 | this.manager.saveSettings(); 40 | this.settingTab.tagDisplay(); 41 | new Notice(this.manager.translator.t('设置_标签设置_通知_一')); 42 | } else { 43 | new Notice(this.manager.translator.t('设置_标签设置_通知_二')); 44 | } 45 | }) 46 | ) 47 | this.manager.settings.TAGS.forEach((tag, index) => { 48 | const item = new Setting(this.containerEl) 49 | item.setClass('manager-setting-tag__item') 50 | // item.setName(`${index + 1}. `) 51 | const isBpmTag = tag.id === BPM_TAG_ID; 52 | item.addColorPicker(cb => cb 53 | .setDisabled(isBpmTag) 54 | .setValue(tag.color) 55 | .onChange((value) => { 56 | tag.color = value; 57 | this.manager.saveSettings(); 58 | this.settingTab.tagDisplay(); 59 | }) 60 | ); 61 | item.addText(cb => cb 62 | .setDisabled(isBpmTag) 63 | .setValue(tag.name) 64 | .onChange((value) => { 65 | tag.name = value; 66 | this.manager.saveSettings(); 67 | }).inputEl.addEventListener('blur', () => { 68 | this.settingTab.tagDisplay(); 69 | }) 70 | ); 71 | item.addExtraButton(cb => cb 72 | .setIcon('trash-2') 73 | .setDisabled(isBpmTag) 74 | .onClick(() => { 75 | const hasTestTag = this.settings.Plugins.some(plugin => plugin.tags && plugin.tags.includes(tag.id)); 76 | if (!hasTestTag) { 77 | this.manager.settings.TAGS = this.manager.settings.TAGS.filter(t => t.id !== tag.id); 78 | this.manager.saveSettings(); 79 | this.settingTab.tagDisplay(); 80 | new Notice(this.manager.translator.t('设置_标签设置_通知_三')); 81 | } else { 82 | new Notice(this.manager.translator.t('设置_标签设置_通知_四')); 83 | } 84 | }) 85 | ); 86 | const tagEl = this.manager.createTag(tag.name, tag.color, this.settings.TAG_STYLE); 87 | item.nameEl.appendChild(tagEl); 88 | item.nameEl.appendText(` [${tag.id}]`); 89 | }); 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lang/locale/ko.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: '플러그인 관리자', 3 | 通用_成功_文本: '성공', 4 | 通用_失败_文本: '실패', 5 | 通用_新增_文本: '추가', 6 | 通用_操作_文本: '작업', 7 | 通用_搜索_文本: '검색', 8 | 通用_名称_文本: '이름', 9 | 通用_无分组_文本: '그룹 없음', 10 | 通用_无标签_文本: '태그 없음', 11 | 通用_无延迟_文本: '딜레이 없음', 12 | 通用_过滤_文本: '필터', 13 | 通用_总计_文本: '총계', 14 | 通用_启用_文本: '활성화', 15 | 通用_禁用_文本: '비활성화', 16 | 17 | 18 | 管理器_GITHUB_描述: '저자의 GitHub 페이지를 방문하여 프로젝트 세부 정보, 업데이트 로그, 토론 참여, 코드 기여를 확인하세요.', 19 | 管理器_视频教程_描述: '비디오 튜토리얼에 액세스', 20 | 管理器_编辑模式_描述: '편집 모드를 활성화하여 플러그인 설정을 자세히 커스터마이징하세요', 21 | 管理器_重载插件_描述: '플러그인을 다시 로드하여 즉시 적용하세요', 22 | 管理器_检查更新_描述: '플러그인 업데이트를 확인하세요', 23 | 管理器_一键禁用_描述: '한 번에 모든 플러그인을 비활성화하세요', 24 | 管理器_一键启用_描述: '한 번에 모든 플러그인을 활성화하세요', 25 | 管理器_插件设置_描述: '플러그인 설정을 관리하세요', 26 | 管理器_仅启用_描述: '활성화된 플러그인만 표시하세요', 27 | 管理器_打开设置_描述: '설정 인터페이스를 엽니다', 28 | 管理器_还原内容_描述: '초기 상태로 복원하세요', 29 | 管理器_打开目录_描述: '플러그인 디렉토리를 엽니다', 30 | 管理器_删除插件_描述: '플러그인을 완전히 삭제하세요', 31 | 管理器_切换状态_描述: '플러그인 상태를 전환하세요', 32 | 菜单_检查更新_标题: '업데이트 확인', 33 | 菜单_隐藏插件_标题: '플러그인 숨기기', 34 | 菜单_复制ID_标题: 'ID 복사', 35 | 管理器_安装_GITHUB_描述: '플러그인 / 테마 설치 (GitHub 저장소)', 36 | 管理器_安装_介绍: 'GitHub 릴리스에서 플러그인·테마 설치 (최신 자산 사용).', 37 | 管理器_安装_类型_标题: '유형', 38 | 管理器_安装_类型_描述: '플러그인 또는 테마를 선택하세요.', 39 | 管理器_安装_类型_插件: '플러그인', 40 | 管理器_安装_类型_主题: '테마', 41 | 管理器_安装_仓库_标题: '저장소', 42 | 管理器_安装_仓库_描述: '/ 또는 https://github.com// 형식 지원.', 43 | 管理器_安装_仓库_占位: 'user/repo', 44 | 管理器_安装_仓库为空提示: '저장소 경로를 입력하세요', 45 | 管理器_安装_版本_标题: '버전', 46 | 管理器_安装_版本_描述: 'GitHub 릴리스를 가져온 뒤 선택합니다. 비우면 최신.', 47 | 管理器_安装_版本_默认最新: '최신 릴리스', 48 | 管理器_安装_版本_获取按钮: '버전 가져오기', 49 | 管理器_安装_版本_获取中: '가져오는 중...', 50 | 管理器_安装_版本_空提示: '릴리스를 찾지 못했습니다. 태그를 직접 입력해 보세요.', 51 | 管理器_安装_版本_失败提示: '릴리스 가져오기 실패. 저장소나 네트워크를 확인하세요.', 52 | 管理器_安装_操作_标题: '작업', 53 | 管理器_安装_操作_按钮: '지금 설치', 54 | 55 | 卸载_标题: '플러그인 제거', 56 | 卸载_提示: '이 플러그인을 제거하시겠습니까? 이 작업은 플러그인 폴더를 삭제합니다.', 57 | 卸载_卸载: '제거', 58 | 卸载_取消: '취소', 59 | 卸载_通知_一: '성공적으로 제거되었습니다', 60 | 61 | 设置_基础设置_前缀: '기본', 62 | 设置_分组设置_前缀: '그룹', 63 | 设置_标签设置_前缀: '태그', 64 | 设置_延迟设置_前缀: '딜레이', 65 | 66 | 67 | 设置_基础设置_语言_标题: '언어 설정', 68 | 设置_基础设置_语言_描述: '선호하는 언어를 선택하세요.', 69 | 设置_基础设置_目录样式_标题: '디렉토리 스타일', 70 | 设置_基础设置_目录样式_描述: '그룹의 스타일을 선택하여 브라우징 경험을 향상하세요.', 71 | 设置_基础设置_分组样式_标题: '그룹 스타일', 72 | 设置_基础设置_分组样式_描述: '그룹의 스타일을 선택하여 더 눈에 띄고 식별하기 쉽게 만드세요.', 73 | 设置_基础设置_标签样式_标题: '태그 스타일', 74 | 设置_基础设置_标签样式_描述: '태그의 스타일을 선택하여 더 눈에 띄고 식별하기 쉽게 만드세요.', 75 | 76 | 设置_基础设置_延时启动_标题: '지연 시작', 77 | 设置_基础设置_延时启动_描述: '지연 시작 기능을 활성화하면 로딩 순서를 최적화할 수 있지만, 일부 플러그인에서 호환성 문제가 발생할 수 있으므로 유의하세요.', 78 | 设置_基础设置_淡化插件_标题: '플러그인 흐리게 표시', 79 | 设置_基础设置_淡化插件_描述: '비활성화된 플러그인에 시각적인 흐림 효과를 제공하여 활성화된 플러그인과 비활성화된 플러그인을 명확히 구분하세요.', 80 | 设置_基础设置_单独命令_标题: '플러그인 명령을 별도로 제어', 81 | 设置_基础设置_单独命令_描述: '이 옵션을 활성화하면 각 플러그인의 활성화/비활성화 상태를 별도로 제어할 수 있습니다. (Obsidian을 다시 시작해야 적용됩니다)', 82 | 设置_基础设置_分组命令_标题: '그룹별 플러그인 명령 제어', 83 | 设置_基础设置_分组命令_描述: '이 옵션을 활성화하면 지정된 그룹의 모든 플러그인을 한 번 클릭으로 활성화하거나 비활성화할 수 있습니다. (Obsidian을 다시 시작해야 적용됩니다)', 84 | 设置_基础设置_启动检查更新_标题: '시작 시 업데이트 확인', 85 | 设置_基础设置_启动检查更新_描述: 'BPM을 열 때 자동으로 업데이트를 확인하고 개수를 간단히 알립니다.', 86 | 87 | 设置_延迟设置_通知_一: '[딜레이] 추가됨', 88 | 设置_延迟设置_通知_二: '[딜레이] ID가 이미 존재하거나 비어 있음', 89 | 设置_延迟设置_通知_三: '[딜레이] 성공적으로 삭제됨', 90 | 设置_延迟设置_通知_四: '[딜레이] 삭제 실패, 이 딜레이하에 플러그인이 존재함', 91 | 92 | 设置_分组设置_通知_一: '[그룹] 추가됨', 93 | 设置_分组设置_通知_二: '[그룹] ID가 이미 존재하거나 비어 있음', 94 | 设置_分组设置_通知_三: '[그룹] 성공적으로 삭제됨', 95 | 设置_分组设置_通知_四: '[그룹] 삭제 실패, 이 그룹하에 플러그인이 존재함', 96 | 97 | 设置_标签设置_通知_一: '[태그] 추가됨', 98 | 设置_标签设置_通知_二: '[태그] ID가 이미 존재하거나 비어 있음', 99 | 设置_标签设置_通知_三: '[태그] 성공적으로 삭제됨', 100 | 设置_标签设置_通知_四: '[태그] 삭제 실패, 이 태그하에 플러그인이 존재함', 101 | 102 | 设置_提示_一_标题: '다른 플러그인과의 충돌이 발생할 경우', 103 | 设置_提示_一_描述: '능력이 제한되어 있어 이 문제를 해결할 수 없습니다. 지연 시작을 비활성화하여 모든 충돌 문제를 해결하세요.', 104 | 通知_可更新数量: '업데이트 가능한 플러그인 {count}개가 있습니다', 105 | 筛选_状态_全部: '상태: 전체', 106 | 筛选_分组_全部: '그룹: 전체', 107 | 筛选_标签_全部: '태그: 전체', 108 | 筛选_延迟_全部: '지연: 전체', 109 | 110 | 命令_管理面板_描述: '플러그인 관리자를 엽니다', 111 | } 112 | -------------------------------------------------------------------------------- /src/lang/locale/ja.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: 'プラグインマネージャー', 3 | 通用_成功_文本: '成功', 4 | 通用_失败_文本: '失敗', 5 | 通用_新增_文本: '追加', 6 | 通用_操作_文本: '操作', 7 | 通用_搜索_文本: '検索', 8 | 通用_名称_文本: '名前', 9 | 通用_无分组_文本: 'グループなし', 10 | 通用_无标签_文本: 'タグなし', 11 | 通用_无延迟_文本: '遅延なし', 12 | 通用_过滤_文本: 'フィルター', 13 | 通用_总计_文本: '合計', 14 | 通用_启用_文本: '有効', 15 | 通用_禁用_文本: '無効', 16 | 17 | 18 | 管理器_GITHUB_描述: '著者のGitHubページを訪れ、プロジェクトの詳細、更新ログ、議論への参加、コードへの貢献を確認してください。', 19 | 管理器_视频教程_描述: 'ビデオチュートリアルにアクセス', 20 | 管理器_编辑模式_描述: '編集モードを有効にして、プラグインの設定をカスタマイズします', 21 | 管理器_重载插件_描述: 'プラグインをリロードして即座に効果を発揮します', 22 | 管理器_检查更新_描述: 'プラグインの更新を確認する', 23 | 管理器_一键禁用_描述: '一度にすべてのプラグインを無効にします', 24 | 管理器_一键启用_描述: '一度にすべてのプラグインを有効にします', 25 | 管理器_插件设置_描述: 'プラグインの設定を管理する', 26 | 管理器_仅启用_描述: '有効なプラグインのみを表示する', 27 | 管理器_打开设置_描述: '設定インターフェースを開く', 28 | 管理器_还原内容_描述: '初期状態に戻す', 29 | 管理器_打开目录_描述: 'プラグインディレクトリを開く', 30 | 管理器_删除插件_描述: 'プラグインを完全に削除する', 31 | 管理器_切换状态_描述: 'プラグインのステータスを切り替える', 32 | 菜单_检查更新_标题: 'アップデートを確認', 33 | 菜单_隐藏插件_标题: 'プラグインを非表示', 34 | 菜单_复制ID_标题: 'IDをコピー', 35 | 管理器_安装_GITHUB_描述: 'プラグイン / テーマをインストール(GitHub リポジトリ)', 36 | 管理器_安装_介绍: 'GitHub リリースからプラグインやテーマをインストール(最新アセットを使用)。', 37 | 管理器_安装_类型_标题: 'タイプ', 38 | 管理器_安装_类型_描述: 'プラグインかテーマを選択してください。', 39 | 管理器_安装_类型_插件: 'プラグイン', 40 | 管理器_安装_类型_主题: 'テーマ', 41 | 管理器_安装_仓库_标题: 'リポジトリ', 42 | 管理器_安装_仓库_描述: '/ または https://github.com// 形式をサポート。', 43 | 管理器_安装_仓库_占位: 'user/repo', 44 | 管理器_安装_仓库为空提示: 'リポジトリのパスを入力してください', 45 | 管理器_安装_版本_标题: 'バージョン', 46 | 管理器_安装_版本_描述: 'GitHub のリリースを取得してから選択します。空なら最新。', 47 | 管理器_安装_版本_默认最新: '最新リリース', 48 | 管理器_安装_版本_获取按钮: 'バージョン取得', 49 | 管理器_安装_版本_获取中: '取得中...', 50 | 管理器_安装_版本_空提示: 'リリースが見つかりません。タグを手入力してみてください。', 51 | 管理器_安装_版本_失败提示: 'リリース取得に失敗しました。リポジトリまたはネットワークを確認してください。', 52 | 管理器_安装_操作_标题: '操作', 53 | 管理器_安装_操作_按钮: 'インストール開始', 54 | 55 | 卸载_标题: 'プラグインのアンインストール', 56 | 卸载_提示: 'このプラグインをアンインストールしてもよろしいですか?プラグインのフォルダが削除されます。', 57 | 卸载_卸载: 'アンインストール', 58 | 卸载_取消: 'キャンセル', 59 | 卸载_通知_一: 'アンインストールに成功しました', 60 | 61 | 设置_基础设置_前缀: '基本', 62 | 设置_分组设置_前缀: 'グループ', 63 | 设置_标签设置_前缀: 'タグ', 64 | 设置_延迟设置_前缀: '遅延', 65 | 66 | 67 | 设置_基础设置_语言_标题: '言語設定', 68 | 设置_基础设置_语言_描述: 'お好みの言語を選択してください。', 69 | 设置_基础设置_目录样式_标题: 'ディレクトリスタイル', 70 | 设置_基础设置_目录样式_描述: 'グループのスタイルを選択して、ブラウジング体験を向上させます。', 71 | 设置_基础设置_分组样式_标题: 'グループスタイル', 72 | 设置_基础设置_分组样式_描述: 'グループのスタイルを選択して、より目立たせやすく識別しやすくします。', 73 | 设置_基础设置_标签样式_标题: 'タグスタイル', 74 | 设置_基础设置_标签样式_描述: 'タグのスタイルを選択して、より目立たせやすく識別しやすくします。', 75 | 76 | 设置_基础设置_延时启动_标题: '遅延スタート', 77 | 设置_基础设置_延时启动_描述: '遅延スタート機能を有効にすると、読み込み順序を最適化できますが、一部のプラグインで互換性問題が発生する場合があります。', 78 | 设置_基础设置_淡化插件_标题: 'プラグインのフェード', 79 | 设置_基础设置_淡化插件_描述: '無効なプラグインに視覚的なフェード効果を提供して、有効と無効のプラグインを明確に区別します。', 80 | 设置_基础设置_单独命令_标题: 'プラグインコマンドを個別に制御', 81 | 设置_基础设置_单独命令_描述: 'このオプションを有効にすると、各プラグインの有効/無効状態を個別に制御できます。(Obsidianを再起動する必要があります)', 82 | 设置_基础设置_分组命令_标题: 'グループごとにプラグインコマンドを制御', 83 | 设置_基础设置_分组命令_描述: 'このオプションを有効にすると、指定されたグループ内のすべてのプラグインをワンクリックで有効または無効にできます。(Obsidianを再起動する必要があります)', 84 | 设置_基础设置_启动检查更新_标题: '起動時に更新をチェック', 85 | 设置_基础设置_启动检查更新_描述: 'BPM を開くときに自動で更新を確認し、件数を短時間表示します。', 86 | 87 | 设置_延迟设置_通知_一: '[遅延] 追加されました', 88 | 设置_延迟设置_通知_二: '[遅延] IDが既に存在するか、空です', 89 | 设置_延迟设置_通知_三: '[遅延] 削除に成功しました', 90 | 设置_延迟设置_通知_四: '[遅延] 削除に失敗しました、この遅延の下にプラグインが存在します', 91 | 92 | 设置_分组设置_通知_一: '[グループ] 追加されました', 93 | 设置_分组设置_通知_二: '[グループ] IDが既に存在するか、空です', 94 | 设置_分组设置_通知_三: '[グループ] 削除に成功しました', 95 | 设置_分组设置_通知_四: '[グループ] 削除に失敗しました、このグループの下にプラグインが存在します', 96 | 97 | 设置_标签设置_通知_一: '[タグ] 追加されました', 98 | 设置_标签设置_通知_二: '[タグ] IDが既に存在するか、空です', 99 | 设置_标签设置_通知_三: '[タグ] 削除に成功しました', 100 | 设置_标签设置_通知_四: '[タグ] 削除に失敗しました、このタグの下にプラグインが存在します', 101 | 102 | 设置_提示_一_标题: '他のプラグインとのコンフリクトが発生した場合', 103 | 设置_提示_一_描述: '能力に限りがあるため、この問題を修正できません。遅延スタートを無効にすることで、すべてのコンフリクト問題を解決してください。', 104 | 通知_可更新数量: '{count} 個のプラグインに更新があります', 105 | 筛选_状态_全部: '状態:全部', 106 | 筛选_分组_全部: 'グループ:全部', 107 | 筛选_标签_全部: 'タグ:全部', 108 | 筛选_延迟_全部: '遅延:全部', 109 | 110 | 命令_管理面板_描述: 'プラグインマネージャーを開く', 111 | } 112 | -------------------------------------------------------------------------------- /src/agreement.ts: -------------------------------------------------------------------------------- 1 | import ShareMyPlugin from "main"; 2 | import { Notice, ObsidianProtocolData, debounce } from "obsidian"; 3 | 4 | // 导出一个全局的 communityPlugins 变量,可在其他模块中使用 5 | export let communityPlugins: any; 6 | 7 | /** 8 | * 插件安装器类,负责处理插件的安装和解析安装参数等操作 9 | */ 10 | 11 | 12 | export default class Agreement { 13 | // 引用 ShareMyPlugin 实例,方便访问主插件的属性和方法 14 | plugin: ShareMyPlugin; 15 | // 存储社区插件信息的对象,键为插件 ID,值为插件详细信息 16 | communityPlugins: Record; 17 | // 标记是否已经加载了社区插件列表 18 | loaded: boolean = false; 19 | // 防抖函数,用于定时刷新社区插件列表,每小时执行一次 20 | debounceFetch = debounce(async () => { await this.fetchCommunityPlugins() }, 1000 * 60 * 60); 21 | 22 | /** 23 | * 从远程获取社区插件列表,并将其转换为以插件 ID 为键的对象 24 | */ 25 | async fetchCommunityPlugins() { 26 | // 从指定的 URL 获取社区插件列表的 JSON 数据 27 | const pluginList = await fetch(`https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json`).then(r => r.json()); 28 | // if (!pluginList.ok) { new Notice(`[插件管理器] 无法连接到Github(跳转主页及下载不可用)`); } 29 | // 创建一个空对象,用于存储以插件 ID 为键的插件信息 30 | const keyedPluginList: Record = {}; 31 | // 遍历插件列表,将每个插件的信息存储到 keyedPluginList 中 32 | for (const item of pluginList) keyedPluginList[item.id] = item; 33 | // 将处理后的插件列表赋值给 communityPlugins 属性 34 | this.communityPlugins = keyedPluginList; 35 | // 标记社区插件列表已加载 36 | this.loaded = true; 37 | } 38 | 39 | /** 40 | * 构造函数,初始化插件安装器 41 | * @param SMPL - ShareMyPlugin 实例 42 | */ 43 | constructor(SMPL: ShareMyPlugin) { 44 | // 保存 ShareMyPlugin 实例 45 | this.plugin = SMPL; 46 | // 调用 fetchCommunityPlugins 方法获取社区插件列表 47 | this.fetchCommunityPlugins(); 48 | } 49 | 50 | /** 51 | * 获取指定的插件 52 | * @param id - 要获取的插件的 ID 53 | */ 54 | public async pluginGithub(id: string) { 55 | // 如果社区插件列表未加载,则先加载 56 | if (!this.loaded) { 57 | await this.fetchCommunityPlugins(); 58 | } 59 | // 从社区插件列表中查找对应插件的 repo 信息 60 | const pluginInfo = this.communityPlugins[id]; 61 | 62 | if (!pluginInfo) { 63 | new Notice(`[插件管理器] 未知插件ID: ${id}`); 64 | return null; 65 | } 66 | window.open(`https://github.com/${pluginInfo.repo}`); 67 | } 68 | 69 | /** 70 | * 安装指定的插件 71 | * @param id - 要安装的插件的 ID 72 | * @param version - 要安装的插件的版本,默认为空字符串,表示不检查版本 73 | * @param enable - 安装后是否启用插件,默认为 false 74 | * @param github - 插件的 GitHub 仓库地址,默认为空字符串 75 | */ 76 | public async pluginInstall(id: string, version: string = "", enable: boolean = false, github: string = "") { 77 | // 打印日志,记录开始安装插件的信息 78 | // console.log(`[插件管理器] 开始安装插件 -- ${id} - ${version} - ${enable} - ${github}`); 79 | // 如果社区插件列表未加载,则先加载 否则,触发防抖函数,定时刷新社区插件列表 80 | if (!this.loaded) await this.fetchCommunityPlugins(); else this.debounceFetch(); 81 | 82 | // 获取 Obsidian 应用的插件注册表 83 | // @ts-ignore 84 | const pluginRegistry = this.plugin.app.plugins; 85 | 86 | // 标记是否需要安装插件 87 | let installFlag = false; 88 | // 获取插件的仓库地址,如果提供了 github 参数,则使用该参数,否则从社区插件列表中获取 89 | const repo = github !== "" ? github : this.communityPlugins[id]?.repo; 90 | console.log(repo) 91 | // 如果找不到插件的仓库地址,显示提示信息并返回 92 | if (!repo) { 93 | new Notice(`[插件管理器] 未知插件ID: ${id}`); 94 | return; 95 | } 96 | 97 | // 检查插件是否已经安装 98 | if (pluginRegistry.manifests[id]) { 99 | // 插件已安装,显示提示信息 100 | new Notice(`[插件管理器] 插件 ${pluginRegistry.manifests[id].name} 已安装`); 101 | // 如果指定了版本且与已安装的版本不同,则标记为需要安装 102 | if (version !== "" && version !== pluginRegistry.manifests[id]?.version) installFlag = true; 103 | } else { 104 | // 插件未安装,标记为需要安装 105 | installFlag = true; 106 | } 107 | 108 | // 如果需要安装插件 109 | if (installFlag) { 110 | // 从 GitHub 仓库获取插件的 manifest.json 文件 111 | const manifest = await fetch(`https://raw.githubusercontent.com/${repo}/HEAD/manifest.json`).then(r => r.json()); 112 | // 如果版本为 "latest" 或空字符串,则使用 manifest 中的版本 113 | if (version.toLowerCase() === "latest" || version === "") version = manifest.version; 114 | // 调用插件注册表的 installPlugin 方法安装插件 115 | await pluginRegistry.installPlugin(repo, version, manifest); 116 | } 117 | 118 | // 根据 enable 参数决定是否启用或禁用插件 119 | if (enable) { 120 | // 启用插件 121 | await pluginRegistry.loadPlugin(id); 122 | await pluginRegistry.enablePluginAndSave(id); 123 | } else { 124 | // 禁用插件 125 | await pluginRegistry.disablePlugin(id); 126 | } 127 | } 128 | 129 | /** 130 | * 解析安装参数并调用 installPlugin 方法安装插件 131 | * @param params - 包含插件安装参数的对象 132 | */ 133 | public async parsePluginInstall(params: ObsidianProtocolData) { 134 | // 解析参数,设置默认值 135 | let args = { 136 | id: params.id, 137 | version: params?.version ?? "", 138 | enable: ["", "true", "1"].includes(params.enable.toLowerCase()), 139 | github: params.github ?? "", 140 | }; 141 | // 调用 installPlugin 方法安装插件 142 | this.pluginInstall(args.id, args.version, args.enable); 143 | } 144 | 145 | /** 146 | * 解析包含插件信息的字符串或对象,获取插件的相关信息 147 | * @param input - 包含插件信息的字符串或对象 148 | * @return - 返回解析后的插件信息对象,如果解析失败则返回 null 149 | */ 150 | public async parsePluginGithub(params: ObsidianProtocolData) { 151 | // 解析参数,设置默认值 152 | let args = { id: params.id }; 153 | await this.pluginGithub(args.id); 154 | } 155 | } -------------------------------------------------------------------------------- /src/lang/locale/zh_cn.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: '插件管理器', 3 | 通用_成功_文本: '成功', 4 | 通用_失败_文本: '失败', 5 | 通用_新增_文本: '新增', 6 | 通用_操作_文本: '操作', 7 | 通用_搜索_文本: '搜索', 8 | 通用_名称_文本: '名称', 9 | 通用_无分组_文本: '全部', 10 | 通用_无标签_文本: '全部', 11 | 通用_无延迟_文本: '无', 12 | 通用_过滤_文本: '过滤', 13 | 通用_总计_文本: '总计', 14 | 通用_启用_文本: '启用', 15 | 通用_禁用_文本: '禁用', 16 | 通用_关闭_文本: '关闭', 17 | 通用_开启_文本: '开启', 18 | 通用_保存_文本: '保存设置', 19 | 通用_取消_文本: '取消', 20 | 21 | 导出_正文提示: '正文区:这里的内容可自行编辑或替换。', 22 | 23 | 命令行_启用_文本: '启用', 24 | 命令行_禁用_文本: '禁用', 25 | 命令行_分组_文本: '分组', 26 | 命令行_一键启用_文本: '一键启用', 27 | 命令行_一键禁用_文本: '一键禁用', 28 | 29 | 管理器_GITHUB_描述: '访问作者的GitHub页面,查看项目详情、更新日志、参与讨论和贡献代码。', 30 | 管理器_视频教程_描述: '访问视频教程', 31 | 管理器_编辑模式_描述: '启用编辑模式,深度自定义插件配置', 32 | 管理器_重载插件_描述: '重载插件,即时生效', 33 | 管理器_检查更新_描述: '检查插件更新', 34 | 管理器_一键禁用_描述: '一键禁用所有插件', 35 | 管理器_一键启用_描述: '一键启用所有插件', 36 | 管理器_插件设置_描述: '管理插件设置', 37 | 管理器_仅启用_描述: '仅显示已启用插件', 38 | 管理器_未分组_描述: '筛选所有未分组插件', 39 | 管理器_打开设置_描述: '打开设置界面', 40 | 管理器_还原内容_描述: '还原初始状态', 41 | 管理器_打开目录_描述: '打开插件目录', 42 | 管理器_删除插件_描述: '彻底删除插件', 43 | 管理器_切换状态_描述: '切换插件状态', 44 | 45 | 设置_基础设置_隐藏BPM标签_标题: '隐藏“bpm安装”标签', 46 | 设置_基础设置_隐藏BPM标签_描述: '开启后列表不显示自动添加的 bpm 安装标签,仅隐藏展示。', 47 | 设置_基础设置_启动检查更新_标题: '启动时检测插件更新', 48 | 设置_基础设置_启动检查更新_描述: '打开 BPM 时自动检测可更新插件,并在右上角提示数量。', 49 | 设置_基础设置_导出目录_标题: '插件信息导出目录', 50 | 设置_基础设置_导出目录_描述: '相对库路径的文件夹,用于导出 BPM 插件信息(支持 Base)。不会在输入时立刻写入,需点击“保存设置”。', 51 | 设置_基础设置_导出目录_示例: '例如: BPM-Export', 52 | 设置_基础设置_导出提示_标题: '前置约定(frontmatter 键名)', 53 | 设置_基础设置_导出提示_描述: '只读: bpm_ro_id/name/group/tags/delay/installed_via_bpm/updated;可写: bpm_rw_desc/note/enabled;条件可写: bpm_rwc_repo(仅官方未匹配且非 BPM 安装时)。', 54 | 设置_基础设置_GITHUB_TOKEN_标题: 'GitHub API Token', 55 | 设置_基础设置_GITHUB_TOKEN_描述: '用于从 GitHub 下载插件/主题,避免频繁的 API 限流。可留空。', 56 | 设置_基础设置_GITHUB_TOKEN_权限: '需要权限:Public repositories', 57 | 58 | 卸载_标题: '卸载插件', 59 | 卸载_提示: '你确定要卸载此插件吗?这将删除插件的文件夹。', 60 | 卸载_卸载: '卸载', 61 | 卸载_取消: '取消', 62 | 卸载_通知_一: '卸载成功', 63 | 64 | 一键_标题: '一键启用/禁用插件', 65 | 一键_提示: '你确定要一键启用/禁用此页面插件吗?这将无法恢复。(启用/禁用过程中请耐心等待)', 66 | 一键_启禁: '启用/禁用', 67 | 一键_取消: '取消', 68 | 一键_通知_一: '启用/禁用成功', 69 | 70 | 菜单_笔记_标题: '笔记', 71 | 菜单_快捷键_标题: '快捷键', 72 | 菜单_GitHub_标题: 'GitHub', 73 | 菜单_检查更新_标题: '检测插件更新', 74 | 菜单_单次启动_描述: '单次启动', 75 | 菜单_重启插件_描述: '重启插件', 76 | 菜单_隐藏插件_标题: '隐藏插件', 77 | 菜单_复制ID_标题: '复制ID', 78 | 管理器_安装_GITHUB_描述: '安装插件 / 主题(GitHub 仓库)', 79 | 管理器_安装_介绍: '从 GitHub 仓库安装插件或主题(读取最新发布资产)。', 80 | 管理器_安装_类型_标题: '类型', 81 | 管理器_安装_类型_描述: '选择要安装插件或主题', 82 | 管理器_安装_类型_插件: '插件', 83 | 管理器_安装_类型_主题: '主题', 84 | 管理器_安装_仓库_标题: '仓库', 85 | 管理器_安装_仓库_描述: 'GitHub 仓库路径,支持 / 和 https://github.com// 两种形式。', 86 | 管理器_安装_仓库_占位: 'user/repo', 87 | 管理器_安装_仓库为空提示: '请输入仓库路径', 88 | 管理器_安装_版本_标题: '版本', 89 | 管理器_安装_版本_描述: '点击获取 GitHub 发布版本后可选择;不选则默认最新。', 90 | 管理器_安装_版本_默认最新: '最新发布', 91 | 管理器_安装_版本_获取按钮: '获取版本', 92 | 管理器_安装_版本_获取中: '获取中...', 93 | 管理器_安装_版本_空提示: '未找到发行版本,尝试手动填写 tag', 94 | 管理器_安装_版本_失败提示: '获取发行版本失败,请检查仓库或网络', 95 | 管理器_安装_操作_标题: '操作', 96 | 管理器_安装_操作_按钮: '开始安装', 97 | 98 | 通知_ID已复制: 'ID已复制', 99 | 100 | 筛选_全部_描述: '全部', 101 | 筛选_仅启用_描述: '仅启用', 102 | 筛选_仅禁用_描述: '仅禁用', 103 | 筛选_已分组_描述: '已分组', 104 | 筛选_未分组_描述: '未分组', 105 | 筛选_有标签_描述: '有标签', 106 | 筛选_无标签_描述: '无标签', 107 | 筛选_有笔记_描述: '有笔记', 108 | 筛选_状态_全部: '状态:全部', 109 | 筛选_分组_全部: '分组:全部', 110 | 筛选_标签_全部: '标签:全部', 111 | 筛选_延迟_全部: '延迟:全部', 112 | 113 | 设置_基础设置_前缀: '基础', 114 | 设置_样式设置_前缀: '样式', 115 | 设置_分组设置_前缀: '分组', 116 | 设置_标签设置_前缀: '标签', 117 | 设置_延迟设置_前缀: '延迟', 118 | 119 | 设置_基础设置_语言_标题: '语言设置', 120 | 设置_基础设置_语言_描述: '选择您喜欢的语言。', 121 | 设置_基础设置_界面居中_标题: '界面居中', 122 | 设置_基础设置_界面居中_描述: '设置管理器界面是否居中', 123 | 124 | 设置_基础设置_目录样式_标题: '目录样式', 125 | 设置_基础设置_目录样式_描述: '选择分组的样式,以提升浏览体验。', 126 | 设置_基础设置_目录样式_选项_一: '始终展开', 127 | 设置_基础设置_目录样式_选项_二: '永不展开', 128 | 设置_基础设置_目录样式_选项_三: '悬浮展开', 129 | 设置_基础设置_目录样式_选项_四: '单击展开', 130 | 131 | 设置_基础设置_分组样式_标题: '分组样式', 132 | 设置_基础设置_分组样式_描述: '选择分组的样式,使分组更加明显,便于识别。', 133 | 设置_基础设置_分组样式_选项_一: '样式一', 134 | 设置_基础设置_分组样式_选项_二: '样式二', 135 | 设置_基础设置_分组样式_选项_三: '样式三', 136 | 设置_基础设置_分组样式_选项_四: '样式四', 137 | 138 | 设置_基础设置_标签样式_标题: '标签样式', 139 | 设置_基础设置_标签样式_描述: '选择标签的样式,使标签更加明显,便于识别。', 140 | 设置_基础设置_标签样式_选项_一: '样式一', 141 | 设置_基础设置_标签样式_选项_二: '样式二', 142 | 设置_基础设置_标签样式_选项_三: '样式三', 143 | 设置_基础设置_标签样式_选项_四: '样式四', 144 | 145 | 设置_基础设置_延时启动_标题: '延时启动', 146 | 设置_基础设置_延时启动_描述: '启用延时启动功能可以优化加载顺序,但请注意,这可能会导致某些插件出现兼容性问题。', 147 | 设置_基础设置_淡化插件_标题: '淡化插件', 148 | 设置_基础设置_淡化插件_描述: '为未启用的插件提供视觉淡化效果,以便清晰地区分启用和未启用的插件。', 149 | 150 | 设置_基础设置_筛选持久化_标题: '筛选持久化', 151 | 设置_基础设置_筛选持久化_描述: '启用后,您将在每次打开管理器时看到相同的插件列表。', 152 | 153 | 设置_基础设置_单独命令_标题: '单独控制插件命令', 154 | 设置_基础设置_单独命令_描述: '启用此选项可以单独控制每个插件的启用和禁用状态。(重启Obsidian生效)', 155 | 设置_基础设置_分组命令_标题: '分组控制插件命令', 156 | 设置_基础设置_分组命令_描述: '启用此选项可以一键启用或禁用指定分组中的所有插件。(重启Obsidian生效)', 157 | 标签_BPM安装_名称: 'bpm安装', 158 | 159 | 设置_延迟设置_通知_一: '[延迟] 已添加', 160 | 设置_延迟设置_通知_二: '[延迟] ID已存在或为空', 161 | 设置_延迟设置_通知_三: '[延迟] 删除成功', 162 | 设置_延迟设置_通知_四: '[延迟] 删除失败,此延迟下存在插件', 163 | 164 | 设置_分组设置_通知_一: '[分组] 已添加', 165 | 设置_分组设置_通知_二: '[分组] ID已存在或为空', 166 | 设置_分组设置_通知_三: '[分组] 删除成功', 167 | 设置_分组设置_通知_四: '[分组] 删除失败,此分组下存在插件', 168 | 169 | 设置_标签设置_通知_一: '[标签] 已添加', 170 | 设置_标签设置_通知_二: '[标签] ID已存在或为空', 171 | 设置_标签设置_通知_三: '[标签] 删除成功', 172 | 设置_标签设置_通知_四: '[标签] 删除失败,此标签下存在插件', 173 | 174 | 设置_提示_一_标题: '如果遇到本插件与其他插件冲突', 175 | 设置_提示_一_描述: '个人能力有限,无法修复此问题,请关闭延时启动,即可解决一切冲突问题。', 176 | 通知_检测更新中文案: '检测插件更新情况中...', 177 | 通知_检查更新失败_建议Token: '网络或 API 受限,建议先配置 GitHub Token(Public repositories 权限)。', 178 | 通知_可更新数量: '发现 {count} 个插件可更新', 179 | 180 | 命令_管理面板_描述: '开启插件管理器', 181 | 管理器_下载更新_描述: '下载指定版本更新(支持预发布)', 182 | 安装_成功_提示: '已安装/更新插件:', 183 | 安装_错误_限速: 'GitHub 请求受限(403),请配置 GitHub Token 后重试。', 184 | 安装_错误_缺少资源: '未找到发布资源或文件,请检查仓库/版本是否存在。', 185 | 安装_错误_通用: '安装失败,请检查仓库地址/版本或网络状态。', 186 | 设置_基础设置_调试模式_标题: '调试模式', 187 | 设置_基础设置_调试模式_描述: '开启后打印调试日志,关闭时仅输出错误信息。', 188 | } 189 | -------------------------------------------------------------------------------- /src/settings/ui/manager-basis.ts: -------------------------------------------------------------------------------- 1 | import BaseSetting from "../base-setting"; 2 | import { DropdownComponent, Setting, ToggleComponent, TextComponent, TFolder } from "obsidian"; 3 | import Commands from "src/command"; 4 | // import { GROUP_STYLE, ITEM_STYLE, TAG_STYLE } from "src/data/data"; 5 | 6 | export default class ManagerBasis extends BaseSetting { 7 | 8 | main(): void { 9 | const languageBar = new Setting(this.containerEl) 10 | .setName(this.manager.translator.t('设置_基础设置_语言_标题')) 11 | .setDesc(this.manager.translator.t('设置_基础设置_语言_描述')); 12 | const languageDropdown = new DropdownComponent(languageBar.controlEl); 13 | languageDropdown.addOptions(this.manager.translator.language); 14 | languageDropdown.setValue(this.settings.LANGUAGE); 15 | languageDropdown.onChange((value) => { 16 | this.settings.LANGUAGE = value; 17 | this.manager.saveSettings(); 18 | this.settingTab.basisDisplay(); 19 | Commands(this.app, this.manager); 20 | this.settingTab.display(); // 重新渲染整个设置界面 21 | this.display(); // 保持当前内容区的刷新 22 | }); 23 | 24 | const DelayBar = new Setting(this.containerEl) 25 | .setName(this.manager.translator.t('设置_基础设置_延时启动_标题')) 26 | .setDesc(this.manager.translator.t('设置_基础设置_延时启动_描述')); 27 | const DelayToggle = new ToggleComponent(DelayBar.controlEl); 28 | DelayToggle.setValue(this.settings.DELAY); 29 | DelayToggle.onChange((value) => { 30 | this.settings.DELAY = value; 31 | this.manager.saveSettings(); 32 | value ? this.manager.enableDelaysForAllPlugins() : this.manager.disableDelaysForAllPlugins(); 33 | this.settingTab.display(); // 重新渲染整个设置界面 34 | this.display(); // 保持当前内容区的刷新 35 | }); 36 | 37 | const persistenceBar = new Setting(this.containerEl) 38 | .setName(this.manager.translator.t('设置_基础设置_筛选持久化_标题')) 39 | .setDesc(this.manager.translator.t('设置_基础设置_筛选持久化_描述')); 40 | const persistenceToggle = new ToggleComponent(persistenceBar.controlEl); 41 | persistenceToggle.setValue(this.settings.PERSISTENCE); 42 | persistenceToggle.onChange((value) => { 43 | this.settings.PERSISTENCE = value; 44 | this.manager.saveSettings(); 45 | }); 46 | 47 | const debugBar = new Setting(this.containerEl) 48 | .setName(this.manager.translator.t('设置_基础设置_调试模式_标题')) 49 | .setDesc(this.manager.translator.t('设置_基础设置_调试模式_描述')); 50 | const debugToggle = new ToggleComponent(debugBar.controlEl); 51 | debugToggle.setValue(this.settings.DEBUG); 52 | debugToggle.onChange((value) => { 53 | this.settings.DEBUG = value; 54 | this.manager.saveSettings(); 55 | }); 56 | 57 | const startupCheckBar = new Setting(this.containerEl) 58 | .setName(this.manager.translator.t('设置_基础设置_启动检查更新_标题')) 59 | .setDesc(this.manager.translator.t('设置_基础设置_启动检查更新_描述')); 60 | const startupCheckToggle = new ToggleComponent(startupCheckBar.controlEl); 61 | startupCheckToggle.setValue(this.settings.STARTUP_CHECK_UPDATES); 62 | startupCheckToggle.onChange((value) => { 63 | this.settings.STARTUP_CHECK_UPDATES = value; 64 | this.manager.saveSettings(); 65 | }); 66 | 67 | const CommandItemBar = new Setting(this.containerEl) 68 | .setName(this.manager.translator.t('设置_基础设置_单独命令_标题')) 69 | .setDesc(this.manager.translator.t('设置_基础设置_单独命令_描述')); 70 | const CommandItemToggle = new ToggleComponent(CommandItemBar.controlEl); 71 | CommandItemToggle.setValue(this.settings.COMMAND_ITEM); 72 | CommandItemToggle.onChange((value) => { 73 | this.settings.COMMAND_ITEM = value; 74 | this.manager.saveSettings(); 75 | Commands(this.app, this.manager); 76 | }); 77 | 78 | const CommandGroupBar = new Setting(this.containerEl) 79 | .setName(this.manager.translator.t('设置_基础设置_分组命令_标题')) 80 | .setDesc(this.manager.translator.t('设置_基础设置_分组命令_描述')); 81 | const CommandGroupToggle = new ToggleComponent(CommandGroupBar.controlEl); 82 | CommandGroupToggle.setValue(this.settings.COMMAND_GROUP); 83 | CommandGroupToggle.onChange((value) => { 84 | this.settings.COMMAND_GROUP = value; 85 | this.manager.saveSettings(); 86 | Commands(this.app, this.manager); 87 | }); 88 | 89 | const hideBpmTagBar = new Setting(this.containerEl) 90 | .setName(this.manager.translator.t('设置_基础设置_隐藏BPM标签_标题')) 91 | .setDesc(this.manager.translator.t('设置_基础设置_隐藏BPM标签_描述')); 92 | const hideBpmTagToggle = new ToggleComponent(hideBpmTagBar.controlEl); 93 | hideBpmTagToggle.setValue(this.settings.HIDE_BPM_TAG); 94 | hideBpmTagToggle.onChange((value) => { 95 | this.settings.HIDE_BPM_TAG = value; 96 | this.manager.saveSettings(); 97 | this.manager.managerModal?.reloadShowData(); 98 | }); 99 | 100 | // 导出目录与前置提示 101 | const exportDirBar = new Setting(this.containerEl) 102 | .setName(this.manager.translator.t('设置_基础设置_导出目录_标题')) 103 | .setDesc(this.manager.translator.t('设置_基础设置_导出目录_描述')); 104 | const exportDirInput = new TextComponent(exportDirBar.controlEl); 105 | exportDirInput.setPlaceholder(this.manager.translator.t('设置_基础设置_导出目录_示例')); 106 | exportDirInput.setValue(this.settings.EXPORT_DIR || ""); 107 | 108 | exportDirInput.inputEl.addEventListener("blur", () => { 109 | exportDirInput.setValue(exportDirInput.getValue().trim()); 110 | }); 111 | 112 | exportDirBar.addButton((btn) => { 113 | btn.setButtonText(this.manager.translator.t('通用_保存_文本')).setCta(); 114 | btn.onClick(() => { 115 | this.settings.EXPORT_DIR = exportDirInput.getValue().trim(); 116 | this.manager.saveSettings(); 117 | this.manager.setupExportWatcher(); 118 | this.manager.exportAllPluginNotes(); 119 | }); 120 | }); 121 | 122 | new Setting(this.containerEl) 123 | .setName(this.manager.translator.t('设置_基础设置_导出提示_标题')) 124 | .setDesc(this.manager.translator.t('设置_基础设置_导出提示_描述')); 125 | 126 | const tokenBar = new Setting(this.containerEl) 127 | .setName(this.manager.translator.t('设置_基础设置_GITHUB_TOKEN_标题')) 128 | .setDesc(`${this.manager.translator.t('设置_基础设置_GITHUB_TOKEN_描述')} (${this.manager.translator.t('设置_基础设置_GITHUB_TOKEN_权限')})`); 129 | const tokenInput = new TextComponent(tokenBar.controlEl); 130 | tokenInput.setPlaceholder("ghp_xxx"); 131 | tokenInput.setValue(this.settings.GITHUB_TOKEN || ""); 132 | tokenInput.onChange((value) => { 133 | this.settings.GITHUB_TOKEN = value.trim(); 134 | this.manager.saveSettings(); 135 | }); 136 | 137 | new Setting(this.containerEl) 138 | .setName(this.manager.translator.t('设置_提示_一_标题')) 139 | .setDesc(this.manager.translator.t('设置_提示_一_描述')); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/lang/locale/es.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: 'Administrador de plugins', 3 | 通用_成功_文本: 'Éxito', 4 | 通用_失败_文本: 'Fallo', 5 | 通用_新增_文本: 'Agregar', 6 | 通用_操作_文本: 'Operación', 7 | 通用_搜索_文本: 'Buscar', 8 | 通用_名称_文本: 'Nombre', 9 | 通用_无分组_文本: 'Sin grupo', 10 | 通用_无标签_文本: 'Sin etiqueta', 11 | 通用_无延迟_文本: 'Sin retraso', 12 | 通用_过滤_文本: 'Filtrar', 13 | 通用_总计_文本: 'Total', 14 | 通用_启用_文本: 'Habilitar', 15 | 通用_禁用_文本: 'Deshabilitar', 16 | 17 | 18 | 管理器_GITHUB_描述: 'Visite la página de GitHub del autor para ver detalles del proyecto, registros de actualizaciones, participar en discusiones y contribuir con código.', 19 | 管理器_视频教程_描述: 'Acceder a tutoriales en video', 20 | 管理器_编辑模式_描述: 'Habilitar modo de edición para una personalización profunda de la configuración del plugin', 21 | 管理器_重载插件_描述: 'Recargar plugins para que surtan efecto inmediatamente', 22 | 管理器_检查更新_描述: 'Comprobar actualizaciones de plugins', 23 | 管理器_一键禁用_描述: 'Deshabilitar todos los plugins a la vez', 24 | 管理器_一键启用_描述: 'Habilitar todos los plugins a la vez', 25 | 管理器_插件设置_描述: 'Administrar configuración de plugins', 26 | 管理器_仅启用_描述: 'Mostrar solo plugins habilitados', 27 | 管理器_打开设置_描述: 'Abrir la interfaz de configuración', 28 | 管理器_还原内容_描述: 'Restaurar al estado inicial', 29 | 管理器_打开目录_描述: 'Abrir el directorio de plugins', 30 | 管理器_删除插件_描述: 'Eliminar completamente el plugin', 31 | 管理器_切换状态_描述: 'Alternar el estado del plugin', 32 | 菜单_检查更新_标题: 'Comprobar actualización', 33 | 菜单_隐藏插件_标题: 'Ocultar plugin', 34 | 菜单_复制ID_标题: 'Copiar ID', 35 | 管理器_安装_GITHUB_描述: 'Instalar plugin / tema (repositorio GitHub)', 36 | 管理器_安装_介绍: 'Instala plugins o temas desde GitHub (lee los assets del último release).', 37 | 管理器_安装_类型_标题: 'Tipo', 38 | 管理器_安装_类型_描述: 'Elige instalar un plugin o un tema.', 39 | 管理器_安装_类型_插件: 'Plugin', 40 | 管理器_安装_类型_主题: 'Tema', 41 | 管理器_安装_仓库_标题: 'Repositorio', 42 | 管理器_安装_仓库_描述: 'Ruta de GitHub, admite / o https://github.com//.', 43 | 管理器_安装_仓库_占位: 'usuario/repo', 44 | 管理器_安装_仓库为空提示: 'Introduce la ruta del repositorio', 45 | 管理器_安装_版本_标题: 'Versión', 46 | 管理器_安装_版本_描述: 'Pulsa para obtener releases; vacío = última.', 47 | 管理器_安装_版本_默认最新: 'Último release', 48 | 管理器_安装_版本_获取按钮: 'Obtener versiones', 49 | 管理器_安装_版本_获取中: 'Obteniendo...', 50 | 管理器_安装_版本_空提示: 'No se encontraron releases, intenta escribir un tag manualmente.', 51 | 管理器_安装_版本_失败提示: 'No se pudieron obtener los releases, revisa repo o red.', 52 | 管理器_安装_操作_标题: 'Acción', 53 | 管理器_安装_操作_按钮: 'Instalar ahora', 54 | 55 | 卸载_标题: 'Desinstalar Plugin', 56 | 卸载_提示: '¿Está seguro de que desea desinstalar este plugin? Esto eliminará la carpeta del plugin.', 57 | 卸载_卸载: 'Desinstalar', 58 | 卸载_取消: 'Cancelar', 59 | 卸载_通知_一: 'Desinstalado correctamente', 60 | 61 | 通用_保存_文本: 'Guardar configuración', 62 | 导出_正文提示: 'Cuerpo: puedes editar o reemplazar este contenido.', 63 | 设置_基础设置_隐藏BPM标签_标题: 'Ocultar etiqueta “bpm install”', 64 | 设置_基础设置_隐藏BPM标签_描述: 'Oculta la etiqueta añadida automáticamente en la lista (solo visual).', 65 | 设置_基础设置_导出目录_标题: 'Directorio de exportación de información', 66 | 设置_基础设置_导出目录_描述: 'Ruta relativa del vault para exportar info BPM (Base). Se aplica al pulsar “Guardar configuración”.', 67 | 设置_基础设置_导出目录_示例: 'p.ej.: BPM-Export', 68 | 设置_基础设置_导出提示_标题: 'Convenciones frontmatter', 69 | 设置_基础设置_导出提示_描述: 'Solo lectura: bpm_ro_id/name/group/tags/delay/installed_via_bpm/updated; editable: bpm_rw_desc/note/enabled; condicional: bpm_rwc_repo (sin coincidencia oficial y no instalado por BPM).', 70 | 设置_基础设置_GITHUB_TOKEN_标题: 'GitHub API Token', 71 | 设置_基础设置_GITHUB_TOKEN_描述: 'Para descargar plugins/temas de GitHub y reducir límites de API. Opcional.', 72 | 73 | 设置_基础设置_前缀: 'Configuración básica', 74 | 设置_分组设置_前缀: 'Grupo', 75 | 设置_标签设置_前缀: 'Etiqueta', 76 | 设置_延迟设置_前缀: 'Retraso', 77 | 78 | 79 | 设置_基础设置_语言_标题: 'Configuración de idioma', 80 | 设置_基础设置_语言_描述: 'Seleccione su idioma preferido.', 81 | 设置_基础设置_目录样式_标题: 'Estilo del directorio', 82 | 设置_基础设置_目录样式_描述: 'Seleccione el estilo del grupo para mejorar la experiencia de navegación.', 83 | 设置_基础设置_分组样式_标题: 'Estilo del grupo', 84 | 设置_基础设置_分组样式_描述: 'Seleccione el estilo del grupo para hacerlo más visible y fácil de identificar.', 85 | 设置_基础设置_标签样式_标题: 'Estilo de la etiqueta', 86 | 设置_基础设置_标签样式_描述: 'Seleccione el estilo de la etiqueta para hacerlo más visible y fácil de identificar.', 87 | 88 | 设置_基础设置_延时启动_标题: 'Inicio con retraso', 89 | 设置_基础设置_延时启动_描述: 'Habilitar la función de inicio con retraso puede optimizar el orden de carga, pero tenga en cuenta que esto puede causar problemas de compatibilidad con algunos plugins.', 90 | 设置_基础设置_淡化插件_标题: 'Atenuar plugins', 91 | 设置_基础设置_淡化插件_描述: 'Proporcione un efecto de atenuación visual para plugins deshabilitados para distinguir claramente entre plugins habilitados y deshabilitados.', 92 | 设置_基础设置_单独命令_标题: 'Controlar comandos de plugins por separado', 93 | 设置_基础设置_单独命令_描述: 'Habilite esta opción para controlar el estado habilitado y deshabilitado de cada plugin por separado. (Reinicie Obsidian para que surtan efecto)', 94 | 设置_基础设置_分组命令_标题: 'Controlar comandos de plugins por grupo', 95 | 设置_基础设置_分组命令_描述: 'Habilite esta opción para habilitar o deshabilitar todos los plugins de un grupo específico con un solo clic. (Reinicie Obsidian para que surtan efecto)', 96 | 设置_基础设置_启动检查更新_标题: 'Comprobar actualizaciones al inicio', 97 | 设置_基础设置_启动检查更新_描述: 'Al abrir BPM, comprueba automáticamente actualizaciones y muestra un aviso breve con el recuento.', 98 | 标签_BPM安装_名称: 'bpm install', 99 | 100 | 设置_延迟设置_通知_一: '[Retraso] Añadido', 101 | 设置_延迟设置_通知_二: '[Retraso] El ID ya existe o está vacío', 102 | 设置_延迟设置_通知_三: '[Retraso] Eliminado correctamente', 103 | 设置_延迟设置_通知_四: '[Retraso] Fallo al eliminar, existen plugins bajo este retraso', 104 | 105 | 设置_分组设置_通知_一: '[Grupo] Añadido', 106 | 设置_分组设置_通知_二: '[Grupo] El ID ya existe o está vacío', 107 | 设置_分组设置_通知_三: '[Grupo] Eliminado correctamente', 108 | 设置_分组设置_通知_四: '[Grupo] Fallo al eliminar, existen plugins bajo este grupo', 109 | 110 | 设置_标签设置_通知_一: '[Etiqueta] Añadido', 111 | 设置_标签设置_通知_二: '[Etiqueta] El ID ya existe o está vacío', 112 | 设置_标签设置_通知_三: '[Etiqueta] Eliminado correctamente', 113 | 设置_标签设置_通知_四: '[Etiqueta] Fallo al eliminar, existen plugins bajo esta etiqueta', 114 | 115 | 设置_提示_一_标题: 'Si encuentra conflictos con otros plugins', 116 | 设置_提示_一_描述: 'Debido a capacidades limitadas, no puedo solucionar este problema. Por favor, deshabilite el inicio con retraso para resolver todos los problemas de conflicto.', 117 | 通知_检测更新中: 'Comprobando actualizaciones…', 118 | 通知_可更新数量: '{count} plugins tienen actualizaciones disponibles', 119 | 筛选_状态_全部: 'Estado: Todos', 120 | 筛选_分组_全部: 'Grupos: Todos', 121 | 筛选_标签_全部: 'Etiquetas: Todas', 122 | 筛选_延迟_全部: 'Retraso: Todos', 123 | 124 | 命令_管理面板_描述: 'Abrir el administrador de plugins', 125 | 管理器_下载更新_描述: 'Descargar actualización (elige versión, incl. pre-release)', 126 | 安装_成功_提示: 'Instalado/actualizado: ', 127 | 安装_错误_限速: 'Solicitud a GitHub limitada (403). Configure un token y reintente.', 128 | 安装_错误_缺少资源: 'No se encontraron assets/archivos de la release. Verifique repositorio/versión.', 129 | 安装_错误_通用: 'Fallo al instalar. Verifique repositorio/versión o red.', 130 | 设置_基础设置_调试模式_标题: 'Modo depuración', 131 | 设置_基础设置_调试模式_描述: 'Si está desactivado, solo se muestran errores; si está activado, se imprimen logs de depuración.', 132 | } 133 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginManifest } from "obsidian"; 2 | import Manager from "./main"; 3 | import { ManagerModal } from "./modal/manager-modal"; 4 | 5 | const Commands = (app: App, manager: Manager) => { 6 | manager.addCommand({ 7 | id: 'manager-view', 8 | name: manager.translator.t('命令_管理面板_描述'), 9 | hotkeys: [ 10 | { 11 | modifiers: ['Ctrl'], 12 | key: 'M', 13 | } 14 | ], 15 | callback: () => { new ManagerModal(app, manager).open() } 16 | }); 17 | 18 | if (manager.settings.DELAY) { 19 | // 单行命令 20 | if (manager.settings.COMMAND_ITEM) { 21 | const plugins: PluginManifest[] = Object.values(manager.appPlugins.manifests).filter((pm: PluginManifest) => pm.id !== manager.manifest.id) as PluginManifest[]; 22 | plugins.forEach(plugin => { 23 | const mp = manager.settings.Plugins.find(mp => mp.id === plugin.id); 24 | if (mp) { 25 | manager.addCommand({ 26 | id: `manager-${mp.id}`, 27 | name: `${mp.enabled ? manager.translator.t('通用_关闭_文本') : manager.translator.t('通用_开启_文本')} ${mp.name} `, 28 | callback: async () => { 29 | if (mp.enabled) { 30 | mp.enabled = false; 31 | await manager.savePluginAndExport(mp.id); 32 | await manager.appPlugins.disablePlugin(plugin.id); 33 | Commands(app, manager); 34 | } else { 35 | mp.enabled = true; 36 | await manager.savePluginAndExport(mp.id); 37 | await manager.appPlugins.enablePlugin(plugin.id); 38 | Commands(app, manager); 39 | } 40 | } 41 | }); 42 | } 43 | }); 44 | } 45 | // 分组命令 46 | if (manager.settings.COMMAND_GROUP) { 47 | manager.settings.GROUPS.forEach((group) => { 48 | manager.addCommand({ 49 | id: `manager-${group.id}-enabled`, 50 | name: `${manager.translator.t('命令行_一键启用_文本')} ${group.name}`, 51 | callback: async () => { 52 | const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id); 53 | filteredPlugins.forEach(async plugin => { 54 | if (plugin && !plugin.enabled) { 55 | await manager.appPlugins.enablePlugin(plugin.id); 56 | plugin.enabled = true; 57 | await manager.savePluginAndExport(plugin.id); 58 | } 59 | }); 60 | Commands(app, manager); 61 | } 62 | }); 63 | manager.addCommand({ 64 | id: `manager-${group.id}-disable`, 65 | name: `${manager.translator.t('命令行_一键禁用_文本')} ${group.name}`, 66 | callback: async () => { 67 | const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id); 68 | filteredPlugins.forEach(async plugin => { 69 | if (plugin && plugin.enabled) { 70 | await manager.appPlugins.disablePlugin(plugin.id); 71 | plugin.enabled = false; 72 | await manager.savePluginAndExport(plugin.id); 73 | } 74 | }); 75 | Commands(app, manager); 76 | } 77 | }); 78 | }); 79 | } 80 | } else { 81 | // 单行命令 82 | if (manager.settings.COMMAND_ITEM) { 83 | const plugins: PluginManifest[] = Object.values(manager.appPlugins.manifests).filter((pm: PluginManifest) => pm.id !== manager.manifest.id) as PluginManifest[]; 84 | plugins.forEach(plugin => { 85 | const enabled = manager.appPlugins.enabledPlugins.has(plugin.id); 86 | manager.addCommand({ 87 | id: `manager-${plugin.id}`, 88 | name: `${enabled ? manager.translator.t('命令行_禁用_文本') : manager.translator.t('命令行_启用_文本')} ${plugin.name} `, 89 | callback: async () => { 90 | if (enabled) { 91 | await manager.appPlugins.disablePluginAndSave(plugin.id); 92 | const mp = manager.settings.Plugins.find(p => p.id === plugin.id); 93 | if (mp) mp.enabled = false; 94 | await manager.savePluginAndExport(plugin.id); 95 | Commands(app, manager); 96 | } else { 97 | await manager.appPlugins.enablePluginAndSave(plugin.id); 98 | const mp = manager.settings.Plugins.find(p => p.id === plugin.id); 99 | if (mp) mp.enabled = true; 100 | await manager.savePluginAndExport(plugin.id); 101 | Commands(app, manager); 102 | } 103 | } 104 | }); 105 | 106 | }); 107 | } 108 | // 分组命令 109 | if (manager.settings.COMMAND_GROUP) { 110 | manager.settings.GROUPS.forEach((group) => { 111 | manager.addCommand({ 112 | id: `manager-${group.id}-enabled`, 113 | name: `${manager.translator.t('命令行_一键启用_文本')} ${group.name} ${manager.translator.t('命令行_分组_文本')}`, 114 | callback: async () => { 115 | const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id); 116 | filteredPlugins.forEach(async plugin => { 117 | await manager.appPlugins.enablePluginAndSave(plugin.id); 118 | const mp = manager.settings.Plugins.find(p => p.id === plugin.id); 119 | if (mp) mp.enabled = true; 120 | await manager.savePluginAndExport(plugin.id); 121 | }); 122 | Commands(app, manager); 123 | } 124 | }); 125 | manager.addCommand({ 126 | id: `manager-${group.id}-disable`, 127 | name: `${manager.translator.t('命令行_一键禁用_文本')} ${group.name} ${manager.translator.t('命令行_分组_文本')}`, 128 | callback: async () => { 129 | const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id); 130 | filteredPlugins.forEach(async plugin => { 131 | await manager.appPlugins.disablePluginAndSave(plugin.id); 132 | const mp = manager.settings.Plugins.find(p => p.id === plugin.id); 133 | if (mp) mp.enabled = false; 134 | await manager.savePluginAndExport(plugin.id); 135 | }); 136 | Commands(app, manager); 137 | } 138 | }); 139 | }); 140 | } 141 | } 142 | } 143 | 144 | export default Commands 145 | -------------------------------------------------------------------------------- /src/lang/locale/ru.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: 'Менеджер плагинов', 3 | 通用_成功_文本: 'Успех', 4 | 通用_失败_文本: 'Неудача', 5 | 通用_新增_文本: 'Добавить', 6 | 通用_操作_文本: 'Операция', 7 | 通用_搜索_文本: 'Поиск', 8 | 通用_过滤_文本: 'Фильтр', 9 | 通用_名称_文本: 'Название', 10 | 通用_无分组_文本: 'Без группы', 11 | 通用_无标签_文本: 'Без метки', 12 | 通用_无延迟_文本: 'Без задержки', 13 | 通用_总计_文本: 'Всего', 14 | 通用_启用_文本: 'Включить', 15 | 通用_禁用_文本: 'Отключить', 16 | 通用_关闭_文本: 'Закрыть', 17 | 通用_开启_文本: 'Открыть', 18 | 通用_保存_文本: 'Сохранить', 19 | 通用_取消_文本: 'Отмена', 20 | 21 | 导出_正文提示: 'Основной текст: вы можете изменить или заменить этот контент.', 22 | 23 | 设置_基础设置_隐藏BPM标签_标题: 'Скрыть тег “bpm install”', 24 | 设置_基础设置_隐藏BPM标签_描述: 'Скрывает автодобавленный тег в списке (только визуально, метка сохраняется).', 25 | 设置_基础设置_导出目录_标题: 'Каталог экспорта информации', 26 | 设置_基础设置_导出目录_描述: 'Путь относительно хранилища для экспорта данных BPM (Base). Применяется при нажатии “Сохранить”.', 27 | 设置_基础设置_导出目录_示例: 'напр.: BPM-Export', 28 | 设置_基础设置_导出提示_标题: 'Правила frontmatter', 29 | 设置_基础设置_导出提示_描述: 'Только чтение: bpm_ro_id/name/group/tags/delay/installed_via_bpm/updated; редактируемые: bpm_rw_desc/note/enabled; условные: bpm_rwc_repo (без официального совпадения и не BPM).', 30 | 设置_基础设置_GITHUB_TOKEN_标题: 'GitHub API Token', 31 | 设置_基础设置_GITHUB_TOKEN_描述: 'Для загрузки из GitHub и уменьшения лимитов API. Опционально.', 32 | 设置_基础设置_GITHUB_TOKEN_权限: 'Необходимы: Public repositories', 33 | 34 | 菜单_笔记_标题: 'Заметка', 35 | 菜单_快捷键_标题: 'Горячие клавиши', 36 | 菜单_GitHub_标题: 'GitHub', 37 | 菜单_检查更新_标题: 'Проверить обновление', 38 | 菜单_单次启动_描述: 'Одноразовый запуск', 39 | 菜单_重启插件_描述: 'Перезапустить плагин', 40 | 菜单_隐藏插件_标题: 'Скрыть плагин', 41 | 菜单_复制ID_标题: 'Копировать ID', 42 | 管理器_安装_GITHUB_描述: 'Установить плагин / тему (репозиторий GitHub)', 43 | 管理器_安装_介绍: 'Устанавливайте плагины или темы из релизов GitHub (берёт последние assets).', 44 | 管理器_安装_类型_标题: 'Тип', 45 | 管理器_安装_类型_描述: 'Выберите, что установить: плагин или тему.', 46 | 管理器_安装_类型_插件: 'Плагин', 47 | 管理器_安装_类型_主题: 'Тема', 48 | 管理器_安装_仓库_标题: 'Репозиторий', 49 | 管理器_安装_仓库_描述: 'Путь GitHub, формат / или https://github.com//.', 50 | 管理器_安装_仓库_占位: 'user/repo', 51 | 管理器_安装_仓库为空提示: 'Введите путь к репозиторию', 52 | 管理器_安装_版本_标题: 'Версия', 53 | 管理器_安装_版本_描述: 'Получите релизы и выберите; пусто = последняя.', 54 | 管理器_安装_版本_默认最新: 'Последний релиз', 55 | 管理器_安装_版本_获取按钮: 'Получить версии', 56 | 管理器_安装_版本_获取中: 'Получение...', 57 | 管理器_安装_版本_空提示: 'Релизы не найдены, попробуйте ввести тег вручную.', 58 | 管理器_安装_版本_失败提示: 'Не удалось получить релизы, проверьте репозиторий или сеть.', 59 | 管理器_安装_操作_标题: 'Действие', 60 | 管理器_安装_操作_按钮: 'Установить', 61 | 62 | 筛选_全部_描述: 'Все', 63 | 筛选_仅启用_描述: 'Только включенные', 64 | 筛选_仅禁用_描述: 'Только отключенные', 65 | 筛选_已分组_描述: 'С группой', 66 | 筛选_未分组_描述: 'Без группы', 67 | 筛选_有标签_描述: 'С тегами', 68 | 筛选_无标签_描述: 'Без тегов', 69 | 筛选_有笔记_描述: 'С заметкой', 70 | 筛选_状态_全部: 'Статус: Все', 71 | 筛选_分组_全部: 'Группы: Все', 72 | 筛选_标签_全部: 'Теги: Все', 73 | 筛选_延迟_全部: 'Задержка: Все', 74 | 75 | 管理器_GITHUB_描述: 'Посетите GitHub автора для деталей, логов, обсуждений и вклада.', 76 | 管理器_视频教程_描述: 'Доступ к видео-урокам', 77 | 管理器_编辑模式_描述: 'Режим редактирования для глубокой настройки', 78 | 管理器_重载插件_描述: 'Перезагрузить плагины для немедленного эффекта', 79 | 管理器_检查更新_描述: 'Проверить обновления плагинов', 80 | 管理器_一键禁用_描述: 'Отключить все плагины разом', 81 | 管理器_一键启用_描述: 'Включить все плагины разом', 82 | 管理器_插件设置_描述: 'Управление настройками плагина', 83 | 管理器_仅启用_描述: 'Показывать только включенные', 84 | 管理器_打开设置_描述: 'Открыть настройки', 85 | 管理器_还原内容_描述: 'Восстановить исходное состояние', 86 | 管理器_打开目录_描述: 'Открыть каталог плагина', 87 | 管理器_删除插件_描述: 'Полностью удалить плагин', 88 | 管理器_切换状态_描述: 'Переключить статус', 89 | 管理器_下载更新_描述: 'Скачать обновление (выбор версии, включая pre-release)', 90 | 91 | 卸载_标题: 'Удалить плагин', 92 | 卸载_提示: 'Удалить этот плагин? Каталог будет удален.', 93 | 卸载_卸载: 'Удалить', 94 | 卸载_取消: 'Отмена', 95 | 卸载_通知_一: 'Удалено успешно', 96 | 97 | 一键_标题: 'Включить/выключить все', 98 | 一键_提示: 'Подтвердите массовое включение/выключение. Действие необратимо.', 99 | 一键_启禁: 'Вкл/Выкл', 100 | 一键_取消: 'Отмена', 101 | 一键_通知_一: 'Операция завершена', 102 | 103 | 设置_基础设置_前缀: 'Базовые', 104 | 设置_样式设置_前缀: 'Стиль', 105 | 设置_分组设置_前缀: 'Группа', 106 | 设置_标签设置_前缀: 'Тег', 107 | 设置_延迟设置_前缀: 'Задержка', 108 | 109 | 设置_基础设置_语言_标题: 'Язык', 110 | 设置_基础设置_语言_描述: 'Выберите предпочитаемый язык.', 111 | 设置_基础设置_界面居中_标题: 'Центрировать интерфейс', 112 | 设置_基础设置_界面居中_描述: 'Определяет центрирование окна менеджера', 113 | 114 | 设置_基础设置_目录样式_标题: 'Стиль списка', 115 | 设置_基础设置_目录样式_描述: 'Выберите стиль для групп.', 116 | 设置_基础设置_目录样式_选项_一: 'Всегда раскрыт', 117 | 设置_基础设置_目录样式_选项_二: 'Никогда не раскрыт', 118 | 设置_基础设置_目录样式_选项_三: 'Раскрывать при наведении', 119 | 设置_基础设置_目录样式_选项_四: 'Раскрывать по клику', 120 | 121 | 设置_基础设置_分组样式_标题: 'Стиль группы', 122 | 设置_基础设置_分组样式_描述: 'Выберите стиль отображения групп.', 123 | 设置_基础设置_分组样式_选项_一: 'Стиль 1', 124 | 设置_基础设置_分组样式_选项_二: 'Стиль 2', 125 | 设置_基础设置_分组样式_选项_三: 'Стиль 3', 126 | 设置_基础设置_分组样式_选项_四: 'Стиль 4', 127 | 128 | 设置_基础设置_标签样式_标题: 'Стиль тегов', 129 | 设置_基础设置_标签样式_描述: 'Выберите стиль отображения тегов.', 130 | 设置_基础设置_标签样式_选项_一: 'Стиль 1', 131 | 设置_基础设置_标签样式_选项_二: 'Стиль 2', 132 | 设置_基础设置_标签样式_选项_三: 'Стиль 3', 133 | 设置_基础设置_标签样式_选项_四: 'Стиль 4', 134 | 135 | 设置_基础设置_延时启动_标题: 'Отложенный запуск', 136 | 设置_基础设置_延时启动_描述: 'Оптимизирует порядок загрузки, но может вызвать несовместимости.', 137 | 设置_基础设置_淡化插件_标题: 'Ослабить видимость', 138 | 设置_基础设置_淡化插件_描述: 'Визуально выделять включенные/выключенные плагины.', 139 | 140 | 设置_基础设置_筛选持久化_标题: 'Сохранять фильтры', 141 | 设置_基础设置_筛选持久化_描述: 'Показывать те же фильтры при каждом открытии.', 142 | 143 | 设置_基础设置_单独命令_标题: 'Команды по отдельности', 144 | 设置_基础设置_单独命令_描述: 'Управлять состоянием каждого плагина отдельно (требует перезапуска).', 145 | 设置_基础设置_分组命令_标题: 'Команды по группам', 146 | 设置_基础设置_分组命令_描述: 'Вкл/выкл группу целиком (требует перезапуска).', 147 | 148 | 设置_基础设置_启动检查更新_标题: 'Проверять обновления при старте', 149 | 设置_基础设置_启动检查更新_描述: 'При запуске BPM проверять обновления и показывать количество.', 150 | 通知_检查更新失败_建议Token: 'Ошибка сети/API. Настройте GitHub Token (Public repositories) и повторите.', 151 | 通知_检测更新中文案: 'Проверка обновлений плагинов...', 152 | 通知_可更新数量: 'Доступны обновления для {count} плагинов', 153 | 154 | 设置_延迟设置_通知_一: '[Задержка] Добавлено', 155 | 设置_延迟设置_通知_二: '[Задержка] ID уже существует или пуст', 156 | 设置_延迟设置_通知_三: '[Задержка] Удалено', 157 | 设置_延迟设置_通知_四: '[Задержка] Не удалось удалить, есть плагины с этой задержкой', 158 | 159 | 设置_分组设置_通知_一: '[Группа] Добавлено', 160 | 设置_分组设置_通知_二: '[Группа] ID уже существует или пуст', 161 | 设置_分组设置_通知_三: '[Группа] Удалено', 162 | 设置_分组设置_通知_四: '[Группа] Не удалось удалить, есть плагины в этой группе', 163 | 164 | 设置_标签设置_通知_一: '[Тег] Добавлено', 165 | 设置_标签设置_通知_二: '[Тег] ID уже существует или пуст', 166 | 设置_标签设置_通知_三: '[Тег] Удалено', 167 | 设置_标签设置_通知_四: '[Тег] Не удалось удалить, есть плагины с этим тегом', 168 | 169 | 设置_提示_一_标题: 'Если возникают конфликты с другими плагинами', 170 | 设置_提示_一_描述: 'Отключите отложенный запуск, чтобы устранить конфликты.', 171 | 172 | 命令_管理面板_描述: 'Открыть менеджер плагинов', 173 | 标签_BPM安装_名称: 'bpm install', 174 | 175 | 安装_成功_提示: 'Установлено/обновлено: ', 176 | 安装_错误_限速: 'Лимит GitHub (403). Настройте токен и повторите.', 177 | 安装_错误_缺少资源: 'Файлы релиза не найдены. Проверьте репозиторий/версию.', 178 | 安装_错误_通用: 'Не удалось установить. Проверьте репозиторий/сеть.', 179 | 设置_基础设置_调试模式_标题: 'Режим отладки', 180 | 设置_基础设置_调试模式_描述: 'Выкл: только ошибки. Вкл: вывод отладочных логов.', 181 | } 182 | -------------------------------------------------------------------------------- /src/modal/share-modal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | ButtonComponent, 4 | ExtraButtonComponent, 5 | Modal, 6 | Notice, 7 | SearchComponent, 8 | Setting, 9 | ToggleComponent, 10 | } from "obsidian"; 11 | 12 | import { ManagerSettings } from "../settings/data"; 13 | 14 | import Manager from "main"; 15 | 16 | interface ExportPluginManifest { 17 | id: string; 18 | name: string; 19 | version: string; 20 | author: string; 21 | description: string; 22 | export: boolean; 23 | } 24 | 25 | interface ImportPluginManifest { 26 | id: string; 27 | name: string; 28 | version: string; 29 | author: string; 30 | description: string; 31 | } 32 | 33 | 34 | // ============================== 35 | // 侧边栏 对话框 翻译 36 | // ============================== 37 | export class ShareModal extends Modal { 38 | manager: Manager; 39 | settings: ManagerSettings; 40 | // this.app.plugins 41 | appPlugins; 42 | // this.app.settings 43 | appSetting; 44 | // [本地][变量] 导入插件列表 45 | importPlugins: ImportPluginManifest[] = []; 46 | // [本地][变量] 导出插件列表 47 | exportPlugins: ExportPluginManifest[] = []; 48 | 49 | // 搜索内容 50 | searchText = ""; 51 | // 操作类型 52 | type = ""; 53 | // 页尾 54 | footEl: HTMLDivElement; 55 | 56 | constructor(app: App, manager: Manager, plugins: (ImportPluginManifest | ExportPluginManifest)[], type: string) { 57 | super(app); 58 | // @ts-ignore 59 | this.appSetting = this.app.setting; 60 | // @ts-ignore 61 | this.appPlugins = this.app.plugins; 62 | this.manager = manager; 63 | this.settings = manager.settings; 64 | this.type = type; 65 | if (this.type == "export") { 66 | 67 | } 68 | // 自动分类插件类型 69 | plugins.forEach(plugin => { if (this.isImportPlugin(plugin)) { this.importPlugins.push(plugin); } else { this.exportPlugins.push(plugin); } }); 70 | } 71 | 72 | public async showHead() { 73 | //@ts-ignore 74 | const modalEl: HTMLElement = this.contentEl.parentElement; 75 | modalEl.addClass("manager-container"); 76 | // 靠上 77 | if (!this.settings.CENTER) modalEl.addClass("manager-container__top"); 78 | modalEl.removeChild(modalEl.getElementsByClassName("modal-close-button")[0]); 79 | this.titleEl.parentElement?.addClass("manager-container__header"); 80 | this.contentEl.addClass("manager-item-container"); 81 | // 添加页尾 82 | this.footEl = document.createElement("div"); 83 | this.footEl.addClass("manager-food"); 84 | this.modalEl.appendChild(this.footEl); 85 | 86 | // [操作行] 87 | const actionBar = new Setting(this.titleEl).setClass("manager-bar__action").setName(this.manager.translator.t("通用_操作_文本") + '[导入]' + '[导出]'); 88 | // [操作行] 导入 89 | if (this.type == "export") { 90 | // [操作行] 导入 91 | const linkButton = new ButtonComponent(actionBar.controlEl); 92 | linkButton.setIcon("link"); 93 | linkButton.setTooltip('导入插件'); 94 | linkButton.onClick(() => { 95 | }); 96 | } 97 | // [操作行] 导入 98 | const tutorialButton = new ButtonComponent(actionBar.controlEl); 99 | tutorialButton.setIcon("file-down"); 100 | tutorialButton.setTooltip('导入插件'); 101 | tutorialButton.onClick(() => { 102 | this.type = "import"; 103 | this.reloadShowData(); 104 | }); 105 | 106 | // [操作行] 导出 107 | const githubButton = new ButtonComponent(actionBar.controlEl); 108 | githubButton.setIcon("file-up"); 109 | githubButton.setTooltip('导出插件'); 110 | githubButton.onClick(() => { 111 | actionBar.setName(this.manager.translator.t("通用_操作_文本")); 112 | this.type = "export"; 113 | this.reloadShowData(); 114 | }); 115 | 116 | // [操作行] 关闭 117 | const closeButton = new ButtonComponent(actionBar.controlEl); 118 | closeButton.setIcon("x"); 119 | closeButton.setTooltip('关闭'); 120 | closeButton.onClick(() => { 121 | actionBar.setName(this.manager.translator.t("通用_操作_文本") + '[导出]'); 122 | this.type = "export"; 123 | this.reloadShowData(); 124 | }); 125 | } 126 | 127 | public async showData() { 128 | if (this.type == "export") { 129 | for (const plugin of this.exportPlugins) { 130 | const itemEl = new Setting(this.contentEl); 131 | itemEl.setClass("manager-item"); 132 | itemEl.nameEl.addClass("manager-item__name-container"); 133 | itemEl.descEl.addClass("manager-item__description-container"); 134 | 135 | // [默认] 名称 136 | const title = createSpan({ text: plugin.name, cls: "manager-item__name-title", }); 137 | itemEl.nameEl.appendChild(title); 138 | 139 | // [默认] 版本 140 | const version = createSpan({ text: `[${plugin.version}]`, cls: ["manager-item__name-version"], }); 141 | itemEl.nameEl.appendChild(version); 142 | 143 | // [默认] 描述 144 | const desc = createDiv({ text: plugin.description, title: plugin.description, cls: ["manager-item__name-desc"], }); 145 | itemEl.descEl.appendChild(desc); 146 | 147 | const shareToggle = new ToggleComponent(itemEl.controlEl); 148 | shareToggle.setValue(plugin.export); 149 | shareToggle.onChange((value) => { 150 | plugin.export = !value; 151 | }); 152 | } 153 | // 计算页尾 154 | this.footEl.innerHTML = `[${this.manager.translator.t("通用_总计_文本")}] ${this.exportPlugins.length} `; 155 | } 156 | if (this.type == "import") { 157 | for (const plugin of this.importPlugins) { 158 | const itemEl = new Setting(this.contentEl); 159 | itemEl.setClass("manager-item"); 160 | itemEl.nameEl.addClass("manager-item__name-container"); 161 | itemEl.descEl.addClass("manager-item__description-container"); 162 | 163 | // [默认] 名称 164 | const title = createSpan({ text: plugin.name, cls: "manager-item__name-title", }); 165 | itemEl.nameEl.appendChild(title); 166 | 167 | // [默认] 版本 168 | const version = createSpan({ text: `[${plugin.version}]`, cls: ["manager-item__name-version"], }); 169 | itemEl.nameEl.appendChild(version); 170 | 171 | // [默认] 描述 172 | const desc = createDiv({ text: plugin.description, title: plugin.description, cls: ["manager-item__name-desc"], }); 173 | itemEl.descEl.appendChild(desc); 174 | 175 | // [按钮] 下载插件 176 | const openPluginDirButton = new ExtraButtonComponent(itemEl.controlEl); 177 | openPluginDirButton.setIcon("download"); 178 | openPluginDirButton.setTooltip('下载插件'); 179 | openPluginDirButton.onClick(() => { window.open(`obsidian://BPM-plugin-install?id=${plugin.id}&enable=true&version=${plugin.version}`); }); 180 | } 181 | // 计算页尾 182 | this.footEl.innerHTML = `[${this.manager.translator.t("通用_总计_文本")}] ${this.importPlugins.length} `; 183 | } 184 | } 185 | 186 | public async reloadShowData() { 187 | let scrollTop = 0; 188 | const modalElement: HTMLElement = this.contentEl; 189 | scrollTop = modalElement.scrollTop; 190 | modalElement.empty(); 191 | this.showData(); 192 | modalElement.scrollTo(0, scrollTop); 193 | } 194 | 195 | public async onOpen() { 196 | await this.showHead(); 197 | await this.showData(); 198 | } 199 | 200 | public async onClose() { 201 | this.contentEl.empty(); 202 | } 203 | 204 | // 新增类型守卫 205 | private isImportPlugin(plugin: any): plugin is ImportPluginManifest { 206 | return 'installLink' in plugin; // 根据实际特征判断 207 | } 208 | 209 | private isExportPlugin(plugin: any): plugin is ExportPluginManifest { 210 | return 'enabled' in plugin; // 根据实际特征判断 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/lang/locale/fr.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: 'Gestionnaire de plugins', 3 | 通用_成功_文本: 'Succès', 4 | 通用_失败_文本: 'Échec', 5 | 通用_新增_文本: 'Ajouter', 6 | 通用_操作_文本: 'Opération', 7 | 通用_搜索_文本: 'Recherche', 8 | 通用_过滤_文本: 'Filtrer', 9 | 通用_名称_文本: 'Nom', 10 | 通用_无分组_文本: 'Aucun groupe', 11 | 通用_无标签_文本: 'Aucun tag', 12 | 通用_无延迟_文本: 'Aucun retard', 13 | 通用_总计_文本: 'Total', 14 | 通用_启用_文本: 'Activer', 15 | 通用_禁用_文本: 'Désactiver', 16 | 通用_关闭_文本: 'Fermer', 17 | 通用_开启_文本: 'Ouvrir', 18 | 通用_保存_文本: 'Enregistrer', 19 | 通用_取消_文本: 'Annuler', 20 | 21 | 导出_正文提示: 'Section corps : vous pouvez modifier ou remplacer ce contenu.', 22 | 23 | 设置_基础设置_隐藏BPM标签_标题: 'Masquer le tag “bpm install”', 24 | 设置_基础设置_隐藏BPM标签_描述: 'Masque simplement l’affichage du tag ajouté automatiquement (le marquage est conservé).', 25 | 设置_基础设置_导出目录_标题: 'Répertoire d’export des infos plugin', 26 | 设置_基础设置_导出目录_描述: 'Chemin relatif au coffre pour exporter les infos BPM (Base). Appliqué en cliquant sur “Enregistrer”.', 27 | 设置_基础设置_导出目录_示例: 'ex. : BPM-Export', 28 | 设置_基础设置_导出提示_标题: 'Conventions frontmatter', 29 | 设置_基础设置_导出提示_描述: 'Lecture seule : bpm_ro_id/name/group/tags/delay/installed_via_bpm/updated ; éditable : bpm_rw_desc/note/enabled ; conditionnel : bpm_rwc_repo (sans correspondance officielle et non BPM).', 30 | 设置_基础设置_GITHUB_TOKEN_标题: 'GitHub API Token', 31 | 设置_基础设置_GITHUB_TOKEN_描述: 'Pour télécharger depuis GitHub et réduire les limites d’API. Optionnel.', 32 | 设置_基础设置_GITHUB_TOKEN_权限: 'Portée requise : Public repositories', 33 | 34 | 菜单_笔记_标题: 'Note', 35 | 菜单_快捷键_标题: 'Raccourcis', 36 | 菜单_GitHub_标题: 'GitHub', 37 | 菜单_检查更新_标题: 'Vérifier les mises à jour', 38 | 菜单_单次启动_描述: 'Démarrage unique', 39 | 菜单_重启插件_描述: 'Redémarrer le plugin', 40 | 菜单_隐藏插件_标题: 'Masquer le plugin', 41 | 菜单_复制ID_标题: 'Copier l’ID', 42 | 管理器_安装_GITHUB_描述: 'Installer plugin / thème (dépôt GitHub)', 43 | 管理器_安装_介绍: 'Installer des plugins ou thèmes depuis GitHub (utilise le dernier release).', 44 | 管理器_安装_类型_标题: 'Type', 45 | 管理器_安装_类型_描述: 'Choisissez d’installer un plugin ou un thème.', 46 | 管理器_安装_类型_插件: 'Plugin', 47 | 管理器_安装_类型_主题: 'Thème', 48 | 管理器_安装_仓库_标题: 'Dépôt', 49 | 管理器_安装_仓库_描述: 'Chemin GitHub, prend en charge / ou https://github.com//.', 50 | 管理器_安装_仓库_占位: 'user/repo', 51 | 管理器_安装_仓库为空提示: 'Veuillez saisir le chemin du dépôt', 52 | 管理器_安装_版本_标题: 'Version', 53 | 管理器_安装_版本_描述: 'Récupérez les releases puis choisissez ; vide = dernière.', 54 | 管理器_安装_版本_默认最新: 'Dernier release', 55 | 管理器_安装_版本_获取按钮: 'Récupérer les versions', 56 | 管理器_安装_版本_获取中: 'Récupération...', 57 | 管理器_安装_版本_空提示: 'Aucune release trouvée, essayez de saisir un tag manuellement.', 58 | 管理器_安装_版本_失败提示: 'Échec de la récupération des releases, vérifiez le dépôt ou le réseau.', 59 | 管理器_安装_操作_标题: 'Action', 60 | 管理器_安装_操作_按钮: 'Installer maintenant', 61 | 62 | 筛选_全部_描述: 'Tout', 63 | 筛选_仅启用_描述: 'Activés uniquement', 64 | 筛选_仅禁用_描述: 'Désactivés uniquement', 65 | 筛选_已分组_描述: 'Groupés', 66 | 筛选_未分组_描述: 'Non groupés', 67 | 筛选_有标签_描述: 'Avec tags', 68 | 筛选_无标签_描述: 'Sans tags', 69 | 筛选_有笔记_描述: 'Avec note', 70 | 筛选_状态_全部: 'État : Tous', 71 | 筛选_分组_全部: 'Groupes : Tous', 72 | 筛选_标签_全部: 'Tags : Tous', 73 | 筛选_延迟_全部: 'Retard : Tous', 74 | 75 | 管理器_GITHUB_描述: 'Visiter la page GitHub de l’auteur pour détails, changelog, discussions et contributions.', 76 | 管理器_视频教程_描述: 'Accéder aux tutoriels vidéo', 77 | 管理器_编辑模式_描述: 'Activer le mode édition pour personnaliser en profondeur', 78 | 管理器_重载插件_描述: 'Recharger les plugins pour prise d’effet immédiate', 79 | 管理器_检查更新_描述: 'Vérifier les mises à jour de plugins', 80 | 管理器_一键禁用_描述: 'Tout désactiver en un clic', 81 | 管理器_一键启用_描述: 'Tout activer en un clic', 82 | 管理器_插件设置_描述: 'Gérer les paramètres du plugin', 83 | 管理器_仅启用_描述: 'Afficher uniquement les plugins activés', 84 | 管理器_未分组_描述: 'Filtrer les plugins non groupés', 85 | 管理器_打开设置_描述: 'Ouvrir l’interface de réglages', 86 | 管理器_还原内容_描述: 'Restaurer l’état initial', 87 | 管理器_打开目录_描述: 'Ouvrir le répertoire du plugin', 88 | 管理器_删除插件_描述: 'Supprimer complètement le plugin', 89 | 管理器_切换状态_描述: 'Basculer l’état du plugin', 90 | 管理器_下载更新_描述: 'Télécharger une mise à jour (choix de version, pré-release incl.)', 91 | 92 | 卸载_标题: 'Désinstaller le plugin', 93 | 卸载_提示: 'Êtes-vous sûr de désinstaller ? Le dossier du plugin sera supprimé.', 94 | 卸载_卸载: 'Désinstaller', 95 | 卸载_取消: 'Annuler', 96 | 卸载_通知_一: 'Désinstallé avec succès', 97 | 98 | 一键_标题: 'Activation/Désactivation en un clic', 99 | 一键_提示: 'Confirmez-vous l’activation/désactivation en masse ? (Patientez pendant l’opération)', 100 | 一键_启禁: 'Activer/Désactiver', 101 | 一键_取消: 'Annuler', 102 | 一键_通知_一: 'Opération terminée', 103 | 104 | 设置_基础设置_前缀: 'Basique', 105 | 设置_样式设置_前缀: 'Style', 106 | 设置_分组设置_前缀: 'Groupe', 107 | 设置_标签设置_前缀: 'Tag', 108 | 设置_延迟设置_前缀: 'Retard', 109 | 110 | 设置_基础设置_语言_标题: 'Langue', 111 | 设置_基础设置_语言_描述: 'Choisissez votre langue préférée.', 112 | 设置_基础设置_界面居中_标题: 'Centrer l’interface', 113 | 设置_基础设置_界面居中_描述: 'Définir si l’interface est centrée', 114 | 115 | 设置_基础设置_目录样式_标题: 'Style d’affichage', 116 | 设置_基础设置_目录样式_描述: 'Choisir le style pour les groupes.', 117 | 设置_基础设置_目录样式_选项_一: 'Toujours développé', 118 | 设置_基础设置_目录样式_选项_二: 'Jamais développé', 119 | 设置_基础设置_目录样式_选项_三: 'Développer au survol', 120 | 设置_基础设置_目录样式_选项_四: 'Développer au clic', 121 | 122 | 设置_基础设置_分组样式_标题: 'Style de groupe', 123 | 设置_基础设置_分组样式_描述: 'Choisir le style des groupes.', 124 | 设置_基础设置_分组样式_选项_一: 'Style 1', 125 | 设置_基础设置_分组样式_选项_二: 'Style 2', 126 | 设置_基础设置_分组样式_选项_三: 'Style 3', 127 | 设置_基础设置_分组样式_选项_四: 'Style 4', 128 | 129 | 设置_基础设置_标签样式_标题: 'Style de tag', 130 | 设置_基础设置_标签样式_描述: 'Choisir le style des tags.', 131 | 设置_基础设置_标签样式_选项_一: 'Style 1', 132 | 设置_基础设置_标签样式_选项_二: 'Style 2', 133 | 设置_基础设置_标签样式_选项_三: 'Style 3', 134 | 设置_基础设置_标签样式_选项_四: 'Style 4', 135 | 136 | 设置_基础设置_延时启动_标题: 'Démarrage différé', 137 | 设置_基础设置_延时启动_描述: 'Optimise l’ordre de chargement mais peut causer des incompatibilités.', 138 | 设置_基础设置_淡化插件_标题: 'Estomper les plugins', 139 | 设置_基础设置_淡化插件_描述: 'Effet visuel pour distinguer activé/désactivé.', 140 | 141 | 设置_基础设置_筛选持久化_标题: 'Persistance des filtres', 142 | 设置_基础设置_筛选持久化_描述: 'Voir la même liste filtrée à chaque ouverture.', 143 | 144 | 设置_基础设置_单独命令_标题: 'Commandes séparées par plugin', 145 | 设置_基础设置_单独命令_描述: 'Contrôler état activé/désactivé par plugin (redémarrage requis).', 146 | 设置_基础设置_分组命令_标题: 'Commandes par groupe', 147 | 设置_基础设置_分组命令_描述: 'Activer/désactiver un groupe en un clic (redémarrage requis).', 148 | 149 | 设置_基础设置_启动检查更新_标题: 'Vérifier les mises à jour au démarrage', 150 | 设置_基础设置_启动检查更新_描述: 'Au lancement de BPM, vérifier les mises à jour et afficher le nombre.', 151 | 通知_检查更新失败_建议Token: 'Échec réseau/API. Configurez un token GitHub (Public repositories) puis réessayez.', 152 | 通知_检测更新中文案: 'Vérification des mises à jour de plugins...', 153 | 通知_可更新数量: '{count} plugins ont des mises à jour disponibles', 154 | 155 | 设置_延迟设置_通知_一: '[Retard] Ajouté', 156 | 设置_延迟设置_通知_二: '[Retard] ID existant ou vide', 157 | 设置_延迟设置_通知_三: '[Retard] Supprimé', 158 | 设置_延迟设置_通知_四: '[Retard] Suppression impossible, des plugins l’utilisent', 159 | 160 | 设置_分组设置_通知_一: '[Groupe] Ajouté', 161 | 设置_分组设置_通知_二: '[Groupe] ID existant ou vide', 162 | 设置_分组设置_通知_三: '[Groupe] Supprimé', 163 | 设置_分组设置_通知_四: '[Groupe] Suppression impossible, des plugins l’utilisent', 164 | 165 | 设置_标签设置_通知_一: '[Tag] Ajouté', 166 | 设置_标签设置_通知_二: '[Tag] ID existant ou vide', 167 | 设置_标签设置_通知_三: '[Tag] Supprimé', 168 | 设置_标签设置_通知_四: '[Tag] Suppression impossible, des plugins l’utilisent', 169 | 170 | 设置_提示_一_标题: 'Conflit avec d’autres plugins', 171 | 设置_提示_一_描述: 'Désactivez le démarrage différé pour résoudre les conflits.', 172 | 173 | 命令_管理面板_描述: 'Ouvrir le gestionnaire de plugins', 174 | 标签_BPM安装_名称: 'bpm install', 175 | 176 | 安装_成功_提示: 'Installé/mis à jour : ', 177 | 安装_错误_限速: 'Limite GitHub (403). Configurez un token puis réessayez.', 178 | 安装_错误_缺少资源: 'Ressources de release introuvables. Vérifiez dépôt/version.', 179 | 安装_错误_通用: 'Échec de l’installation. Vérifiez dépôt/version ou réseau.', 180 | 设置_基础设置_调试模式_标题: 'Mode debug', 181 | 设置_基础设置_调试模式_描述: 'Désactivé : erreurs uniquement. Activé : logs de debug.', 182 | } 183 | -------------------------------------------------------------------------------- /src/lang/locale/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 通用_管理器_文本: 'Plugin Manager', 3 | 通用_成功_文本: 'Success', 4 | 通用_失败_文本: 'Failure', 5 | 通用_新增_文本: 'Add', 6 | 通用_操作_文本: 'Operation', 7 | 通用_搜索_文本: 'Search', 8 | 通用_名称_文本: 'Name', 9 | 通用_无分组_文本: 'ALL', 10 | 通用_无标签_文本: 'ALL', 11 | 通用_无延迟_文本: 'No Delay', 12 | 通用_过滤_文本: 'Filter', 13 | 通用_总计_文本: 'Total', 14 | 通用_启用_文本: 'Enable', 15 | 通用_禁用_文本: 'Disable', 16 | 通用_关闭_文本: 'Disable', 17 | 通用_开启_文本: 'Enable', 18 | 通用_保存_文本: 'Save settings', 19 | 通用_取消_文本: 'Cancel', 20 | 21 | 导出_正文提示: 'Body section: you can edit or replace this content.', 22 | 23 | 设置_基础设置_隐藏BPM标签_标题: 'Hide “bpm install” tag', 24 | 设置_基础设置_隐藏BPM标签_描述: 'Hide the auto-added bpm install tag in the list (only hide display, keep mark).', 25 | 设置_基础设置_导出目录_标题: 'Export directory for plugin info', 26 | 设置_基础设置_导出目录_描述: 'Relative vault path to export BPM plugin info (for Base). Changes apply when clicking “Save settings”.', 27 | 设置_基础设置_导出目录_示例: 'e.g., BPM-Export', 28 | 设置_基础设置_导出提示_标题: 'Frontmatter conventions', 29 | 设置_基础设置_导出提示_描述: 'Read-only: bpm_ro_id/name/group/tags/delay/installed_via_bpm/updated; writable: bpm_rw_desc/note/enabled; conditional: bpm_rwc_repo (only when official match is missing and not BPM-installed).', 30 | 设置_基础设置_GITHUB_TOKEN_标题: 'GitHub API Token', 31 | 设置_基础设置_GITHUB_TOKEN_描述: 'Used to download plugins/themes from GitHub and reduce rate limits. Optional.', 32 | 设置_基础设置_GITHUB_TOKEN_权限: 'Required scope: Public repositories', 33 | 命令行_启用_文本: 'Enable', 34 | 命令行_禁用_文本: 'Disable', 35 | 命令行_分组_文本: 'Group', 36 | 命令行_一键启用_文本: 'One - click Enable', 37 | 命令行_一键禁用_文本: 'One - click Disable', 38 | 39 | 菜单_笔记_标题: 'Note', 40 | 菜单_快捷键_标题: 'Hotkeys', 41 | 菜单_GitHub_标题: 'GitHub', 42 | 菜单_检查更新_标题: 'Check update', 43 | 菜单_单次启动_描述: 'Single start', 44 | 菜单_重启插件_描述: 'Restart plugin', 45 | 菜单_隐藏插件_标题: 'Hide plugin', 46 | 菜单_复制ID_标题: 'Copy ID', 47 | 管理器_安装_GITHUB_描述: 'Install plugin / theme (GitHub repo)', 48 | 管理器_安装_介绍: 'Install plugins or themes from GitHub releases (reads latest assets).', 49 | 管理器_安装_类型_标题: 'Type', 50 | 管理器_安装_类型_描述: 'Choose to install a plugin or a theme.', 51 | 管理器_安装_类型_插件: 'Plugin', 52 | 管理器_安装_类型_主题: 'Theme', 53 | 管理器_安装_仓库_标题: 'Repository', 54 | 管理器_安装_仓库_描述: 'GitHub path, supports / or https://github.com//.', 55 | 管理器_安装_仓库_占位: 'user/repo', 56 | 管理器_安装_仓库为空提示: 'Please enter a repository path', 57 | 管理器_安装_版本_标题: 'Version', 58 | 管理器_安装_版本_描述: 'Click to fetch releases, then choose; empty = latest.', 59 | 管理器_安装_版本_默认最新: 'Latest release', 60 | 管理器_安装_版本_获取按钮: 'Fetch versions', 61 | 管理器_安装_版本_获取中: 'Fetching...', 62 | 管理器_安装_版本_空提示: 'No releases found, try typing a tag manually.', 63 | 管理器_安装_版本_失败提示: 'Failed to fetch releases, check repo or network.', 64 | 管理器_安装_操作_标题: 'Action', 65 | 管理器_安装_操作_按钮: 'Install now', 66 | 67 | 筛选_全部_描述: 'All', 68 | 筛选_仅启用_描述: 'Enabled only', 69 | 筛选_仅禁用_描述: 'Disabled only', 70 | 筛选_已分组_描述: 'Grouped', 71 | 筛选_未分组_描述: 'Ungrouped', 72 | 筛选_有标签_描述: 'With tags', 73 | 筛选_无标签_描述: 'Without tags', 74 | 筛选_有笔记_描述: 'With notes', 75 | 筛选_状态_全部: 'Status: All', 76 | 筛选_分组_全部: 'Groups: All', 77 | 筛选_标签_全部: 'Tags: All', 78 | 筛选_延迟_全部: 'Delay: All', 79 | 80 | 管理器_GITHUB_描述: 'Visit the author\'s GitHub page to view project details, update logs, participate in discussions, and contribute code.', 81 | 管理器_视频教程_描述: 'Access video tutorials', 82 | 管理器_编辑模式_描述: 'Enable edit mode for in-depth plugin configuration customization', 83 | 管理器_重载插件_描述: 'Reload plugins to take effect immediately', 84 | 管理器_检查更新_描述: 'Check for plugin updates', 85 | 管理器_一键禁用_描述: 'Disable all plugins at once', 86 | 管理器_一键启用_描述: 'Enable all plugins at once', 87 | 管理器_插件设置_描述: 'Manage plugin settings', 88 | 管理器_仅启用_描述: 'Only display enabled plugins', 89 | 管理器_未分组_描述: 'Filter all ungrouped plugins', 90 | 管理器_打开设置_描述: 'Open the settings interface', 91 | 管理器_还原内容_描述: 'Restore to the initial state', 92 | 管理器_打开目录_描述: 'Open the plugin directory', 93 | 管理器_删除插件_描述: 'Completely delete the plugin', 94 | 管理器_切换状态_描述: 'Toggle the plugin status', 95 | 96 | 卸载_标题: 'Uninstall Plugin', 97 | 卸载_提示: 'Are you sure you want to uninstall this plugin? This will delete the plugin\'s folder.', 98 | 卸载_卸载: 'Uninstall', 99 | 卸载_取消: 'Cancel', 100 | 卸载_通知_一: 'Uninstalled successfully', 101 | 102 | 一键_标题: 'One-click Enable/Disable Plugins', 103 | 一键_提示: 'Are you sure you want to enable/disable the plugins on this page with one click? This action cannot be undone. (Please wait patiently during the enable/disable process)', 104 | 一键_启禁: 'Enable/Disable', 105 | 一键_取消: 'Cancel', 106 | 一键_通知_一: 'Enable/Disable Successful', 107 | 108 | 设置_基础设置_前缀: 'Basic', 109 | 设置_样式设置_前缀: 'Style', 110 | 设置_分组设置_前缀: 'Group', 111 | 设置_标签设置_前缀: 'Tag', 112 | 设置_延迟设置_前缀: 'Delay', 113 | 114 | 设置_基础设置_语言_标题: 'Language Settings', 115 | 设置_基础设置_语言_描述: 'Choose your preferred language.', 116 | 设置_基础设置_界面居中_标题: 'Center the interface', 117 | 设置_基础设置_界面居中_描述: 'Set whether the manager interface is centered', 118 | 119 | 设置_基础设置_目录样式_标题: 'Directory Style', 120 | 设置_基础设置_目录样式_描述: 'Select the style of the group to enhance the browsing experience.', 121 | 设置_基础设置_目录样式_选项_一: 'Always Expanded', 122 | 设置_基础设置_目录样式_选项_二: 'Never Expanded', 123 | 设置_基础设置_目录样式_选项_三: 'Hover to Expand', 124 | 设置_基础设置_目录样式_选项_四: 'Click to Expand', 125 | 126 | 设置_基础设置_分组样式_标题: 'Group Style', 127 | 设置_基础设置_分组样式_描述: 'Select the style of the group to make it more noticeable and easy to identify.', 128 | 设置_基础设置_分组样式_选项_一: 'Style One', 129 | 设置_基础设置_分组样式_选项_二: 'Style Two', 130 | 设置_基础设置_分组样式_选项_三: 'Style Three', 131 | 设置_基础设置_分组样式_选项_四: 'Style Four', 132 | 133 | 设置_基础设置_标签样式_标题: 'Tag Style', 134 | 设置_基础设置_标签样式_描述: 'Select the style of the tag to make it more noticeable and easy to identify.', 135 | 设置_基础设置_标签样式_选项_一: 'Style One', 136 | 设置_基础设置_标签样式_选项_二: 'Style Two', 137 | 设置_基础设置_标签样式_选项_三: 'Style Three', 138 | 设置_基础设置_标签样式_选项_四: 'Style Four', 139 | 140 | 设置_基础设置_延时启动_标题: 'Delayed Startup', 141 | 设置_基础设置_延时启动_描述: 'Enabling the delayed startup feature can optimize the loading order, but please note that this may cause compatibility issues with some plugins.', 142 | 设置_基础设置_淡化插件_标题: 'Fade Plugins', 143 | 设置_基础设置_淡化插件_描述: 'Provide a visual fade effect for disabled plugins to clearly distinguish between enabled and disabled plugins.', 144 | 设置_基础设置_启动检查更新_标题: 'Check updates on startup', 145 | 设置_基础设置_启动检查更新_描述: 'When BPM opens, automatically check for plugin updates and briefly show the count.', 146 | 147 | 设置_基础设置_筛选持久化_标题: 'Filter Persistence', 148 | 设置_基础设置_筛选持久化_描述: 'After enabling, you will see the same plugin list every time you open the manager.', 149 | 150 | 设置_基础设置_单独命令_标题: 'Control Plugin Commands Separately', 151 | 设置_基础设置_单独命令_描述: 'Enable this option to control the enabled and disabled state of each plugin separately. (Restart Obsidian to take effect)', 152 | 设置_基础设置_分组命令_标题: 'Control Plugin Commands by Group', 153 | 设置_基础设置_分组命令_描述: 'Enable this option to enable or disable all plugins in a specified group with one click. (Restart Obsidian to take effect)', 154 | 155 | 设置_延迟设置_通知_一: '[Delay] Added', 156 | 设置_延迟设置_通知_二: '[Delay] ID already exists or is empty', 157 | 设置_延迟设置_通知_三: '[Delay] Deleted successfully', 158 | 设置_延迟设置_通知_四: '[Delay] Deletion failed, plugins exist under this delay', 159 | 160 | 设置_分组设置_通知_一: '[Group] Added', 161 | 设置_分组设置_通知_二: '[Group] ID already exists or is empty', 162 | 设置_分组设置_通知_三: '[Group] Deleted successfully', 163 | 设置_分组设置_通知_四: '[Group] Deletion failed, plugins exist under this group', 164 | 165 | 设置_标签设置_通知_一: '[Tag] Added', 166 | 设置_标签设置_通知_二: '[Tag] ID already exists or is empty', 167 | 设置_标签设置_通知_三: '[Tag] Deleted successfully', 168 | 设置_标签设置_通知_四: '[Tag] Deletion failed, plugins exist under this tag', 169 | 170 | 设置_提示_一_标题: 'If You Encounter Conflicts with Other Plugins', 171 | 设置_提示_一_描述: 'Due to limited capabilities, I cannot fix this issue. Please disable delayed startup to resolve all conflict issues.', 172 | 通知_检测更新中文案: 'Checking plugin updates...', 173 | 通知_可更新数量: '{count} plugins have updates available', 174 | 通知_检查更新失败_建议Token: 'Network/API limited. Please configure a GitHub Token (Public repositories scope) and retry.', 175 | 176 | 命令_管理面板_描述: 'Open the plugin manager', 177 | 标签_BPM安装_名称: 'bpm install', 178 | 管理器_下载更新_描述: 'Download an update (choose version, incl. pre-release)', 179 | 安装_成功_提示: 'Installed/updated: ', 180 | 安装_错误_限速: 'GitHub rate-limited (403). Please set a GitHub token and retry.', 181 | 安装_错误_缺少资源: 'Release assets/files not found. Please check repo/version.', 182 | 安装_错误_通用: 'Install failed. Please check repo/version or network.', 183 | 设置_基础设置_调试模式_标题: 'Debug mode', 184 | 设置_基础设置_调试模式_描述: 'When off, only errors are logged; when on, debug logs are printed.', 185 | } 186 | -------------------------------------------------------------------------------- /src/modal/group-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, ExtraButtonComponent, Modal, Notice, Setting } from 'obsidian'; 2 | import { ManagerSettings } from '../settings/data'; 3 | import Manager from 'main'; 4 | import { ManagerModal } from './manager-modal'; 5 | import { ManagerPlugin } from 'src/data/types'; 6 | import Commands from 'src/command'; 7 | 8 | export class GroupModal extends Modal { 9 | settings: ManagerSettings; 10 | manager: Manager; 11 | managerModal: ManagerModal; 12 | managerPlugin: ManagerPlugin; 13 | selected: string; 14 | add: boolean; 15 | private defaultGroupColor = ''; 16 | 17 | constructor(app: App, manager: Manager, managerModal: ManagerModal, managerPlugin: ManagerPlugin) { 18 | super(app); 19 | this.settings = manager.settings; 20 | this.manager = manager; 21 | this.managerModal = managerModal; 22 | this.managerPlugin = managerPlugin; 23 | this.selected = ''; 24 | this.add = false; 25 | } 26 | 27 | private async showHead() { 28 | //@ts-ignore 29 | const modalEl: HTMLElement = this.contentEl.parentElement; 30 | modalEl.addClass('manager-editor__container'); 31 | modalEl.removeChild(modalEl.getElementsByClassName('modal-close-button')[0]); 32 | this.titleEl.parentElement?.addClass('manager-container__header'); 33 | this.contentEl.addClass('manager-item-container'); 34 | 35 | // [标题行] 36 | const titleBar = new Setting(this.titleEl).setClass('manager-bar__title').setName(`[${this.managerPlugin.name}]`); 37 | // [标题行] 关闭按钮 38 | const closeButton = new ExtraButtonComponent(titleBar.controlEl) 39 | closeButton.setIcon('circle-x') 40 | closeButton.onClick(() => this.close()); 41 | } 42 | 43 | private async showData() { 44 | // 预先计算一个缺省颜色,避免与现有颜色过近 45 | if (!this.defaultGroupColor) { 46 | this.defaultGroupColor = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color)); 47 | } 48 | for (const group of this.settings.GROUPS) { 49 | const itemEl = new Setting(this.contentEl) 50 | itemEl.setClass('manager-editor__item') 51 | if (this.selected == '' || this.selected != group.id) { 52 | itemEl.addExtraButton(cb => cb 53 | .setIcon('settings') 54 | .onClick(() => { 55 | this.selected = group.id; 56 | this.reloadShowData(); 57 | }) 58 | ) 59 | itemEl.addToggle(cb => cb 60 | .setValue(group.id === this.managerPlugin.group) 61 | .onChange(async () => { 62 | this.managerPlugin.group = this.managerPlugin.group === group.id ? '' : group.id; 63 | await this.manager.savePluginAndExport(this.managerPlugin.id); 64 | this.managerModal.reloadShowData(); 65 | this.reloadShowData(); 66 | }) 67 | ) 68 | const groupEl = createSpan({ cls: 'manager-item__name-group' }); 69 | itemEl.nameEl.appendChild(groupEl); 70 | const tag = this.manager.createTag(group.name, group.color, this.settings.GROUP_STYLE); 71 | groupEl.appendChild(tag); 72 | } 73 | if (this.selected != '' && this.selected == group.id) { 74 | itemEl.addColorPicker(cb => cb 75 | .setValue(group.color) 76 | .onChange((value) => { 77 | group.color = value; 78 | this.manager.saveSettings(); 79 | this.reloadShowData(); 80 | }) 81 | ) 82 | itemEl.addText(cb => cb 83 | .setValue(group.name) 84 | .onChange((value) => { 85 | group.name = value; 86 | this.manager.saveSettings(); 87 | }) 88 | .inputEl.addClass('manager-editor__item-input') 89 | ) 90 | itemEl.addExtraButton(cb => cb 91 | .setIcon('trash-2') 92 | .onClick(() => { 93 | const hasTestGroup = this.settings.Plugins.some(plugin => plugin.group === group.id); 94 | if (!hasTestGroup) { 95 | this.manager.settings.GROUPS = this.manager.settings.GROUPS.filter(t => t.id !== group.id); 96 | this.manager.saveSettings(); 97 | this.reloadShowData(); 98 | Commands(this.app, this.manager); 99 | new Notice(this.manager.translator.t('设置_分组设置_通知_三')); 100 | } else { 101 | new Notice(this.manager.translator.t('设置_分组设置_通知_四')); 102 | } 103 | }) 104 | ) 105 | itemEl.addExtraButton(cb => cb 106 | .setIcon('save') 107 | .onClick(() => { 108 | this.selected = ''; 109 | this.reloadShowData(); 110 | this.managerModal.reloadShowData(); 111 | }) 112 | ) 113 | const groupEl = createSpan({ cls: 'manager-item__name-group' }); 114 | itemEl.nameEl.appendChild(groupEl); 115 | const tag = this.manager.createTag(group.name, group.color, this.settings.GROUP_STYLE); 116 | groupEl.appendChild(tag); 117 | } 118 | } 119 | if (this.add) { 120 | let id = ''; 121 | let name = ''; 122 | let color = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color)); 123 | const foodBar = new Setting(this.contentEl).setClass('manager-bar__title'); 124 | foodBar.infoEl.remove(); 125 | foodBar.addColorPicker(cb => cb 126 | .setValue(color) 127 | .onChange((value) => { 128 | color = value; 129 | }) 130 | ) 131 | foodBar.addText(cb => cb 132 | .setPlaceholder('ID') 133 | .onChange((value) => { id = value; this.manager.saveSettings(); }) 134 | .inputEl.addClass('manager-editor__item-input') 135 | ) 136 | foodBar.addText(cb => cb 137 | .setPlaceholder(this.manager.translator.t('通用_名称_文本')) 138 | .onChange((value) => { name = value; }) 139 | .inputEl.addClass('manager-editor__item-input') 140 | ) 141 | foodBar.addExtraButton(cb => cb 142 | .setIcon('plus') 143 | .onClick(() => { 144 | const containsId = this.manager.settings.GROUPS.some(tag => tag.id === id); 145 | if (!containsId && id !== '') { 146 | if (color === '') color = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color)); 147 | this.manager.settings.GROUPS.push({ id, name, color }); 148 | this.manager.saveSettings(); 149 | this.add = false; 150 | this.reloadShowData(); 151 | Commands(this.app, this.manager); 152 | new Notice(this.manager.translator.t('设置_分组设置_通知_一')); 153 | } else { 154 | new Notice(this.manager.translator.t('设置_分组设置_通知_二')); 155 | } 156 | }) 157 | ) 158 | } else { 159 | // [底部行] 新增 160 | const foodBar = new Setting(this.contentEl).setClass('manager-bar__title').setName(this.manager.translator.t('通用_新增_文本')); 161 | const addButton = new ExtraButtonComponent(foodBar.controlEl) 162 | addButton.setIcon('circle-plus') 163 | addButton.onClick(() => { 164 | this.add = true; 165 | this.reloadShowData(); 166 | }); 167 | } 168 | } 169 | 170 | private async reloadShowData() { 171 | let scrollTop = 0; 172 | const modalElement: HTMLElement = this.contentEl; 173 | scrollTop = modalElement.scrollTop; 174 | modalElement.empty(); 175 | await this.showData(); 176 | modalElement.scrollTo(0, scrollTop); 177 | } 178 | 179 | async onOpen() { 180 | await this.showHead(); 181 | await this.showData(); 182 | } 183 | 184 | async onClose() { 185 | this.contentEl.empty(); 186 | } 187 | 188 | private pickDistinctColor(existing: string[]): string { 189 | const palette = ['#FF6B6B', '#4ECDC4', '#FFD166', '#A78BFA', '#48BB78', '#F472B6', '#38BDF8', '#F59E0B', '#22D3EE', '#F97316', '#10B981', '#E11D48', '#6366F1', '#14B8A6']; 190 | const toRgb = (hex: string) => { 191 | const clean = hex.replace('#', ''); 192 | const num = parseInt(clean, 16); 193 | return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; 194 | }; 195 | const dist = (a: string, b: string) => { 196 | const [ar, ag, ab] = toRgb(a); 197 | const [br, bg, bb] = toRgb(b); 198 | return Math.sqrt((ar - br) ** 2 + (ag - bg) ** 2 + (ab - bb) ** 2); 199 | }; 200 | const MIN_DIST = 80; 201 | for (const c of palette) { 202 | const min = existing.length ? Math.min(...existing.map((ex) => dist(ex, c))) : Infinity; 203 | if (min === Infinity || min > MIN_DIST) return c; 204 | } 205 | return palette[0]; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/github-install.ts: -------------------------------------------------------------------------------- 1 | import { Notice, normalizePath, requestUrl } from "obsidian"; 2 | import Manager from "main"; 3 | import { BPM_TAG_ID } from "./repo-resolver"; 4 | 5 | interface ReleaseAsset { 6 | name: string; 7 | browser_download_url: string; 8 | } 9 | 10 | interface ReleaseResponse { 11 | tag_name?: string; 12 | assets?: ReleaseAsset[]; 13 | } 14 | 15 | const API_BASE = "https://api.github.com"; 16 | 17 | const buildHeaders = (token?: string) => { 18 | const headers: Record = { 19 | Accept: "application/vnd.github+json", 20 | }; 21 | if (token) headers.Authorization = `Bearer ${token}`; 22 | return headers; 23 | }; 24 | 25 | export const sanitizeRepo = (input: string): string => { 26 | let repo = (input || "").trim(); 27 | repo = repo.replace(/^https?:\/\/github.com\//i, ""); 28 | repo = repo.replace(/^git@github.com:/i, ""); 29 | repo = repo.replace(/\.git$/i, ""); 30 | repo = repo.replace(/\/$/, ""); 31 | // 只保留 owner/repo 前两段 32 | const parts = repo.split("/"); 33 | if (parts.length >= 2) repo = `${parts[0]}/${parts[1]}`; 34 | return repo; 35 | }; 36 | 37 | const enrichError = (res: any, msg?: string) => { 38 | const err: any = new Error(msg || `GitHub request failed: ${res?.status}`); 39 | err.status = res?.status; 40 | err.rateRemaining = res?.headers?.["x-ratelimit-remaining"]; 41 | err.rateReset = res?.headers?.["x-ratelimit-reset"]; 42 | return err; 43 | }; 44 | 45 | const fetchJson = async (url: string, token?: string) => { 46 | const res = await requestUrl({ url, headers: buildHeaders(token) }); 47 | if (res.status >= 400) throw enrichError(res); 48 | return res.json as Record; 49 | }; 50 | 51 | const fetchText = async (url: string, token?: string) => { 52 | const res = await requestUrl({ url, headers: buildHeaders(token) }); 53 | if (res.status >= 400) throw enrichError(res); 54 | return res.text; 55 | }; 56 | 57 | const getRelease = async (repo: string, version?: string, token?: string): Promise => { 58 | const url = version && version.trim() !== "" 59 | ? `${API_BASE}/repos/${repo}/releases/tags/${version}` 60 | : `${API_BASE}/repos/${repo}/releases/latest`; 61 | return (await fetchJson(url, token)) as ReleaseResponse; 62 | }; 63 | 64 | const pickAsset = (release: ReleaseResponse, name: string) => release.assets?.find((a) => a.name === name)?.browser_download_url ?? null; 65 | 66 | export interface ReleaseVersion { 67 | version: string; 68 | prerelease: boolean; 69 | } 70 | 71 | const fetchRawFromTag = async (repo: string, tag: string, file: string, token?: string) => { 72 | const candidates = [ 73 | `https://raw.githubusercontent.com/${repo}/${tag}/${file}`, 74 | `https://raw.githubusercontent.com/${repo}/v${tag}/${file}`, 75 | ]; 76 | for (const url of candidates) { 77 | try { 78 | return await fetchText(url, token); 79 | } catch { 80 | // try next 81 | } 82 | } 83 | throw new Error(`raw file missing: ${file}`); 84 | }; 85 | 86 | export const fetchReleaseVersions = async (manager: Manager, repoInput: string): Promise => { 87 | const repo = sanitizeRepo(repoInput); 88 | const token = manager.settings.GITHUB_TOKEN?.trim() || undefined; 89 | const url = `${API_BASE}/repos/${repo}/releases?per_page=50`; 90 | const releases = (await fetchJson(url, token)) as unknown as ReleaseResponse[]; 91 | if (!Array.isArray(releases)) return []; 92 | return releases.map((r) => ({ 93 | version: r.tag_name || "", 94 | prerelease: Boolean((r as any).prerelease), 95 | })).filter((r) => r.version); 96 | }; 97 | 98 | export const installPluginFromGithub = async (manager: Manager, repoInput: string, version?: string, markAsBpm: boolean = true): Promise => { 99 | try { 100 | const repo = sanitizeRepo(repoInput); 101 | const token = manager.settings.GITHUB_TOKEN?.trim() || undefined; 102 | const release = await getRelease(repo, version, token); 103 | const tag = release.tag_name || version || ""; 104 | if (manager.settings.DEBUG) console.log("[BPM] install from GitHub", { repo, version, tag }); 105 | 106 | const manifestUrl = pickAsset(release, "manifest.json"); 107 | const mainJsUrl = pickAsset(release, "main.js"); 108 | let manifestText: string | null = null; 109 | let mainJs: string | null = null; 110 | let styles: string | null = null; 111 | 112 | // 优先 release 资产 113 | try { 114 | if (manifestUrl) manifestText = await fetchText(manifestUrl, token); 115 | if (mainJsUrl) mainJs = await fetchText(mainJsUrl, token); 116 | const stylesUrl = pickAsset(release, "styles.css"); 117 | if (stylesUrl) styles = await fetchText(stylesUrl, token); 118 | if (manager.settings.DEBUG) console.log("[BPM] release assets picked", { manifestUrl: !!manifestUrl, mainJsUrl: !!mainJsUrl, styles: !!styles }); 119 | } catch (e) { 120 | if (manager.settings.DEBUG) console.log("[BPM] release asset fetch failed, will fallback", e); 121 | } 122 | 123 | // 资产缺失则回退到 tag raw 文件 124 | if (!manifestText || !mainJs) { 125 | if (!tag) throw new Error("未找到发布 tag,无法下载原始文件"); 126 | try { 127 | manifestText = await fetchRawFromTag(repo, tag, "manifest.json", token); 128 | mainJs = await fetchRawFromTag(repo, tag, "main.js", token); 129 | try { styles = await fetchRawFromTag(repo, tag, "styles.css", token); } catch { /* optional */ } 130 | if (manager.settings.DEBUG) console.log("[BPM] fallback to raw tag", { repo, tag, manifest: !!manifestText, main: !!mainJs, styles: !!styles }); 131 | } catch (e) { 132 | console.error("fallback to raw tag failed", e); 133 | } 134 | } 135 | 136 | if (!manifestText || !mainJs) { 137 | new Notice("未找到 manifest.json 或 main.js,请检查发布资产或仓库 tag。"); 138 | return false; 139 | } 140 | 141 | const manifest = JSON.parse(manifestText) as { id: string; name: string; version?: string }; 142 | if (!manifest?.id) { 143 | new Notice("manifest.json 缺少 id 字段,无法安装。"); 144 | return false; 145 | } 146 | if (manager.settings.DEBUG) console.log("[BPM] manifest parsed", { id: manifest.id, version: manifest.version }); 147 | 148 | const adapter = manager.app.vault.adapter; 149 | const pluginDir = normalizePath(`${manager.app.vault.configDir}/plugins/${manifest.id}`); 150 | const pluginPath = `${pluginDir}/`; 151 | if (!(await adapter.exists(pluginDir))) await adapter.mkdir(pluginDir); 152 | 153 | if (manager.settings.DEBUG) console.log("[BPM] writing files", { pluginDir, manifestSize: manifestText.length, mainSize: mainJs.length, stylesSize: styles?.length }); 154 | await adapter.write(`${pluginPath}manifest.json`, manifestText); 155 | await adapter.write(`${pluginPath}main.js`, mainJs); 156 | if (styles) await adapter.write(`${pluginPath}styles.css`, styles); 157 | 158 | // 如果已安装则重载,否则加载并启用 159 | try { 160 | await manager.appPlugins.disablePlugin(manifest.id); 161 | } catch { /* noop */ } 162 | await manager.appPlugins.enablePluginAndSave(manifest.id); 163 | 164 | // 记录来源:仅在明确从 BPM 下载页面安装时标记 bpm 安装 165 | if (markAsBpm) { 166 | if (!manager.settings.BPM_INSTALLED.includes(manifest.id)) { 167 | manager.settings.BPM_INSTALLED.push(manifest.id); 168 | } 169 | const mp = manager.settings.Plugins.find((p) => p.id === manifest.id); 170 | if (mp && !mp.tags.includes(BPM_TAG_ID)) mp.tags.push(BPM_TAG_ID); 171 | } 172 | await manager.repoResolver.setRepo(manifest.id, repo); 173 | // 刷新设置并标签 174 | await manager.appPlugins.loadManifests(); 175 | if (manager.settings.DEBUG) { 176 | const loaded = (manager.appPlugins.manifests as any)?.[manifest.id]; 177 | console.log("[BPM] manifest after reload", { id: manifest.id, loadedVersion: loaded?.version, expected: manifest.version }); 178 | } 179 | manager.synchronizePlugins(Object.values(manager.appPlugins.manifests).filter((pm: any) => pm.id !== manager.manifest.id) as any); 180 | manager.saveSettings(); 181 | manager.exportPluginNote(manifest.id); 182 | if (manager.settings.DEBUG) console.log("[BPM] install complete", { id: manifest.id, markAsBpm }); 183 | 184 | new Notice(`${manager.translator.t("安装_成功_提示")}${manifest.name || manifest.id}`); 185 | return true; 186 | } catch (error) { 187 | const err: any = error; 188 | console.error(error); 189 | if (err?.status === 403 && !manager.settings.GITHUB_TOKEN) { 190 | new Notice(manager.translator.t("安装_错误_限速")); 191 | } else if (err?.status === 404) { 192 | new Notice(manager.translator.t("安装_错误_缺少资源")); 193 | } else { 194 | new Notice(manager.translator.t("安装_错误_通用")); 195 | } 196 | return false; 197 | } 198 | }; 199 | 200 | export const installThemeFromGithub = async (manager: Manager, repoInput: string, version?: string): Promise => { 201 | try { 202 | const repo = sanitizeRepo(repoInput); 203 | const token = manager.settings.GITHUB_TOKEN?.trim() || undefined; 204 | const release = await getRelease(repo, version, token); 205 | 206 | const manifestUrl = pickAsset(release, "manifest.json"); 207 | const themeUrl = pickAsset(release, "theme.css") ?? pickAsset(release, "themes.css") ?? pickAsset(release, "theme-beta.css"); 208 | if (!manifestUrl || !themeUrl) { 209 | new Notice("未找到 manifest.json 或 theme.css 资源。"); 210 | return false; 211 | } 212 | 213 | const manifestText = await fetchText(manifestUrl, token); 214 | const manifest = JSON.parse(manifestText) as { name: string }; 215 | if (!manifest?.name) { 216 | new Notice("主题 manifest 缺少 name 字段。"); 217 | return false; 218 | } 219 | 220 | const themeCss = await fetchText(themeUrl, token); 221 | const adapter = manager.app.vault.adapter; 222 | const themeDir = normalizePath(`${manager.app.vault.configDir}/themes/${manifest.name}`); 223 | const themePath = `${themeDir}/`; 224 | if (!(await adapter.exists(themeDir))) await adapter.mkdir(themeDir); 225 | 226 | await adapter.write(`${themePath}theme.css`, themeCss); 227 | await adapter.write(`${themePath}manifest.json`, manifestText); 228 | 229 | // 应用主题 230 | // @ts-ignore 231 | manager.app.customCss?.setTheme?.(manifest.name); 232 | 233 | new Notice(`已安装/更新主题:${manifest.name}`); 234 | return true; 235 | } catch (error) { 236 | console.error(error); 237 | new Notice("主题安装失败,请检查仓库地址/版本或网络状态。"); 238 | return false; 239 | } 240 | }; 241 | -------------------------------------------------------------------------------- /src/modal/tags-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, ExtraButtonComponent, Modal, Notice, Setting } from 'obsidian'; 2 | import { ManagerSettings } from '../settings/data'; 3 | import Manager from 'main'; 4 | import { ManagerModal } from './manager-modal'; 5 | import { ManagerPlugin } from 'src/data/types'; 6 | import Commands from 'src/command'; 7 | import { BPM_TAG_ID } from 'src/repo-resolver'; 8 | 9 | export class TagsModal extends Modal { 10 | settings: ManagerSettings; 11 | manager: Manager; 12 | managerModal: ManagerModal; 13 | managerPlugin: ManagerPlugin; 14 | selected: string; 15 | add: boolean; 16 | private defaultTagColor = ''; 17 | 18 | constructor(app: App, manager: Manager, managerModal: ManagerModal, managerPlugin: ManagerPlugin) { 19 | super(app); 20 | this.settings = manager.settings; 21 | this.manager = manager; 22 | this.managerModal = managerModal; 23 | this.managerPlugin = managerPlugin; 24 | this.selected = ''; 25 | this.add = false; 26 | } 27 | 28 | private async showHead() { 29 | //@ts-ignore 30 | const modalEl: HTMLElement = this.contentEl.parentElement; 31 | modalEl.addClass('manager-editor__container'); 32 | modalEl.removeChild(modalEl.getElementsByClassName('modal-close-button')[0]); 33 | this.titleEl.parentElement?.addClass('manager-container__header'); 34 | this.contentEl.addClass('manager-item-container'); 35 | // [标题行] 36 | const titleBar = new Setting(this.titleEl).setClass('manager-bar__title').setName(this.managerPlugin.name); 37 | // [标题行] 关闭按钮 38 | const closeButton = new ExtraButtonComponent(titleBar.controlEl) 39 | closeButton.setIcon('circle-x') 40 | closeButton.onClick(() => this.close()); 41 | } 42 | 43 | private async showData() { 44 | // 预先生成缺省颜色,避免与现有标签颜色过近 45 | if (!this.defaultTagColor) { 46 | this.defaultTagColor = this.pickDistinctColor(this.settings.TAGS.map(t => t.color)); 47 | } 48 | for (const tag of this.settings.TAGS) { 49 | const itemEl = new Setting(this.contentEl) 50 | itemEl.setClass('manager-editor__item') 51 | if (this.selected == '' || this.selected != tag.id) { 52 | itemEl.addExtraButton(cb => cb 53 | .setIcon('settings') 54 | .setDisabled(tag.id === BPM_TAG_ID) 55 | .onClick(() => { 56 | this.selected = tag.id; 57 | this.reloadShowData(); 58 | }) 59 | ) 60 | itemEl.addToggle(cb => cb 61 | .setValue(this.managerPlugin.tags.includes(tag.id)) 62 | .setDisabled(tag.id === BPM_TAG_ID) 63 | .onChange(async (isChecked) => { 64 | if (isChecked) { 65 | // 添加开启的标签 66 | if (!this.managerPlugin.tags.includes(tag.id)) { 67 | this.managerPlugin.tags.push(tag.id); 68 | } 69 | } else { 70 | // 移除关闭的标签 71 | this.managerPlugin.tags = this.managerPlugin.tags.filter(t => t !== tag.id); 72 | } 73 | await this.manager.savePluginAndExport(this.managerPlugin.id); 74 | this.managerModal.reloadShowData(); 75 | }) 76 | ); 77 | const tempEl = createSpan({ cls: 'manager-item__name-group' }); 78 | itemEl.nameEl.appendChild(tempEl); 79 | const tagEl = this.manager.createTag(tag.name, tag.color, this.settings.TAG_STYLE); 80 | tempEl.appendChild(tagEl); 81 | } 82 | if (this.selected != '' && this.selected == tag.id) { 83 | if (tag.id === BPM_TAG_ID) { 84 | this.selected = ''; 85 | continue; 86 | } 87 | itemEl.addColorPicker(cb => cb 88 | .setValue(tag.color) 89 | .onChange((value) => { 90 | tag.color = value; 91 | this.manager.saveSettings(); 92 | this.reloadShowData(); 93 | }) 94 | ) 95 | itemEl.addText(cb => cb 96 | .setValue(tag.name) 97 | .onChange((value) => { 98 | tag.name = value; 99 | this.manager.saveSettings(); 100 | }) 101 | .inputEl.addClass('manager-editor__item-input') 102 | ) 103 | itemEl.addExtraButton(cb => cb 104 | .setIcon('trash-2') 105 | .setDisabled(tag.id === BPM_TAG_ID) 106 | .onClick(() => { 107 | const hasTestTag = this.settings.Plugins.some(plugin => plugin.tags && plugin.tags.includes(tag.id)); 108 | if (!hasTestTag) { 109 | this.manager.settings.TAGS = this.manager.settings.TAGS.filter(t => t.id !== tag.id); 110 | this.manager.saveSettings(); 111 | this.reloadShowData(); 112 | Commands(this.app, this.manager); 113 | new Notice(this.manager.translator.t('设置_标签设置_通知_三')); 114 | } else { 115 | new Notice(this.manager.translator.t('设置_标签设置_通知_四')); 116 | } 117 | }) 118 | ) 119 | 120 | itemEl.addExtraButton(cb => cb 121 | .setIcon('save') 122 | .onClick(() => { 123 | this.selected = ''; 124 | this.reloadShowData(); 125 | this.managerModal.reloadShowData(); 126 | }) 127 | ) 128 | const groupEl = createSpan({ cls: 'manager-item__name-group' }); 129 | itemEl.nameEl.appendChild(groupEl); 130 | const tagEl = this.manager.createTag(tag.name, tag.color, this.settings.TAG_STYLE); 131 | groupEl.appendChild(tagEl); 132 | } 133 | } 134 | if (this.add) { 135 | let id = ''; 136 | let name = ''; 137 | let color = this.pickDistinctColor(this.settings.TAGS.map(t => t.color)); 138 | const foodBar = new Setting(this.contentEl).setClass('manager-bar__title'); 139 | foodBar.infoEl.remove(); 140 | foodBar.addColorPicker(cb => cb 141 | .setValue(color) 142 | .onChange((value) => { color = value; }) 143 | ) 144 | foodBar.addText(cb => cb 145 | .setPlaceholder('ID') 146 | .onChange((value) => { id = value; this.manager.saveSettings(); }) 147 | .inputEl.addClass('manager-editor__item-input') 148 | ) 149 | foodBar.addText(cb => cb 150 | .setPlaceholder(this.manager.translator.t('通用_名称_文本')) 151 | .onChange((value) => { name = value; }) 152 | .inputEl.addClass('manager-editor__item-input') 153 | ) 154 | foodBar.addExtraButton(cb => cb 155 | .setIcon('plus') 156 | .onClick(() => { 157 | const containsId = this.manager.settings.TAGS.some(tag => tag.id === id); 158 | if (!containsId && id !== '' && id !== BPM_TAG_ID) { 159 | if (color === '') color = this.pickDistinctColor(this.settings.TAGS.map(t => t.color)); 160 | this.manager.settings.TAGS.push({ id, name, color }); 161 | this.manager.saveSettings(); 162 | this.add = false; 163 | this.reloadShowData(); 164 | Commands(this.app, this.manager); 165 | new Notice(this.manager.translator.t('设置_标签设置_通知_一')); 166 | } else { 167 | new Notice(this.manager.translator.t('设置_标签设置_通知_二')); 168 | } 169 | }) 170 | ) 171 | } else { 172 | // [底部行] 新增 173 | const foodBar = new Setting(this.contentEl).setClass('manager-bar__title').setName(this.manager.translator.t('通用_新增_文本')); 174 | const addButton = new ExtraButtonComponent(foodBar.controlEl) 175 | addButton.setIcon('circle-plus') 176 | addButton.onClick(() => { 177 | this.add = true; 178 | this.reloadShowData(); 179 | }); 180 | } 181 | } 182 | 183 | private async reloadShowData() { 184 | let scrollTop = 0; 185 | const modalElement: HTMLElement = this.contentEl; 186 | scrollTop = modalElement.scrollTop; 187 | modalElement.empty(); 188 | await this.showData(); 189 | modalElement.scrollTo(0, scrollTop); 190 | } 191 | 192 | async onOpen() { 193 | await this.showHead(); 194 | await this.showData(); 195 | } 196 | 197 | async onClose() { 198 | this.contentEl.empty(); 199 | } 200 | 201 | private pickDistinctColor(existing: string[]): string { 202 | const palette = ['#FF6B6B', '#4ECDC4', '#FFD166', '#A78BFA', '#48BB78', '#F472B6', '#38BDF8', '#F59E0B', '#22D3EE', '#F97316', '#10B981', '#E11D48', '#6366F1', '#14B8A6']; 203 | const toRgb = (hex: string) => { 204 | const clean = hex.replace('#', ''); 205 | const num = parseInt(clean, 16); 206 | return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; 207 | }; 208 | const dist = (a: string, b: string) => { 209 | const [ar, ag, ab] = toRgb(a); 210 | const [br, bg, bb] = toRgb(b); 211 | return Math.sqrt((ar - br) ** 2 + (ag - bg) ** 2 + (ab - bb) ** 2); 212 | }; 213 | const MIN_DIST = 80; 214 | for (const c of palette) { 215 | const min = existing.length ? Math.min(...existing.map((ex) => dist(ex, c))) : Infinity; 216 | if (min === Infinity || min > MIN_DIST) return c; 217 | } 218 | return palette[0]; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/modal/hide-modal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | ButtonComponent, 4 | DropdownComponent, 5 | ExtraButtonComponent, 6 | Modal, 7 | Notice, 8 | PluginManifest, 9 | SearchComponent, 10 | Setting, 11 | ToggleComponent, 12 | } from "obsidian"; 13 | 14 | import { ManagerSettings } from "../settings/data"; 15 | 16 | import Manager from "main"; 17 | import { ManagerModal } from "./manager-modal"; 18 | import { TagsModal } from "./tags-modal"; 19 | 20 | interface ExportPluginManifest { 21 | id: string; 22 | name: string; 23 | version: string; 24 | author: string; 25 | description: string; 26 | export: boolean; 27 | } 28 | 29 | interface ImportPluginManifest { 30 | id: string; 31 | name: string; 32 | version: string; 33 | author: string; 34 | description: string; 35 | } 36 | 37 | 38 | // ============================== 39 | // 侧边栏 对话框 翻译 40 | // ============================== 41 | export class HideModal extends Modal { 42 | manager: Manager; 43 | managerModal: ManagerModal; 44 | settings: ManagerSettings; 45 | // this.app.plugins 46 | appPlugins; 47 | // this.app.settings 48 | appSetting; 49 | // [本地][变量] 导出插件列表 50 | plugins: PluginManifest[] = []; 51 | 52 | // 搜索内容 53 | searchText = ""; 54 | // 搜索结果 55 | searchEl: SearchComponent; 56 | delay: string = ""; 57 | tag: string = ""; 58 | group: string = ""; 59 | filter: string = "all"; 60 | 61 | constructor(app: App, manager: Manager, managerModal: ManagerModal, plugins: PluginManifest[]) { 62 | super(app); 63 | // @ts-ignore 64 | this.appSetting = this.app.setting; 65 | // @ts-ignore 66 | this.appPlugins = this.app.plugins; 67 | this.manager = manager; 68 | this.managerModal = managerModal; 69 | this.settings = manager.settings; 70 | this.plugins = plugins; 71 | } 72 | 73 | public async showHead() { 74 | //@ts-ignore 75 | const modalEl: HTMLElement = this.contentEl.parentElement; 76 | modalEl.addClass("manager-container"); 77 | // 靠上 78 | if (!this.settings.CENTER) modalEl.addClass("manager-container__top"); 79 | modalEl.removeChild(modalEl.getElementsByClassName("modal-close-button")[0]); 80 | this.titleEl.parentElement?.addClass("manager-container__header"); 81 | this.contentEl.addClass("manager-item-container"); 82 | 83 | // [操作行] 84 | const actionBar = new Setting(this.titleEl).setClass("manager-bar__action").setName(this.manager.translator.t("菜单_隐藏插件_标题")); 85 | 86 | // [操作行] 关闭 87 | const closeButton = new ButtonComponent(actionBar.controlEl); 88 | closeButton.setIcon("x"); 89 | closeButton.onClick(() => { this.close(); }); 90 | 91 | // [搜索行] 92 | const searchBar = new Setting(this.titleEl).setClass("manager-bar__search").setName(this.manager.translator.t("通用_搜索_文本")); 93 | 94 | const filterOptions = { 95 | "all": this.manager.translator.t("筛选_全部_描述"), 96 | "enabled": this.manager.translator.t("筛选_仅启用_描述"), 97 | "disabled": this.manager.translator.t("筛选_仅禁用_描述"), 98 | "grouped": this.manager.translator.t("筛选_已分组_描述"), 99 | "ungrouped": this.manager.translator.t("筛选_未分组_描述"), 100 | "tagged": this.manager.translator.t("筛选_有标签_描述"), 101 | "untagged": this.manager.translator.t("筛选_无标签_描述"), 102 | "noted": this.manager.translator.t("筛选_有笔记_描述"), 103 | }; 104 | // 过滤器 105 | const filterDropdown = new DropdownComponent(searchBar.controlEl); 106 | filterDropdown.addOptions(filterOptions); 107 | filterDropdown.setValue(this.filter); 108 | filterDropdown.onChange((value) => { this.filter = value; this.reloadShowData(); }); 109 | 110 | // [搜索行] 分组选择列表 111 | const groupCounts = this.settings.Plugins.reduce((acc: { [key: string]: number }, plugin) => { const groupId = plugin.group || ""; acc[groupId] = (acc[groupId] || 0) + 1; return acc; }, { "": 0 }); 112 | const groups = this.settings.GROUPS.reduce((acc: { [key: string]: string }, item) => { acc[item.id] = `${item.name} [${groupCounts[item.id] || 0}]`; return acc; }, { "": this.manager.translator.t("通用_无分组_文本") }); 113 | const groupsDropdown = new DropdownComponent(searchBar.controlEl); 114 | groupsDropdown.addOptions(groups); 115 | groupsDropdown.setValue(this.group); 116 | groupsDropdown.onChange((value) => { this.group = value; this.reloadShowData(); }); 117 | 118 | // [搜索行] 标签选择列表 119 | const tagCounts: { [key: string]: number } = this.settings.Plugins.reduce((acc, plugin) => { plugin.tags.forEach((tag) => { acc[tag] = (acc[tag] || 0) + 1; }); return acc; }, {} as { [key: string]: number }); 120 | const tags = this.settings.TAGS.reduce((acc: { [key: string]: string }, item) => { acc[item.id] = `${item.name} [${tagCounts[item.id] || 0}]`; return acc; }, { "": this.manager.translator.t("通用_无标签_文本") }); 121 | const tagsDropdown = new DropdownComponent(searchBar.controlEl); 122 | tagsDropdown.addOptions(tags); 123 | tagsDropdown.setValue(this.tag); 124 | tagsDropdown.onChange((value) => { this.tag = value; this.reloadShowData(); }); 125 | 126 | // [搜索行] 延迟选择列表 127 | if (this.settings.DELAY) { 128 | const delayCounts = this.settings.Plugins.reduce((acc: { [key: string]: number }, plugin) => { const delay = plugin.delay || ""; acc[delay] = (acc[delay] || 0) + 1; return acc; }, { "": 0 }); 129 | const delays = this.settings.DELAYS.reduce((acc: { [key: string]: string }, item) => { acc[item.id] = `${item.name} (${delayCounts[item.id] || 0})`; return acc; }, { "": this.manager.translator.t("通用_无延迟_文本") }); 130 | const delaysDropdown = new DropdownComponent(searchBar.controlEl); 131 | delaysDropdown.addOptions(delays); 132 | delaysDropdown.setValue(this.delay || ""); 133 | delaysDropdown.onChange((value) => { this.delay = value; this.reloadShowData(); }); 134 | } 135 | 136 | // [搜索行] 搜索框 137 | this.searchEl = new SearchComponent(searchBar.controlEl); 138 | this.searchEl.onChange((value: string) => { this.searchText = value; this.reloadShowData(); }); 139 | } 140 | 141 | public async showData() { 142 | for (const plugin of this.plugins) { 143 | const ManagerPlugin = this.manager.settings.Plugins.find((mp) => mp.id === plugin.id); 144 | // 插件是否开启 145 | const isEnabled = this.settings.DELAY ? ManagerPlugin?.enabled : this.appPlugins.enabledPlugins.has(plugin.id); 146 | if (ManagerPlugin) { 147 | // [过滤] 条件 148 | switch (this.filter) { 149 | case "enabled": 150 | if (!isEnabled) continue; // 仅显示启用插件 151 | break; 152 | case "disabled": 153 | if (isEnabled) continue; // 仅显示禁用插件 154 | break; 155 | case "grouped": 156 | if (ManagerPlugin.group === "") continue; // 仅显示有分组的插件 157 | break; 158 | case "ungrouped": 159 | if (ManagerPlugin.group !== "") continue; // 仅显示未分组插件 160 | break; 161 | case "tagged": 162 | if (ManagerPlugin.tags.length === 0) continue; // 修正为标签数组长度判断 163 | break; 164 | case "untagged": 165 | if (ManagerPlugin.tags.length > 0) continue; // 修正为标签数组长度判断 166 | break; 167 | case "noted": 168 | if (!ManagerPlugin.note || ManagerPlugin.note === "") continue; // 新增笔记判断 169 | break; 170 | default: 171 | break; // 其他情况显示所有插件 172 | } 173 | // [过滤] 分组 标签 延时 174 | if (this.group !== "" && (ManagerPlugin.group !== this.group)) continue; 175 | if (this.tag !== "" && !ManagerPlugin.tags.includes(this.tag)) continue; 176 | if (this.delay !== "" && ManagerPlugin.delay !== this.delay) continue; 177 | // [过滤] 搜索 178 | if (this.searchText !== "" && ManagerPlugin.name.toLowerCase().indexOf(this.searchText.toLowerCase()) == -1 && ManagerPlugin.desc.toLowerCase().indexOf(this.searchText.toLowerCase()) == -1 && plugin.author.toLowerCase().indexOf(this.searchText.toLowerCase()) == -1) continue; 179 | // [过滤] 自身 180 | if (plugin.id === this.manager.manifest.id) continue; 181 | 182 | const itemEl = new Setting(this.contentEl); 183 | itemEl.setClass("manager-item"); 184 | itemEl.nameEl.addClass("manager-item__name-container"); 185 | itemEl.descEl.addClass("manager-item__description-container"); 186 | 187 | // [默认] 分组 188 | if (ManagerPlugin.group !== "") { 189 | const group = createSpan({ cls: "manager-item__name-group", }); 190 | itemEl.nameEl.appendChild(group); 191 | const item = this.settings.GROUPS.find((t) => t.id === ManagerPlugin.group); 192 | if (item) { const tag = this.manager.createTag(item.name, item.color, this.settings.GROUP_STYLE); group.appendChild(tag); } 193 | } 194 | 195 | // [默认] 名称 196 | const title = createSpan({ text: ManagerPlugin.name, cls: "manager-item__name-title", }); 197 | itemEl.nameEl.appendChild(title); 198 | 199 | // [默认] 版本 200 | const version = createSpan({ text: `[${plugin.version}]`, cls: ["manager-item__name-version"], }); 201 | itemEl.nameEl.appendChild(version); 202 | 203 | // [默认] 延迟 204 | if (this.settings.DELAY && ManagerPlugin.delay !== "") { 205 | const d = this.settings.DELAYS.find((item) => item.id === ManagerPlugin.delay); 206 | if (d) { 207 | const delay = createSpan({ text: `${d.time}s`, cls: ["manager-item__name-delay"], }); 208 | itemEl.nameEl.appendChild(delay); 209 | } 210 | } 211 | 212 | // [默认] 描述 213 | const desc = createDiv({ text: ManagerPlugin.desc, cls: ["manager-item__name-desc"], }); 214 | itemEl.descEl.appendChild(desc); 215 | 216 | // [默认] 标签组 217 | const tags = createDiv(); 218 | itemEl.descEl.appendChild(tags); 219 | ManagerPlugin.tags.map((id: string) => { 220 | const item = this.settings.TAGS.find((item) => item.id === id); 221 | if (item) { const tag = this.manager.createTag(item.name, item.color, this.settings.TAG_STYLE); tags.appendChild(tag); } 222 | }); 223 | 224 | const hiddenToggle = new ToggleComponent(itemEl.controlEl); 225 | // 判断当前插件是否在隐藏列表 226 | const isHidden = this.settings.HIDES.includes(plugin.id); 227 | hiddenToggle.setValue(isHidden); 228 | hiddenToggle.onChange((value) => { 229 | // 更新隐藏列表 230 | if (value) this.settings.HIDES.push(plugin.id); else this.settings.HIDES = this.settings.HIDES.filter(id => id !== plugin.id); 231 | this.manager.saveSettings(); 232 | this.managerModal.reloadShowData(); 233 | }); 234 | } 235 | } 236 | } 237 | 238 | public async reloadShowData() { 239 | let scrollTop = 0; 240 | const modalElement: HTMLElement = this.contentEl; 241 | scrollTop = modalElement.scrollTop; 242 | modalElement.empty(); 243 | this.showData(); 244 | modalElement.scrollTo(0, scrollTop); 245 | } 246 | 247 | public async onOpen() { 248 | await this.showHead(); 249 | await this.showData(); 250 | } 251 | 252 | public async onClose() { 253 | this.contentEl.empty(); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --i18n-border-radius: 4px; 3 | --el-font-size-base: 14px; 4 | --i18n-button-font-weight: 500px; 5 | --i18n-tag-font-size: 10px; 6 | --i18n-tag-border-radius: 3px; 7 | --i18n-tag-border-radius-rounded: 9999px; 8 | } 9 | 10 | body { 11 | --item-inactive: 0.5; 12 | } 13 | 14 | body.theme-dark { 15 | --item-inactive: 0.5; 16 | } 17 | 18 | .manager-display-none { 19 | display: none; 20 | } 21 | 22 | .manager-display-block { 23 | display: block; 24 | } 25 | 26 | /* [通用] Tag (标签) 27 | ---------------------------------------------------------------- */ 28 | .manager-tag { 29 | display: inline-flex; 30 | justify-content: center; 31 | align-items: center; 32 | vertical-align: middle; 33 | height: 20px; 34 | padding: 0 6px; 35 | margin-left: 5px; 36 | font-size: var(--i18n-tag-font-size); 37 | line-height: 1; 38 | border-width: 1px; 39 | border-style: solid; 40 | border-radius: var(--i18n-tag-border-radius); 41 | box-sizing: border-box; 42 | white-space: nowrap; 43 | } 44 | 45 | .manager-tag:first-child { 46 | margin-left: 0px; 47 | } 48 | 49 | 50 | /* 插件管理器 模块 51 | ---------------------------------------------------------------- */ 52 | .manager-container { 53 | display: flex; 54 | flex-direction: column; 55 | z-index: 100; 56 | width: 800px; 57 | min-width: 550px; 58 | border-radius: var(--i18n-border-radius); 59 | background-color: var(--background-primary); 60 | } 61 | 62 | .manager-container--mobile { 63 | position: fixed; 64 | top: calc(4vh + env(safe-area-inset-top, 0px)); 65 | left: 0; 66 | right: 0; 67 | margin: 0 auto; 68 | width: min(92vw, 900px); 69 | min-width: 0; 70 | height: calc(100vh - 8vh - env(safe-area-inset-top, 0px)); 71 | max-height: calc(100vh - 8vh - env(safe-area-inset-top, 0px)); 72 | overflow: hidden; 73 | border-radius: 14px; 74 | transform: none !important; 75 | } 76 | 77 | .manager-container--editing { 78 | box-shadow: 0 0 0 3px #f97316, 0 12px 30px rgba(0, 0, 0, 0.25); 79 | border: 2px solid #f97316; 80 | } 81 | 82 | .manager-container::-webkit-scrollbar { 83 | display: none; 84 | } 85 | 86 | .manager-container__top { 87 | position: absolute; 88 | top: 100px; 89 | } 90 | 91 | .manager-container__header { 92 | margin-bottom: 0px; 93 | } 94 | 95 | .manager-bar__action { 96 | margin: 0; 97 | padding-top: 10px; 98 | padding-bottom: 10px; 99 | border: #fff solid 0px; 100 | } 101 | 102 | .manager-bar__action:first-child { 103 | padding-top: 0px; 104 | } 105 | 106 | .manager-bar__search { 107 | margin: 0; 108 | padding-top: 0px; 109 | padding-bottom: 10px; 110 | border: #fff solid 0px; 111 | display: flex; 112 | flex-direction: column; 113 | gap: 6px; 114 | } 115 | 116 | .manager-container--mobile .manager-bar__action { 117 | display: block; 118 | padding: 6px 0; 119 | width: 100%; 120 | max-width: 100%; 121 | } 122 | 123 | .manager-container--mobile .manager-bar__action.setting-item { 124 | width: 100%; 125 | max-width: 100%; 126 | } 127 | .manager-container--mobile .manager-bar__action .setting-item-info { 128 | display: none; 129 | } 130 | 131 | .manager-container--mobile .manager-bar__search { 132 | display: flex; 133 | flex-direction: column; 134 | gap: 10px; 135 | padding: 8px 0; 136 | } 137 | .manager-container--mobile .manager-bar__search .setting-item-control { 138 | flex-direction: column; 139 | align-items: stretch; 140 | } 141 | .manager-container--mobile .manager-bar__search .setting-item-control > * { 142 | width: 100%; 143 | max-width: 100%; 144 | } 145 | .manager-container--mobile .manager-bar__search .dropdown { 146 | width: 100%; 147 | } 148 | .manager-container--mobile .manager-bar__search .search-input-container, 149 | .manager-container--mobile .manager-bar__search .search-input-container input { 150 | width: 100%; 151 | } 152 | 153 | .manager-container--mobile .manager-bar__action .setting-item-name, 154 | .manager-container--mobile .manager-bar__search .setting-item-name { 155 | font-size: clamp(13px, 2.5vw, 15px); 156 | } 157 | 158 | .manager-container--mobile .manager-bar__action .setting-item-control, 159 | .manager-container--mobile .manager-bar__search .setting-item-control { 160 | width: 100%; 161 | max-width: 100%; 162 | flex: 1 1 100%; 163 | display: grid; 164 | grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); 165 | gap: 10px; 166 | } 167 | 168 | .manager-container--mobile .manager-bar__action .setting-item-control button { 169 | width: 100%; 170 | justify-content: center; 171 | padding: 8px; 172 | min-height: 38px; 173 | } 174 | 175 | .manager-container--mobile .manager-bar__action .setting-item-control button svg { 176 | width: 16px; 177 | height: 16px; 178 | } 179 | 180 | .manager-container--mobile .manager-bar__search .setting-item-control > * { 181 | width: 100%; 182 | } 183 | .manager-container--mobile .manager-bar__search .setting-item-control { 184 | flex-direction: column; 185 | align-items: stretch; 186 | } 187 | 188 | .manager-container--mobile .setting-item { 189 | flex-direction: column; 190 | align-items: flex-start; 191 | gap: 6px; 192 | } 193 | 194 | .manager-container--mobile .setting-item-control { 195 | width: 100%; 196 | justify-content: flex-start; 197 | flex-wrap: wrap; 198 | gap: 6px; 199 | } 200 | 201 | .manager-container--mobile .manager-item { 202 | flex-direction: column; 203 | align-items: flex-start; 204 | padding: clamp(10px, 2vw, 14px); 205 | border: 1px solid var(--background-modifier-border); 206 | border-radius: 12px; 207 | background-color: var(--background-secondary); 208 | } 209 | 210 | .manager-container--mobile .manager-item__name-container { 211 | width: 100%; 212 | display: flex; 213 | justify-content: space-between; 214 | align-items: center; 215 | } 216 | 217 | .manager-container--mobile .manager-item__description-container { 218 | width: 100%; 219 | } 220 | 221 | .manager-container--mobile .search-input-container, 222 | .manager-container--mobile .search-input-container input { 223 | width: 100%; 224 | } 225 | 226 | .manager-container--mobile .setting-item-description { 227 | width: 100%; 228 | margin-top: 4px; 229 | } 230 | 231 | .manager-container--mobile .manager-item-container { 232 | max-height: calc(100vh - 240px); 233 | } 234 | 235 | .manager-container--mobile .modal-content { 236 | overflow: auto; 237 | } 238 | 239 | /* collapsible sections (mobile & desktop) */ 240 | .manager-section { 241 | width: 100%; 242 | max-width: 100%; 243 | margin-bottom: 10px; 244 | border: 1px solid var(--background-modifier-border); 245 | border-radius: 12px; 246 | padding: 10px 14px; 247 | background: var(--background-primary); 248 | display: flex; 249 | flex-direction: row; 250 | align-items: stretch; 251 | gap: 12px; 252 | } 253 | 254 | .manager-section--row { 255 | flex-direction: row; 256 | } 257 | 258 | .manager-section__header { 259 | display: flex; 260 | align-items: center; 261 | justify-content: flex-start; 262 | gap: 6px; 263 | padding: 0; 264 | font-weight: 600; 265 | cursor: pointer; 266 | color: var(--text-normal); 267 | flex: 0 0 auto; 268 | } 269 | 270 | .manager-section__arrow { 271 | margin-right: 4px; 272 | font-size: 12px; 273 | } 274 | 275 | .manager-section__content { 276 | flex: 1 1 100%; 277 | width: 100%; 278 | max-width: 100%; 279 | padding-top: 6px; 280 | } 281 | .manager-section__content--actions .manager-bar__action, 282 | .manager-section__content--filters .manager-bar__search { 283 | width: 100%; 284 | } 285 | .manager-bar__search .setting-item-control { 286 | display: grid; 287 | grid-template-columns: repeat(4, minmax(140px, 1fr)); 288 | gap: 8px; 289 | align-items: stretch; 290 | } 291 | .manager-bar__search .dropdown, 292 | .manager-bar__search .search-input-container { 293 | width: 100%; 294 | max-width: 100%; 295 | min-width: 0; 296 | } 297 | .manager-bar__search .dropdown select, 298 | .manager-bar__search .search-input-container input { 299 | width: 100%; 300 | min-width: 0; 301 | white-space: nowrap; 302 | text-overflow: ellipsis; 303 | overflow: hidden; 304 | } 305 | .manager-bar__search .setting-item-control > * { 306 | min-width: 0; 307 | } 308 | .manager-bar__search .setting-item-control > .search-input-container { 309 | grid-column: span 1; 310 | flex: 1 1 auto; 311 | } 312 | @media (max-width: 1024px) { 313 | .manager-bar__search .setting-item-control { 314 | grid-template-columns: repeat(2, minmax(150px, 1fr)); 315 | } 316 | } 317 | @media (max-width: 720px) { 318 | .manager-bar__search .setting-item-control { 319 | grid-template-columns: repeat(1, minmax(160px, 1fr)); 320 | } 321 | } 322 | 323 | .manager-section__content.is-collapsed { 324 | display: none !important; 325 | } 326 | 327 | .manager-section.is-collapsed { 328 | background: var(--background-secondary); 329 | border-color: var(--background-modifier-border); 330 | } 331 | 332 | .manager-section:not(.is-collapsed) { 333 | background: var(--background-secondary-alt); 334 | } 335 | 336 | /* mobile-only overrides */ 337 | .manager-container--mobile .modal-header, 338 | .manager-container--mobile .modal-title { 339 | width: 100%; 340 | display: block; 341 | } 342 | .manager-container--mobile .manager-section { 343 | flex-direction: column; 344 | align-items: stretch; 345 | gap: 6px; 346 | width: 100%; 347 | max-width: 100%; 348 | } 349 | .manager-container--mobile .manager-section__header { 350 | width: 100%; 351 | } 352 | .manager-container--mobile .manager-section--row { 353 | flex-direction: column; 354 | } 355 | .manager-container--mobile .manager-section__content { 356 | width: 100%; 357 | max-width: 100%; 358 | } 359 | .manager-container--mobile .manager-section__content--actions .manager-bar__action { 360 | width: 100%; 361 | } 362 | 363 | /* extra mobile breakpoints */ 364 | @media (max-width: 420px) { 365 | .manager-container--mobile .manager-bar__action, 366 | .manager-container--mobile .manager-bar__search { 367 | grid-template-columns: repeat(3, minmax(0, 1fr)); 368 | } 369 | .manager-container--mobile .manager-item-container { 370 | max-height: calc(100vh - 260px); 371 | } 372 | } 373 | 374 | @media (min-width: 600px) and (orientation: landscape) { 375 | .manager-container--mobile { 376 | top: calc(3vh + env(safe-area-inset-top, 0px)); 377 | height: calc(100vh - 6vh - env(safe-area-inset-top, 0px)); 378 | max-height: calc(100vh - 6vh - env(safe-area-inset-top, 0px)); 379 | left: 24px; 380 | right: 24px; 381 | width: calc(100vw - 48px); 382 | } 383 | .manager-container--mobile .manager-bar__action, 384 | .manager-container--mobile .manager-bar__search { 385 | grid-template-columns: repeat(5, minmax(0, 1fr)); 386 | } 387 | .manager-container--mobile .manager-item-container { 388 | max-height: calc(100vh - 200px); 389 | } 390 | } 391 | 392 | /* 目录容器 */ 393 | .manager-item-container { 394 | overflow: auto; 395 | padding: 8px; 396 | } 397 | 398 | .manager-item-container::-webkit-scrollbar { 399 | display: none; 400 | } 401 | 402 | /* 目录项 */ 403 | .manager-item { 404 | display: flex; 405 | align-items: flex-start; 406 | gap: 12px; 407 | height: 100%; 408 | padding: 14px 16px; 409 | border: 1px solid var(--background-modifier-border); 410 | border-radius: 12px; 411 | background-color: var(--background-secondary); 412 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); 413 | margin-bottom: 10px; 414 | } 415 | 416 | .manager-item > .setting-item-info { 417 | padding-left: 6px; 418 | padding-right: 4px; 419 | } 420 | 421 | .manager-item:first-child { 422 | padding-top: 10px; 423 | } 424 | 425 | .manager-item:hover { 426 | background-color: var(--background-modifier-hover); 427 | } 428 | 429 | .manager-item.inactive { 430 | opacity: var(--item-inactive); 431 | filter: none; 432 | } 433 | 434 | /* 目录项 名称行*/ 435 | .manager-item__name-container { 436 | display: flex; 437 | align-items: center; 438 | gap: 6px; 439 | height: auto; 440 | line-height: 20px; 441 | font-weight: bold; 442 | font-size: 15px; 443 | padding: 0; 444 | } 445 | 446 | .manager-item__name-group { 447 | display: flex; 448 | margin-right: 5px; 449 | } 450 | 451 | .manager-item__name-title { 452 | display: flex; 453 | margin-right: 5px; 454 | } 455 | 456 | .manager-item__name-version { 457 | display: inline-flex; 458 | align-items: center; 459 | font-size: 11px; 460 | margin-right: 5px; 461 | padding: 2px 6px; 462 | border-radius: 6px; 463 | background: var(--background-modifier-border); 464 | } 465 | .manager-item__versions { 466 | display: inline-flex; 467 | align-items: center; 468 | gap: 6px; 469 | margin-left: 4px; 470 | } 471 | .manager-item__name-remote { 472 | display: inline-flex; 473 | align-items: center; 474 | font-size: 11px; 475 | font-weight: 600; 476 | padding: 2px 6px; 477 | border-radius: 6px; 478 | border: 1px solid color-mix(in srgb, var(--text-accent) 60%, transparent); 479 | color: color-mix(in srgb, var(--text-accent) 80%, var(--text-normal)); 480 | background: color-mix(in srgb, var(--interactive-accent) 10%, transparent); 481 | } 482 | .manager-item__name-remote-arrow { 483 | font-size: 12px; 484 | color: var(--text-muted); 485 | opacity: 0.8; 486 | } 487 | 488 | .manager-item__name-delay { 489 | font-weight: bold; 490 | color: var(--interactive-accent); 491 | } 492 | 493 | .manager-item__name-desc { 494 | margin-bottom: 4px; 495 | padding: 6px 8px; 496 | border-radius: 8px; 497 | background: var(--background-modifier-hover); 498 | } 499 | 500 | /* 目录项 描述行*/ 501 | .manager-item__description-container { 502 | margin-left: 0px; 503 | margin-top: 4px; 504 | line-height: 1.4; 505 | transition: opacity 3s ease-out; 506 | width: 100%; 507 | } 508 | 509 | 510 | 511 | .manager-editor__container { 512 | display: flex; 513 | flex-direction: column; 514 | z-index: 200; 515 | width: 350px; 516 | border-radius: var(--i18n-border-radius); 517 | background-color: var(--background-primary); 518 | } 519 | 520 | .manager-editor__container::-webkit-scrollbar { 521 | display: none; 522 | } 523 | 524 | .manager-bar__title { 525 | margin: 0; 526 | padding-top: 0px; 527 | padding-bottom: 6px; 528 | border: #fff solid 0px; 529 | } 530 | 531 | .manager-editor__item { 532 | display: flex; 533 | align-items: center; 534 | height: 100%; 535 | padding: 6px 3px; 536 | border: #fff solid 0px; 537 | border-radius: var(--i18n-border-radius); 538 | } 539 | 540 | .manager-editor__item:first-child { 541 | padding-top: 6px; 542 | } 543 | 544 | .manager-editor__item:hover { 545 | background-color: var(--i18n-background-modifier-hover); 546 | } 547 | 548 | .manager-editor__item-input { 549 | width: 100px; 550 | } 551 | 552 | /* 设置界面CSS */ 553 | .manager-setting__container { 554 | display: flex; 555 | flex-direction: column; 556 | width: 100%; 557 | height: 100%; 558 | } 559 | 560 | .manager-setting__tabs { 561 | display: flex; 562 | flex-direction: row; 563 | height: 40px; 564 | border-bottom: 1px solid var(--interactive-accent); 565 | } 566 | 567 | .manager-setting__content { 568 | display: flex; 569 | flex-direction: column; 570 | flex-grow: 1; 571 | margin-top: 25px; 572 | overflow-y: auto; 573 | overflow-x: hidden; 574 | } 575 | 576 | .manager-setting__content::-webkit-scrollbar { 577 | display: none; 578 | } 579 | 580 | .manager-setting__tabs-item { 581 | display: flex; 582 | align-items: center; 583 | justify-content: center; 584 | padding: 0 15px; 585 | height: 40px; 586 | font-weight: bold; 587 | font-size: 14px; 588 | border-top: 1px solid var(--interactive-accent); 589 | border-right: 1px solid var(--interactive-accent); 590 | } 591 | 592 | .manager-setting__tabs-item:first-child { 593 | border-top-left-radius: 4px; 594 | border-left: 1px solid var(--interactive-accent); 595 | } 596 | 597 | .manager-setting__tabs-item:last-child { 598 | border-top-right-radius: 4px; 599 | } 600 | 601 | .manager-setting__tabs-item_is-active { 602 | color: var(--interactive-accent); 603 | } 604 | 605 | /* 标签设置页面 */ 606 | .manager-setting-tag__container { 607 | display: flex; 608 | flex-direction: row; 609 | flex-wrap: wrap; 610 | margin-top: 10px; 611 | } 612 | 613 | .manager-setting-tag__item { 614 | padding: 3px 0px; 615 | border-top: 0px solid #fff; 616 | } 617 | 618 | .manager-setting-tag__item:hover { 619 | background-color: var(--background-modifier-hover); 620 | } 621 | 622 | /* 分组设置页面 */ 623 | .manager-setting-group__container { 624 | padding: 3px; 625 | border-top: 0px solid #fff; 626 | border-radius: var(--i18n-border-radius); 627 | } 628 | 629 | .manager-setting-group__item { 630 | padding: 3px 0px; 631 | border-top: 0px solid #fff; 632 | border-radius: var(--i18n-border-radius); 633 | } 634 | 635 | .manager-setting-group__item:hover { 636 | background-color: var(--background-modifier-hover); 637 | } 638 | 639 | /* 卸载 */ 640 | .manager-delete__title { 641 | margin: 0; 642 | padding-top: 0px; 643 | padding-bottom: 6px; 644 | border: #fff solid 0px; 645 | } 646 | 647 | .manager-delete__action { 648 | padding-bottom: 0px; 649 | } 650 | 651 | .manager-food { 652 | margin-top: 10px; 653 | } 654 | 655 | .bpm-update-progress { 656 | display: flex; 657 | flex-direction: column; 658 | gap: 6px; 659 | min-width: 180px; 660 | } 661 | 662 | .bpm-progress { 663 | position: relative; 664 | width: 100%; 665 | height: 6px; 666 | border-radius: 4px; 667 | overflow: hidden; 668 | background: var(--background-modifier-border); 669 | } 670 | 671 | .bpm-progress__bar { 672 | position: absolute; 673 | inset: 0; 674 | background: var(--text-accent); 675 | transition: width 0.2s ease; 676 | } 677 | 678 | .bpm-progress__cancel { 679 | align-self: flex-end; 680 | background: transparent; 681 | border: 1px solid var(--text-accent); 682 | color: var(--text-accent); 683 | border-radius: 6px; 684 | padding: 4px 8px; 685 | cursor: pointer; 686 | } 687 | 688 | /* 笔记模块 CSS 689 | ---------------------------------------------------------------- */ 690 | .manager-note__container { 691 | display: flex; 692 | flex-direction: column; 693 | z-index: 200; 694 | width: 700px; 695 | height: 700px; 696 | border-radius: var(--i18n-border-radius); 697 | background-color: var(--background-primary); 698 | } 699 | 700 | .manager-note__container textarea{ 701 | width: 100%; 702 | height: 100%; 703 | resize: none; 704 | } 705 | --------------------------------------------------------------------------------