mirror of
https://github.com/alberti42/obsidian-plugins-annotations.git
synced 2026-07-22 10:10:24 +00:00
fix: support Obsidian 1.13 community plugins tab redesign
Obsidian 1.13.0 rewrote the community-plugins settings tab: the plugin list is now a declarative schema, renderInstalledPlugin's signature is (setting, manifest), the installedPlugins container and its .installed-plugins-container class are gone (list is now .setting-group.mod-list), the name element gained version/author spans, and the render() method was removed. Retarget the monkey-patch at the new API: take settingEl and the plugin id straight from the render arguments, place the lock icon in the installed-plugins group heading, and fix the bulk re-scan and theme-observer selectors. Raise minAppVersion to 1.13.0 accordingly; older Obsidian keeps being offered 1.7.11 via versions.json.
This commit is contained in:
parent
5e72862016
commit
4c77b281b6
3 changed files with 86 additions and 62 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"id": "plugins-annotations",
|
||||
"name": "Plugins Annotations",
|
||||
"version": "1.7.11",
|
||||
"minAppVersion": "1.5.0",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "Allows adding personal comments to each installed plugin.",
|
||||
"author": "Andrea Alberti",
|
||||
"authorUrl": "https://www.linkedin.com/in/dr-andrea-alberti/",
|
||||
|
|
|
|||
135
src/main.ts
135
src/main.ts
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import {
|
||||
Plugin,
|
||||
Setting,
|
||||
SettingTab,
|
||||
Platform,
|
||||
Plugins,
|
||||
|
|
@ -413,38 +414,24 @@ export default class PluginsAnnotations extends Plugin {
|
|||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
|
||||
// Monkey patch for uninstallPlugin
|
||||
// Monkey patch the per-plugin renderer of the community-plugins tab.
|
||||
// Obsidian's declarative settings tab calls `renderInstalledPlugin(setting, manifest)`
|
||||
// for every plugin row, on both the initial render and every later re-render, so this
|
||||
// is the single hook we need for injecting annotations and the lock icon.
|
||||
const removeMonkeyPatchForRender = around(tab, {
|
||||
renderInstalledPlugin: (next: SettingTab['renderInstalledPlugin']) => {
|
||||
|
||||
return function (this: SettingTab,
|
||||
pluginManifest: PluginManifest,
|
||||
nameMatch: boolean | null,
|
||||
authorMatch: boolean | null,
|
||||
descriptionMatch: boolean | null
|
||||
): void {
|
||||
next.call(this, pluginManifest, nameMatch, authorMatch, descriptionMatch);
|
||||
return function (this: SettingTab, setting: Setting, manifest: PluginManifest): void {
|
||||
// Render the plugin row using Obsidian's own implementation first.
|
||||
next.call(this, setting, manifest);
|
||||
|
||||
// Custom code for personal annotations here
|
||||
const listEl = (this as SettingTab & { installedPlugins?: { listEl?: HTMLElement } }).installedPlugins?.listEl;
|
||||
const targetElement = listEl?.lastElementChild;
|
||||
if (targetElement instanceof HTMLElement) {
|
||||
self.addAnnotation(targetElement);
|
||||
const settingEl = setting.settingEl;
|
||||
if (settingEl instanceof HTMLElement) {
|
||||
self.addAnnotation(settingEl, manifest.id);
|
||||
// (Re)add the lock icon to the group heading. It is idempotent.
|
||||
self.addLockIcon(this.containerEl);
|
||||
}
|
||||
};
|
||||
},
|
||||
// Patch for `render` method
|
||||
render: (next: (
|
||||
isInitialRender: boolean
|
||||
) => void) => {
|
||||
|
||||
return function (this: SettingTab, isInitialRender: boolean): void {
|
||||
self.listGitHubIcons = [];
|
||||
|
||||
// Call the original `render` function
|
||||
next.call(this, isInitialRender);
|
||||
self.addLockIcon(this.containerEl);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -507,27 +494,36 @@ export default class PluginsAnnotations extends Plugin {
|
|||
// regardless of the OS-level color scheme.
|
||||
this.themeObserver = new MutationObserver(() => {
|
||||
const isDarkMode = document.body.classList.contains('theme-dark');
|
||||
const pluginsContainer = tabContainer.querySelector('.installed-plugins-container');
|
||||
if (pluginsContainer) {
|
||||
const githubIcons = pluginsContainer.querySelectorAll(
|
||||
'div.setting-item > div.setting-item-control > div.github-icon'
|
||||
);
|
||||
// Update each icon to match the new theme
|
||||
githubIcons.forEach((icon) => {
|
||||
icon.innerHTML = isDarkMode ? svg_github_dark : svg_github_light;
|
||||
});
|
||||
}
|
||||
const githubIcons = tabContainer.querySelectorAll(
|
||||
'div.setting-item > div.setting-item-control > div.github-icon'
|
||||
);
|
||||
// Update each icon to match the new theme
|
||||
githubIcons.forEach((icon) => {
|
||||
icon.innerHTML = isDarkMode ? svg_github_dark : svg_github_light;
|
||||
});
|
||||
});
|
||||
|
||||
// Watch only the 'class' attribute to minimise observer overhead
|
||||
this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
|
||||
// Locate the control area of the "Installed plugins" group heading, where the lock
|
||||
// icon lives. This tab renders several group headings, so we target the installed-plugins
|
||||
// group (`.setting-group.mod-list`) specifically rather than the first heading found.
|
||||
private findInstalledPluginsHeadingControl(containerEl: HTMLElement): Element | null {
|
||||
const group = containerEl.querySelector('.setting-group.mod-list');
|
||||
return group ? group.querySelector('.setting-item-heading .setting-item-control') : null;
|
||||
}
|
||||
|
||||
async addLockIcon(containerEl: HTMLElement) {
|
||||
// Add new icon to the existing icons container
|
||||
const headingContainer = containerEl.querySelector('.setting-item-heading .setting-item-control');
|
||||
const headingContainer = this.findInstalledPluginsHeadingControl(containerEl);
|
||||
if (headingContainer) {
|
||||
|
||||
// Idempotent: skip if a lock icon is already present in this heading.
|
||||
if (this.lockIcon && this.lockIcon.isConnected && headingContainer.contains(this.lockIcon)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lockIcon = document.createElement('div');
|
||||
|
||||
const lockIcon = this.lockIcon;
|
||||
|
|
@ -597,9 +593,18 @@ export default class PluginsAnnotations extends Plugin {
|
|||
}
|
||||
|
||||
removeGitHubIcons() {
|
||||
// Remove any GitHub icons currently in the tab. We query the DOM (rather than rely
|
||||
// solely on the tracked list) so icons re-created by Obsidian's render reconciliation
|
||||
// are also cleaned up.
|
||||
if (this.communityPluginTab) {
|
||||
this.communityPluginTab.containerEl.querySelectorAll('.github-icon').forEach((iconEl:Element) => {
|
||||
iconEl.remove();
|
||||
});
|
||||
}
|
||||
this.listGitHubIcons.forEach((iconEl:Element) => {
|
||||
iconEl.remove();
|
||||
})
|
||||
});
|
||||
this.listGitHubIcons = [];
|
||||
}
|
||||
|
||||
async loadCommunityPluginsJson() {
|
||||
|
|
@ -632,21 +637,41 @@ export default class PluginsAnnotations extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
addAnnotation(pluginDOMElement: Element) {
|
||||
const pluginNameDiv = pluginDOMElement.querySelector('.setting-item-name');
|
||||
const pluginName = pluginNameDiv ? pluginNameDiv.textContent : null;
|
||||
// Extract the plugin name from a `.setting-item-name` element. Newer Obsidian appends
|
||||
// version/author <span>s to this element, so we read only the leading text node(s)
|
||||
// rather than `textContent` (which would include the version and author).
|
||||
private extractPluginName(nameEl: Element): string {
|
||||
let text = '';
|
||||
nameEl.childNodes.forEach((node: ChildNode) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
text += node.textContent ?? '';
|
||||
}
|
||||
});
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
if (!pluginName) {
|
||||
console.warn('Plugin name not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginId = this.pluginNameToIdMap[pluginName];
|
||||
addAnnotation(pluginDOMElement: Element, pluginId?: string) {
|
||||
if (!pluginId) {
|
||||
console.warn(`Plugin ID not found for plugin name: ${pluginName}`);
|
||||
// No id supplied (e.g. bulk re-scan): derive it from the displayed plugin name.
|
||||
const pluginNameDiv = pluginDOMElement.querySelector('.setting-item-name');
|
||||
const pluginName = pluginNameDiv ? this.extractPluginName(pluginNameDiv) : null;
|
||||
|
||||
if (!pluginName) {
|
||||
console.warn('Plugin name not found');
|
||||
return;
|
||||
}
|
||||
|
||||
pluginId = this.pluginNameToIdMap[pluginName];
|
||||
}
|
||||
|
||||
if (!pluginId) {
|
||||
console.warn('Plugin ID not found for the plugin row.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the canonical name from the installed manifests (falls back to the id).
|
||||
const pluginName = this.pluginIdToNameMap[pluginId] ?? pluginId;
|
||||
|
||||
const settingItemInfo = pluginDOMElement.querySelector('.setting-item-info');
|
||||
if (settingItemInfo) {
|
||||
const descriptionDiv = settingItemInfo.querySelector('.setting-item-description');
|
||||
|
|
@ -687,13 +712,17 @@ export default class PluginsAnnotations extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
const pluginsContainer = this.communityPluginTab.containerEl.querySelector('.installed-plugins-container');
|
||||
const containerEl = this.communityPluginTab.containerEl;
|
||||
// The installed plugins are rendered inside `.setting-group.mod-list`; its
|
||||
// `.setting-items` element holds the individual plugin rows.
|
||||
const pluginsContainer = containerEl.querySelector('.setting-group.mod-list .setting-items');
|
||||
if (!pluginsContainer) {
|
||||
console.warn("Annotations could not be added because installed-plugins-container was not detected.")
|
||||
console.warn("Annotations could not be added because the installed-plugins list was not detected.")
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginDOMElements = pluginsContainer.querySelectorAll('.setting-item');
|
||||
|
||||
// Only iterate direct plugin rows, skipping the group heading (also a `.setting-item`).
|
||||
const pluginDOMElements = pluginsContainer.querySelectorAll(':scope > .setting-item:not(.setting-item-heading)');
|
||||
pluginDOMElements.forEach(pluginDOMElement => {
|
||||
this.addAnnotation(pluginDOMElement);
|
||||
});
|
||||
|
|
|
|||
11
src/types/obsidian-augment.d.ts
vendored
11
src/types/obsidian-augment.d.ts
vendored
|
|
@ -18,14 +18,9 @@ declare module "obsidian" {
|
|||
id: string;
|
||||
name: string;
|
||||
navEl: HTMLElement;
|
||||
// updateSearch(e: string): void;
|
||||
render(isInitialRender:boolean):void;
|
||||
renderInstalledPlugin(
|
||||
pluginManifest: PluginManifest,
|
||||
nameMatch: boolean | null,
|
||||
authorMatch: boolean | null,
|
||||
descriptionMatch: boolean | null
|
||||
): void;
|
||||
// Per-plugin row renderer of the community-plugins tab (Obsidian >= 1.13):
|
||||
// `setting` is the row's Setting component (exposes `settingEl`), `manifest` its plugin manifest.
|
||||
renderInstalledPlugin(setting: Setting, manifest: PluginManifest): void;
|
||||
}
|
||||
interface Setting {
|
||||
onOpen(): void;
|
||||
|
|
|
|||
Loading…
Reference in a new issue