chore: 1.0.4

This commit is contained in:
eondrcode 2026-05-29 00:00:00 +08:00
parent fbbf5c1a5d
commit 60330e191c
9 changed files with 2108 additions and 212 deletions

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.3",
"version": "1.0.4",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {

View file

@ -62,7 +62,7 @@ export interface ReleaseVersion {
}
const API_BASE = "https://api.github.com";
const RELEASES_PER_PAGE = 50;
const RELEASES_PER_PAGE = 100;
const RELEASE_VERSION_CACHE_TTL_MS = 5 * 60 * 1000;
/**
@ -292,9 +292,14 @@ export const fetchReleaseVersions = async (manager: Manager, repoInput: string):
return cloneReleaseVersions(cached.versions);
}
const url = `${API_BASE}/repos/${repo}/releases?per_page=${RELEASES_PER_PAGE}`;
const releases = await fetchJson<ReleaseResponse[]>(url, token);
if (!Array.isArray(releases)) return [];
const releases: ReleaseResponse[] = [];
for (let page = 1; ; page++) {
const url = `${API_BASE}/repos/${repo}/releases?per_page=${RELEASES_PER_PAGE}&page=${page}`;
const pageReleases = await fetchJson<ReleaseResponse[]>(url, token);
if (!Array.isArray(pageReleases) || pageReleases.length === 0) break;
releases.push(...pageReleases);
if (pageReleases.length < RELEASES_PER_PAGE) break;
}
const versions = releases
.map((release) => ({

View file

@ -6,6 +6,8 @@ export default {
_操作_文本: 'Operation',
_搜索_文本: 'Search',
_名称_文本: 'Name',
_ID_文本: 'ID',
_颜色_文本: 'Color',
_无分组_文本: 'ALL',
_无标签_文本: 'ALL',
_无延迟_文本: 'No Delay',
@ -90,7 +92,13 @@ export default {
_选择版本_获取失败提示: 'Failed to fetch version list. Please try again later.',
_选择版本_无版本提示: 'No version list available. Will try the detected or latest version.',
_选择版本_下载按钮: 'Download update',
_选择版本_切换按钮: 'Switch to selected version',
_选择版本_成功提示: 'Downloaded and updated plugin',
_选择版本_说明: 'Choose the GitHub Release version to install.',
_选择版本_列表标题: 'Available versions',
_选择版本_当前版本: 'Installed: {version}',
_选择版本_版本数量: '{count} versions',
_选择版本_选择版本: 'Select version {version}',
_全部_描述: 'All',
_仅启用_描述: 'Enabled only',
@ -469,6 +477,27 @@ export default {
_应用更改中_提示: 'Applying changes. Please avoid repeated actions.',
_暂无匹配插件: 'No matching plugins',
_安装_操作_安装中: 'Installing...',
_入口: 'Bulk edit',
_标题: 'Bulk edit',
_已选择数量: 'Selected {count}/{total}',
_全选当前列表: 'Select current list',
_清空选择: 'Clear selection',
_设置分组: 'Set group',
_清除分组: 'Clear group',
_添加标签: 'Add tag',
_移除标签: 'Remove tag',
_清除全部标签: 'Clear all tags',
_设置延迟: 'Set delay',
_选择插件: 'Select {name}',
_清除全部标签确认: 'Clear all tags from {count} plugins? Built-in BPM tags will be kept.',
_启用确认: 'Enable {count} plugins?',
_禁用确认: 'Disable {count} plugins?',
_无可操作插件: 'No actionable plugins',
_已更新分组: 'Updated group for {count} plugins',
_已添加标签: 'Added tag to {count} plugins',
_已移除标签: 'Updated tags for {count} plugins',
_已更新延迟: 'Updated delay for {count} plugins',
_已更新状态: 'Updated status for {count} plugins',
_未获取到远端版本: 'Remote version not found',
_无来源无法检测: 'No source available, cannot check',
_刷新插件_提示: 'Refresh plugin',
@ -570,6 +599,7 @@ export default {
_分组名称_标签: 'Group name',
_创建分组: 'Create group',
_创建全局分组: 'Create a new global group.',
_打开切换: 'Change group for {name}',
_标题: 'Edit Tags',
_说明: 'Assign tags to the current plugin, or maintain tag names and colors directly.',
_分配标题: 'Tag assignment',

View file

@ -6,6 +6,8 @@ export default {
_操作_文本: '操作',
_搜索_文本: '搜索',
_名称_文本: '名称',
_ID_文本: 'ID',
_颜色_文本: '颜色',
_无分组_文本: '全部',
_无标签_文本: '全部',
_无延迟_文本: '无',
@ -149,7 +151,13 @@ export default {
_选择版本_获取失败提示: '获取版本列表失败,请稍后再试',
_选择版本_无版本提示: '未获取到版本列表,将尝试使用检测到的版本或最新版本。',
_选择版本_下载按钮: '下载更新',
_选择版本_切换按钮: '切换到所选版本',
_选择版本_成功提示: '已下载并更新插件',
_选择版本_说明: '选择要安装的 GitHub Release 版本。',
_选择版本_列表标题: '可用版本',
_选择版本_当前版本: '当前安装:{version}',
_选择版本_版本数量: '{count} 个版本',
_选择版本_选择版本: '选择版本 {version}',
_ID已复制: 'ID已复制',
@ -471,6 +479,27 @@ export default {
_应用更改中_提示: '正在应用更改,请勿频繁操作。',
_暂无匹配插件: '暂无匹配插件',
_安装_操作_安装中: '安装中...',
_入口: '批量编辑',
_标题: '批量编辑',
_已选择数量: '已选择 {count}/{total}',
_全选当前列表: '选择当前列表',
_清空选择: '清空选择',
_设置分组: '设置分组',
_清除分组: '清除分组',
_添加标签: '添加标签',
_移除标签: '移除标签',
_清除全部标签: '清除全部标签',
_设置延迟: '设置延迟',
_选择插件: '选择 {name}',
_清除全部标签确认: '确定清除 {count} 个插件的全部标签吗BPM 内置标签会保留。',
_启用确认: '确定启用 {count} 个插件吗?',
_禁用确认: '确定禁用 {count} 个插件吗?',
_无可操作插件: '没有可操作的插件',
_已更新分组: '已更新 {count} 个插件的分组',
_已添加标签: '已为 {count} 个插件添加标签',
_已移除标签: '已更新 {count} 个插件的标签',
_已更新延迟: '已更新 {count} 个插件的延迟',
_已更新状态: '已更新 {count} 个插件的状态',
_未获取到远端版本: '未获取到远端版本',
_无来源无法检测: '无来源,无法检测',
_刷新插件_提示: '刷新插件',
@ -572,6 +601,7 @@ export default {
_分组名称_标签: '分组名称',
_创建分组: '创建分组',
_创建全局分组: '创建新的全局分组。',
_打开切换: '切换 {name} 的分组',
_标题: '编辑标签',
_说明: '为当前插件分配标签,或直接维护标签名称与颜色。',
_分配标题: '标签分配',

View file

@ -83,8 +83,15 @@ export class GroupModal extends Modal {
summaryMain.createDiv({ cls: 'manager-tag-editor__summary-title', text: t('分组编辑_选择标题') });
summaryMain.createDiv({ cls: 'manager-tag-editor__summary-desc', text: t('分组编辑_选择说明', { id: this.managerPlugin.id, selected: selectedCount, total: this.settings.GROUPS.length }) });
const summaryStats = summary.createDiv('manager-tag-editor__summary-stats');
summaryStats.createSpan({ cls: 'manager-tag-editor__summary-stat', text: t('设置_分类_统计', { label: t('通用_全部_文本'), count: this.settings.GROUPS.length }) });
summaryStats.createSpan({ cls: 'manager-tag-editor__summary-stat', text: t('设置_分类_统计', { label: t('通用_已选择_文本'), count: selectedCount }) });
const createSummaryStat = (label: string, value: number, icon: string) => {
const stat = summaryStats.createSpan({ cls: 'manager-tag-editor__summary-stat' });
const statIcon = stat.createSpan({ cls: 'manager-tag-editor__summary-stat-icon' });
setIcon(statIcon, icon);
stat.createSpan({ cls: 'manager-tag-editor__summary-stat-label', text: label });
stat.createSpan({ cls: 'manager-tag-editor__summary-stat-value', text: `${value}` });
};
createSummaryStat(t('通用_全部_文本'), this.settings.GROUPS.length, 'folders');
createSummaryStat(t('通用_已选择_文本'), selectedCount, 'check');
const list = page.createDiv('manager-tag-editor__list');
for (const group of this.settings.GROUPS) {
@ -103,7 +110,8 @@ export class GroupModal extends Modal {
const previewLine = itemEl.nameEl.createDiv('manager-tag-editor__preview-line');
const tag = this.manager.createTag(group.name || group.id, group.color, this.settings.GROUP_STYLE);
tag.addClass('manager-tag-editor__chip');
previewLine.appendChild(tag);
const chipSlot = previewLine.createSpan({ cls: 'manager-tag-editor__chip-slot' });
chipSlot.appendChild(tag);
previewLine.createSpan({ cls: 'manager-tag-editor__id', text: group.id });
if (selected) previewLine.createSpan({ cls: 'manager-tag-editor__badge is-assigned', text: t('通用_已选择_文本') });
@ -181,54 +189,103 @@ export class GroupModal extends Modal {
}
}
if (this.add) {
let id = '';
let name = '';
let color = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color));
const foodBar = new Setting(page).setClass('manager-bar__title');
foodBar.settingEl.addClass('manager-tag-editor__add-panel');
foodBar.nameEl.empty();
foodBar.descEl.empty();
const addTitle = foodBar.nameEl.createDiv('manager-tag-editor__add-title');
const addIcon = addTitle.createSpan({ cls: 'manager-tag-editor__add-title-icon' });
setIcon(addIcon, 'plus');
addTitle.createSpan({ text: t('设置_分组设置_新增分组') });
foodBar.descEl.setText(t('设置_分类_ID说明'));
foodBar.addColorPicker(cb => cb
.setValue(color)
.onChange((value) => {
color = value;
})
)
foodBar.addText(cb => cb
.setPlaceholder('ID')
.onChange((value) => { id = value; })
.inputEl.addClass('manager-editor__item-input')
)
foodBar.addText(cb => cb
.setPlaceholder(this.manager.translator.t('通用_名称_文本'))
.onChange((value) => { name = value; })
.inputEl.addClass('manager-editor__item-input')
)
foodBar.addExtraButton((cb) => {
cb.setIcon('plus');
this.prepareIconButton(cb, t('分组编辑_创建分组'), 'manager-tag-editor__save-button');
cb.onClick(() => {
const nextId = id.trim();
const nextName = name.trim() || nextId;
const containsId = this.manager.settings.GROUPS.some(tag => tag.id === nextId);
if (!containsId && nextId !== '') {
if (color === '') color = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color));
this.manager.settings.GROUPS.push({ id: nextId, name: nextName, color });
this.manager.saveSettings();
this.add = false;
this.reloadShowData();
Commands(this.app, this.manager);
new Notice(this.manager.translator.t('设置_分组设置_通知_一'));
} else {
new Notice(this.manager.translator.t('设置_分组设置_通知_二'));
}
});
})
const addPanel = page.createDiv('manager-tag-editor__create-panel');
const preview = addPanel.createDiv('manager-tag-editor__create-preview');
const previewMain = preview.createDiv('manager-tag-editor__create-preview-main');
const previewChip = this.manager.createTag(t('通用_名称_文本'), color, this.settings.GROUP_STYLE);
previewChip.addClass('manager-tag-editor__chip');
previewChip.addClass('manager-tag-editor__create-chip');
const previewChipSlot = previewMain.createSpan({ cls: 'manager-tag-editor__chip-slot manager-tag-editor__chip-slot--preview' });
previewChipSlot.appendChild(previewChip);
const previewText = previewMain.createDiv('manager-tag-editor__create-preview-text');
previewText.createDiv({ cls: 'manager-tag-editor__create-title', text: t('设置_分组设置_新增分组') });
const previewId = previewText.createDiv({ cls: 'manager-tag-editor__create-id', text: 'ID' });
preview.createDiv({ cls: 'manager-tag-editor__create-desc', text: t('设置_分类_新增描述') });
const form = addPanel.createDiv('manager-tag-editor__create-form');
const createField = (label: string, className = '') => {
const field = form.createDiv(`manager-tag-editor__create-field ${className}`);
field.createDiv({ cls: 'manager-tag-editor__create-label', text: label });
return field;
};
const colorField = createField(t('通用_颜色_文本'), 'manager-tag-editor__create-field--color');
const colorInput = colorField.createEl('input');
colorInput.type = 'color';
colorInput.value = color;
colorInput.addClass('manager-tag-editor__create-color');
colorInput.setAttribute('aria-label', t('通用_颜色_文本'));
const idField = createField(t('通用_ID_文本'));
const idInput = idField.createEl('input');
idInput.type = 'text';
idInput.placeholder = 'group-id';
idInput.spellcheck = false;
idInput.addClass('manager-editor__item-input');
idInput.addClass('manager-tag-editor__create-input');
idInput.setAttribute('aria-label', t('通用_ID_文本'));
const nameField = createField(t('通用_名称_文本'));
const nameInput = nameField.createEl('input');
nameInput.type = 'text';
nameInput.placeholder = t('通用_名称_文本');
nameInput.addClass('manager-editor__item-input');
nameInput.addClass('manager-tag-editor__create-input');
nameInput.setAttribute('aria-label', t('通用_名称_文本'));
const updatePreview = () => {
const nextId = idInput.value.trim();
const nextName = nameInput.value.trim();
previewChip.textContent = nextName || nextId || t('通用_名称_文本');
previewChip.setAttribute('style', this.manager.generateTagStyle(color, this.settings.GROUP_STYLE));
previewId.setText(nextId || 'ID');
};
const submit = () => {
const nextId = idInput.value.trim();
const nextName = nameInput.value.trim() || nextId;
const containsId = this.manager.settings.GROUPS.some(tag => tag.id === nextId);
if (!containsId && nextId !== '') {
this.manager.settings.GROUPS.push({ id: nextId, name: nextName, color });
this.manager.saveSettings();
this.add = false;
this.reloadShowData();
Commands(this.app, this.manager);
new Notice(this.manager.translator.t('设置_分组设置_通知_一'));
} else {
new Notice(this.manager.translator.t('设置_分组设置_通知_二'));
}
};
const onEnter = (event: KeyboardEvent) => {
if (event.key !== 'Enter') return;
event.preventDefault();
submit();
};
colorInput.addEventListener('input', () => {
color = colorInput.value;
updatePreview();
});
idInput.addEventListener('input', () => updatePreview());
nameInput.addEventListener('input', () => updatePreview());
idInput.addEventListener('keydown', onEnter);
nameInput.addEventListener('keydown', onEnter);
const actions = addPanel.createDiv('manager-tag-editor__create-actions');
const cancelButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action' });
cancelButton.type = 'button';
cancelButton.setAttribute('aria-label', t('通用_取消_文本'));
setIcon(cancelButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'x');
cancelButton.createSpan({ text: t('通用_取消_文本') });
cancelButton.addEventListener('click', () => {
this.add = false;
this.reloadShowData();
});
const submitButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action manager-tag-editor__create-action--primary' });
submitButton.type = 'button';
submitButton.setAttribute('aria-label', t('分组编辑_创建分组'));
setIcon(submitButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'plus');
submitButton.createSpan({ text: t('分组编辑_创建分组') });
submitButton.addEventListener('click', submit);
window.setTimeout(() => idInput.focus(), 0);
} else {
// [底部行] 新增
const foodBar = new Setting(page).setClass('manager-bar__title').setName(this.manager.translator.t('通用_新增_文本'));

View file

@ -138,6 +138,8 @@ export class ManagerModal extends Modal {
groupDropdown?: DropdownComponent;
tagDropdown?: DropdownComponent;
delayDropdown?: DropdownComponent;
private bulkEditMode = false;
private bulkSelectedPluginIds = new Set<string>();
actionCollapsed = false;
filterCollapsed = false;
private reloadingManifests = false;
@ -496,12 +498,50 @@ export class ManagerModal extends Modal {
return ((button as any).extraSettingsEl || (button as any).buttonEl) as HTMLElement | undefined;
}
private async openPluginVersionList(pluginId: string, updateInfo?: PluginUpdateViewStatus | null) {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const progress = this.showInlineProgress(t("通知_获取版本中文案"), pluginId);
progress.update(0, 1, pluginId);
try {
let status = updateInfo ?? (this.manager.updateStatus?.[pluginId] as PluginUpdateViewStatus | undefined);
let repo = status?.repo || this.manager.settings.REPO_MAP?.[pluginId] || null;
if (!repo) {
try {
repo = await this.manager.repoResolver.resolveRepo(pluginId);
} catch {
repo = null;
}
}
if (!repo) {
new Notice(t("下载更新_缺少仓库提示"));
return;
}
const versions = await fetchReleaseVersions(this.manager, repo);
if (!status) status = {};
status.repo = repo;
status.versions = versions;
const stableVersion = versions.find((release) => !release.prerelease)?.version;
status.remoteVersion = stableVersion || versions[0]?.version || status.remoteVersion || null;
this.manager.updateStatus[pluginId] = {
...(this.manager.updateStatus?.[pluginId] ?? {}),
...status,
} as any;
progress.update(1, 1, pluginId);
this.refreshSinglePluginUpdateUi(pluginId);
this.updateStats();
new UpdateModal(this.app, this.manager, pluginId, versions, status.remoteVersion ?? null, repo).open();
} catch (e) {
console.error("[BPM] fetch remote versions failed", e);
new Notice(t("管理器_选择版本_获取失败提示"), 4000);
} finally {
progress.hide();
}
}
private openPluginUpdateModal(pluginId: string, updateInfo: PluginUpdateViewStatus) {
if (!updateInfo.remoteVersion) return;
const versions = updateInfo.versions && updateInfo.versions.length > 0
? updateInfo.versions
: [{ version: updateInfo.remoteVersion, prerelease: false }];
new UpdateModal(this.app, this.manager, pluginId, versions, updateInfo.remoteVersion, updateInfo.repo || undefined).open();
void this.openPluginVersionList(pluginId, updateInfo);
}
private addPluginDownloadButton(controlEl: HTMLElement, pluginId: string, updateInfo: PluginUpdateViewStatus, prepend = false) {
@ -723,6 +763,265 @@ export class ManagerModal extends Modal {
await this.reloadShowData();
}
private getSelectedManagerPlugins(): ManagerPlugin[] {
return this.settings.Plugins.filter((plugin) => this.bulkSelectedPluginIds.has(plugin.id));
}
private getSelectableDisplayedPluginIds(): string[] {
return this.displayPlugins
.map((plugin) => plugin.id)
.filter((id) => id !== this.manager.manifest.id);
}
private getBulkSelectedCount(): number {
return this.getSelectedManagerPlugins().length;
}
private cleanupBulkSelection() {
const validIds = new Set(this.settings.Plugins.map((plugin) => plugin.id));
for (const id of Array.from(this.bulkSelectedPluginIds)) {
if (!validIds.has(id) || id === this.manager.manifest.id) this.bulkSelectedPluginIds.delete(id);
}
}
private setBulkEditMode(value: boolean) {
if (value && this.activePage !== "plugins") return;
this.bulkEditMode = value;
if (value) this.editorMode = false;
if (!value) this.bulkSelectedPluginIds.clear();
this.applyEditingStyle();
this.renderContent();
if (Platform.isMobileApp) this.showHeadMobile();
}
private toggleBulkPluginSelection(pluginId: string, selected: boolean) {
if (pluginId === this.manager.manifest.id) return;
if (selected) this.bulkSelectedPluginIds.add(pluginId);
else this.bulkSelectedPluginIds.delete(pluginId);
this.reloadShowData();
}
private selectDisplayedPlugins() {
this.bulkSelectedPluginIds = new Set(this.getSelectableDisplayedPluginIds());
this.reloadShowData();
}
private clearBulkSelection() {
this.bulkSelectedPluginIds.clear();
this.reloadShowData();
}
private createBulkActionButton(container: HTMLElement, icon: string, label: string, onClick: (event: MouseEvent) => void, disabled = false) {
const button = container.createEl("button", { cls: "manager-bulk-bar__button" });
button.type = "button";
button.disabled = disabled;
button.setAttribute("aria-label", label);
button.setAttribute("title", label);
const iconEl = button.createSpan({ cls: "manager-bulk-bar__button-icon" });
iconEl.setAttribute("aria-hidden", "true");
setIcon(iconEl, icon);
button.createSpan({ cls: "manager-bulk-bar__button-label", text: label });
button.addEventListener("click", onClick);
this.bindLongPressTooltip(button, label);
return button;
}
private showBulkGroupMenu(event: MouseEvent) {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const menu = new Menu();
menu.addItem((item) => item
.setTitle(t("批量编辑_清除分组"))
.setIcon("folder-x")
.onClick(() => { void this.applyBulkGroup(""); }));
if (this.settings.GROUPS.length > 0) menu.addSeparator();
for (const group of this.settings.GROUPS) {
menu.addItem((item) => item
.setTitle(group.name || group.id)
.setIcon("folder-tree")
.onClick(() => { void this.applyBulkGroup(group.id); }));
}
menu.showAtMouseEvent(event);
}
private showBulkTagMenu(event: MouseEvent, mode: "add" | "remove") {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const menu = new Menu();
const tags = this.settings.TAGS.filter((tag) => tag.id !== BPM_TAG_ID);
if (mode === "remove") {
menu.addItem((item) => item
.setTitle(t("批量编辑_清除全部标签"))
.setIcon("tags")
.onClick(() => { void this.applyBulkClearTags(); }));
if (tags.length > 0) menu.addSeparator();
}
for (const tag of tags) {
menu.addItem((item) => item
.setTitle(tag.name || tag.id)
.setIcon(mode === "add" ? "tag" : "tag-x")
.onClick(() => {
if (mode === "add") void this.applyBulkAddTag(tag.id);
else void this.applyBulkRemoveTag(tag.id);
}));
}
menu.showAtMouseEvent(event);
}
private showBulkDelayMenu(event: MouseEvent) {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const menu = new Menu();
menu.addItem((item) => item
.setTitle(t("通用_无延迟_文本"))
.setIcon("timer-off")
.onClick(() => { void this.applyBulkDelay(""); }));
if (this.settings.DELAYS.length > 0) menu.addSeparator();
for (const delay of this.settings.DELAYS) {
menu.addItem((item) => item
.setTitle(`${delay.name || delay.id} (${delay.time}s)`)
.setIcon("timer")
.onClick(() => { void this.applyBulkDelay(delay.id); }));
}
menu.showAtMouseEvent(event);
}
private showBulkMoreMenu(event: MouseEvent) {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const selectedCount = this.getBulkSelectedCount();
const disabled = selectedCount === 0;
const menu = new Menu();
menu.addItem((item) => item
.setTitle(t("通用_启用_文本"))
.setIcon("power")
.setDisabled(disabled)
.onClick(() => { void this.applyBulkEnabled(true); }));
menu.addItem((item) => item
.setTitle(t("通用_禁用_文本"))
.setIcon("power-off")
.setDisabled(disabled)
.onClick(() => { void this.applyBulkEnabled(false); }));
menu.addSeparator();
menu.addItem((item) => item
.setTitle(t("批量编辑_全选当前列表"))
.setIcon("list-checks")
.onClick(() => this.selectDisplayedPlugins()));
menu.addItem((item) => item
.setTitle(t("批量编辑_清空选择"))
.setIcon("x")
.setDisabled(this.bulkSelectedPluginIds.size === 0)
.onClick(() => this.clearBulkSelection()));
menu.showAtMouseEvent(event);
}
private renderBulkBar(container: HTMLElement) {
if (!this.bulkEditMode) return;
this.cleanupBulkSelection();
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const selectedCount = this.getBulkSelectedCount();
const displayedCount = this.getSelectableDisplayedPluginIds().length;
const disabled = selectedCount === 0;
const bar = container.createDiv("manager-bulk-bar");
const summary = bar.createDiv("manager-bulk-bar__summary");
const summaryIcon = summary.createSpan({ cls: "manager-bulk-bar__summary-icon" });
setIcon(summaryIcon, "square-check-big");
const text = summary.createDiv("manager-bulk-bar__summary-text");
text.createSpan({ cls: "manager-bulk-bar__title", text: t("批量编辑_标题") });
text.createSpan({ cls: "manager-bulk-bar__count", text: t("批量编辑_已选择数量", { count: selectedCount, total: displayedCount }) });
const actions = bar.createDiv("manager-bulk-bar__actions");
this.createBulkActionButton(actions, "list-checks", t("批量编辑_全选当前列表"), () => this.selectDisplayedPlugins(), displayedCount === 0);
this.createBulkActionButton(actions, "folder-tree", t("批量编辑_设置分组"), (event) => this.showBulkGroupMenu(event), disabled);
this.createBulkActionButton(actions, "tag", t("批量编辑_添加标签"), (event) => this.showBulkTagMenu(event, "add"), disabled);
this.createBulkActionButton(actions, "tag-x", t("批量编辑_移除标签"), (event) => this.showBulkTagMenu(event, "remove"), disabled);
if (this.settings.DELAY) this.createBulkActionButton(actions, "timer", t("批量编辑_设置延迟"), (event) => this.showBulkDelayMenu(event), disabled);
this.createBulkActionButton(actions, "more-horizontal", t("管理器_更多操作_描述"), (event) => this.showBulkMoreMenu(event));
this.createBulkActionButton(actions, "x", t("通用_完成_文本"), () => this.setBulkEditMode(false));
}
private async applyBulkGroup(groupId: string) {
const plugins = this.getSelectedManagerPlugins();
plugins.forEach((plugin) => { plugin.group = groupId; });
await this.finishBulkMetadataEdit("批量编辑_已更新分组", plugins.length);
}
private async applyBulkAddTag(tagId: string) {
const plugins = this.getSelectedManagerPlugins();
plugins.forEach((plugin) => {
if (!plugin.tags.includes(tagId)) plugin.tags.push(tagId);
});
await this.finishBulkMetadataEdit("批量编辑_已添加标签", plugins.length);
}
private async applyBulkRemoveTag(tagId: string) {
const plugins = this.getSelectedManagerPlugins();
plugins.forEach((plugin) => {
plugin.tags = plugin.tags.filter((id) => id !== tagId);
});
await this.finishBulkMetadataEdit("批量编辑_已移除标签", plugins.length);
}
private async applyBulkClearTags() {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const plugins = this.getSelectedManagerPlugins();
if (plugins.length === 0) return;
if (!window.confirm(t("批量编辑_清除全部标签确认", { count: plugins.length }))) return;
plugins.forEach((plugin) => {
plugin.tags = plugin.tags.filter((id) => id === BPM_TAG_ID);
});
await this.finishBulkMetadataEdit("批量编辑_已移除标签", plugins.length);
}
private async applyBulkDelay(delayId: string) {
const plugins = this.getSelectedManagerPlugins().filter((plugin) => !plugin.tags.includes(BPM_IGNORE_TAG));
if (plugins.length === 0) {
new Notice(this.manager.translator.t("批量编辑_无可操作插件"));
return;
}
plugins.forEach((plugin) => {
plugin.delay = delayId;
});
await this.finishBulkMetadataEdit("批量编辑_已更新延迟", plugins.length);
}
private async finishBulkMetadataEdit(messageKey: string, count: number) {
if (count === 0) return;
await this.manager.saveSettings();
Commands(this.app, this.manager);
await this.refreshFilterOptions(true);
new Notice(this.manager.translator.t(messageKey, { count }));
}
private async applyBulkEnabled(targetEnabled: boolean) {
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const plugins = this.getSelectedManagerPlugins()
.filter((plugin) => plugin.id !== this.manager.manifest.id && !plugin.tags.includes(BPM_IGNORE_TAG));
if (plugins.length === 0) {
new Notice(t("批量编辑_无可操作插件"));
return;
}
if (!window.confirm(t(targetEnabled ? "批量编辑_启用确认" : "批量编辑_禁用确认", { count: plugins.length }))) return;
const progress = this.showInlineProgress(t("管理器_应用更改中_提示"));
let processed = 0;
for (const plugin of plugins) {
const isEnabled = this.isManagedPluginEnabled(plugin.id);
if (isEnabled !== targetEnabled) {
plugin.enabled = targetEnabled;
if (this.settings.DELAY) {
if (targetEnabled) await this.appPlugins.enablePlugin(plugin.id);
else await this.appPlugins.disablePlugin(plugin.id);
} else {
if (targetEnabled) await this.appPlugins.enablePluginAndSave(plugin.id);
else await this.appPlugins.disablePluginAndSave(plugin.id);
}
}
processed++;
progress.update(processed, plugins.length, plugin.id);
}
progress.hide();
await this.manager.saveSettings();
Commands(this.app, this.manager);
await this.reloadShowData();
new Notice(t("批量编辑_已更新状态", { count: plugins.length }));
}
private runDisplayedPluginsToggle() {
const targetEnabled = this.getBulkToggleTarget();
new DisableModal(this.app, this.manager, async () => {
@ -752,6 +1051,7 @@ export class ManagerModal extends Modal {
modalContainer?: HTMLElement;
private desktopActionWrapper?: HTMLElement;
private desktopFilterWrapper?: HTMLElement;
private bulkEditButtonEl?: HTMLButtonElement;
private pluginTabEl?: HTMLButtonElement;
private installTabEl?: HTMLButtonElement;
private sourcesTabEl?: HTMLButtonElement;
@ -936,19 +1236,32 @@ export class ManagerModal extends Modal {
const tools = toolbar.createDiv("manager-toolbar__tools");
const actionBar = new Setting(tools).setClass("manager-bar__action").setName("");
const markTool = (btn: ButtonComponent, scope: "plugin" | "install" | "global" | "ribbon" | "layout" | "transfer" | "resource") => {
const markTool = (btn: ButtonComponent, scope: "plugin" | "install" | "global" | "ribbon" | "layout" | "transfer" | "resource", order?: number) => {
btn.buttonEl.addClass("manager-tool");
btn.buttonEl.addClass(`manager-tool--${scope}`);
if (order !== undefined) btn.buttonEl.style.setProperty("--manager-tool-order", `${order}`);
};
const bulkEditButton = new ButtonComponent(actionBar.controlEl);
markTool(bulkEditButton, "plugin", 20);
bulkEditButton.setIcon(this.bulkEditMode ? "square-check-big" : "list-plus");
bulkEditButton.setTooltip(t("批量编辑_入口"));
bulkEditButton.buttonEl.setAttribute("aria-label", t("批量编辑_入口"));
bulkEditButton.buttonEl.classList.toggle("is-active", this.bulkEditMode);
this.bulkEditButtonEl = bulkEditButton.buttonEl;
this.bindLongPressTooltip(bulkEditButton.buttonEl, t("批量编辑_入口"));
bulkEditButton.onClick(() => {
this.setBulkEditMode(!this.bulkEditMode);
});
// [操作行] 检查更新
const updateButton = new ButtonComponent(actionBar.controlEl);
markTool(updateButton, "plugin");
markTool(updateButton, "plugin", 10);
this.preparePluginUpdateButton(updateButton);
// [操作行] 全选/全部取消当前列表
const toggleAllButton = new ButtonComponent(actionBar.controlEl);
markTool(toggleAllButton, "plugin");
markTool(toggleAllButton, "plugin", 30);
toggleAllButton.setIcon("list-checks");
toggleAllButton.setTooltip(this.manager.translator.t("管理器_全选取消_描述"));
this.bindLongPressTooltip(toggleAllButton.buttonEl, this.manager.translator.t("管理器_全选取消_描述"));
@ -958,7 +1271,7 @@ export class ManagerModal extends Modal {
// [操作行] 重载插件
const reloadButton = new ButtonComponent(actionBar.controlEl);
markTool(reloadButton, "plugin");
markTool(reloadButton, "plugin", 40);
reloadButton.setIcon("refresh-ccw");
reloadButton.setTooltip(this.manager.translator.t("管理器_重载插件_描述"));
this.bindLongPressTooltip(reloadButton.buttonEl, this.manager.translator.t("管理器_重载插件_描述"));
@ -990,12 +1303,16 @@ export class ManagerModal extends Modal {
// [操作行] 编辑模式
const editorButton = new ButtonComponent(actionBar.controlEl);
markTool(editorButton, "plugin");
markTool(editorButton, "plugin", 50);
this.editorMode ? editorButton.setIcon("pen-off") : editorButton.setIcon("pen");
editorButton.setTooltip(this.manager.translator.t("管理器_编辑模式_描述"));
this.bindLongPressTooltip(editorButton.buttonEl, this.manager.translator.t("管理器_编辑模式_描述"));
editorButton.onClick(async () => {
this.editorMode = !this.editorMode;
if (this.editorMode && this.bulkEditMode) {
this.bulkEditMode = false;
this.bulkSelectedPluginIds.clear();
}
this.editorMode ? editorButton.setIcon("pen-off") : editorButton.setIcon("pen");
this.applyEditingStyle();
if (!this.editorMode) {
@ -1006,7 +1323,7 @@ export class ManagerModal extends Modal {
});
const ribbonResetButton = new ButtonComponent(actionBar.controlEl);
markTool(ribbonResetButton, "ribbon");
markTool(ribbonResetButton, "ribbon", 10);
ribbonResetButton.setIcon("rotate-ccw");
ribbonResetButton.setTooltip(t("Ribbon_重置_提示"));
ribbonResetButton.buttonEl.setAttribute("aria-label", t("Ribbon_重置_提示"));
@ -1020,7 +1337,7 @@ export class ManagerModal extends Modal {
});
const addSeparatorButton = new ButtonComponent(actionBar.controlEl);
markTool(addSeparatorButton, "layout");
markTool(addSeparatorButton, "layout", 60);
addSeparatorButton.setIcon("separator-horizontal");
addSeparatorButton.setTooltip(t("管理器_布局_添加分割线"));
addSeparatorButton.buttonEl.setAttribute("aria-label", t("管理器_布局_添加分割线"));
@ -1030,7 +1347,7 @@ export class ManagerModal extends Modal {
});
const hiddenResetButton = new ButtonComponent(actionBar.controlEl);
markTool(hiddenResetButton, "layout");
markTool(hiddenResetButton, "layout", 61);
hiddenResetButton.setIcon("rotate-ccw");
hiddenResetButton.setTooltip(t("管理器_布局_按名称重置"));
hiddenResetButton.buttonEl.setAttribute("aria-label", t("管理器_布局_按名称重置"));
@ -1041,21 +1358,21 @@ export class ManagerModal extends Modal {
});
const githubButton = new ButtonComponent(actionBar.controlEl);
markTool(githubButton, "resource");
markTool(githubButton, "resource", 120);
githubButton.setIcon("github");
githubButton.setTooltip(this.manager.translator.t("管理器_GITHUB_描述"));
this.bindLongPressTooltip(githubButton.buttonEl, this.manager.translator.t("管理器_GITHUB_描述"));
githubButton.onClick(() => { window.open("https://github.com/zenozero-dev/obsidian-manager"); });
const tutorialButton = new ButtonComponent(actionBar.controlEl);
markTool(tutorialButton, "resource");
markTool(tutorialButton, "resource", 110);
tutorialButton.setIcon("book-open");
tutorialButton.setTooltip(this.manager.translator.t("管理器_视频教程_描述"));
this.bindLongPressTooltip(tutorialButton.buttonEl, this.manager.translator.t("管理器_视频教程_描述"));
tutorialButton.onClick(() => { window.open("https://www.bilibili.com/video/BV1WyrkYMEce/"); });
const supportGroupButton = new ButtonComponent(actionBar.controlEl);
markTool(supportGroupButton, "resource");
markTool(supportGroupButton, "resource", 130);
supportGroupButton.setIcon("message-circle");
supportGroupButton.setTooltip(SUPPORT_QQ_GROUP_TOOLTIP);
this.bindLongPressTooltip(supportGroupButton.buttonEl, SUPPORT_QQ_GROUP_TOOLTIP);
@ -1063,7 +1380,7 @@ export class ManagerModal extends Modal {
// [操作行] 插件市场
const marketButton = new ButtonComponent(actionBar.controlEl);
markTool(marketButton, "global");
markTool(marketButton, "global", 70);
marketButton.setIcon("store");
marketButton.setTooltip(this.manager.translator.t("管理器_插件市场_描述"));
this.bindLongPressTooltip(marketButton.buttonEl, this.manager.translator.t("管理器_插件市场_描述"));
@ -1073,7 +1390,7 @@ export class ManagerModal extends Modal {
// [操作行] 插件设置
const settingsButton = new ButtonComponent(actionBar.controlEl);
markTool(settingsButton, "global");
markTool(settingsButton, "global", 80);
settingsButton.setIcon("settings");
settingsButton.setTooltip(this.manager.translator.t("管理器_插件设置_描述"));
this.bindLongPressTooltip(settingsButton.buttonEl, this.manager.translator.t("管理器_插件设置_描述"));
@ -1087,7 +1404,7 @@ export class ManagerModal extends Modal {
// [测试行] 刷新插件
if (this.developerMode) {
const testButton = new ButtonComponent(actionBar.controlEl);
markTool(testButton, "plugin");
markTool(testButton, "plugin", 90);
testButton.setIcon("refresh-ccw");
testButton.setTooltip(t("开发_刷新插件_提示"));
testButton.onClick(async () => {
@ -1100,7 +1417,7 @@ export class ManagerModal extends Modal {
// [测试行] 测试插件
if (this.developerMode) {
const testButton = new ButtonComponent(actionBar.controlEl);
markTool(testButton, "plugin");
markTool(testButton, "plugin", 91);
testButton.setIcon("test-tube");
testButton.setTooltip(t("开发_测试插件_提示"));
testButton.onClick(async () => {
@ -1246,6 +1563,10 @@ export class ManagerModal extends Modal {
const topActions = topRow.createDiv("bpm-mobile-header__actions");
// 检查更新按钮
const updateBtn = new ButtonComponent(topActions);
this.preparePluginUpdateButton(updateBtn);
// 编辑模式
const editorBtn = new ButtonComponent(topActions);
editorBtn.setIcon(this.editorMode ? "pen-off" : "pen");
@ -1253,6 +1574,10 @@ export class ManagerModal extends Modal {
this.bindLongPressTooltip(editorBtn.buttonEl, t("管理器_编辑模式_描述"));
editorBtn.onClick(async () => {
this.editorMode = !this.editorMode;
if (this.editorMode && this.bulkEditMode) {
this.bulkEditMode = false;
this.bulkSelectedPluginIds.clear();
}
this.applyEditingStyle();
if (!this.editorMode) {
await this.refreshFilterOptions(true);
@ -1262,6 +1587,15 @@ export class ManagerModal extends Modal {
this.showHeadMobile();
});
const bulkBtn = new ButtonComponent(topActions);
bulkBtn.setIcon(this.bulkEditMode ? "square-check-big" : "list-plus");
bulkBtn.setTooltip(t("批量编辑_入口"));
bulkBtn.buttonEl.toggleClass("is-active", this.bulkEditMode);
this.bindLongPressTooltip(bulkBtn.buttonEl, t("批量编辑_入口"));
bulkBtn.onClick(() => {
this.setBulkEditMode(!this.bulkEditMode);
});
// 安装/返回
const installBtn = new ButtonComponent(topActions);
installBtn.setIcon(this.installMode ? "arrow-left" : "download");
@ -1275,10 +1609,6 @@ export class ManagerModal extends Modal {
this.showHeadMobile();
});
// 检查更新按钮
const updateBtn = new ButtonComponent(topActions);
this.preparePluginUpdateButton(updateBtn);
// 更多操作菜单
const moreBtn = new ButtonComponent(topActions);
moreBtn.setIcon("more-vertical");
@ -1286,6 +1616,9 @@ export class ManagerModal extends Modal {
this.bindLongPressTooltip(moreBtn.buttonEl, t("管理器_更多操作_描述"));
moreBtn.buttonEl.addEventListener("click", (ev) => {
const menu = new Menu();
menu.addItem((item) => item.setTitle(t("批量编辑_入口")).setIcon("list-plus").onClick(() => {
this.setBulkEditMode(!this.bulkEditMode);
}));
menu.addItem((item) => item.setTitle(t("管理器_全选取消_描述")).setIcon("list-checks").onClick(() => {
this.runDisplayedPluginsToggle();
}));
@ -1576,6 +1909,12 @@ export class ManagerModal extends Modal {
});
footer.appendChild(toggleAllBtn);
const bulkBtn = createFooterBtn(this.bulkEditMode ? "square-check-big" : "list-plus", t("批量编辑_入口"), () => {
this.setBulkEditMode(!this.bulkEditMode);
});
bulkBtn.toggleClass("is-active", this.bulkEditMode);
footer.appendChild(bulkBtn);
// 检查更新按钮
const updateBtn = createFooterBtn("rss", t("管理器_检查更新_描述"), () => {
void this.runPluginUpdateCheck(updateBtn);
@ -1630,6 +1969,7 @@ export class ManagerModal extends Modal {
public async showData(renderGeneration = this.renderGeneration) {
this.syncPluginOverviewLayoutClass();
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
// 使用 manifests 按 id 去重,防止重复渲染
const page: ManagerPage = "plugins";
if (!this.isRenderCurrent(renderGeneration, page)) return;
@ -1657,7 +1997,9 @@ export class ManagerModal extends Modal {
if (this.settings.DEBUG) console.log("[BPM] render showData before loop, children:", this.contentEl.children.length);
this.displayPlugins = [];
const bulkBarHost = this.bulkEditMode ? this.contentEl.createDiv("manager-bulk-bar-host") : null;
const renderedIds = new Set<string>();
let renderedCount = 0;
for (const [layoutIndex, layoutItem] of layoutItems.entries()) {
if (!this.isRenderCurrent(renderGeneration, page)) return;
if (layoutItem.type === "separator") {
@ -1717,6 +2059,7 @@ export class ManagerModal extends Modal {
itemEl.settingEl.toggleClass("has-update", Boolean(this.manager.updateStatus?.[plugin.id]?.hasUpdate));
itemEl.settingEl.toggleClass("is-bpm-ignored", ManagerPlugin.tags.includes(BPM_IGNORE_TAG));
itemEl.settingEl.toggleClass("is-hidden-layout", hiddenPluginIds.has(plugin.id));
itemEl.settingEl.toggleClass("is-bulk-selected", this.bulkSelectedPluginIds.has(plugin.id));
itemEl.nameEl.addClass("manager-item__name-container");
itemEl.nameEl.addClass("manager-plugin-card__header");
itemEl.descEl.addClass("manager-item__description-container");
@ -1729,9 +2072,26 @@ export class ManagerModal extends Modal {
itemEl.settingEl.addClass("manager-plugin-card--layout-editing");
this.bindPluginLayoutDragHandle(itemEl.settingEl, layoutIndex, ManagerPlugin.name || plugin.name || plugin.id);
}
if (this.bulkEditMode) {
const selection = itemEl.settingEl.createDiv("manager-plugin-card__bulk-select");
const checkbox = selection.createEl("input", { type: "checkbox" });
checkbox.checked = this.bulkSelectedPluginIds.has(plugin.id);
checkbox.disabled = isSelf;
checkbox.setAttribute("aria-label", t("批量编辑_选择插件", { name: ManagerPlugin.name || plugin.name || plugin.id }));
checkbox.addEventListener("click", (event) => event.stopPropagation());
checkbox.addEventListener("change", () => {
this.toggleBulkPluginSelection(plugin.id, checkbox.checked);
});
itemEl.settingEl.addEventListener("click", (event) => {
const target = event.target;
if (target instanceof HTMLElement && target.closest(".manager-plugin-card__actions, .manager-plugin-card__bulk-select, .manager-tag, .clickable-icon, button, input, select, textarea, a")) return;
this.toggleBulkPluginSelection(plugin.id, !this.bulkSelectedPluginIds.has(plugin.id));
});
}
// [右键操作]
itemEl.settingEl.addEventListener("contextmenu", (event) => {
if (this.bulkEditMode) return;
event.preventDefault(); // 阻止默认的右键菜单
const menu = new Menu();
let hasContextMenuItems = false;
@ -1916,9 +2276,10 @@ export class ManagerModal extends Modal {
// [批量操作]
this.displayPlugins.push(plugin);
renderedCount++;
// [目录样式]
if (!this.editorMode) {
if (!this.editorMode && !this.bulkEditMode) {
switch (this.settings.ITEM_STYLE) {
case "alwaysExpand": itemEl.descEl.addClass("manager-display-block"); break;
case "neverExpand": itemEl.descEl.addClass("manager-display-none"); break;
@ -1981,7 +2342,20 @@ export class ManagerModal extends Modal {
const item = groupSettingsById.get(ManagerPlugin.group);
if (item) {
const tag = this.manager.createTag(item.name, item.color, this.settings.GROUP_STYLE);
if (this.editorMode) tag.onclick = () => { new GroupModal(this.app, this.manager, this, ManagerPlugin).open(); };
tag.addClass("manager-item__group-chip");
tag.setAttribute("role", "button");
tag.setAttribute("tabindex", "0");
tag.setAttribute("aria-label", this.manager.translator.t("分组编辑_打开切换", { name: ManagerPlugin.name || plugin.name || plugin.id }));
const openGroupModal = (event?: Event) => {
event?.preventDefault();
event?.stopPropagation();
new GroupModal(this.app, this.manager, this, ManagerPlugin).open();
};
tag.onclick = openGroupModal;
tag.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
openGroupModal(event);
});
group.appendChild(tag);
}
}
@ -2024,26 +2398,8 @@ export class ManagerModal extends Modal {
versionWrap.appendChild(version);
if (!this.editorMode) {
versionWrap.addClass("manager-item__versions--clickable");
versionWrap.addEventListener("click", async () => {
const progress = this.showInlineProgress(this.manager.translator.t("通知_获取版本中文案"), plugin.id);
progress.update(0, 1, plugin.id);
try {
const st = await this.manager.checkUpdateForPlugin(plugin.id);
progress.update(1, 1, plugin.id);
this.refreshSinglePluginUpdateUi(plugin.id);
this.updateStats();
const versions = st?.versions && st.versions.length > 0
? st.versions
: st?.remoteVersion
? [{ version: st.remoteVersion, prerelease: /-/.test(st.remoteVersion) }]
: [];
new UpdateModal(this.app, this.manager, plugin.id, versions, st?.remoteVersion ?? null, st?.repo || undefined).open();
} catch (e) {
console.error("[BPM] fetch remote versions failed", e);
new Notice(this.manager.translator.t("管理器_选择版本_获取失败提示"), 4000);
} finally {
progress.hide();
}
versionWrap.addEventListener("click", () => {
void this.openPluginVersionList(plugin.id, this.manager.updateStatus?.[plugin.id] as PluginUpdateViewStatus | undefined);
});
}
const updateInfo = this.manager.updateStatus?.[plugin.id];
@ -2561,6 +2917,13 @@ export class ManagerModal extends Modal {
console.log("[BPM] render showData after loop, cards:", cards.length, "ids:", cards.map(el => el.getAttribute("data-plugin-id")).filter(Boolean).join(","));
}
}
if (bulkBarHost) this.renderBulkBar(bulkBarHost);
if (renderedCount === 0) {
const empty = this.contentEl.createDiv("bpm-empty-state manager-plugin-page__empty");
const icon = empty.createDiv();
setIcon(icon, "search-x");
empty.createDiv({ cls: "bpm-empty-state__text", text: t("管理器_暂无匹配插件") });
}
// 计算页尾
this.updateStats();
}
@ -2678,6 +3041,8 @@ export class ManagerModal extends Modal {
this.desktopActionWrapper?.classList.toggle("is-ribbon-page", isRibbon);
this.desktopActionWrapper?.classList.toggle("is-troubleshoot-page", isTroubleshoot);
this.desktopActionWrapper?.classList.toggle("is-layout-editing", showLayoutTools);
this.desktopActionWrapper?.classList.toggle("is-bulk-editing", isPlugins && this.bulkEditMode);
this.bulkEditButtonEl?.classList.toggle("is-active", this.bulkEditMode);
if (this.desktopFilterWrapper) {
this.desktopFilterWrapper.classList.toggle("manager-display-none", !isPlugins);
this.desktopFilterWrapper.style.display = isPlugins ? "" : "none";
@ -2694,6 +3059,11 @@ export class ManagerModal extends Modal {
return;
}
this.activePage = page;
if (page !== "plugins" && this.bulkEditMode) {
this.bulkEditMode = false;
this.bulkSelectedPluginIds.clear();
this.applyEditingStyle();
}
this.syncPageChrome();
this.renderContent();
}
@ -5351,15 +5721,21 @@ export class ManagerModal extends Modal {
this.contentEl.empty();
if (this.manager.ribbonModal === this.ribbonPage) this.manager.ribbonModal = null;
if (this.modalContainer) this.modalContainer.removeClass("manager-container--editing");
if (this.modalContainer) this.modalContainer.removeClass("manager-container--bulk-editing");
}
private applyEditingStyle() {
if (!this.modalContainer) return;
if (this.editorMode) {
this.modalContainer.addClass("manager-container--editing");
this.modalContainer.addClass("manager-container--editing");
} else {
this.modalContainer.removeClass("manager-container--editing");
}
if (this.bulkEditMode) {
this.modalContainer.addClass("manager-container--bulk-editing");
} else {
this.modalContainer.removeClass("manager-container--bulk-editing");
}
if (this.desktopActionWrapper) this.syncPageChrome();
}
}

View file

@ -115,7 +115,8 @@ export class TagsModal extends Modal {
const previewLine = itemEl.nameEl.createDiv('manager-tag-editor__preview-line');
const tagEl = this.manager.createTag(tag.name || tag.id, tag.color, this.settings.TAG_STYLE);
tagEl.addClass('manager-tag-editor__chip');
previewLine.appendChild(tagEl);
const chipSlot = previewLine.createSpan({ cls: 'manager-tag-editor__chip-slot' });
chipSlot.appendChild(tagEl);
previewLine.createSpan({ cls: 'manager-tag-editor__id', text: tag.id });
if (isPreset) previewLine.createSpan({ cls: 'manager-tag-editor__badge is-system', text: t('通用_系统_文本') });
if (assigned) previewLine.createSpan({ cls: 'manager-tag-editor__badge is-assigned', text: t('通用_已分配_文本') });
@ -211,52 +212,103 @@ export class TagsModal extends Modal {
}
}
if (this.add) {
let id = '';
let name = '';
let color = this.pickDistinctColor(this.settings.TAGS.map(t => t.color));
const foodBar = new Setting(page).setClass('manager-bar__title');
foodBar.settingEl.addClass('manager-tag-editor__add-panel');
foodBar.nameEl.empty();
foodBar.descEl.empty();
const addTitle = foodBar.nameEl.createDiv('manager-tag-editor__add-title');
const addIcon = addTitle.createSpan({ cls: 'manager-tag-editor__add-title-icon' });
setIcon(addIcon, 'plus');
addTitle.createSpan({ text: t('设置_标签设置_新增标签') });
foodBar.descEl.setText(t('设置_分类_ID说明'));
foodBar.addColorPicker(cb => cb
.setValue(color)
.onChange((value) => { color = value; })
)
foodBar.addText(cb => cb
.setPlaceholder('ID')
.onChange((value) => { id = value; })
.inputEl.addClass('manager-editor__item-input')
)
foodBar.addText(cb => cb
.setPlaceholder(this.manager.translator.t('通用_名称_文本'))
.onChange((value) => { name = value; })
.inputEl.addClass('manager-editor__item-input')
)
foodBar.addExtraButton((cb) => {
cb.setIcon('plus');
this.prepareIconButton(cb, t('标签编辑_创建标签'), 'manager-tag-editor__save-button');
cb.onClick(() => {
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 (color === '') color = this.pickDistinctColor(this.settings.TAGS.map(t => t.color));
this.manager.settings.TAGS.push({ id: nextId, name: nextName, color });
this.manager.saveSettings();
this.add = false;
this.reloadShowData();
Commands(this.app, this.manager);
new Notice(this.manager.translator.t('设置_标签设置_通知_一'));
} else {
new Notice(this.manager.translator.t('设置_标签设置_通知_二'));
}
});
})
const addPanel = page.createDiv('manager-tag-editor__create-panel');
const preview = addPanel.createDiv('manager-tag-editor__create-preview');
const previewMain = preview.createDiv('manager-tag-editor__create-preview-main');
const previewChip = this.manager.createTag(t('通用_名称_文本'), color, this.settings.TAG_STYLE);
previewChip.addClass('manager-tag-editor__chip');
previewChip.addClass('manager-tag-editor__create-chip');
const previewChipSlot = previewMain.createSpan({ cls: 'manager-tag-editor__chip-slot manager-tag-editor__chip-slot--preview' });
previewChipSlot.appendChild(previewChip);
const previewText = previewMain.createDiv('manager-tag-editor__create-preview-text');
previewText.createDiv({ cls: 'manager-tag-editor__create-title', text: t('设置_标签设置_新增标签') });
const previewId = previewText.createDiv({ cls: 'manager-tag-editor__create-id', text: 'ID' });
preview.createDiv({ cls: 'manager-tag-editor__create-desc', text: t('设置_分类_新增描述') });
const form = addPanel.createDiv('manager-tag-editor__create-form');
const createField = (label: string, className = '') => {
const field = form.createDiv(`manager-tag-editor__create-field ${className}`);
field.createDiv({ cls: 'manager-tag-editor__create-label', text: label });
return field;
};
const colorField = createField(t('通用_颜色_文本'), 'manager-tag-editor__create-field--color');
const colorInput = colorField.createEl('input');
colorInput.type = 'color';
colorInput.value = color;
colorInput.addClass('manager-tag-editor__create-color');
colorInput.setAttribute('aria-label', t('通用_颜色_文本'));
const idField = createField(t('通用_ID_文本'));
const idInput = idField.createEl('input');
idInput.type = 'text';
idInput.placeholder = 'tag-id';
idInput.spellcheck = false;
idInput.addClass('manager-editor__item-input');
idInput.addClass('manager-tag-editor__create-input');
idInput.setAttribute('aria-label', t('通用_ID_文本'));
const nameField = createField(t('通用_名称_文本'));
const nameInput = nameField.createEl('input');
nameInput.type = 'text';
nameInput.placeholder = t('通用_名称_文本');
nameInput.addClass('manager-editor__item-input');
nameInput.addClass('manager-tag-editor__create-input');
nameInput.setAttribute('aria-label', t('通用_名称_文本'));
const updatePreview = () => {
const nextId = idInput.value.trim();
const nextName = nameInput.value.trim();
previewChip.textContent = nextName || nextId || t('通用_名称_文本');
previewChip.setAttribute('style', this.manager.generateTagStyle(color, this.settings.TAG_STYLE));
previewId.setText(nextId || 'ID');
};
const submit = () => {
const nextId = idInput.value.trim();
const nextName = nameInput.value.trim() || nextId;
const containsId = this.manager.settings.TAGS.some(tag => tag.id === nextId);
if (!containsId && nextId !== '' && nextId !== BPM_TAG_ID && nextId !== BPM_IGNORE_TAG) {
this.manager.settings.TAGS.push({ id: nextId, name: nextName, color });
this.manager.saveSettings();
this.add = false;
this.reloadShowData();
Commands(this.app, this.manager);
new Notice(this.manager.translator.t('设置_标签设置_通知_一'));
} else {
new Notice(this.manager.translator.t('设置_标签设置_通知_二'));
}
};
const onEnter = (event: KeyboardEvent) => {
if (event.key !== 'Enter') return;
event.preventDefault();
submit();
};
colorInput.addEventListener('input', () => {
color = colorInput.value;
updatePreview();
});
idInput.addEventListener('input', () => updatePreview());
nameInput.addEventListener('input', () => updatePreview());
idInput.addEventListener('keydown', onEnter);
nameInput.addEventListener('keydown', onEnter);
const actions = addPanel.createDiv('manager-tag-editor__create-actions');
const cancelButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action' });
cancelButton.type = 'button';
cancelButton.setAttribute('aria-label', t('通用_取消_文本'));
setIcon(cancelButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'x');
cancelButton.createSpan({ text: t('通用_取消_文本') });
cancelButton.addEventListener('click', () => {
this.add = false;
this.reloadShowData();
});
const submitButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action manager-tag-editor__create-action--primary' });
submitButton.type = 'button';
submitButton.setAttribute('aria-label', t('标签编辑_创建标签'));
setIcon(submitButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'plus');
submitButton.createSpan({ text: t('标签编辑_创建标签') });
submitButton.addEventListener('click', submit);
window.setTimeout(() => idInput.focus(), 0);
} else {
// [底部行] 新增
const foodBar = new Setting(page).setClass('manager-bar__title').setName(this.manager.translator.t('通用_新增_文本'));

View file

@ -1,4 +1,4 @@
import { App, DropdownComponent, Modal, Notice, Setting } from "obsidian";
import { App, ExtraButtonComponent, Modal, Notice, setIcon, Setting } from "obsidian";
import Manager from "main";
import { ReleaseVersion, fetchReleaseVersions } from "src/github-install";
@ -8,6 +8,9 @@ export class UpdateModal extends Modal {
private versions: ReleaseVersion[];
private defaultVersion?: string | null;
private repo?: string;
private selectedVersion = "";
private downloadButton?: HTMLButtonElement;
private versionListEl?: HTMLElement;
constructor(app: App, manager: Manager, pluginId: string, versions: ReleaseVersion[], defaultVersion?: string | null, repo?: string) {
super(app);
@ -20,15 +23,55 @@ export class UpdateModal extends Modal {
async onOpen() {
const { contentEl } = this;
const t = (k: any) => this.manager.translator.t(k);
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
const manifest = this.manager.appPlugins.manifests?.[this.pluginId];
contentEl.empty();
this.selectedVersion = this.defaultVersion || manifest?.version || (this.versions[0]?.version ?? "");
const title = contentEl.createEl("h3", { text: t("管理器_选择版本_标题") });
title.style.marginBottom = "8px";
const modalEl = contentEl.parentElement as HTMLElement | null;
modalEl?.addClass("manager-editor__container");
modalEl?.addClass("manager-version-picker");
modalEl?.getElementsByClassName("modal-close-button")[0]?.remove();
this.titleEl.parentElement?.addClass("manager-container__header");
contentEl.addClass("manager-item-container");
contentEl.addClass("manager-version-picker__body");
const titleBar = new Setting(this.titleEl).setClass("manager-bar__title");
titleBar.settingEl.addClass("manager-version-picker__titlebar");
titleBar.nameEl.empty();
titleBar.descEl.empty();
const titleWrap = titleBar.nameEl.createDiv("manager-version-picker__title");
const titleIcon = titleWrap.createSpan({ cls: "manager-version-picker__title-icon" });
setIcon(titleIcon, "package-down");
const titleText = titleWrap.createDiv("manager-version-picker__title-text");
titleText.createDiv({ cls: "manager-version-picker__eyebrow", text: t("管理器_选择版本_标题") });
titleText.createDiv({ cls: "manager-version-picker__plugin-name", text: manifest?.name || this.pluginId });
titleBar.descEl.setText(t("管理器_选择版本_说明"));
const closeButton = new ExtraButtonComponent(titleBar.controlEl);
closeButton.setIcon("x");
closeButton.setTooltip(t("通用_取消_文本"));
const closeEl = ((closeButton as any).extraSettingsEl || (closeButton as any).buttonEl) as HTMLElement | undefined;
closeEl?.setAttribute("aria-label", t("通用_取消_文本"));
closeButton.onClick(() => this.close());
const page = contentEl.createDiv("manager-version-picker__page");
const summary = page.createDiv("manager-version-picker__summary");
const summaryMain = summary.createDiv("manager-version-picker__summary-main");
summaryMain.createDiv({ cls: "manager-version-picker__summary-title", text: t("管理器_选择版本_列表标题") });
summaryMain.createDiv({
cls: "manager-version-picker__summary-desc",
text: t("管理器_选择版本_当前版本", { version: manifest?.version || "-" }),
});
const summaryStats = summary.createDiv("manager-version-picker__summary-stats");
const countStat = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" });
setIcon(countStat.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "tags");
countStat.createSpan({ text: `${this.versions.length}` });
this.versionListEl = page.createDiv("manager-version-picker__list");
let versionList = [...this.versions];
if (versionList.length === 0 && this.repo) {
const loading = contentEl.createDiv();
const loading = this.versionListEl.createDiv("manager-version-picker__empty");
loading.setText(t("管理器_选择版本_获取中"));
try {
versionList = await fetchReleaseVersions(this.manager, this.repo);
@ -41,30 +84,42 @@ export class UpdateModal extends Modal {
this.versions = versionList;
}
let selected = this.defaultVersion || (versionList[0]?.version ?? "");
if (versionList.length > 0) {
new Setting(contentEl)
.setName(t("管理器_选择版本_版本_标题"))
.addDropdown((dd: DropdownComponent) => {
versionList.forEach(v => {
dd.addOption(v.version, `${v.version}${v.prerelease ? " (pre)" : ""}`);
});
dd.setValue(selected);
dd.onChange((v) => { selected = v; });
});
const hasDefaultVersion = Boolean(this.defaultVersion && versionList.some((release) => release.version === this.defaultVersion));
const hasLocalVersion = Boolean(manifest?.version && versionList.some((release) => release.version === manifest.version));
this.selectedVersion = hasDefaultVersion
? this.defaultVersion!
: hasLocalVersion
? manifest!.version
: (versionList[0]?.version ?? "");
summaryStats.empty();
const releaseCount = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" });
setIcon(releaseCount.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "tags");
releaseCount.createSpan({ text: t("管理器_选择版本_版本数量", { count: versionList.length }) });
const currentStat = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" });
setIcon(currentStat.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "badge-check");
currentStat.createSpan({ text: this.selectedVersion || t("管理器_安装_版本_默认最新") });
if (versionList.length > 0 && this.versionListEl) {
this.renderVersionList(versionList, manifest?.version || "");
} else {
const info = contentEl.createDiv();
const info = this.versionListEl?.createDiv("manager-version-picker__empty") ?? page.createDiv("manager-version-picker__empty");
info.setText(t("管理器_选择版本_无版本提示"));
}
new Setting(contentEl)
const footer = new Setting(page);
footer.settingEl.addClass("manager-version-picker__footer");
footer.nameEl.empty();
footer.descEl.empty();
footer
.addButton((btn) => {
btn.setButtonText(t("管理器_选择版本_下载按钮"));
btn.setButtonText(t("管理器_选择版本_切换按钮"));
btn.setCta();
this.downloadButton = btn.buttonEl;
btn.setDisabled(!this.selectedVersion);
btn.onClick(async () => {
btn.setDisabled(true);
try {
const ok = await this.manager.downloadUpdate(this.pluginId, selected);
const ok = await this.manager.downloadUpdate(this.pluginId, this.selectedVersion);
if (ok) {
new Notice(t("管理器_选择版本_成功提示"), 3000);
this.close();
@ -79,5 +134,49 @@ export class UpdateModal extends Modal {
btn.onClick(() => this.close());
});
}
private renderVersionList(versionList: ReleaseVersion[], localVersion: string) {
if (!this.versionListEl) return;
const t = (k: any, vars?: Record<string, string | number | boolean | null | undefined>) => this.manager.translator.t(k, vars);
this.versionListEl.empty();
versionList.forEach((release) => {
const item = this.versionListEl!.createEl("button", { cls: "manager-version-picker__item" });
item.type = "button";
item.toggleClass("is-selected", release.version === this.selectedVersion);
item.toggleClass("is-current", Boolean(localVersion && release.version === localVersion));
item.setAttribute("aria-pressed", `${release.version === this.selectedVersion}`);
item.setAttribute("aria-label", t("管理器_选择版本_选择版本", { version: release.version }));
const main = item.createDiv("manager-version-picker__item-main");
const title = main.createDiv("manager-version-picker__item-title");
title.createSpan({ cls: "manager-version-picker__item-version", text: release.version });
if (release.version === localVersion) {
title.createSpan({ cls: "manager-version-picker__badge", text: t("来源_当前") });
}
title.createSpan({
cls: `manager-version-picker__badge ${release.prerelease ? "is-prerelease" : "is-stable"}`,
text: t(release.prerelease ? "安装_发布类型_预发布" : "安装_发布类型_正式版"),
});
const meta = main.createDiv("manager-version-picker__item-meta");
if (release.name && release.name !== release.version) meta.createSpan({ text: release.name });
if (release.publishedAt) meta.createSpan({ text: this.formatDate(release.publishedAt) });
const check = item.createSpan({ cls: "manager-version-picker__item-check" });
setIcon(check, release.version === this.selectedVersion ? "check" : "circle");
item.addEventListener("click", () => {
this.selectedVersion = release.version;
this.renderVersionList(versionList, localVersion);
this.downloadButton?.removeAttribute("disabled");
});
});
}
private formatDate(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString();
}
}

1335
styles.css

File diff suppressed because it is too large Load diff