From e9b11020da5aeef07648528b9aefef3f9ed1ffa4 Mon Sep 17 00:00:00 2001 From: eondrcode <2106626532@qq.com> Date: Mon, 8 Jun 2026 23:46:07 +0800 Subject: [PATCH] chore: 1.0.6 --- manifest.json | 2 +- package.json | 4 +- src/data/types.ts | 1 + src/github-install.ts | 8 +- src/lang/locale/en.ts | 3 + src/lang/locale/zh_cn.ts | 3 + src/main.ts | 373 +++++++++----- src/modal/manager-modal.ts | 675 ++++++++++++++++++------- src/modal/ribbon-modal.ts | 14 + src/settings/data.ts | 11 + src/settings/ui/manager-basis.ts | 18 + src/settings/ui/manager-tag.ts | 6 +- src/troubleshoot/troubleshoot-panel.ts | 2 +- styles.css | 234 ++++++++- versions.json | 3 +- 15 files changed, 1011 insertions(+), 346 deletions(-) diff --git a/manifest.json b/manifest.json index 4f13866..315af18 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "better-plugins-manager", "name": "Better Plugins Manager", - "version": "1.0.5", + "version": "1.0.6", "minAppVersion": "1.5.8", "description": "Plugin Manager: Simplify, Enhance, Personalize | 插件管理器:简化操作、增强功能、个性化设置", "author": "zero", diff --git a/package.json b/package.json index 594ec01..60d1c8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-sample-plugin", - "version": "1.0.5", + "version": "1.0.6", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { @@ -21,4 +21,4 @@ "tslib": "2.4.0", "typescript": "4.7.4" } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/data/types.ts b/src/data/types.ts index 1f06ede..ee0f4db 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -24,6 +24,7 @@ export interface Tag { // 已知的特殊 Tag export const BPM_TAG_ID = "bpm-install"; // BPM 安装标识 export const BPM_IGNORE_TAG = "bpm-ignore"; // BPM 忽略标识 +export const EONDR_PLUGIN_TAG_ID = "bpm-eondr-plugin"; // Eondr 自有插件标识 export interface Delay { diff --git a/src/github-install.ts b/src/github-install.ts index 40ff1a3..478bad4 100644 --- a/src/github-install.ts +++ b/src/github-install.ts @@ -1,6 +1,7 @@ import { Notice, normalizePath, requestUrl } from "obsidian"; import type { PluginManifest, RequestUrlResponse } from "obsidian"; import Manager from "main"; +import type { ManagerPlugin } from "./data/types"; import { BPM_TAG_ID } from "./repo-resolver"; /** @@ -260,10 +261,11 @@ const upsertInstalledPluginRecord = ( plugin.desc = nextDesc || plugin.desc; plugin.enabled = true; if (shouldHaveBpmTag && !plugin.tags.includes(BPM_TAG_ID)) plugin.tags.push(BPM_TAG_ID); + manager.applySpecialPluginTags(plugin); return; } - manager.settings.Plugins.push({ + const record: ManagerPlugin = { id: pluginId, name: nextName, desc: nextDesc, @@ -272,7 +274,9 @@ const upsertInstalledPluginRecord = ( enabled: true, delay: "", note: "", - }); + }; + manager.applySpecialPluginTags(record); + manager.settings.Plugins.push(record); }; /** diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 4bffef9..3c2ffe5 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -216,6 +216,8 @@ export default { 设置_基础设置_淡化插件_描述: 'Provide a visual fade effect for disabled plugins to clearly distinguish between enabled and disabled plugins.', 设置_基础设置_隐藏BPM标签_标题: 'Hide Preset Tags (Install/Ignore)', 设置_基础设置_隐藏BPM标签_描述: 'Hides automatically added bpm-install and bpm-ignore tags from the list.', + 设置_基础设置_边栏编排_标题: 'Enable ribbon order', + 设置_基础设置_边栏编排_描述: 'When enabled, BPM controls Ribbon icon order and visibility. When disabled, BPM removes its overrides and uses Obsidian native order.', 设置_基础设置_启动检查更新_标题: 'Check updates on startup', 设置_基础设置_启动检查更新_描述: 'When BPM opens, automatically check for plugin updates and briefly show the count.', 设置_基础设置_来源启动检查更新_标题: 'Check source updates on startup', @@ -274,6 +276,7 @@ export default { 命令_管理面板_描述: 'Open the plugin manager', 标签_BPM安装_名称: 'bpm install', + 标签_Eondr插件_名称: 'Eondr plugin', 管理器_下载更新_描述: 'Download an update (choose version, incl. pre-release)', 安装_成功_提示: 'Installed/updated: {name}', 安装_错误_限速: 'GitHub rate-limited (403). Please set a GitHub token and retry.', diff --git a/src/lang/locale/zh_cn.ts b/src/lang/locale/zh_cn.ts index 63939a5..2caebee 100644 --- a/src/lang/locale/zh_cn.ts +++ b/src/lang/locale/zh_cn.ts @@ -64,6 +64,8 @@ export default { 设置_基础设置_隐藏BPM标签_标题: '隐藏 BPM 预设标签(安装/忽略)', 设置_基础设置_隐藏BPM标签_描述: '开启后列表不显示自动添加的 BPM 安装和 BPM 忽略标签。', + 设置_基础设置_边栏编排_标题: '启用边栏编排', + 设置_基础设置_边栏编排_描述: '开启后 BPM 接管 Ribbon 图标的排序和显隐;关闭后移除接管,使用 Obsidian 原生边栏顺序。', 设置_基础设置_启动检查更新_标题: '启动时检测插件更新', 设置_基础设置_启动检查更新_描述: '打开 BPM 时自动检测可更新插件,并在右上角提示数量。', 设置_基础设置_来源启动检查更新_标题: '启动时检测来源更新', @@ -233,6 +235,7 @@ export default { 设置_基础设置_分组命令_标题: '分组控制插件命令', 设置_基础设置_分组命令_描述: '启用此选项可以一键启用或禁用指定分组中的所有插件。(重启Obsidian生效)', 标签_BPM安装_名称: 'BPM 安装', + 标签_Eondr插件_名称: 'Eondr 出品', 设置_主页面功能_标题: '主页面功能', 设置_主页面功能_描述: '选择插件管理页中每个插件功能是直接展示在 item 上,还是收纳在右键菜单里。', diff --git a/src/main.ts b/src/main.ts index 9cc4848..2cc4739 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ import Commands from './command'; import Agreement from 'src/agreement'; import { RepoResolver, ensureBpmTagExists, BPM_TAG_ID } from './repo-resolver'; import { Notice, Platform, requestUrl } from 'obsidian'; -import { BetaSource, ManagerPlugin, BPM_IGNORE_TAG } from './data/types'; +import { BetaSource, ManagerPlugin, BPM_IGNORE_TAG, EONDR_PLUGIN_TAG_ID } from './data/types'; import { runMigrations } from './migrations'; import { fetchReleaseVersions, installPluginFromGithub, installThemeFromGithub, ReleaseVersion, sanitizeRepo } from './github-install'; import { performSelfCheck } from './self-check'; @@ -15,7 +15,7 @@ import { SystemRibbonManager } from './manager/system-ribbon-manager'; import { RibbonItem } from './data/types'; type UpdateSource = 'official' | 'github' | 'unknown'; -interface UpdateStatus { +interface UpdateStatus { source: UpdateSource; localVersion?: string; remoteVersion?: string | null; @@ -25,7 +25,12 @@ interface UpdateStatus { checkedAt?: number; repo?: string | null; versions?: ReleaseVersion[]; -} +} + +const EONDR_PLUGIN_RULES = [ + { id: "i18n", repo: "eondrcode/obsidian-i18n" }, +]; +const EONDR_REPO_OWNER = "eondrcode"; export default class Manager extends Plugin { public settings: ManagerSettings; @@ -38,7 +43,7 @@ export default class Manager extends Plugin { public agreement: Agreement; public repoResolver: RepoResolver; - public systemRibbonManager: SystemRibbonManager; + public systemRibbonManager?: SystemRibbonManager; public updateStatus: Record = {}; private updateProgressNotice: Notice | null = null; @@ -62,36 +67,41 @@ export default class Manager extends Plugin { this.settings.LANGUAGE_INITIALIZED = true; await this.saveSettings(); } - // 初始化语言系统 - this.translator = new Translator(this); - let builtinTagsChanged = false; - const tagCountBeforeBpmEnsure = this.settings.TAGS.length; - ensureBpmTagExists(this); - builtinTagsChanged = this.settings.TAGS.length !== tagCountBeforeBpmEnsure; - this.ensureBpmTagAndRecords(); - this.ensureSelfPluginRecord(); - - // 确保 BPM Ignore 标签存在 - if (!this.settings.TAGS.some(t => t.id === BPM_IGNORE_TAG)) { - this.settings.TAGS.push({ + // 初始化语言系统 + this.translator = new Translator(this); + let builtinTagsChanged = false; + const tagCountBeforeBpmEnsure = this.settings.TAGS.length; + ensureBpmTagExists(this); + builtinTagsChanged = this.settings.TAGS.length !== tagCountBeforeBpmEnsure; + + // 确保 BPM Ignore 标签存在 + if (!this.settings.TAGS.some(t => t.id === BPM_IGNORE_TAG)) { + this.settings.TAGS.push({ id: BPM_IGNORE_TAG, name: this.translator.t("标签_BPM忽略_名称") || "BPM Ignored", color: "#6c757d" // 灰色 - }); - builtinTagsChanged = true; - } - if (this.normalizeBuiltinTagNames() || builtinTagsChanged) await this.saveSettings(); + }); + builtinTagsChanged = true; + } + if (!this.settings.TAGS.some(t => t.id === EONDR_PLUGIN_TAG_ID)) { + this.settings.TAGS.push({ + id: EONDR_PLUGIN_TAG_ID, + name: this.translator.t("标签_Eondr插件_名称") || "Eondr Plugin", + color: "#B36BFF", + }); + builtinTagsChanged = true; + } + this.ensureBpmTagAndRecords(); + this.ensureSelfPluginRecord(); + if (this.normalizeBuiltinTagNames() || builtinTagsChanged) await this.saveSettings(); this.repoResolver = new RepoResolver(this); - // 初始化 Ribbon 管理器。功能编排不写入 Obsidian workspace 配置,只用 BPM 数据驱动运行时样式。 - this.systemRibbonManager = new SystemRibbonManager(this.app, this); - const savedRibbonItems = [...(this.settings.RIBBON_SETTINGS || [])] - .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); - const orderedRibbonIds = savedRibbonItems.map((item) => item.id); - const hiddenRibbonStatus: Record = {}; - savedRibbonItems.forEach((item) => hiddenRibbonStatus[item.id] = !item.visible); - await this.syncRibbonConfig(orderedRibbonIds, hiddenRibbonStatus); + if (this.isRibbonManagerEnabled()) { + await this.syncStoredRibbonConfig(); + } else { + this.clearRibbonStyleOverrides(); + } // 初始化侧边栏图标 this.addRibbonIcon('folder-cog', this.translator.t('通用_管理器_文本'), () => { this.managerModal = new ManagerModal(this.app, this); this.managerModal.open(); }); @@ -111,53 +121,50 @@ export default class Manager extends Plugin { await this.agreement.parsePluginGithub(params); }); - this.app.workspace.onLayoutReady(() => { - this.updateRibbonStyles(); - if (Platform.isMobile) { - this.setupMenuObserver(); - } else { - // 仅桌面端启用“拖出即隐藏”功能 - this.setupDragToHideObserver(); - } - // 延迟启动自检,确保 Obsidian 初始化完成,避免自动接管被覆盖 - setTimeout(() => { - this.cleanRibbonItems(); // 启动后清理一次 - if (this.settings.DELAY) performSelfCheck(this); - }, 2000); - }); - } - - public async onunload() { - if (this.dragObserverCleanup) { - this.dragObserverCleanup(); - this.dragObserverCleanup = null; - } - - if (this.settings.DELAY) this.disableDelaysForAllPlugins(); - if (this.menuObserver) { - this.menuObserver.disconnect(); - } - - // 临走前再清理一次 - this.cleanRibbonItems(); - - this.systemRibbonManager?.stopWatch(); - } - - private setupDragToHideObserver() { - const handlePointerDown = (e: PointerEvent) => { - const target = e.target as HTMLElement; - // 检查是否是 Ribbon Icon - if (target && target.closest && target.closest('.side-dock-ribbon-action')) { + this.app.workspace.onLayoutReady(() => { + this.startRibbonRuntimeFeatures(); + // 延迟启动自检,确保 Obsidian 初始化完成,避免自动接管被覆盖 + setTimeout(() => { + if (this.isRibbonManagerEnabled()) this.cleanRibbonItems(); // 启动后清理一次 + if (this.settings.DELAY) performSelfCheck(this); + }, 2000); + }); + } + + public async onunload() { + this.stopRibbonRuntimeFeatures(); + + if (this.settings.DELAY) this.disableDelaysForAllPlugins(); + + // 临走前再清理一次 + if (this.isRibbonManagerEnabled()) this.cleanRibbonItems(); + + this.systemRibbonManager?.stopWatch(); + this.clearRibbonStyleOverrides(); + } + + private setupDragToHideObserver() { + if (!this.isRibbonManagerEnabled() || this.dragObserverCleanup) return; + + const handlePointerDown = (e: PointerEvent) => { + if (!this.isRibbonManagerEnabled()) return; + const target = e.target as HTMLElement; + // 检查是否是 Ribbon Icon + if (target && target.closest && target.closest('.side-dock-ribbon-action')) { this.isRibbonDragging = true; this.draggedRibbonItem = target.closest('.side-dock-ribbon-action') as HTMLElement; } }; - - const handlePointerUp = async (e: PointerEvent) => { - if (!this.isRibbonDragging || !this.draggedRibbonItem) { - this.isRibbonDragging = false; - this.draggedRibbonItem = null; + + const handlePointerUp = async (e: PointerEvent) => { + if (!this.isRibbonManagerEnabled()) { + this.isRibbonDragging = false; + this.draggedRibbonItem = null; + return; + } + if (!this.isRibbonDragging || !this.draggedRibbonItem) { + this.isRibbonDragging = false; + this.draggedRibbonItem = null; return; } @@ -201,11 +208,13 @@ export default class Manager extends Plugin { document.removeEventListener('pointerdown', handlePointerDown, true); document.removeEventListener('pointerup', handlePointerUp, true); }; - } - - private async hideRibbonItemByLabel(label: string) { - // 查找对应的 Item ID - const items = this.settings.RIBBON_SETTINGS; + } + + private async hideRibbonItemByLabel(label: string) { + if (!this.isRibbonManagerEnabled()) return; + + // 查找对应的 Item ID + const items = this.settings.RIBBON_SETTINGS; const targetItem = items.find(i => i.name === label); // name 通常就是 label let targetId = targetItem?.id; @@ -250,10 +259,88 @@ export default class Manager extends Plugin { this.updateRibbonStyles(); new Notice(this.translator.t("Ribbon_已隐藏_通知", { name: label })); - } - } - - public async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } + } + } + + public isRibbonManagerEnabled(): boolean { + return this.settings?.RIBBON_MANAGER_ENABLED !== false; + } + + private ensureSystemRibbonManager() { + if (!this.systemRibbonManager) this.systemRibbonManager = new SystemRibbonManager(this.app, this); + } + + private clearRibbonStyleOverrides() { + document.getElementById("bpm-ribbon-manager-style")?.remove(); + } + + private stopRibbonRuntimeFeatures() { + if (this.dragObserverCleanup) { + this.dragObserverCleanup(); + this.dragObserverCleanup = null; + } + this.isRibbonDragging = false; + this.draggedRibbonItem = null; + + if (this.menuObserver) { + this.menuObserver.disconnect(); + this.menuObserver = null; + } + } + + private async syncStoredRibbonConfig() { + if (!this.isRibbonManagerEnabled()) return; + this.ensureSystemRibbonManager(); + const savedRibbonItems = [...(this.settings.RIBBON_SETTINGS || [])] + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + const orderedRibbonIds = savedRibbonItems.map((item) => item.id); + const hiddenRibbonStatus: Record = {}; + savedRibbonItems.forEach((item) => hiddenRibbonStatus[item.id] = !item.visible); + await this.syncRibbonConfig(orderedRibbonIds, hiddenRibbonStatus); + } + + private startRibbonRuntimeFeatures() { + if (!this.isRibbonManagerEnabled()) { + this.stopRibbonRuntimeFeatures(); + this.clearRibbonStyleOverrides(); + return; + } + + this.ensureSystemRibbonManager(); + this.updateRibbonStyles(); + if (Platform.isMobile) { + this.setupMenuObserver(); + } else { + // 仅桌面端启用“拖出即隐藏”功能 + this.setupDragToHideObserver(); + } + } + + public async refreshRibbonManagerFeature() { + if (this.isRibbonManagerEnabled()) { + await this.syncStoredRibbonConfig(); + this.startRibbonRuntimeFeatures(); + } else { + this.stopRibbonRuntimeFeatures(); + this.systemRibbonManager?.stopWatch(); + this.systemRibbonManager = undefined; + this.clearRibbonStyleOverrides(); + try { + this.ribbonModal?.close?.(); + } catch { + // ignore + } + this.ribbonModal = null; + } + + try { + await this.managerModal?.refreshRibbonFeatureAvailability?.(); + } catch { + // ignore + } + } + + public async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } public async saveSettings() { await this.saveData(this.settings); } // 保存单个插件配置。保留方法名以兼容旧调用点。 @@ -434,14 +521,32 @@ export default class Manager extends Plugin { await this.saveSettings(); } - public ensureBpmTagAndRecords() { - ensureBpmTagExists(this); - // 确保 BPM 安装的插件拥有标签 - this.settings.BPM_INSTALLED.forEach((id) => { - const mp = this.settings.Plugins.find(p => p.id === id); - if (mp && !mp.tags.includes(BPM_TAG_ID)) mp.tags.push(BPM_TAG_ID); - }); - } + public ensureBpmTagAndRecords() { + ensureBpmTagExists(this); + // 确保 BPM 安装的插件拥有标签 + this.settings.BPM_INSTALLED.forEach((id) => { + const mp = this.settings.Plugins.find(p => p.id === id); + if (mp && !mp.tags.includes(BPM_TAG_ID)) mp.tags.push(BPM_TAG_ID); + }); + this.settings.Plugins.forEach((plugin) => this.applySpecialPluginTags(plugin)); + } + + private isEondrPlugin(pluginId: string): boolean { + const normalizedId = pluginId.trim().toLowerCase(); + if (EONDR_PLUGIN_RULES.some((rule) => rule.id.toLowerCase() === normalizedId)) return true; + + const mappedRepo = sanitizeRepo(this.settings.REPO_MAP?.[pluginId] || "").toLowerCase(); + return Boolean(mappedRepo && ( + mappedRepo.startsWith(`${EONDR_REPO_OWNER}/`) + || EONDR_PLUGIN_RULES.some((rule) => rule.repo.toLowerCase() === mappedRepo) + )); + } + + public applySpecialPluginTags(plugin: ManagerPlugin) { + if (this.isEondrPlugin(plugin.id) && !plugin.tags.includes(EONDR_PLUGIN_TAG_ID)) { + plugin.tags.push(EONDR_PLUGIN_TAG_ID); + } + } private normalizeBuiltinTagNames(): boolean { let changed = false; @@ -464,12 +569,17 @@ export default class Manager extends Plugin { "BPM Install", "BPM Installed", ], "#409EFF"); - normalize(BPM_IGNORE_TAG, this.translator.t("标签_BPM忽略_名称") || "BPM 忽略", [ - "BPM Ignore", - "BPM Ignored", - ], "#6c757d"); - return changed; - } + normalize(BPM_IGNORE_TAG, this.translator.t("标签_BPM忽略_名称") || "BPM 忽略", [ + "BPM Ignore", + "BPM Ignored", + ], "#6c757d"); + normalize(EONDR_PLUGIN_TAG_ID, this.translator.t("标签_Eondr插件_名称") || "Eondr 出品", [ + "Eondr Plugin", + "Eondr Plugins", + "Eondr", + ], "#B36BFF"); + return changed; + } // 确保 BPM 自身也存在于插件记录中(用于面板显示) public ensureSelfPluginRecord() { @@ -632,10 +742,11 @@ export default class Manager extends Plugin { nextPlugins.push(mp); pluginSettingsById.set(p1Item.id, mp); } - if (bpmInstalledIds.has(p1Item.id) && !mp.tags.includes(BPM_TAG_ID)) { - mp.tags.push(BPM_TAG_ID); - } - }); + if (bpmInstalledIds.has(p1Item.id) && !mp.tags.includes(BPM_TAG_ID)) { + mp.tags.push(BPM_TAG_ID); + } + this.applySpecialPluginTags(mp); + }); this.settings.Plugins = nextPlugins; // BPM 自身保持启用且不允许延迟 this.ensureSelfPluginRecord(); @@ -980,11 +1091,15 @@ export default class Manager extends Plugin { return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; } - public updateRibbonStyles() { - if (!this.settings) return; - - let styleEl = document.getElementById("bpm-ribbon-manager-style"); - if (!styleEl) { + public updateRibbonStyles() { + if (!this.settings) return; + if (!this.isRibbonManagerEnabled()) { + this.clearRibbonStyleOverrides(); + return; + } + + let styleEl = document.getElementById("bpm-ribbon-manager-style"); + if (!styleEl) { styleEl = document.createElement("style"); styleEl.id = "bpm-ribbon-manager-style"; document.head.appendChild(styleEl); @@ -1035,12 +1150,15 @@ export default class Manager extends Plugin { } } - private menuObserver: MutationObserver | null = null; - - setupMenuObserver() { - this.menuObserver = new MutationObserver((mutations) => { - let shouldProcess = false; - let targetNode: HTMLElement | null = null; + private menuObserver: MutationObserver | null = null; + + setupMenuObserver() { + if (!this.isRibbonManagerEnabled() || this.menuObserver) return; + + this.menuObserver = new MutationObserver((mutations) => { + if (!this.isRibbonManagerEnabled()) return; + let shouldProcess = false; + let targetNode: HTMLElement | null = null; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { @@ -1072,11 +1190,13 @@ export default class Manager extends Plugin { childList: true, subtree: true, }); - } - - processMenuItems(menuScrollElement: HTMLElement) { - // [修复] 移动端 Obsidian 菜单项包裹在 .menu-group 中 - // 必须在 .menu-group 内排序,否则会破坏样式布局 + } + + processMenuItems(menuScrollElement: HTMLElement) { + if (!this.isRibbonManagerEnabled()) return; + + // [修复] 移动端 Obsidian 菜单项包裹在 .menu-group 中 + // 必须在 .menu-group 内排序,否则会破坏样式布局 let containerElement: HTMLElement = menuScrollElement; const menuGroup = menuScrollElement.querySelector(".menu-group"); if (menuGroup) { @@ -1157,14 +1277,23 @@ export default class Manager extends Plugin { - // 功能编排只应用运行时样式,不写入 Obsidian workspace 配置或 Ribbon 内存状态。 - applyRibbonConfigToMemory(orderedIds: string[], hiddenStatus: Record) { - this.updateRibbonStyles(); - } - - public async syncRibbonConfig(orderedIds: string[], hiddenStatus: Record) { - // 更新本地设置以匹配原生配置 - const currentItems = this.settings.RIBBON_SETTINGS || []; + // 功能编排只应用运行时样式,不写入 Obsidian workspace 配置或 Ribbon 内存状态。 + applyRibbonConfigToMemory(orderedIds: string[], hiddenStatus: Record) { + if (!this.isRibbonManagerEnabled()) { + this.clearRibbonStyleOverrides(); + return; + } + this.updateRibbonStyles(); + } + + public async syncRibbonConfig(orderedIds: string[], hiddenStatus: Record) { + if (!this.isRibbonManagerEnabled()) { + this.clearRibbonStyleOverrides(); + return; + } + + // 更新本地设置以匹配原生配置 + const currentItems = this.settings.RIBBON_SETTINGS || []; const itemMap = new Map(currentItems.map(i => [i.id, i])); const newItems: RibbonItem[] = []; diff --git a/src/modal/manager-modal.ts b/src/modal/manager-modal.ts index 488fbf1..39a7500 100644 --- a/src/modal/manager-modal.ts +++ b/src/modal/manager-modal.ts @@ -16,7 +16,7 @@ import { Platform, } from "obsidian"; -import { BetaSource, BPM_IGNORE_TAG, InstallHistoryItem, ManagerPlugin, PluginLayoutItem } from "../data/types"; +import { BetaSource, BPM_IGNORE_TAG, EONDR_PLUGIN_TAG_ID, InstallHistoryItem, ManagerPlugin, PluginLayoutItem } from "../data/types"; import { DEFAULT_MAIN_PAGE_ACTION_PLACEMENT, FilterOperator, MainPageActionId, ManagerSettings } from "../settings/data"; import { managerOpen } from "../utils"; @@ -95,6 +95,15 @@ type PluginSearchIndexEntry = { type StatusFilterValue = "all" | "enabled" | "disabled" | "grouped" | "ungrouped" | "tagged" | "untagged" | "noted" | "has-update" | "hidden"; +type MultiSelectFilterControl = { + rootEl: HTMLElement; + buttonEl: HTMLButtonElement; + menuEl: HTMLElement; + setValues: (values: string[]) => void; + refreshOptions: (options: Array<[string, string]>, values?: string[]) => void; + close: () => void; +}; + // ============================== @@ -116,15 +125,19 @@ export class ManagerModal extends Modal { // 过滤器 filter = ""; + statusFilters: string[] = []; statusOperator: FilterOperator = "contains"; // 分组内容 group = ""; + groups: string[] = []; groupOperator: FilterOperator = "contains"; // 标签内容 tag = ""; + tags: string[] = []; tagOperator: FilterOperator = "contains"; // 标签内容 delay = ""; + delays: string[] = []; delayOperator: FilterOperator = "contains"; // 搜索内容 searchText = ""; @@ -138,11 +151,11 @@ export class ManagerModal extends Modal { installVersions: ReleaseVersion[] = []; installTrackSource = true; searchBarEl?: HTMLElement; - statusDropdown?: DropdownComponent; + statusMultiSelect?: MultiSelectFilterControl; statusOperatorDropdown?: DropdownComponent; - groupDropdown?: DropdownComponent; - tagDropdown?: DropdownComponent; - delayDropdown?: DropdownComponent; + groupMultiSelect?: MultiSelectFilterControl; + tagMultiSelect?: MultiSelectFilterControl; + delayMultiSelect?: MultiSelectFilterControl; private bulkEditMode = false; private bulkSelectedPluginIds = new Set(); actionCollapsed = false; @@ -207,9 +220,17 @@ export class ManagerModal extends Modal { return renderGeneration === this.renderGeneration && this.activePage === page; } + private isRibbonManagerEnabled(): boolean { + return this.settings.RIBBON_MANAGER_ENABLED !== false; + } + + private getAvailableDesktopPages(): ManagerPage[] { + return this.desktopPages.filter((page) => page !== "ribbon" || this.isRibbonManagerEnabled()); + } + private normalizeManagerPage(page: ManagerPage): ManagerPage { if (!SHARED_VAULTS_ENABLED && page === "vaults") return "plugins"; - return this.desktopPages.includes(page) ? page : "plugins"; + return this.getAvailableDesktopPages().includes(page) ? page : "plugins"; } private ensureAllowedActivePage() { @@ -306,32 +327,92 @@ export class ManagerModal extends Modal { return value === "not-contains" ? "not-contains" : "contains"; } + private normalizeFilterValues(values: unknown, allValue: string): string[] { + const normalized: string[] = []; + const source = Array.isArray(values) ? values : []; + for (const value of source) { + const next = `${value || ""}`.trim(); + if (!next || next === allValue || normalized.includes(next)) continue; + normalized.push(next); + } + return normalized; + } + + private filterValuesByAvailable(values: unknown, availableValues: string[], allValue: string): string[] { + const available = new Set(availableValues.filter((value) => value && value !== allValue)); + return this.normalizeFilterValues(values, allValue).filter((value) => available.has(value)); + } + + private valuesFromSingleFilter(value: string | undefined, allValue: string): string[] { + return this.normalizeFilterValues(value ? [value] : [], allValue); + } + + private getStatusFilterValues(): string[] { + const availableValues = Object.keys(this.getStatusFilterOptions()); + const values = this.settings.PERSISTENCE + ? this.filterValuesByAvailable(this.settings.FILTER_STATUS_VALUES, availableValues, "all") + : this.filterValuesByAvailable(this.statusFilters, availableValues, "all"); + if (values.length > 0) return values; + const fallback = this.settings.PERSISTENCE ? this.settings.FILTER_STATUS : this.filter; + return this.filterValuesByAvailable([fallback || "all"], availableValues, "all"); + } + private getStatusFilterValue(): string { - return this.settings.PERSISTENCE ? (this.settings.FILTER_STATUS || "all") : (this.filter || "all"); + return this.getStatusFilterValues()[0] || "all"; } private getStatusFilterOperator(): FilterOperator { return this.normalizeFilterOperator(this.settings.PERSISTENCE ? this.settings.FILTER_STATUS_OPERATOR : this.statusOperator); } + private getGroupFilterValues(): string[] { + const availableValues = this.settings.GROUPS.map((group) => group.id); + const values = this.settings.PERSISTENCE + ? this.filterValuesByAvailable(this.settings.FILTER_GROUP_VALUES, availableValues, "") + : this.filterValuesByAvailable(this.groups, availableValues, ""); + if (values.length > 0) return values; + const fallback = this.settings.PERSISTENCE ? this.settings.FILTER_GROUP : this.group; + return this.filterValuesByAvailable([fallback], availableValues, ""); + } + private getGroupFilterValue(): string { - return this.settings.PERSISTENCE ? this.settings.FILTER_GROUP : this.group; + return this.getGroupFilterValues()[0] || ""; } private getGroupFilterOperator(): FilterOperator { return this.normalizeFilterOperator(this.settings.PERSISTENCE ? this.settings.FILTER_GROUP_OPERATOR : this.groupOperator); } + private getTagFilterValues(): string[] { + const availableValues = this.settings.TAGS.map((tag) => tag.id); + const values = this.settings.PERSISTENCE + ? this.filterValuesByAvailable(this.settings.FILTER_TAG_VALUES, availableValues, "") + : this.filterValuesByAvailable(this.tags, availableValues, ""); + if (values.length > 0) return values; + const fallback = this.settings.PERSISTENCE ? this.settings.FILTER_TAG : this.tag; + return this.filterValuesByAvailable([fallback], availableValues, ""); + } + private getTagFilterValue(): string { - return this.settings.PERSISTENCE ? this.settings.FILTER_TAG : this.tag; + return this.getTagFilterValues()[0] || ""; } private getTagFilterOperator(): FilterOperator { return this.normalizeFilterOperator(this.settings.PERSISTENCE ? this.settings.FILTER_TAG_OPERATOR : this.tagOperator); } + private getDelayFilterValues(): string[] { + const availableValues = this.settings.DELAYS.map((delay) => delay.id); + const values = this.settings.PERSISTENCE + ? this.filterValuesByAvailable(this.settings.FILTER_DELAY_VALUES, availableValues, "") + : this.filterValuesByAvailable(this.delays, availableValues, ""); + if (values.length > 0) return values; + const fallback = this.settings.PERSISTENCE ? this.settings.FILTER_DELAY : this.delay; + return this.filterValuesByAvailable([fallback], availableValues, ""); + } + private getDelayFilterValue(): string { - return this.settings.PERSISTENCE ? this.settings.FILTER_DELAY : this.delay; + return this.getDelayFilterValues()[0] || ""; } private getDelayFilterOperator(): FilterOperator { @@ -339,18 +420,89 @@ export class ManagerModal extends Modal { } private hasActiveStatusFilter(): boolean { - const filter = this.getStatusFilterValue(); - return Boolean(filter && filter !== "all"); + return this.getStatusFilterValues().length > 0; + } + + public persistCurrentFilters() { + const statusValues = this.getStatusFilterValues(); + const groupValues = this.getGroupFilterValues(); + const tagValues = this.getTagFilterValues(); + const delayValues = this.getDelayFilterValues(); + this.settings.FILTER_SEARCH = this.searchText || ""; + this.settings.FILTER_STATUS_VALUES = statusValues; + this.settings.FILTER_STATUS = statusValues[0] || "all"; + this.settings.FILTER_STATUS_OPERATOR = this.getStatusFilterOperator(); + this.settings.FILTER_GROUP_VALUES = groupValues; + this.settings.FILTER_GROUP = groupValues[0] || ""; + this.settings.FILTER_GROUP_OPERATOR = this.getGroupFilterOperator(); + this.settings.FILTER_TAG_VALUES = tagValues; + this.settings.FILTER_TAG = tagValues[0] || ""; + this.settings.FILTER_TAG_OPERATOR = this.getTagFilterOperator(); + this.settings.FILTER_DELAY_VALUES = delayValues; + this.settings.FILTER_DELAY = delayValues[0] || ""; + this.settings.FILTER_DELAY_OPERATOR = this.getDelayFilterOperator(); + } + + public usePersistedFiltersAsSessionFilters() { + const statusValues = this.normalizeFilterValues(this.settings.FILTER_STATUS_VALUES, "all"); + const groupValues = this.normalizeFilterValues(this.settings.FILTER_GROUP_VALUES, ""); + const tagValues = this.normalizeFilterValues(this.settings.FILTER_TAG_VALUES, ""); + const delayValues = this.normalizeFilterValues(this.settings.FILTER_DELAY_VALUES, ""); + this.statusFilters = statusValues.length > 0 ? statusValues : this.valuesFromSingleFilter(this.settings.FILTER_STATUS, "all"); + this.filter = this.statusFilters[0] || "all"; + this.groups = groupValues.length > 0 ? groupValues : this.valuesFromSingleFilter(this.settings.FILTER_GROUP, ""); + this.group = this.groups[0] || ""; + this.tags = tagValues.length > 0 ? tagValues : this.valuesFromSingleFilter(this.settings.FILTER_TAG, ""); + this.tag = this.tags[0] || ""; + this.delays = delayValues.length > 0 ? delayValues : this.valuesFromSingleFilter(this.settings.FILTER_DELAY, ""); + this.delay = this.delays[0] || ""; + this.statusOperator = this.normalizeFilterOperator(this.settings.FILTER_STATUS_OPERATOR); + this.groupOperator = this.normalizeFilterOperator(this.settings.FILTER_GROUP_OPERATOR); + this.tagOperator = this.normalizeFilterOperator(this.settings.FILTER_TAG_OPERATOR); + this.delayOperator = this.normalizeFilterOperator(this.settings.FILTER_DELAY_OPERATOR); + this.searchText = this.settings.FILTER_SEARCH || this.searchText || ""; + } + + private migratePersistedFilterValues() { + if (!this.settings.PERSISTENCE) return; + const statusValues = this.getStatusFilterValues(); + const groupValues = this.getGroupFilterValues(); + const tagValues = this.getTagFilterValues(); + const delayValues = this.getDelayFilterValues(); + const changed = JSON.stringify(this.settings.FILTER_STATUS_VALUES || []) !== JSON.stringify(statusValues) + || JSON.stringify(this.settings.FILTER_GROUP_VALUES || []) !== JSON.stringify(groupValues) + || JSON.stringify(this.settings.FILTER_TAG_VALUES || []) !== JSON.stringify(tagValues) + || JSON.stringify(this.settings.FILTER_DELAY_VALUES || []) !== JSON.stringify(delayValues) + || this.settings.FILTER_STATUS !== (statusValues[0] || "all") + || this.settings.FILTER_GROUP !== (groupValues[0] || "") + || this.settings.FILTER_TAG !== (tagValues[0] || "") + || this.settings.FILTER_DELAY !== (delayValues[0] || ""); + if (!changed) return; + this.settings.FILTER_STATUS_VALUES = statusValues; + this.settings.FILTER_STATUS = statusValues[0] || "all"; + this.settings.FILTER_GROUP_VALUES = groupValues; + this.settings.FILTER_GROUP = groupValues[0] || ""; + this.settings.FILTER_TAG_VALUES = tagValues; + this.settings.FILTER_TAG = tagValues[0] || ""; + this.settings.FILTER_DELAY_VALUES = delayValues; + this.settings.FILTER_DELAY = delayValues[0] || ""; + void this.manager.saveSettings(); + } + + private setStatusFilterValues(values: string[]) { + const next = this.normalizeFilterValues(values, "all"); + if (this.settings.PERSISTENCE) { + this.settings.FILTER_STATUS_VALUES = next; + this.settings.FILTER_STATUS = next[0] || "all"; + this.manager.saveSettings(); + } else { + this.statusFilters = next; + this.filter = next[0] || "all"; + } } private setStatusFilterValue(value: string) { - const next = value || "all"; - if (this.settings.PERSISTENCE) { - this.settings.FILTER_STATUS = next; - this.manager.saveSettings(); - } else { - this.filter = next; - } + this.setStatusFilterValues(value && value !== "all" ? [value] : []); } private setStatusFilterOperator(value: string) { @@ -368,21 +520,28 @@ export class ManagerModal extends Modal { this.installMode = false; this.setStatusFilterOperator("contains"); this.setStatusFilterValue(value); - this.statusDropdown?.setValue(value); + this.statusMultiSelect?.setValues(value === "all" ? [] : [value]); this.statusOperatorDropdown?.setValue("contains"); this.syncPageChrome(); void this.reloadShowData(); } - private setGroupFilterValue(value: string) { + private setGroupFilterValues(values: string[]) { + const next = this.normalizeFilterValues(values, ""); if (this.settings.PERSISTENCE) { - this.settings.FILTER_GROUP = value; + this.settings.FILTER_GROUP_VALUES = next; + this.settings.FILTER_GROUP = next[0] || ""; this.manager.saveSettings(); } else { - this.group = value; + this.groups = next; + this.group = next[0] || ""; } } + private setGroupFilterValue(value: string) { + this.setGroupFilterValues(value ? [value] : []); + } + private setGroupFilterOperator(value: string) { const next = this.normalizeFilterOperator(value); if (this.settings.PERSISTENCE) { @@ -393,15 +552,22 @@ export class ManagerModal extends Modal { } } - private setTagFilterValue(value: string) { + private setTagFilterValues(values: string[]) { + const next = this.normalizeFilterValues(values, ""); if (this.settings.PERSISTENCE) { - this.settings.FILTER_TAG = value; + this.settings.FILTER_TAG_VALUES = next; + this.settings.FILTER_TAG = next[0] || ""; this.manager.saveSettings(); } else { - this.tag = value; + this.tags = next; + this.tag = next[0] || ""; } } + private setTagFilterValue(value: string) { + this.setTagFilterValues(value ? [value] : []); + } + private setTagFilterOperator(value: string) { const next = this.normalizeFilterOperator(value); if (this.settings.PERSISTENCE) { @@ -412,15 +578,22 @@ export class ManagerModal extends Modal { } } - private setDelayFilterValue(value: string) { + private setDelayFilterValues(values: string[]) { + const next = this.normalizeFilterValues(values, ""); if (this.settings.PERSISTENCE) { - this.settings.FILTER_DELAY = value; + this.settings.FILTER_DELAY_VALUES = next; + this.settings.FILTER_DELAY = next[0] || ""; this.manager.saveSettings(); } else { - this.delay = value; + this.delays = next; + this.delay = next[0] || ""; } } + private setDelayFilterValue(value: string) { + this.setDelayFilterValues(value ? [value] : []); + } + private setDelayFilterOperator(value: string) { const next = this.normalizeFilterOperator(value); if (this.settings.PERSISTENCE) { @@ -435,28 +608,31 @@ export class ManagerModal extends Modal { return operator === "contains" ? matched : !matched; } - private matchesSingleValueFilter(value: string, filterValue: string, operator: FilterOperator): boolean { - if (!filterValue) return true; - return this.matchesOperator(value === filterValue, operator); + private matchesSingleValueFilter(value: string, filterValue: string | string[], operator: FilterOperator): boolean { + const values = this.normalizeFilterValues(Array.isArray(filterValue) ? filterValue : [filterValue], ""); + if (values.length === 0) return true; + return this.matchesOperator(values.includes(value), operator); } - private matchesTagFilter(pluginTags: string[] = [], tagId: string, operator: FilterOperator): boolean { - if (!tagId) return true; - return this.matchesOperator(pluginTags.includes(tagId), operator); + private matchesTagFilter(pluginTags: string[] = [], tagId: string | string[], operator: FilterOperator): boolean { + const values = this.normalizeFilterValues(Array.isArray(tagId) ? tagId : [tagId], ""); + if (values.length === 0) return true; + return this.matchesOperator(values.some((value) => pluginTags.includes(value)), operator); } private matchesStatusFilter( plugin: ManagerPlugin, manifest: PluginManifest, isEnabled: boolean, - filter = this.getStatusFilterValue(), + filter: string | string[] = this.getStatusFilterValues(), operator = this.getStatusFilterOperator(), hiddenPluginIds?: Set ): boolean { - if (!filter || filter === "all") return true; + const values = this.normalizeFilterValues(Array.isArray(filter) ? filter : [filter], "all"); + if (values.length === 0) return true; - const matched = (() => { - switch (filter) { + const matchesStatus = (status: string) => { + switch (status) { case "enabled": return isEnabled; case "disabled": @@ -478,11 +654,156 @@ export class ManagerModal extends Modal { default: return true; } - })(); + }; + const matched = values.some(matchesStatus); return this.matchesOperator(matched, operator); } + private formatMultiSelectSummary(values: string[], options: Array<[string, string]>, allLabel: string): string { + if (values.length === 0) return allLabel; + const labelsByValue = new Map(options.map(([value, label]) => [value, label])); + if (values.length === 1) return labelsByValue.get(values[0]) || values[0]; + const firstLabel = labelsByValue.get(values[0]) || values[0]; + return `${firstLabel} +${values.length - 1}`; + } + + private createMultiSelectFilter( + container: HTMLElement, + options: Array<[string, string]>, + values: string[], + allValue: string, + allLabel: string, + ariaLabel: string, + onChange: (values: string[]) => void + ): MultiSelectFilterControl { + const rootEl = container.createDiv("manager-multiselect-filter"); + const buttonEl = rootEl.createEl("button", { cls: "manager-multiselect-filter__trigger" }); + buttonEl.type = "button"; + buttonEl.setAttribute("aria-label", ariaLabel); + buttonEl.setAttribute("aria-haspopup", "listbox"); + buttonEl.setAttribute("aria-expanded", "false"); + const summaryEl = buttonEl.createSpan({ cls: "manager-multiselect-filter__summary" }); + const countEl = buttonEl.createSpan({ cls: "manager-multiselect-filter__count" }); + const chevronEl = buttonEl.createSpan({ cls: "manager-multiselect-filter__chevron" }); + setIcon(chevronEl, "chevron-down"); + const menuEl = rootEl.createDiv("manager-multiselect-filter__menu"); + menuEl.setAttribute("role", "listbox"); + menuEl.setAttribute("aria-multiselectable", "true"); + menuEl.setAttribute("aria-label", ariaLabel); + + let currentOptions = options; + let selectedValues = this.normalizeFilterValues(values, allValue); + let isOpen = false; + + const updateButton = () => { + summaryEl.setText(this.formatMultiSelectSummary(selectedValues, currentOptions, allLabel)); + countEl.setText(selectedValues.length > 0 ? `${selectedValues.length}` : ""); + countEl.toggleClass("is-empty", selectedValues.length === 0); + buttonEl.toggleClass("has-selection", selectedValues.length > 0); + }; + + function handleDocumentClick() { + close(); + } + + function close() { + isOpen = false; + rootEl.removeClass("is-open"); + buttonEl.setAttribute("aria-expanded", "false"); + document.removeEventListener("click", handleDocumentClick); + } + + const toggleOpen = () => { + if (isOpen) { + close(); + return; + } + isOpen = true; + rootEl.addClass("is-open"); + buttonEl.setAttribute("aria-expanded", "true"); + window.setTimeout(() => document.addEventListener("click", handleDocumentClick), 0); + }; + + const emitChange = () => { + updateButton(); + onChange([...selectedValues]); + }; + + const renderOptions = () => { + menuEl.empty(); + const allOption = menuEl.createEl("button", { cls: "manager-multiselect-filter__option" }); + allOption.type = "button"; + allOption.setAttribute("role", "option"); + allOption.setAttribute("aria-selected", `${selectedValues.length === 0}`); + allOption.toggleClass("is-selected", selectedValues.length === 0); + const allCheck = allOption.createSpan({ cls: "manager-multiselect-filter__check" }); + setIcon(allCheck, selectedValues.length === 0 ? "check" : "circle"); + allOption.createSpan({ cls: "manager-multiselect-filter__option-label", text: allLabel }); + allOption.addEventListener("click", () => { + selectedValues = []; + renderOptions(); + emitChange(); + }); + + for (const [value, label] of currentOptions) { + if (value === allValue) continue; + const selected = selectedValues.includes(value); + const optionEl = menuEl.createEl("button", { cls: "manager-multiselect-filter__option" }); + optionEl.type = "button"; + optionEl.setAttribute("role", "option"); + optionEl.setAttribute("aria-selected", `${selected}`); + optionEl.toggleClass("is-selected", selected); + const check = optionEl.createSpan({ cls: "manager-multiselect-filter__check" }); + setIcon(check, selected ? "square-check-big" : "square"); + optionEl.createSpan({ cls: "manager-multiselect-filter__option-label", text: label }); + optionEl.addEventListener("click", () => { + selectedValues = selected + ? selectedValues.filter((item) => item !== value) + : [...selectedValues, value]; + renderOptions(); + emitChange(); + }); + } + }; + + buttonEl.addEventListener("click", (event) => { + event.stopPropagation(); + toggleOpen(); + }); + rootEl.addEventListener("click", (event) => event.stopPropagation()); + rootEl.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + event.preventDefault(); + close(); + } + }); + + const control: MultiSelectFilterControl = { + rootEl, + buttonEl, + menuEl, + setValues: (nextValues: string[]) => { + selectedValues = this.normalizeFilterValues(nextValues, allValue); + renderOptions(); + updateButton(); + }, + refreshOptions: (nextOptions: Array<[string, string]>, nextValues?: string[]) => { + currentOptions = nextOptions; + const availableValues = new Set(nextOptions.map(([value]) => value).filter((value) => value !== allValue)); + selectedValues = this.normalizeFilterValues(nextValues || selectedValues, allValue) + .filter((value) => availableValues.has(value)); + renderOptions(); + updateButton(); + }, + close, + }; + + renderOptions(); + updateButton(); + return control; + } + private openSupportQQGroup() { window.open(SUPPORT_QQ_GROUP_URL); } @@ -639,9 +960,15 @@ export class ManagerModal extends Modal { this.addPluginDownloadButton(controlEl, pluginId, updateInfo, true); } - if (this.getStatusFilterValue() === "has-update" && this.getStatusFilterOperator() === "contains" && !hasUpdate) { - card.remove(); - this.displayPlugins = this.displayPlugins.filter((plugin) => plugin.id !== pluginId); + const manifest = this.getUniquePluginManifests().find((plugin) => plugin.id === pluginId); + const managerPlugin = this.manager.settings.Plugins.find((plugin) => plugin.id === pluginId); + if (manifest && managerPlugin) { + const isEnabled = this.settings.DELAY ? managerPlugin.enabled : this.appPlugins.enabledPlugins.has(pluginId); + const hiddenPluginIds = new Set(this.settings.HIDES || []); + if (!this.matchesStatusFilter(managerPlugin, manifest, isEnabled, this.getStatusFilterValues(), this.getStatusFilterOperator(), hiddenPluginIds)) { + card.remove(); + this.displayPlugins = this.displayPlugins.filter((plugin) => plugin.id !== pluginId); + } } } @@ -1019,8 +1346,10 @@ export class ManagerModal extends Modal { const plugins = this.getSelectedManagerPlugins(); if (plugins.length === 0) return; if (!window.confirm(t("批量编辑_清除全部标签确认", { count: plugins.length }))) return; + const protectedTagIds = new Set([BPM_TAG_ID, BPM_IGNORE_TAG, EONDR_PLUGIN_TAG_ID]); plugins.forEach((plugin) => { - plugin.tags = plugin.tags.filter((id) => id === BPM_TAG_ID); + plugin.tags = plugin.tags.filter((id) => protectedTagIds.has(id)); + this.manager.applySpecialPluginTags(plugin); }); await this.finishBulkMetadataEdit("批量编辑_已移除标签", plugins.length); } @@ -1211,6 +1540,7 @@ export class ManagerModal extends Modal { } public async showHead() { + this.migratePersistedFilterValues(); const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); //@ts-ignore const modalEl: HTMLElement = this.contentEl.parentElement; @@ -1287,7 +1617,9 @@ export class ManagerModal extends Modal { this.vaultsTabEl = SHARED_VAULTS_ENABLED ? createTab("vaults", t("共享库_Tab_标题"), "folder-sync", t("共享库_Tab_说明")) : undefined; - this.ribbonTabEl = createTab("ribbon", t("管理器_Tab_功能编排"), "grip-vertical", t("Ribbon_功能编排_说明")); + this.ribbonTabEl = this.isRibbonManagerEnabled() + ? createTab("ribbon", t("管理器_Tab_功能编排"), "grip-vertical", t("Ribbon_功能编排_说明")) + : undefined; this.troubleshootTabEl = createTab("troubleshoot", t("排查_Tab_短标题"), "search-check"); const tools = toolbar.createDiv("manager-toolbar__tools"); @@ -1379,19 +1711,22 @@ export class ManagerModal extends Modal { } }); - const ribbonResetButton = new ButtonComponent(actionBar.controlEl); - markTool(ribbonResetButton, "ribbon", 10); - ribbonResetButton.setIcon("rotate-ccw"); - ribbonResetButton.setTooltip(t("Ribbon_重置_提示")); - ribbonResetButton.buttonEl.setAttribute("aria-label", t("Ribbon_重置_提示")); - this.bindLongPressTooltip(ribbonResetButton.buttonEl, t("Ribbon_重置_提示")); - ribbonResetButton.onClick(async () => { - if (!window.confirm(t("Ribbon_重置_确认"))) return; - if (!this.ribbonPage) this.ribbonPage = new RibbonModal(this.app, this.manager); - this.manager.ribbonModal = this.ribbonPage; - await this.ribbonPage.syncRibbonItems(); - await this.ribbonPage.resetRibbonLayout(); - }); + if (this.isRibbonManagerEnabled()) { + const ribbonResetButton = new ButtonComponent(actionBar.controlEl); + markTool(ribbonResetButton, "ribbon", 10); + ribbonResetButton.setIcon("rotate-ccw"); + ribbonResetButton.setTooltip(t("Ribbon_重置_提示")); + ribbonResetButton.buttonEl.setAttribute("aria-label", t("Ribbon_重置_提示")); + this.bindLongPressTooltip(ribbonResetButton.buttonEl, t("Ribbon_重置_提示")); + ribbonResetButton.onClick(async () => { + if (!window.confirm(t("Ribbon_重置_确认"))) return; + if (!this.isRibbonManagerEnabled()) return; + if (!this.ribbonPage) this.ribbonPage = new RibbonModal(this.app, this.manager); + this.manager.ribbonModal = this.ribbonPage; + await this.ribbonPage.syncRibbonItems(); + await this.ribbonPage.resetRibbonLayout(); + }); + } const addSeparatorButton = new ButtonComponent(actionBar.controlEl); markTool(addSeparatorButton, "layout", 60); @@ -1539,27 +1874,19 @@ export class ManagerModal extends Modal { // 过滤器 const statusControl = createFilterSelectField(t("通用_状态_文本"), "list-filter", "compound"); this.statusOperatorDropdown = createOperatorDropdown(statusControl, this.getStatusFilterOperator(), t("筛选_状态取反_标签"), (value) => this.setStatusFilterOperator(value)); - const filterDropdown = new DropdownComponent(statusControl); - filterDropdown.addOptions(this.getStatusFilterOptions()); - filterDropdown.setValue(this.getStatusFilterValue()); - filterDropdown.selectEl.setAttribute("aria-label", t("筛选_状态_标签")); - filterDropdown.onChange((value) => { - this.setStatusFilterValue(value); + const statusOptions = Object.entries(this.getStatusFilterOptions()); + this.statusMultiSelect = this.createMultiSelectFilter(statusControl, statusOptions, this.getStatusFilterValues(), "all", this.getStatusFilterOptions()["all"], t("筛选_状态_标签"), (values) => { + this.setStatusFilterValues(values); this.reloadShowData(); }); - this.statusDropdown = filterDropdown; // [过滤行] 分组选择列表 const groups = this.getGroupFilterOptions(this.manager.translator.t("筛选_全部_描述")); const groupControl = createFilterSelectField(t("通用_分组_文本"), "folder-tree", "compound"); createOperatorDropdown(groupControl, this.getGroupFilterOperator(), t("筛选_分组取反_标签"), (value) => this.setGroupFilterOperator(value)); - this.groupDropdown = new DropdownComponent(groupControl); - this.addOrderedOptions(this.groupDropdown, groups); - this.groupDropdown.setValue(this.getGroupFilterValue()); - this.groupDropdown.selectEl.setAttribute("aria-label", t("筛选_分组_标签")); - this.groupDropdown.onChange((value) => { - this.setGroupFilterValue(value); + this.groupMultiSelect = this.createMultiSelectFilter(groupControl, groups, this.getGroupFilterValues(), "", groups[0]?.[1] || t("筛选_全部_描述"), t("筛选_分组_标签"), (values) => { + this.setGroupFilterValues(values); this.reloadShowData(); }); @@ -1567,12 +1894,8 @@ export class ManagerModal extends Modal { const tags = this.getTagFilterOptions(this.manager.translator.t("筛选_全部_描述")); const tagControl = createFilterSelectField(t("通用_标签_文本"), "tags", "compound"); createOperatorDropdown(tagControl, this.getTagFilterOperator(), t("筛选_标签取反_标签"), (value) => this.setTagFilterOperator(value)); - this.tagDropdown = new DropdownComponent(tagControl); - this.addOrderedOptions(this.tagDropdown, tags); - this.tagDropdown.setValue(this.getTagFilterValue()); - this.tagDropdown.selectEl.setAttribute("aria-label", t("筛选_标签_标签")); - this.tagDropdown.onChange((value) => { - this.setTagFilterValue(value); + this.tagMultiSelect = this.createMultiSelectFilter(tagControl, tags, this.getTagFilterValues(), "", tags[0]?.[1] || t("筛选_全部_描述"), t("筛选_标签_标签"), (values) => { + this.setTagFilterValues(values); this.reloadShowData(); }); @@ -1581,12 +1904,8 @@ export class ManagerModal extends Modal { const delays = this.getDelayFilterOptions(this.manager.translator.t("筛选_全部_描述"), true); const delayControl = createFilterSelectField(t("通用_延迟_文本"), "timer", "compound"); createOperatorDropdown(delayControl, this.getDelayFilterOperator(), t("筛选_延迟取反_标签"), (value) => this.setDelayFilterOperator(value)); - this.delayDropdown = new DropdownComponent(delayControl); - this.addOrderedOptions(this.delayDropdown, delays); - this.delayDropdown.setValue(this.getDelayFilterValue()); - this.delayDropdown.selectEl.setAttribute("aria-label", t("筛选_延迟_标签")); - this.delayDropdown.onChange((value) => { - this.setDelayFilterValue(value); + this.delayMultiSelect = this.createMultiSelectFilter(delayControl, delays, this.getDelayFilterValues(), "", delays[0]?.[1] || t("筛选_全部_描述"), t("筛选_延迟_标签"), (values) => { + this.setDelayFilterValues(values); this.reloadShowData(); }); } @@ -1594,6 +1913,7 @@ export class ManagerModal extends Modal { private showHeadMobile() { const t = (k: any) => this.manager.translator.t(k); + this.migratePersistedFilterValues(); this.titleEl.empty(); const header = this.titleEl.createDiv("bpm-mobile-header"); @@ -1726,11 +2046,14 @@ export class ManagerModal extends Modal { await this.resetPluginLayout(); })); } - menu.addSeparator(); - // Ribbon 管理 - menu.addItem((item) => item.setTitle(t("管理器_Ribbon管理_描述")).setIcon("grip-vertical").onClick(() => { - new RibbonModal(this.app, this.manager).open(); - })); + if (this.isRibbonManagerEnabled()) { + menu.addSeparator(); + // Ribbon 管理 + menu.addItem((item) => item.setTitle(t("管理器_Ribbon管理_描述")).setIcon("grip-vertical").onClick(() => { + if (!this.isRibbonManagerEnabled()) return; + new RibbonModal(this.app, this.manager).open(); + })); + } // 插件市场 menu.addItem((item) => item.setTitle(t("管理器_插件市场_描述")).setIcon("store").onClick(() => { void this.openPluginMarket(); @@ -1778,69 +2101,57 @@ export class ManagerModal extends Modal { const activeFiltersContainer = header.createDiv("bpm-active-filters"); const updateActiveFilters = () => { activeFiltersContainer.empty(); - const currentStatus = this.getStatusFilterValue(); - const currentGroup = this.getGroupFilterValue(); - const currentTag = this.getTagFilterValue(); - const currentDelay = this.getDelayFilterValue(); + const currentStatuses = this.getStatusFilterValues(); + const currentGroups = this.getGroupFilterValues(); + const currentTags = this.getTagFilterValues(); + const currentDelays = this.getDelayFilterValues(); - // 状态筛选标签 - if (currentStatus && currentStatus !== "all") { - const filterLabels = this.getStatusFilterOptions(); + const addChip = (label: string, operator: FilterOperator, onRemove: () => void) => { const chip = activeFiltersContainer.createDiv("bpm-active-filter-chip"); - chip.setText(this.formatFilterChipLabel(filterLabels[currentStatus] || currentStatus, this.getStatusFilterOperator())); + chip.setText(this.formatFilterChipLabel(label, operator)); const closeIcon = chip.createSpan("bpm-active-filter-chip__close"); setIcon(closeIcon, "x"); chip.addEventListener("click", () => { - this.setStatusFilterValue("all"); + onRemove(); this.showHeadMobile(); this.reloadShowData(); }); + }; + + // 状态筛选标签 + const filterLabels = this.getStatusFilterOptions(); + for (const status of currentStatuses) { + addChip(filterLabels[status] || status, this.getStatusFilterOperator(), () => { + this.setStatusFilterValues(this.getStatusFilterValues().filter((value) => value !== status)); + }); } // 分组筛选标签 - if (currentGroup) { - const groupItem = this.settings.GROUPS.find(g => g.id === currentGroup); + for (const group of currentGroups) { + const groupItem = this.settings.GROUPS.find(g => g.id === group); if (groupItem) { - const chip = activeFiltersContainer.createDiv("bpm-active-filter-chip"); - chip.setText(this.formatFilterChipLabel(groupItem.name, this.getGroupFilterOperator())); - const closeIcon = chip.createSpan("bpm-active-filter-chip__close"); - setIcon(closeIcon, "x"); - chip.addEventListener("click", () => { - this.setGroupFilterValue(""); - this.showHeadMobile(); - this.reloadShowData(); + addChip(groupItem.name, this.getGroupFilterOperator(), () => { + this.setGroupFilterValues(this.getGroupFilterValues().filter((value) => value !== group)); }); } } // 标签筛选标签 - if (currentTag) { - const tagItem = this.settings.TAGS.find(t => t.id === currentTag); + for (const tag of currentTags) { + const tagItem = this.settings.TAGS.find(t => t.id === tag); if (tagItem) { - const chip = activeFiltersContainer.createDiv("bpm-active-filter-chip"); - chip.setText(this.formatFilterChipLabel(tagItem.name, this.getTagFilterOperator())); - const closeIcon = chip.createSpan("bpm-active-filter-chip__close"); - setIcon(closeIcon, "x"); - chip.addEventListener("click", () => { - this.setTagFilterValue(""); - this.showHeadMobile(); - this.reloadShowData(); + addChip(tagItem.name, this.getTagFilterOperator(), () => { + this.setTagFilterValues(this.getTagFilterValues().filter((value) => value !== tag)); }); } } // 延迟筛选标签 - if (currentDelay) { - const delayItem = this.settings.DELAYS.find(d => d.id === currentDelay); + for (const delay of currentDelays) { + const delayItem = this.settings.DELAYS.find(d => d.id === delay); if (delayItem) { - const chip = activeFiltersContainer.createDiv("bpm-active-filter-chip"); - chip.setText(this.formatFilterChipLabel(delayItem.name, this.getDelayFilterOperator())); - const closeIcon = chip.createSpan("bpm-active-filter-chip__close"); - setIcon(closeIcon, "x"); - chip.addEventListener("click", () => { - this.setDelayFilterValue(""); - this.showHeadMobile(); - this.reloadShowData(); + addChip(delayItem.name, this.getDelayFilterOperator(), () => { + this.setDelayFilterValues(this.getDelayFilterValues().filter((value) => value !== delay)); }); } } @@ -1873,38 +2184,31 @@ export class ManagerModal extends Modal { // 状态 const statusSetting = new Setting(filterPanel).setName(t("通用_状态_文本")); addMobileOperatorDropdown(statusSetting, this.getStatusFilterOperator(), t("筛选_状态取反_标签"), (value) => this.setStatusFilterOperator(value)); - statusSetting.addDropdown((dd) => { - dd.addOptions(this.getStatusFilterOptions()); - dd.setValue(this.getStatusFilterValue()); - dd.onChange((v) => { this.setStatusFilterValue(v); this.showHeadMobile(); this.reloadShowData(); }); + const statusOptions = Object.entries(this.getStatusFilterOptions()); + this.createMultiSelectFilter(statusSetting.controlEl, statusOptions, this.getStatusFilterValues(), "all", this.getStatusFilterOptions()["all"], t("筛选_状态_标签"), (values) => { + this.setStatusFilterValues(values); + this.showHeadMobile(); + this.reloadShowData(); }); // 分组 const groups = this.getGroupFilterOptions(t("筛选_全部_描述")); const groupSetting = new Setting(filterPanel).setName(t("通用_分组_文本")); addMobileOperatorDropdown(groupSetting, this.getGroupFilterOperator(), t("筛选_分组取反_标签"), (value) => this.setGroupFilterOperator(value)); - groupSetting.addDropdown((dd) => { - this.addOrderedOptions(dd, groups); - dd.setValue(this.getGroupFilterValue()); - dd.onChange((value) => { - this.setGroupFilterValue(value); - this.showHeadMobile(); - this.reloadShowData(); - }); + this.createMultiSelectFilter(groupSetting.controlEl, groups, this.getGroupFilterValues(), "", groups[0]?.[1] || t("筛选_全部_描述"), t("筛选_分组_标签"), (values) => { + this.setGroupFilterValues(values); + this.showHeadMobile(); + this.reloadShowData(); }); // 标签 const tags = this.getTagFilterOptions(t("筛选_全部_描述")); const tagSetting = new Setting(filterPanel).setName(t("通用_标签_文本")); addMobileOperatorDropdown(tagSetting, this.getTagFilterOperator(), t("筛选_标签取反_标签"), (value) => this.setTagFilterOperator(value)); - tagSetting.addDropdown((dd) => { - this.addOrderedOptions(dd, tags); - dd.setValue(this.getTagFilterValue()); - dd.onChange((value) => { - this.setTagFilterValue(value); - this.showHeadMobile(); - this.reloadShowData(); - }); + this.createMultiSelectFilter(tagSetting.controlEl, tags, this.getTagFilterValues(), "", tags[0]?.[1] || t("筛选_全部_描述"), t("筛选_标签_标签"), (values) => { + this.setTagFilterValues(values); + this.showHeadMobile(); + this.reloadShowData(); }); // 延迟 @@ -1912,14 +2216,10 @@ export class ManagerModal extends Modal { const delays = this.getDelayFilterOptions(t("筛选_全部_描述")); const delaySetting = new Setting(filterPanel).setName(t("通用_延迟_文本")); addMobileOperatorDropdown(delaySetting, this.getDelayFilterOperator(), t("筛选_延迟取反_标签"), (value) => this.setDelayFilterOperator(value)); - delaySetting.addDropdown((dd) => { - this.addOrderedOptions(dd, delays); - dd.setValue(this.getDelayFilterValue()); - dd.onChange((value) => { - this.setDelayFilterValue(value); - this.showHeadMobile(); - this.reloadShowData(); - }); + this.createMultiSelectFilter(delaySetting.controlEl, delays, this.getDelayFilterValues(), "", delays[0]?.[1] || t("筛选_全部_描述"), t("筛选_延迟_标签"), (values) => { + this.setDelayFilterValues(values); + this.showHeadMobile(); + this.reloadShowData(); }); } } @@ -2031,13 +2331,13 @@ export class ManagerModal extends Modal { const delaySettingsById = new Map(this.settings.DELAYS.map((delay) => [delay.id, delay])); const hiddenPluginIds = new Set(this.settings.HIDES || []); const lowerSearchText = this.searchText.trim().toLowerCase(); - const statusFilter = this.getStatusFilterValue(); + const statusFilter = this.getStatusFilterValues(); const statusOperator = this.getStatusFilterOperator(); - const groupFilter = this.getGroupFilterValue(); + const groupFilter = this.getGroupFilterValues(); const groupOperator = this.getGroupFilterOperator(); - const tagFilter = this.getTagFilterValue(); + const tagFilter = this.getTagFilterValues(); const tagOperator = this.getTagFilterOperator(); - const delayFilter = this.getDelayFilterValue(); + const delayFilter = this.getDelayFilterValues(); const delayOperator = this.getDelayFilterOperator(); const getBasePath = (this.app.vault.adapter as any)?.getBasePath?.() as string | undefined; const basePath = getBasePath ? normalizePath(getBasePath) : ""; @@ -2082,7 +2382,7 @@ export class ManagerModal extends Modal { if (!this.matchesTagFilter(ManagerPlugin.tags, tagFilter, tagOperator)) continue; if (!this.matchesSingleValueFilter(ManagerPlugin.delay, delayFilter, delayOperator)) continue; if (lowerSearchText !== "" && !this.getPluginSearchText(ManagerPlugin, plugin).includes(lowerSearchText)) continue; - if (!this.editorMode && !isSelf && hiddenPluginIds.has(plugin.id) && statusFilter !== "hidden") continue; + if (!this.editorMode && !isSelf && hiddenPluginIds.has(plugin.id) && !statusFilter.includes("hidden")) continue; const rawDir = plugin.dir || `plugins/${plugin.id}`; const isAbsolute = new RegExp("^(?:[a-zA-Z]:[\\\\/]|[\\\\/])").test(rawDir); let pluginDir: string; @@ -2850,11 +3150,9 @@ export class ManagerModal extends Modal { isRestoring = false; return; } - const statusFilter = this.getStatusFilterValue(); + const statusFilter = this.getStatusFilterValues(); const statusOperator = this.getStatusFilterOperator(); - const removeByFilter = statusOperator === "contains" - ? ((statusFilter === "enabled" && !targetEnabled) || (statusFilter === "disabled" && targetEnabled)) - : ((statusFilter === "enabled" && targetEnabled) || (statusFilter === "disabled" && !targetEnabled)); + const removeByFilter = !this.matchesStatusFilter(ManagerPlugin, plugin, targetEnabled, statusFilter, statusOperator, hiddenPluginIds); const updateCardUI = () => { itemEl.settingEl.toggleClass("is-enabled", targetEnabled); itemEl.settingEl.toggleClass("is-disabled", !targetEnabled); @@ -2930,6 +3228,7 @@ export class ManagerModal extends Modal { ManagerPlugin.group = ""; ManagerPlugin.delay = ""; ManagerPlugin.tags = []; + this.manager.applySpecialPluginTags(ManagerPlugin); await this.manager.savePluginAndExport(plugin.id); this.reloadShowData(); }); @@ -3020,7 +3319,7 @@ export class ManagerModal extends Modal { const updateStatuses = this.manager.updateStatus || {}; const checkedCount = Object.keys(updateStatuses).length; const updateCount = this.getPluginUpdateCount(updateStatuses); - const activeStatusFilter = this.getStatusFilterValue(); + const activeStatusFilters = this.getStatusFilterValues(); this.footEl.empty(); const statItems: Array<{ cls: string; icon: string; label: string; value: number; filter: StatusFilterValue }> = [ @@ -3041,11 +3340,12 @@ export class ManagerModal extends Modal { statItems.forEach((item) => { const chip = this.footEl.createSpan({ cls: `bpm-stat-chip ${item.cls}` }); chip.addClass("bpm-stat-chip--interactive"); - chip.toggleClass("is-active", activeStatusFilter === item.filter && this.getStatusFilterOperator() === "contains"); + const isActive = (item.filter === "all" ? activeStatusFilters.length === 0 : activeStatusFilters.includes(item.filter)) && this.getStatusFilterOperator() === "contains"; + chip.toggleClass("is-active", isActive); chip.setAttribute("role", "button"); chip.setAttribute("tabindex", "0"); chip.setAttribute("aria-label", `${item.label} ${item.value}`); - chip.setAttribute("aria-pressed", `${activeStatusFilter === item.filter && this.getStatusFilterOperator() === "contains"}`); + chip.setAttribute("aria-pressed", `${isActive}`); const icon = chip.createSpan({ cls: "bpm-stat-chip__icon" }); setIcon(icon, item.icon); chip.createSpan({ cls: "bpm-stat-chip__label", text: item.label }); @@ -3203,9 +3503,9 @@ export class ManagerModal extends Modal { private shouldRenderPluginLayoutSeparators(): boolean { return !this.hasActiveStatusFilter() - && !this.getGroupFilterValue() - && !this.getTagFilterValue() - && !this.getDelayFilterValue() + && this.getGroupFilterValues().length === 0 + && this.getTagFilterValues().length === 0 + && this.getDelayFilterValues().length === 0 && !this.searchText; } @@ -4312,6 +4612,15 @@ export class ManagerModal extends Modal { } private async showRibbonPanel(renderGeneration = this.renderGeneration) { + if (!this.isRibbonManagerEnabled()) { + this.activePage = "plugins"; + this.installMode = false; + this.syncPageChrome(); + this.contentEl.empty(); + await this.showData(renderGeneration); + return; + } + this.contentEl.empty(); const page = this.contentEl.createDiv("manager-ribbon-page ribbon-manager-modal"); page.createDiv({ @@ -5749,32 +6058,26 @@ export class ManagerModal extends Modal { private async refreshFilterOptions(preserveScroll = false) { const scrollTop = preserveScroll ? this.contentEl.scrollTop : 0; // 重新计算并刷新分组/标签/延迟下拉的计数 - if (this.groupDropdown) { - const currentGroup = this.groupDropdown.selectEl.value ?? (this.settings.PERSISTENCE ? this.settings.FILTER_GROUP : this.group); + if (this.groupMultiSelect) { const groups = this.getGroupFilterOptions(this.manager.translator.t("筛选_全部_描述")); - const current = this.settings.PERSISTENCE ? this.settings.FILTER_GROUP : currentGroup; - this.resetDropdown(this.groupDropdown, groups, current); + this.groupMultiSelect.refreshOptions(groups, this.getGroupFilterValues()); } - if (this.tagDropdown) { - const currentTag = this.tagDropdown.selectEl.value ?? (this.settings.PERSISTENCE ? this.settings.FILTER_TAG : this.tag); + if (this.tagMultiSelect) { const tags = this.getTagFilterOptions(this.manager.translator.t("筛选_全部_描述")); - const current = this.settings.PERSISTENCE ? this.settings.FILTER_TAG : currentTag; - this.resetDropdown(this.tagDropdown, tags, current); + this.tagMultiSelect.refreshOptions(tags, this.getTagFilterValues()); } - if (this.settings.DELAY && this.delayDropdown) { - const currentDelay = this.delayDropdown.selectEl.value ?? (this.settings.PERSISTENCE ? this.settings.FILTER_DELAY : this.delay); + if (this.settings.DELAY && this.delayMultiSelect) { const delays = this.getDelayFilterOptions(this.manager.translator.t("筛选_全部_描述")); - const current = this.settings.PERSISTENCE ? this.settings.FILTER_DELAY : currentDelay; - this.resetDropdown(this.delayDropdown, delays, current); + this.delayMultiSelect.refreshOptions(delays, this.getDelayFilterValues()); } await this.reloadShowData(); if (preserveScroll) this.contentEl.scrollTo({ top: scrollTop }); } - private resetDropdown(dropdown: DropdownComponent, options: Array<[string, string]>, value: string) { - dropdown.selectEl.empty(); - this.addOrderedOptions(dropdown, options); - dropdown.setValue(options.some(([optionValue]) => optionValue === value) ? value : options[0]?.[0] || ""); + public async refreshRibbonFeatureAvailability() { + this.ensureAllowedActivePage(); + await this.showHead(); + this.renderContent(); } public async onOpen() { diff --git a/src/modal/ribbon-modal.ts b/src/modal/ribbon-modal.ts index 63693bb..b4ebb49 100644 --- a/src/modal/ribbon-modal.ts +++ b/src/modal/ribbon-modal.ts @@ -24,6 +24,11 @@ export class RibbonModal extends Modal { } async onOpen() { + if (!this.manager.isRibbonManagerEnabled()) { + this.close(); + return; + } + this.manager.ribbonModal = this; this.modalEl.addClass("ribbon-manager-modal"); this.titleEl.setText(this.manager.translator.t("Ribbon_标题")); @@ -33,6 +38,8 @@ export class RibbonModal extends Modal { // 同步 Ribbon 项:读取当前工作区的 Ribbon,合并到设置中 async syncRibbonItems() { + if (!this.manager.isRibbonManagerEnabled()) return; + // 以 BPM 自己的 data.json 为源头,只从运行时内存补齐新出现的 Ribbon 项。 const savedItems = [...(this.manager.settings.RIBBON_SETTINGS || [])] .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); @@ -76,6 +83,7 @@ export class RibbonModal extends Modal { this.renderRootEl = contentEl; this.renderToolbarInRoot = showToolbar; contentEl.empty(); + if (!this.manager.isRibbonManagerEnabled()) return; if (showToolbar) this.renderToolbar(contentEl); this.renderDraggableList(contentEl); @@ -283,6 +291,8 @@ export class RibbonModal extends Modal { } async moveItem(oldIndex: number, newIndex: number) { + if (!this.manager.isRibbonManagerEnabled()) return; + const items = this.manager.settings.RIBBON_SETTINGS; if (oldIndex < 0 || oldIndex >= items.length || newIndex < 0 || newIndex > items.length) { this.display(); @@ -297,6 +307,8 @@ export class RibbonModal extends Modal { } private async persistRibbonConfig() { + if (!this.manager.isRibbonManagerEnabled()) return; + const items = this.manager.settings.RIBBON_SETTINGS; items.forEach((item, idx) => item.order = idx); await this.manager.saveSettings(); @@ -311,6 +323,8 @@ export class RibbonModal extends Modal { } async resetRibbonLayout() { + if (!this.manager.isRibbonManagerEnabled()) return; + const items = this.manager.settings.RIBBON_SETTINGS; items.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)); items.forEach((item, idx) => { diff --git a/src/settings/data.ts b/src/settings/data.ts index 942ee53..e718e19 100644 --- a/src/settings/data.ts +++ b/src/settings/data.ts @@ -58,6 +58,8 @@ export interface ManagerSettings { DELAY: boolean; /** 是否在插件卡片中隐藏 BPM 内置标签,例如 BPM 管理标识和 BPM 忽略。 */ HIDE_BPM_TAG: boolean; + /** 是否启用边栏编排接管;关闭时 BPM 不再排序或隐藏 Ribbon 图标。 */ + RIBBON_MANAGER_ENABLED: boolean; /** 管理页插件功能显示位置;item 表示直接展示在插件卡片上,menu 表示收纳到右键菜单。 */ MAIN_PAGE_ACTION_PLACEMENT: MainPageActionPlacementSettings; /** 自检发现非 BPM 管理插件时,是否自动接管 community-plugins.json。 */ @@ -81,15 +83,19 @@ export interface ManagerSettings { /** 持久化的搜索关键词,仅在 PERSISTENCE 开启时作为管理页默认搜索值。 */ FILTER_SEARCH: string; FILTER_STATUS: string; + FILTER_STATUS_VALUES?: string[]; FILTER_STATUS_OPERATOR: FilterOperator; /** 持久化的分组筛选 id,仅在 PERSISTENCE 开启时生效。 */ FILTER_GROUP: string; + FILTER_GROUP_VALUES?: string[]; FILTER_GROUP_OPERATOR: FilterOperator; /** 持久化的标签筛选 id,仅在 PERSISTENCE 开启时生效。 */ FILTER_TAG: string; + FILTER_TAG_VALUES?: string[]; FILTER_TAG_OPERATOR: FilterOperator; /** 持久化的延迟配置筛选 id,仅在 PERSISTENCE 与 DELAY 开启时生效。 */ FILTER_DELAY: string; + FILTER_DELAY_VALUES?: string[]; FILTER_DELAY_OPERATOR: FilterOperator; // 样式设置页 @@ -161,6 +167,7 @@ export const DEFAULT_SETTINGS: ManagerSettings = { CENTER: false, DELAY: false, HIDE_BPM_TAG: false, + RIBBON_MANAGER_ENABLED: false, MAIN_PAGE_ACTION_PLACEMENT: { ...DEFAULT_MAIN_PAGE_ACTION_PLACEMENT }, AUTO_TAKEOVER: false, SELF_CHECK_IGNORED: false, @@ -174,12 +181,16 @@ export const DEFAULT_SETTINGS: ManagerSettings = { // 管理页筛选状态 FILTER_SEARCH: "", FILTER_STATUS: "all", + FILTER_STATUS_VALUES: [], FILTER_STATUS_OPERATOR: "contains", FILTER_GROUP: "", + FILTER_GROUP_VALUES: [], FILTER_GROUP_OPERATOR: "contains", FILTER_TAG: "", + FILTER_TAG_VALUES: [], FILTER_TAG_OPERATOR: "contains", FILTER_DELAY: "", + FILTER_DELAY_VALUES: [], FILTER_DELAY_OPERATOR: "contains", // 样式设置页 diff --git a/src/settings/ui/manager-basis.ts b/src/settings/ui/manager-basis.ts index 8573c26..a5e6d46 100644 --- a/src/settings/ui/manager-basis.ts +++ b/src/settings/ui/manager-basis.ts @@ -35,7 +35,14 @@ export default class ManagerBasis extends BaseSetting { const persistenceToggle = new ToggleComponent(persistenceBar.controlEl); persistenceToggle.setValue(this.settings.PERSISTENCE); persistenceToggle.onChange((value) => { + const managerModal = this.manager.managerModal; + if (value) { + managerModal?.persistCurrentFilters(); + } this.settings.PERSISTENCE = value; + if (!value && managerModal) { + managerModal.usePersistedFiltersAsSessionFilters(); + } this.manager.saveSettings(); }); @@ -111,6 +118,17 @@ export default class ManagerBasis extends BaseSetting { this.manager.managerModal?.reloadShowData(); }); + const ribbonManagerBar = new Setting(this.containerEl) + .setName(this.manager.translator.t('设置_基础设置_边栏编排_标题')) + .setDesc(this.manager.translator.t('设置_基础设置_边栏编排_描述')); + const ribbonManagerToggle = new ToggleComponent(ribbonManagerBar.controlEl); + ribbonManagerToggle.setValue(this.settings.RIBBON_MANAGER_ENABLED !== false); + ribbonManagerToggle.onChange(async (value) => { + this.settings.RIBBON_MANAGER_ENABLED = value; + await this.manager.saveSettings(); + await this.manager.refreshRibbonManagerFeature(); + }); + heading('设置_基础设置_分组_命令'); const CommandItemBar = new Setting(this.containerEl) diff --git a/src/settings/ui/manager-tag.ts b/src/settings/ui/manager-tag.ts index 26df88d..f85259b 100644 --- a/src/settings/ui/manager-tag.ts +++ b/src/settings/ui/manager-tag.ts @@ -1,7 +1,7 @@ import BaseSetting from "../base-setting"; import { Notice, setIcon, Setting } from "obsidian"; import { BPM_TAG_ID } from "src/repo-resolver"; -import { BPM_IGNORE_TAG } from "src/data/types"; +import { BPM_IGNORE_TAG, EONDR_PLUGIN_TAG_ID } from "src/data/types"; import Commands from "src/command"; export default class ManagerTag extends BaseSetting { @@ -10,7 +10,7 @@ export default class ManagerTag extends BaseSetting { } private isPresetTag(tagId: string): boolean { - return tagId === BPM_TAG_ID || tagId === BPM_IGNORE_TAG; + return tagId === BPM_TAG_ID || tagId === BPM_IGNORE_TAG || tagId === EONDR_PLUGIN_TAG_ID; } main(): void { @@ -77,7 +77,7 @@ export default class ManagerTag extends BaseSetting { const nextId = id.trim(); const nextName = name.trim() || nextId; const containsId = this.manager.settings.TAGS.some(tag => tag.id === nextId); - if (!containsId && nextId !== '' && nextId !== BPM_TAG_ID && nextId !== BPM_IGNORE_TAG) { + if (!containsId && nextId !== '' && !this.isPresetTag(nextId)) { if (color === '') color = this.manager.generateAutoColor(this.manager.settings.TAGS.map(t => t.color)); this.manager.settings.TAGS.push({ id: nextId, name: nextName, color }); this.manager.saveSettings(); diff --git a/src/troubleshoot/troubleshoot-panel.ts b/src/troubleshoot/troubleshoot-panel.ts index f0dd81c..c9b959d 100644 --- a/src/troubleshoot/troubleshoot-panel.ts +++ b/src/troubleshoot/troubleshoot-panel.ts @@ -110,7 +110,7 @@ export class TroubleshootPanel { startBtn.setButtonText(this.t("排查_开始_按钮")); startBtn.setCta(); startBtn.setDisabled(enabledPlugins.length === 0); - startBtn.onClick(async () => { + startBtn.onClick(async () => { await this.startTroubleshoot(); }); diff --git a/styles.css b/styles.css index 972a332..cb522d3 100644 --- a/styles.css +++ b/styles.css @@ -1894,15 +1894,16 @@ body.theme-dark { } /* 筛选面板展开动画 */ -.manager-container--mobile .bpm-mobile-header__filters { - transition: max-height 0.25s ease, opacity 0.2s ease, padding 0.25s ease, margin 0.25s ease; - overflow: hidden; - max-height: 500px; - opacity: 1; -} - -.manager-container--mobile .bpm-mobile-header__filters.is-collapsed { - max-height: 0 !important; +.manager-container--mobile .bpm-mobile-header__filters { + transition: max-height 0.25s ease, opacity 0.2s ease, padding 0.25s ease, margin 0.25s ease; + overflow: visible; + max-height: 500px; + opacity: 1; +} + +.manager-container--mobile .bpm-mobile-header__filters.is-collapsed { + overflow: hidden; + max-height: 0 !important; opacity: 0 !important; padding-top: 0 !important; padding-bottom: 0 !important; @@ -2602,17 +2603,25 @@ body.theme-dark { } .manager-filter-field { - display: flex; - align-items: center; - flex: 1 1 0; - min-width: 0; + display: flex; + align-items: center; + flex: 1 1 0; + min-width: 0; max-width: 100%; height: 34px; border: 1px solid var(--bpm-border); border-radius: var(--bpm-radius-sm); background: var(--bpm-surface); overflow: hidden; - transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease; +} + +.manager-filter-field:has(.manager-multiselect-filter) { + overflow: visible; +} + +.manager-filter-field:has(.manager-multiselect-filter.is-open) { + z-index: 30; } .manager-filter-field--select { @@ -2623,6 +2632,7 @@ body.theme-dark { .manager-filter-field--compound { flex: 1 1 220px; max-width: none; + overflow: visible; } .manager-filter-field--search { @@ -2692,6 +2702,11 @@ body.theme-dark { width: auto; } +.manager-filter-select-group .manager-multiselect-filter { + flex: 1 1 auto; + min-width: 0; +} + .manager-filter-select-group .manager-filter-operator { font-size: 12px; color: var(--text-muted); @@ -2738,20 +2753,183 @@ body.theme-dark { transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease; } -.manager-container .manager-bar__search .dropdown select:focus, -.manager-container .manager-bar__search .search-input-container input:focus, -.manager-container .bpm-mobile-header__filters .dropdown select:focus, -.manager-container .bpm-mobile-header__search .search-input-container input:focus { - border-color: var(--interactive-accent); - box-shadow: none; -} - -.manager-container--mobile .bpm-mobile-header__filters .dropdown select { - border: 1px solid var(--bpm-border); - border-radius: var(--bpm-radius-sm); - background: var(--bpm-surface); -} - +.manager-container .manager-bar__search .dropdown select:focus, +.manager-container .manager-bar__search .search-input-container input:focus, +.manager-container .bpm-mobile-header__filters .dropdown select:focus, +.manager-container .bpm-mobile-header__search .search-input-container input:focus { + border-color: var(--interactive-accent); + box-shadow: none; +} + +.manager-multiselect-filter { + position: relative; + width: 100%; + min-width: 0; + height: 100%; +} + +.manager-multiselect-filter__trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + width: 100%; + min-width: 0; + min-height: 34px; + height: 100%; + padding: 0 8px; + border: 0; + border-radius: 0; + background: transparent; + color: var(--text-normal); + font-size: 13px; + text-align: left; + cursor: pointer; +} + +.manager-multiselect-filter__trigger:hover, +.manager-multiselect-filter__trigger:focus-visible { + background: var(--background-modifier-hover); +} + +.manager-multiselect-filter__summary { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-multiselect-filter__count { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 9px; + background: color-mix(in srgb, var(--interactive-accent) 16%, transparent); + color: var(--text-accent); + font-size: 11px; + font-weight: 700; + line-height: 1; +} + +.manager-multiselect-filter__count.is-empty { + display: none; +} + +.manager-multiselect-filter__chevron { + display: inline-flex; + flex: 0 0 auto; + width: 14px; + height: 14px; + color: var(--text-muted); + transition: transform 0.15s ease; +} + +.manager-multiselect-filter__chevron svg { + width: 14px; + height: 14px; +} + +.manager-multiselect-filter.is-open .manager-multiselect-filter__chevron { + transform: rotate(180deg); +} + +.manager-multiselect-filter__menu { + position: absolute; + z-index: 1000; + top: calc(100% + 4px); + left: 0; + right: 0; + display: none; + max-height: min(280px, 48vh); + overflow-y: auto; + padding: 4px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--background-primary); + box-shadow: var(--shadow-s); +} + +.manager-multiselect-filter.is-open .manager-multiselect-filter__menu { + display: flex; + flex-direction: column; +} + +.manager-multiselect-filter__option { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-height: 32px; + padding: 6px 8px; + border: 0; + border-radius: var(--bpm-radius-xs); + background: transparent; + color: var(--text-normal); + text-align: left; + cursor: pointer; +} + +.manager-multiselect-filter__option:hover, +.manager-multiselect-filter__option:focus-visible { + background: var(--background-modifier-hover); +} + +.manager-multiselect-filter__option.is-selected { + background: color-mix(in srgb, var(--interactive-accent) 12%, transparent); + color: var(--text-accent); +} + +.manager-multiselect-filter__check { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 16px; + height: 16px; +} + +.manager-multiselect-filter__check svg { + width: 16px; + height: 16px; +} + +.manager-multiselect-filter__option-label { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-container--mobile .bpm-mobile-header__filters .dropdown select { + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-surface); +} + +.manager-container--mobile .bpm-mobile-header__filters .manager-multiselect-filter { + flex: 1 1 auto; + max-width: 220px; + height: 34px; +} + +.manager-container--mobile .bpm-mobile-header__filters .manager-multiselect-filter__trigger { + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-surface); +} + +.manager-container--mobile .bpm-mobile-header__filters .manager-multiselect-filter__menu { + right: 0; + left: auto; + width: min(260px, 70vw); +} + .manager-container--mobile .bpm-mobile-header__filters .manager-filter-operator { color: var(--text-muted); } diff --git a/versions.json b/versions.json index 7c5560c..fc71916 100644 --- a/versions.json +++ b/versions.json @@ -4,5 +4,6 @@ "1.0.2": "1.5.8", "1.0.3": "1.5.8", "1.0.4": "1.5.8", - "1.0.5": "1.5.8" + "1.0.5": "1.5.8", + "1.0.6": "1.5.8" } \ No newline at end of file