Compare commits

...

19 commits
1.7.8 ... main

Author SHA1 Message Date
Andrea Alberti
565e5cb37b
build: version bump to 1.7.12 2026-07-10 10:38:56 +02:00
Andrea Alberti
6ba7c001be
build: new release notes 2026-07-10 10:38:48 +02:00
Andrea Alberti
4c77b281b6
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.
2026-07-10 10:34:49 +02:00
Andrea Alberti
5e72862016
chore: update minimatch to patch CVE-2026-27903 2026-03-18 21:39:50 +01:00
Andrea Alberti
4acac3602b
build: version bump to 1.7.11 2026-02-25 15:34:05 +01:00
Andrea Alberti
d6c6c468ad
build: new release notes 2026-02-25 15:33:43 +01:00
Andrea Alberti
a2c0bc8a1e
build: added automated workflow for publishing releases
- versions.json — added "1.7.10": "1.5.0" so the current version is represented; npm version will keep it updated from here on
- .github/workflows/release.yml — new file; triggers on any X.Y.Z tag, builds the plugin, uses the matching release-notes/release-X.Y.Z.md (or generates a placeholder), appends a full-changelog link, and creates a draft release with dist/main.js, dist/manifest.json, dist/styles.css

To cut a release going forward:

npm version patch   # or minor / major / explicit version
git push && git push --tags

Then review and publish the draft on GitHub.
2026-02-25 15:33:42 +01:00
Andrea Alberti
83e84bfa15
Added previous releases 2026-02-25 15:33:42 +01:00
Andrea Alberti
6ffeebb771
Fix github icon appearing with the correct light/dark theme 2026-02-25 15:01:25 +01:00
Andrea Alberti
c076b8175b
Merge PR: Fix mobile annotation focus and touch handling (refs #24, PR #25)
Update annotation_control.ts
2026-01-24 20:57:40 +01:00
Andrea Alberti
3ba82f30c8
Merge branch 'main' into testing_annotation_control 2026-01-24 20:57:13 +01:00
Andrea Alberti
4a8f777602 Handle nested link clicks in annotations 2026-01-24 20:44:31 +01:00
Stef Nado
4845905453 Fix mobile annotation focus and touch handling (refs #24, PR #25) 2026-01-24 20:14:45 +01:00
Andrea Alberti
5740615518 Fix community plugins hook for Obsidian 1.11.5 (refs #26) 2026-01-24 19:36:40 +01:00
Andrea Alberti
50f36810d3 Added Sublime Text project file 2026-01-24 16:40:26 +01:00
Andrea Alberti
cdfe973a1f Updated gitignore 2026-01-19 22:48:26 +01:00
Stef Nado
6974fd0c3d
Update annotation_control.ts 2025-10-13 20:07:08 -04:00
Andrea Alberti
84ecaaeaa2 eslint.config.js 2024-11-09 09:34:21 +01:00
Andrea Alberti
1ef188de8c Version bump 2024-10-20 16:15:41 +02:00
56 changed files with 560 additions and 165 deletions

View file

@ -1,3 +0,0 @@
node_modules/
main.js

50
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Release
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- name: Prepare release notes
run: |
VERSION="${GITHUB_REF_NAME}"
NOTES="release-notes/release-${VERSION}.md"
if [ ! -f "$NOTES" ]; then
echo "No release notes provided for version ${VERSION}." > "$NOTES"
fi
PREV_TAG=$(git tag --sort=-version:refname | grep -A1 "^${VERSION}$" | tail -1)
if [ -n "$PREV_TAG" ] && [ "$PREV_TAG" != "$VERSION" ]; then
echo "" >> "$NOTES"
echo "**Full changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${VERSION}" >> "$NOTES"
fi
- uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
body_path: release-notes/release-${{ github.ref_name }}.md
make_latest: true
draft: true
prerelease: false
files: |
dist/main.js
dist/manifest.json
dist/styles.css

6
.gitignore vendored
View file

@ -18,3 +18,9 @@ data.json
# Exclude macOS Finder (System Explorer) View States # Exclude macOS Finder (System Explorer) View States
.DS_Store .DS_Store
# iCLoud
*.nosync
# Sublime
*sublime-workspace

3
.npmrc
View file

@ -1 +1,2 @@
tag-version-prefix="" tag-version-prefix=""
message=build: version bump to %s

43
eslint.config.js Normal file
View file

@ -0,0 +1,43 @@
module.exports = {
env: {
node: true, // Add this line to define Node.js global variables like `module`
es6: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
],
ignorePatterns: ['node_modules', 'dist', 'coverage', '**/*.d.ts', 'tests'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'interface',
format: ['PascalCase'],
custom: {
regex: '^I[A-Z]',
match: true
}
}
],
'@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/quotes': [
'error',
'single',
{ avoidEscape: true, allowTemplateLiterals: false }
],
curly: ['error', 'all'],
eqeqeq: 'error',
'prefer-arrow-callback': 'error'
}
};

View file

@ -1,11 +1,11 @@
{ {
"id": "plugins-annotations", "id": "plugins-annotations",
"name": "Plugins Annotations", "name": "Plugins Annotations",
"version": "1.7.7", "version": "1.7.12",
"minAppVersion": "1.5.0", "minAppVersion": "1.13.0",
"description": "Allows adding personal comments to each installed plugin.", "description": "Allows adding personal comments to each installed plugin.",
"author": "Andrea Alberti", "author": "Andrea Alberti",
"authorUrl": "https://www.linkedin.com/in/dr-andrea-alberti/", "authorUrl": "https://www.linkedin.com/in/dr-andrea-alberti/",
"fundingUrl": "https://buymeacoffee.com/alberti", "fundingUrl": "https://buymeacoffee.com/alberti",
"isDesktopOnly": false "isDesktopOnly": false
} }

View file

@ -0,0 +1,29 @@
{
"folders": [
{
"path": ".",
"folder_exclude_patterns": [".git", "node_modules", "dist"]
}
],
"settings": {
"tab_size": 4,
"translate_tabs_to_spaces": true,
"typescript_tsdk": "./node_modules/typescript/lib"
},
"build_systems": [
{
"name": "TypeScript Build",
"shell_cmd": "npm run build",
"working_dir": "${folder}", // Use ${folder} to point to the project root
"file_regex": "^\\s*(.+?\\.ts)\\((\\d+),(\\d+)\\):\\s*(.*)$",
"selector": "source.ts"
},
{
"name": "TypeScript Dev",
"shell_cmd": "npm run dev",
"working_dir": "${folder}", // Use ${folder} to point to the project root
"file_regex": "^\\s*(.+?\\.ts):(\\d+):(\\d+):\\s*(.*)$",
"selector": "source.ts"
}
]
}

11
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "plugins-annotations", "name": "plugins-annotations",
"version": "1.0.0", "version": "1.7.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "plugins-annotations", "name": "plugins-annotations",
"version": "1.0.0", "version": "1.7.12",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"monkey-around": "^3.0.0" "monkey-around": "^3.0.0"
@ -1871,10 +1871,11 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"

View file

@ -1,6 +1,6 @@
{ {
"name": "plugins-annotations", "name": "plugins-annotations",
"version": "1.0.0", "version": "1.7.12",
"description": "Allows adding personal comments to each installed plugin.", "description": "Allows adding personal comments to each installed plugin.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,12 @@
# Changelog
## [1.0.12] - 2024-06-23
### Bug fixes
- **Mobile Optimization**:
- Resolved the issue where the caret and keyboard were not shown when clicking on placeholder text in mobile devices.
- Clicking on a placeholder text now selects the entire text, allowing users to easily start typing.
### Bug Fixes
- **Annotation Deletion**:
- Fixed an issue where annotations were not correctly deleted when the text was removed.

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,7 @@
# Changelog version 1.1.0
## Added features
- Setting panels for configuring plugin annotations.
- Option to hide annotations when they are empty, improving the cleanliness of the interface.
- Enhanced the user experience by allowing the removal of annotations for uninstalled plugins from memory.

View file

@ -0,0 +1 @@
Small fix correcting a typo.

View file

@ -0,0 +1,5 @@
# Change log
## Added:
- Editing notes as simple text, Markdown, and HTML.

View file

@ -0,0 +1,5 @@
# Change log
## Fixes
- Trim annotations to remove empty lines and spaces. If the user wants to enforce empty spaces, HTML-formatted code can be used.

View file

@ -0,0 +1,5 @@
# Change log
## New features
- Added a lock button to lock annotations and prevent empty annotation fields from showing up when hovering over with the mouse.

View file

@ -0,0 +1,5 @@
# Change log
# New minor feature
- Added customization of the placeholder label appearing when no user annotation is provided. It supports HTML-formatted text.

View file

@ -0,0 +1,14 @@
# Changelog for Version 1.4.0
## New Features
- Added an option to automatically remove annotations when a plugin is uninstalled.
- Displaying the names of uninstalled plugins, allowing users to manually remove stored annotations if desired.
## Bug Fixes
- Fixed an issue where personal annotations were not displayed when filtering installed plugins using the search field.
## Under the Hood Improvements
- Enhanced data format to store the names of plugins.
- Refactored code for better performance and maintainability.
This release includes significant improvements, bug fixes, and new features to enhance your experience with the plugin. Enjoy the updates!

View file

@ -0,0 +1,12 @@
# Change log
## New features
- Possible to provide a Markdown file (for example in `00 Meta/Misc/Plugins annotations.md`), where the annotations are stored. This allows editing the annotations directly from the Obsidian editor. Most importantly, it allows one to use Obsidian links and having the links automatically updated when the linked files in the vault are renamed.
- Added to the preference panes a place to manage backups of the annotations.
- Added possibility to export and import annotations as json file.
- Many other improvements under the hood and refactored the code.
## Changes
- The default type for the plugin annotations has now changed from `text` to `markdown`.

View file

@ -0,0 +1,12 @@
# Change log
## New features
- Possible to provide a Markdown file (for example in `00 Meta/Misc/Plugins annotations.md`), where the annotations are stored. This allows editing the annotations directly from the Obsidian editor. Most importantly, it allows one to use Obsidian links and having the links automatically updated when the linked files in the vault are renamed.
- Added to the preference panes a place to manage backups of the annotations.
- Added possibility to export and import annotations as json file.
- Many other improvements under the hood and refactored the code.
## Changes
- The default type for the plugin annotations has now changed from `text` to `markdown`.

View file

@ -0,0 +1,12 @@
# Change log
## New features
- Possible to provide a Markdown file (for example in `00 Meta/Misc/Plugins annotations.md`), where the annotations are stored. This allows editing the annotations directly from the Obsidian editor. Most importantly, it allows one to use Obsidian links and having the links automatically updated when the linked files in the vault are renamed.
- Added to the preference panes a place to manage backups of the annotations.
- Added possibility to export and import annotations as json file.
- Many other improvements under the hood and refactored the code.
## Changes
- The default type for the plugin annotations has now changed from `text` to `markdown`.

View file

@ -0,0 +1,6 @@
# Change log
## Minor improvements
- Improved explanation of backup functionality in the preference pane
- Improve visualization of the preference pane for mobile devices (optimized for with small screens).

View file

@ -0,0 +1,16 @@
# Change log
## New features
- Added backup of annotations
## Bugs and improvements
- Corrected a bug where the links were not clickable if the notes were not editable.
- Corrected a bug where the lock icon was not correctly displayed the first time the pane was opened.
- Refactoring the code, which improves clarity and allows more extensions in the future.
- Improved explanations in the preference pane.
## Removed features
- HTML and text annotations have been discontinued. It was too much of an effort to maintain them, while there is no advantage over using Markdown. You can still use HTML in Markdown.

View file

@ -0,0 +1,17 @@
# Change log
## New features
- Added backup of annotations
- Notes exported to the markdown file are sorted out alphabetically.
## Bugs and improvements
- Corrected a bug where the links were not clickable if the notes were not editable.
- Corrected a bug where the lock icon was not correctly displayed the first time the pane was opened.
- Refactoring the code, which improves clarity and allows more extensions in the future.
- Improved explanations in the preference pane.
## Removed features
- HTML and text annotations have been discontinued. It was too much of an effort to maintain them, while there is no advantage over using Markdown. You can still use HTML in Markdown.

View file

@ -0,0 +1,11 @@
# Change log
## New features
- Added restore buttons to the settings for default configurations
- Added the possibility to edit annotations of uninstalled plugins in the preference pane.
- Improved explanation in the preference pane.
## Bug fixes
- Many bugs corrected (still testing import / export / backup functions).

View file

@ -0,0 +1,6 @@
# Change log
## Bug fixes and performance improvements
- Improved debounced saving functions.
- Fixed a bug when switching between the preference pane and the community plugins pane, where the latest changes in the settings were lost.

View file

@ -0,0 +1,5 @@
# Change log
## Bug fixes
- Fixed bug where buttons resetting to default conditions did not properly save the new setting.

View file

@ -0,0 +1,6 @@
# Change log
## Bug fixes
- When disabling the plugin, the lock icon is now removed.
- This should fix the bug of multiple lock icons being created when the plugin `Divide & Conquer` was used. The plugin disables and re-enables plugins. Every time this plugin was reloaded, a new icon was created because the previous one was never removed.

View file

@ -0,0 +1,5 @@
# Change log
## Bug fixes
- Fixed bug where annotations could not be added when some text is provided in the search field to restrict the range of annotations displayed.

View file

@ -0,0 +1,6 @@
# Change log
## New features
- Added GitHub icons linking to the plugin's GitHub page. A configuration setting allows one to decide whether to show or not these icons.
- Various improvements under the hood.

View file

@ -0,0 +1,7 @@
# Change log
## New features
- Added GitHub icons linking to the plugin's GitHub page. A configuration setting allows one to decide whether to show or not these icons.
- Various improvements under the hood.
- Some fixes resolving minor bugs in 1.7.0

View file

@ -0,0 +1,5 @@
# Change log
## Bug fix
- Fix mobile annotation focus and touch handling (refs #24, PR #25)

View file

@ -0,0 +1,5 @@
# Change log
## Bug fix
- Correctly identify Obsidian appearance (light/dark theme) for displaying GitHub icon matched to the theme

View file

@ -0,0 +1,9 @@
# Change log
## Bug fix
- Restore personal annotations in the **Community plugins** settings tab. Obsidian 1.13 redesigned that tab, which stopped annotations (and the lock icon and GitHub icons) from appearing. The plugin now hooks into the new settings UI.
## Compatibility
- Requires Obsidian **1.13.0** or newer. Users on older Obsidian versions keep receiving 1.7.11.

View file

@ -0,0 +1,7 @@
# Change log
## New features
- Added GitHub icons linking to the plugin's GitHub page. A configuration setting allows one to decide whether to show or not these icons.
- Various improvements under the hood.
- Some fixes resolving minor bugs in 1.7.1

View file

@ -0,0 +1,7 @@
# Change log
## Bug fixes
- Fixed bug where a warning was shown when a new plugin was installed.
- Fixed small bugs where the code is now more efficient, i.e., avoids repeating unnecessary operations
- Corrected a bug where, when enabling this plugin, the personal annotations were not immediately shown in the preference pane. Instead, it required to exit the preference pane and enter it again. Now, this is no longer necessary.

View file

@ -0,0 +1,5 @@
# Change log
## Bug fixes
- Fixed a bug that caused a conflict with [Hotkey Helper plugin](https://github.com/pjeby/hotkey-helper) and possibly could have conflicted with other plugins that make use of `openTab` Obsidian function for opening preference panes.

View file

@ -0,0 +1,5 @@
# Change log
## Bug fixes
- Streamlined code and remove potential weak spot where `openTab` function was called with an asynchronous mechanism. See discussion on the GitHub [page](https://github.com/alberti42/obsidian-plugins-annotations/issues/14).

View file

@ -0,0 +1,5 @@
# Change log
## New features
- Detect changes to `data.json` made by the synchronisation engine (Obsidian or Google Drive or Dropbox etc) or any external editor. If the Community Preferences window is active, the changes will be shown immediately. Similarly, if changes are made using an external editor to the MarkDown file that stores the personal annotations, these changes will be detected and the preferences pane will be updated immediately to reflect the new changes.

View file

@ -0,0 +1,5 @@
# Change log
## New features
- Detect changes to data.json which can be caused by the synchronization engine or an external editor and update the setting pane automatically.

View file

@ -0,0 +1,5 @@
# Change log
## Bug fix
- Fix community plugins hook for Obsidian 1.11.5 (refs #26)

View file

@ -55,13 +55,20 @@ export class AnnotationControl {
} }
addEventListeners() { addEventListeners() {
this.annotation_div.addEventListener('mousedown', (event:MouseEvent) => { const linkInteractionHandler = (event: MouseEvent | TouchEvent) => {
if (event.target && (event.target as HTMLElement).tagName === 'A') { const target = event.target as HTMLElement | null;
// Use closest('a') so clicks on nested elements (e.g., spans inside links) still register as link clicks.
// This scenario is likely never occurring, but closest('a') keeps link detection reliable (just in case).
if (target && target.closest('a')) {
this.clickedLink = true; this.clickedLink = true;
} else { } else {
this.clickedLink = false; this.clickedLink = false;
} }
}); };
this.annotation_div.addEventListener('mousedown', linkInteractionHandler);
this.annotation_div.addEventListener('touchstart', linkInteractionHandler, { passive: true });
// Prevent click event propagation to parent // Prevent click event propagation to parent
this.annotation_div.addEventListener('click', (event:MouseEvent) => { this.annotation_div.addEventListener('click', (event:MouseEvent) => {
@ -69,6 +76,10 @@ export class AnnotationControl {
return; return;
} else { } else {
event.stopPropagation(); event.stopPropagation();
if (!this.clickedLink) {
// Explicitly focus to help mobile keyboards appear, especially on Android.
this.annotation_div.focus();
}
} }
}); });

View file

@ -2,6 +2,7 @@
import { import {
Plugin, Plugin,
Setting,
SettingTab, SettingTab,
Platform, Platform,
Plugins, Plugins,
@ -38,14 +39,14 @@ export default class PluginsAnnotations extends Plugin {
private community_plugins = {} as CommunityPluginInfoDict; private community_plugins = {} as CommunityPluginInfoDict;
private handleThemeChange: ((event: MediaQueryListEvent) => void) | null = null; // MutationObserver watching document.body class changes to detect Obsidian theme switches
private colorSchemeMedia: MediaQueryList | null = null; private themeObserver: MutationObserver | null = null;
// Declare class methods that will be initialized in the constructor // Declare class methods that will be initialized in the constructor
debouncedSaveAnnotations: (callback?: () => void) => void; debouncedSaveAnnotations: (callback?: () => void) => void;
waitForSaveToComplete: () => Promise<void>; waitForSaveToComplete: () => Promise<void>;
private communityPluginSettingTabPatched = false; private communityPluginSettingTabPatched = false;
private listGitHubIcons:HTMLDivElement[] = []; private listGitHubIcons:HTMLDivElement[] = [];
@ -104,25 +105,26 @@ export default class PluginsAnnotations extends Plugin {
} }
} }
async onLayoutReady() { async onLayoutReady() {
// Load settings // Load settings
const loadSettingsPromise = this.loadSettings(); const loadSettingsPromise = this.loadSettings();
// Load the big json file containing the GitHub address of all community plugins // Load the big json file containing the GitHub address of all community plugins
const loadCommunityPluginsJsonPromise = this.loadCommunityPluginsJson(); const loadCommunityPluginsJsonPromise = this.loadCommunityPluginsJson();
// Store a reference to the community plugin tab // Store a reference to the community plugin tab
this.communityPluginTab = this.app.setting.settingTabs.find((tab:SettingTab):boolean => tab.id === "community-plugins"); this.communityPluginTab = this.app.setting.settingTabs.find((tab:SettingTab):boolean => tab.id === "community-plugins");
// Patch the rendering function of the community plugin preference pane // Patch the rendering function of the community plugin preference pane
if(this.communityPluginTab) this.patchCommunityPluginSettingTab(this.communityPluginTab); if(this.communityPluginTab) {
this.patchCommunityPluginSettingTab(this.communityPluginTab);
} else {
console.warn('[Plugins Annotations] Community plugins tab not found; hook not installed.');
}
// Monkey-patch functions to detect when community plugins are installed and uninstalled. // Monkey-patch functions to detect when community plugins are installed and uninstalled.
this.hookOnInstallAndUninstallPlugins(); this.hookOnInstallAndUninstallPlugins();
// Detect color scheme
this.colorSchemeMedia = matchMedia('(prefers-color-scheme: dark)');
// Install listener to theme changes // Install listener to theme changes
if(this.communityPluginTab && this.communityPluginTab.containerEl) this.listenForThemeChange(this.communityPluginTab.containerEl); if(this.communityPluginTab && this.communityPluginTab.containerEl) this.listenForThemeChange(this.communityPluginTab.containerEl);
@ -406,49 +408,32 @@ export default class PluginsAnnotations extends Plugin {
return invertedMap; return invertedMap;
} }
patchCommunityPluginSettingTab(tab:SettingTab) { patchCommunityPluginSettingTab(tab:SettingTab) {
if(this.communityPluginSettingTabPatched) return; if(this.communityPluginSettingTabPatched) return;
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; const self = this;
// Monkey patch for uninstallPlugin // Monkey patch the per-plugin renderer of the community-plugins tab.
const removeMonkeyPatchForRender = around(tab, { // Obsidian's declarative settings tab calls `renderInstalledPlugin(setting, manifest)`
renderInstalledPlugin: (next: ( // for every plugin row, on both the initial render and every later re-render, so this
pluginManifest: PluginManifest, // is the single hook we need for injecting annotations and the lock icon.
containerEl:HTMLElement, const removeMonkeyPatchForRender = around(tab, {
nameMatch: boolean | null, renderInstalledPlugin: (next: SettingTab['renderInstalledPlugin']) => {
authorMatch: boolean | null,
descriptionMatch: boolean | null
) => void ) => {
return function (this: SettingTab, return function (this: SettingTab, setting: Setting, manifest: PluginManifest): void {
pluginManifest: PluginManifest, // Render the plugin row using Obsidian's own implementation first.
containerEl: HTMLElement, next.call(this, setting, manifest);
nameMatch: boolean | null,
authorMatch: boolean | null,
descriptionMatch: boolean | null
): void {
next.call(this, pluginManifest, containerEl, nameMatch, authorMatch, descriptionMatch);
// Custom code for personal annotations here const settingEl = setting.settingEl;
if(containerEl && containerEl.lastElementChild) self.addAnnotation(containerEl.lastElementChild); if (settingEl instanceof HTMLElement) {
}; self.addAnnotation(settingEl, manifest.id);
}, // (Re)add the lock icon to the group heading. It is idempotent.
// Patch for `render` method self.addLockIcon(this.containerEl);
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);
};
}
});
// Register the patch to ensure it gets cleaned up // Register the patch to ensure it gets cleaned up
this.register(removeMonkeyPatchForRender); this.register(removeMonkeyPatchForRender);
@ -501,46 +486,44 @@ export default class PluginsAnnotations extends Plugin {
} }
listenForThemeChange(tabContainer: HTMLElement) { listenForThemeChange(tabContainer: HTMLElement) {
// If listener is already install, we can directly return // If observer is already installed, return immediately to avoid duplicates
if(this.handleThemeChange) return; if (this.themeObserver) return;
// Check if the color scheme was detected // Obsidian sets 'theme-dark' or 'theme-light' on document.body; observe class
if (this.colorSchemeMedia===null) { // attribute changes so GitHub icons update whenever the user switches themes,
console.warn("Color scheme could not be determined."); // regardless of the OS-level color scheme.
return; this.themeObserver = new MutationObserver(() => {
} const isDarkMode = document.body.classList.contains('theme-dark');
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;
});
});
// Create the event listener with the correct signature // Watch only the 'class' attribute to minimise observer overhead
this.handleThemeChange = (event: MediaQueryListEvent): void => { this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
const pluginsContainer = tabContainer.querySelector('.installed-plugins-container'); }
const isDarkMode = event.matches; // true means dark mode is active // 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
if (pluginsContainer) { // group (`.setting-group.mod-list`) specifically rather than the first heading found.
const githubIcons = pluginsContainer.querySelectorAll( private findInstalledPluginsHeadingControl(containerEl: HTMLElement): Element | null {
'div.setting-item > div.setting-item-control > div.github-icon' const group = containerEl.querySelector('.setting-group.mod-list');
); return group ? group.querySelector('.setting-item-heading .setting-item-control') : null;
// Iterate over each github icon and set the appropriate SVG
githubIcons.forEach((icon) => {
if (isDarkMode) {
icon.innerHTML = svg_github_dark;
} else {
icon.innerHTML = svg_github_light;
}
});
}
}
// Add an event listener for changes to the appearance mode
this.colorSchemeMedia.addEventListener("change", this.handleThemeChange);
} }
async addLockIcon(containerEl: HTMLElement) { async addLockIcon(containerEl: HTMLElement) {
// Add new icon to the existing icons container // 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) { 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'); this.lockIcon = document.createElement('div');
const lockIcon = this.lockIcon; const lockIcon = this.lockIcon;
@ -610,9 +593,18 @@ export default class PluginsAnnotations extends Plugin {
} }
removeGitHubIcons() { 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) => { this.listGitHubIcons.forEach((iconEl:Element) => {
iconEl.remove(); iconEl.remove();
}) });
this.listGitHubIcons = [];
} }
async loadCommunityPluginsJson() { async loadCommunityPluginsJson() {
@ -645,53 +637,74 @@ export default class PluginsAnnotations extends Plugin {
} }
} }
addAnnotation(pluginDOMElement: Element) { // Extract the plugin name from a `.setting-item-name` element. Newer Obsidian appends
const pluginNameDiv = pluginDOMElement.querySelector('.setting-item-name'); // version/author <span>s to this element, so we read only the leading text node(s)
const pluginName = pluginNameDiv ? pluginNameDiv.textContent : null; // 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) { addAnnotation(pluginDOMElement: Element, pluginId?: string) {
console.warn('Plugin name not found'); if (!pluginId) {
return; // 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;
const pluginId = this.pluginNameToIdMap[pluginName]; if (!pluginName) {
if (!pluginId) { console.warn('Plugin name not found');
console.warn(`Plugin ID not found for plugin name: ${pluginName}`); return;
return; }
}
const settingItemInfo = pluginDOMElement.querySelector('.setting-item-info'); pluginId = this.pluginNameToIdMap[pluginName];
if (settingItemInfo) { }
const descriptionDiv = settingItemInfo.querySelector('.setting-item-description');
if (descriptionDiv) { if (!pluginId) {
const commentDiv = descriptionDiv.querySelector('.plugin-comment'); console.warn('Plugin ID not found for the plugin row.');
if (!commentDiv) { return;
const annotation_container = document.createElement('div'); }
annotation_container.className = 'plugin-comment';
// 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');
if (descriptionDiv) {
const commentDiv = descriptionDiv.querySelector('.plugin-comment');
if (!commentDiv) {
const annotation_container = document.createElement('div');
annotation_container.className = 'plugin-comment';
const annotationControl = new AnnotationControl(this,annotation_container,pluginId,pluginName); const annotationControl = new AnnotationControl(this,annotation_container,pluginId,pluginName);
descriptionDiv.appendChild(annotation_container); descriptionDiv.appendChild(annotation_container);
if(this.settings.show_github_icons) { if(this.settings.show_github_icons) {
// Get the repository of the plugin // Get the repository of the plugin
const community_plugins = this.community_plugins[pluginId]; const community_plugins = this.community_plugins[pluginId];
const repo = community_plugins ? community_plugins.repo : undefined; const repo = community_plugins ? community_plugins.repo : undefined;
if (repo) { if (repo) {
const controlDiv = pluginDOMElement.querySelector('.setting-item-control'); const controlDiv = pluginDOMElement.querySelector('.setting-item-control');
if(controlDiv) { if(controlDiv) {
if(this.colorSchemeMedia) { {
const isDarkMode = this.colorSchemeMedia.matches; // Read Obsidian's active theme from document.body at render time
const gitHubIcon = annotationControl.addGitHubIcon(controlDiv,repo, isDarkMode); const isDarkMode = document.body.classList.contains('theme-dark');
if(gitHubIcon) this.listGitHubIcons.push(gitHubIcon); const gitHubIcon = annotationControl.addGitHubIcon(controlDiv,repo, isDarkMode);
} if(gitHubIcon) this.listGitHubIcons.push(gitHubIcon);
} }
} }
} }
} }
} }
} }
} }
}
addAnnotations() { addAnnotations() {
if(this.communityPluginTab===undefined) { if(this.communityPluginTab===undefined) {
@ -699,13 +712,17 @@ export default class PluginsAnnotations extends Plugin {
return; 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) { 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; 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 => { pluginDOMElements.forEach(pluginDOMElement => {
this.addAnnotation(pluginDOMElement); this.addAnnotation(pluginDOMElement);
}); });
@ -721,11 +738,10 @@ export default class PluginsAnnotations extends Plugin {
} }
removeHandleThemeChangeListener() { removeHandleThemeChangeListener() {
// Remove listeners // Disconnect and release the MutationObserver used for theme detection
if (this.handleThemeChange) { if (this.themeObserver) {
if (this.colorSchemeMedia) { this.themeObserver.disconnect();
this.colorSchemeMedia.removeEventListener("change", this.handleThemeChange); this.themeObserver = null;
}
} }
} }
@ -760,4 +776,3 @@ export default class PluginsAnnotations extends Plugin {
return uninstalledPlugins; return uninstalledPlugins;
} }
} }

View file

@ -18,15 +18,9 @@ declare module "obsidian" {
id: string; id: string;
name: string; name: string;
navEl: HTMLElement; navEl: HTMLElement;
// updateSearch(e: string): void; // Per-plugin row renderer of the community-plugins tab (Obsidian >= 1.13):
render(isInitialRender:boolean):void; // `setting` is the row's Setting component (exposes `settingEl`), `manifest` its plugin manifest.
renderInstalledPlugin( renderInstalledPlugin(setting: Setting, manifest: PluginManifest): void;
pluginManifest: PluginManifest,
containerEl: HTMLElement,
nameMatch: boolean | null,
authorMatch: boolean | null,
descriptionMatch: boolean | null
): void;
} }
interface Setting { interface Setting {
onOpen(): void; onOpen(): void;
@ -70,4 +64,4 @@ declare module "obsidian" {
textInputEl: HTMLInputElement; textInputEl: HTMLInputElement;
} }
} }

View file

@ -1,3 +1,6 @@
{ {
"1.0.0": "0.15.0" "1.0.0": "0.15.0",
} "1.7.10": "1.5.0",
"1.7.11": "1.5.0",
"1.7.12": "1.13.0"
}