diff --git a/src/defaults.ts b/src/defaults.ts index 002e5d0..ba40836 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -3,22 +3,22 @@ import { AnnotationType, PluginsAnnotationsSettings } from './types'; export const DEFAULT_SETTINGS: PluginsAnnotationsSettings = { - annotations: { - "plugins-annotations": { - "name": "Plugins Annotations", - "desc": "Allows writing annotations (just like this one) about the community plugins installed in the vault.", - "type": AnnotationType.markdown, - } - }, - plugins_annotations_uuid: 'BC56AB7B-A46F-4ACF-9BA1-3A4461F74C79', - hide_placeholders: false, - delete_placeholder_string_on_insertion: false, - label_mobile: 'Annotation: ', - label_desktop: 'Personal annotation: ', - label_placeholder : "Add your personal comment about ${plugin_name} here...", - editable: true, - automatic_remove: false, - markdown_file_path: '', - backups: [], - compatibility: '1.5.0', + annotations: { + "plugins-annotations": { + "name": "Plugins Annotations", + "desc": "Allows writing annotations (just like this one) about the community plugins installed in the vault.", + "type": AnnotationType.markdown, + } + }, + plugins_annotations_uuid: 'BC56AB7B-A46F-4ACF-9BA1-3A4461F74C79', + hide_placeholders: false, + delete_placeholder_string_on_insertion: false, + label_mobile: 'Annotation: ', + label_desktop: 'Personal annotation: ', + label_placeholder : "Add your personal comment about ${plugin_name} here...", + editable: true, + automatic_remove: false, + markdown_file_path: '', + backups: [], + compatibility: '1.5.0', } diff --git a/src/defaults_legacy.ts b/src/defaults_legacy.ts index c96363d..b82f86a 100644 --- a/src/defaults_legacy.ts +++ b/src/defaults_legacy.ts @@ -10,9 +10,9 @@ const { annotations:_, markdown_file_path:__, compatibility:___, backups:____, . export const DEFAULT_SETTINGS_1_4_0: PluginsAnnotationsSettings_1_4_0 = { - ...theRestingSettings_1_4_0, - annotations: {}, - plugins_annotations_uuid: 'B265C5B2-A6AD-4194-9E4C-C1327DB1EA18', + ...theRestingSettings_1_4_0, + annotations: {}, + plugins_annotations_uuid: 'B265C5B2-A6AD-4194-9E4C-C1327DB1EA18', } diff --git a/src/main.ts b/src/main.ts index 1df7b0d..f8f27d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,17 @@ // main.ts import { - Plugin, - Setting, - SettingTab, - Platform, - MarkdownRenderer, - Plugins, - PluginManifest, - FileSystemAdapter, - TAbstractFile, - // PluginSettingTab, - // App, + Plugin, + Setting, + SettingTab, + Platform, + MarkdownRenderer, + Plugins, + PluginManifest, + FileSystemAdapter, + TAbstractFile, + // PluginSettingTab, + // App, } from 'obsidian'; import { around } from 'monkey-around'; import { AnnotationType, isPluginAnnotation, isPluginsAnnotationsSettings, parseAnnotation, PluginAnnotationDict, PluginBackup, PluginsAnnotationsSettings } from './types'; @@ -24,481 +24,481 @@ import { readAnnotationsFromMdFile, writeAnnotationsToMdFile } from 'manageAnnot import { sortPluginAnnotationsByName } from 'utils'; export default class PluginsAnnotations extends Plugin { - settings: PluginsAnnotationsSettings = structuredClone(DEFAULT_SETTINGS); - pluginNameToIdMap: Record = {}; - pluginIdToNameMap: Record = {}; + settings: PluginsAnnotationsSettings = structuredClone(DEFAULT_SETTINGS); + pluginNameToIdMap: Record = {}; + pluginIdToNameMap: Record = {}; - private mutationObserver: MutationObserver | null = null; - private saveTimeout: number | null = null; - private observedTab: SettingTab | null = null; - private vaultPath: string | null = null; + private mutationObserver: MutationObserver | null = null; + private saveTimeout: number | null = null; + private observedTab: SettingTab | null = null; + private vaultPath: string | null = null; - async onload() { + async onload() { - // console.clear(); - - // console.log('Loading Plugins Annotations'); + // console.clear(); + + // console.log('Loading Plugins Annotations'); - // Add settings tab. It avoids loading the setting at this stage - // because the cache about the files in the vault is not created yet. - this.addSettingTab(new PluginsAnnotationsSettingTab(this.app, this)); - - this.app.workspace.onLayoutReady(() => { - this.patchSettings(); + // Add settings tab. It avoids loading the setting at this stage + // because the cache about the files in the vault is not created yet. + this.addSettingTab(new PluginsAnnotationsSettingTab(this.app, this)); + + this.app.workspace.onLayoutReady(() => { + this.patchSettings(); - const activeTab = this.app.setting.activeTab; - if (activeTab && activeTab.id === 'community-plugins') { - this.observeTab(activeTab); - } - }); + const activeTab = this.app.setting.activeTab; + if (activeTab && activeTab.id === 'community-plugins') { + this.observeTab(activeTab); + } + }); - this.app.vault.on('modify', (modifiedFile: TAbstractFile) => { - if(this.settings.markdown_file_path !== '') { - if (modifiedFile.path === this.settings.markdown_file_path) { - readAnnotationsFromMdFile(this); - } - } - }); - } + this.app.vault.on('modify', (modifiedFile: TAbstractFile) => { + if(this.settings.markdown_file_path !== '') { + if (modifiedFile.path === this.settings.markdown_file_path) { + readAnnotationsFromMdFile(this); + } + } + }); + } - /* Load settings for different versions */ - async importSettings(data: unknown): Promise<{importedSettings: unknown, wasUpdated: boolean}> { + /* Load settings for different versions */ + async importSettings(data: unknown): Promise<{importedSettings: unknown, wasUpdated: boolean}> { - // Set to true when the settings are updated to the new format - let wasUpdated = false; - - // Nested function to handle different versions of settings - const getSettingsFromData = async (data: unknown): Promise => { - - if(data === null) { // if the file is empty - return data; - } else if (isPluginsAnnotationsSettings(data)) { - const settings: PluginsAnnotationsSettings = data; - return settings; - } else if (isSettingsFormat_1_4_0(data)) { // previous versions where the name of the plugins was not stored - // Make a backup - await this.backupSettings('Settings before upgrade from 1.4 to 1.5',data); + // Set to true when the settings are updated to the new format + let wasUpdated = false; + + // Nested function to handle different versions of settings + const getSettingsFromData = async (data: unknown): Promise => { + + if(data === null) { // if the file is empty + return data; + } else if (isPluginsAnnotationsSettings(data)) { + const settings: PluginsAnnotationsSettings = data; + return settings; + } else if (isSettingsFormat_1_4_0(data)) { // previous versions where the name of the plugins was not stored + // Make a backup + await this.backupSettings('Settings before upgrade from 1.4 to 1.5',data); - const default_settings = DEFAULT_SETTINGS; + const default_settings = DEFAULT_SETTINGS; - // Upgrade annotations format - const upgradedAnnotations: PluginAnnotationDict = {}; - for (const pluginId in data.annotations) { - const annotation = data.annotations[pluginId]; - const {type,content} = parseAnnotation_1_4_0(annotation.anno); - upgradedAnnotations[pluginId] = { - name: annotation.name, - desc: content, - type: type, - }; - } + // Upgrade annotations format + const upgradedAnnotations: PluginAnnotationDict = {}; + for (const pluginId in data.annotations) { + const annotation = data.annotations[pluginId]; + const {type,content} = parseAnnotation_1_4_0(annotation.anno); + upgradedAnnotations[pluginId] = { + name: annotation.name, + desc: content, + type: type, + }; + } - const oldSettings: PluginsAnnotationsSettings_1_4_0 = data; + const oldSettings: PluginsAnnotationsSettings_1_4_0 = data; - // Update the data with the new format - const newSettings: PluginsAnnotationsSettings = { - ...oldSettings, - annotations: upgradedAnnotations, - plugins_annotations_uuid: default_settings.plugins_annotations_uuid, - backups: this.settings.backups, - compatibility: default_settings.compatibility, - markdown_file_path: default_settings.markdown_file_path - }; - wasUpdated = true; + // Update the data with the new format + const newSettings: PluginsAnnotationsSettings = { + ...oldSettings, + annotations: upgradedAnnotations, + plugins_annotations_uuid: default_settings.plugins_annotations_uuid, + backups: this.settings.backups, + compatibility: default_settings.compatibility, + markdown_file_path: default_settings.markdown_file_path + }; + wasUpdated = true; - return await getSettingsFromData(newSettings); - } else if (isSettingsFormat_1_3_0(data)) { // previous versions where the name of the plugins was not stored - // Make a backup - await this.backupSettings('Settings before upgrade from 1.3 to 1.4',data); + return await getSettingsFromData(newSettings); + } else if (isSettingsFormat_1_3_0(data)) { // previous versions where the name of the plugins was not stored + // Make a backup + await this.backupSettings('Settings before upgrade from 1.3 to 1.4',data); - const default_settings_1_4_0 = DEFAULT_SETTINGS_1_4_0 + const default_settings_1_4_0 = DEFAULT_SETTINGS_1_4_0 - // Upgrade annotations format - const upgradedAnnotations: PluginAnnotationDict_1_4_0 = {}; - - for (const pluginId in data.annotations) { - const annotation = data.annotations[pluginId]; - upgradedAnnotations[pluginId] = { - name: this.pluginIdToNameMap[pluginId] || pluginId, - anno: annotation, - }; - } - const oldSettings: PluginsAnnotationsSettings_1_3_0 = data; + // Upgrade annotations format + const upgradedAnnotations: PluginAnnotationDict_1_4_0 = {}; + + for (const pluginId in data.annotations) { + const annotation = data.annotations[pluginId]; + upgradedAnnotations[pluginId] = { + name: this.pluginIdToNameMap[pluginId] || pluginId, + anno: annotation, + }; + } + const oldSettings: PluginsAnnotationsSettings_1_3_0 = data; - // Update the data with the new format - const newSettings: PluginsAnnotationsSettings_1_4_0 = { - ...oldSettings, - annotations: upgradedAnnotations, - plugins_annotations_uuid: default_settings_1_4_0.plugins_annotations_uuid, - }; - wasUpdated = true; - return await getSettingsFromData(newSettings); - } else { - // Make a backup - await this.backupSettings('Settings before upgrade from 1.0 to 1.3',data); + // Update the data with the new format + const newSettings: PluginsAnnotationsSettings_1_4_0 = { + ...oldSettings, + annotations: upgradedAnnotations, + plugins_annotations_uuid: default_settings_1_4_0.plugins_annotations_uuid, + }; + wasUpdated = true; + return await getSettingsFromData(newSettings); + } else { + // Make a backup + await this.backupSettings('Settings before upgrade from 1.0 to 1.3',data); - const default_settings_1_3_0 = structuredClone(DEFAULT_SETTINGS_1_3_0); + const default_settings_1_3_0 = structuredClone(DEFAULT_SETTINGS_1_3_0); - // Very first version of the plugin 1.0 -- no options were stored, only the dictionary of annotations - const newSettings: PluginsAnnotationsSettings_1_3_0 = default_settings_1_3_0; - newSettings.annotations = isPluginAnnotationDictFormat_1_3_0(data) ? data : default_settings_1_3_0.annotations; - wasUpdated = true; - return await getSettingsFromData(newSettings); - } - }; + // Very first version of the plugin 1.0 -- no options were stored, only the dictionary of annotations + const newSettings: PluginsAnnotationsSettings_1_3_0 = default_settings_1_3_0; + newSettings.annotations = isPluginAnnotationDictFormat_1_3_0(data) ? data : default_settings_1_3_0.annotations; + wasUpdated = true; + return await getSettingsFromData(newSettings); + } + }; - const importedSettings = await getSettingsFromData(data); + const importedSettings = await getSettingsFromData(data); - return {importedSettings, wasUpdated}; - } + return {importedSettings, wasUpdated}; + } - async backupSettings(backupName: string, settings: unknown) { - // Ensure settings is an object - if (typeof settings !== 'object' || settings === null) return; + async backupSettings(backupName: string, settings: unknown) { + // Ensure settings is an object + if (typeof settings !== 'object' || settings === null) return; - let settingsWithoutBackup; + let settingsWithoutBackup; - // Remove the backups field from the settings to be backed up - if (settings.hasOwnProperty('backups')) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { backups: _, ...rest } = settings as { backups: unknown }; - settingsWithoutBackup = rest; - } else { - settingsWithoutBackup = settings; - } + // Remove the backups field from the settings to be backed up + if (settings.hasOwnProperty('backups')) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { backups: _, ...rest } = settings as { backups: unknown }; + settingsWithoutBackup = rest; + } else { + settingsWithoutBackup = settings; + } - // Deep copy - const deepCopiedSettings = structuredClone(settingsWithoutBackup); + // Deep copy + const deepCopiedSettings = structuredClone(settingsWithoutBackup); - // Add the backup with the deep-copied settings - this.settings.backups.push({ - name: backupName, - date: new Date(), - settings: deepCopiedSettings - }); + // Add the backup with the deep-copied settings + this.settings.backups.push({ + name: backupName, + date: new Date(), + settings: deepCopiedSettings + }); - await this.saveSettings(); - } + await this.saveSettings(); + } - async loadSettings(data?: unknown, forceSave?: boolean): Promise { - - // Create a mapping of names to IDs for the installed plugins - this.pluginNameToIdMap = this.constructPluginNameToIdMap(); - this.pluginIdToNameMap = this.generateInvertedMap(this.pluginNameToIdMap); - - if(data === undefined) { - data = await this.loadData(); - } + async loadSettings(data?: unknown, forceSave?: boolean): Promise { + + // Create a mapping of names to IDs for the installed plugins + this.pluginNameToIdMap = this.constructPluginNameToIdMap(); + this.pluginIdToNameMap = this.generateInvertedMap(this.pluginNameToIdMap); + + if(data === undefined) { + data = await this.loadData(); + } - if(forceSave === undefined) { - forceSave = false; - } + if(forceSave === undefined) { + forceSave = false; + } - if (!data || typeof data !== 'object') { - console.error('Invalid settings.'); - return; - } + if (!data || typeof data !== 'object') { + console.error('Invalid settings.'); + return; + } - const {importedSettings, wasUpdated} = await this.importSettings(data); + const {importedSettings, wasUpdated} = await this.importSettings(data); - // Merge loaded settings with default settings - this.settings = Object.assign({}, structuredClone(DEFAULT_SETTINGS), importedSettings); - - if (this.settings.backups) { - this.settings.backups.forEach((backup: PluginBackup) => { - backup.date = new Date(backup.date); // Convert the date string to a Date object - }); - } + // Merge loaded settings with default settings + this.settings = Object.assign({}, structuredClone(DEFAULT_SETTINGS), importedSettings); + + if (this.settings.backups) { + this.settings.backups.forEach((backup: PluginBackup) => { + backup.date = new Date(backup.date); // Convert the date string to a Date object + }); + } - if(forceSave || wasUpdated) { // if it requires to store the new settings, the .md file will be overwritten - await this.saveSettings(); - } else { // otherwise read from the md file - if(this.settings.markdown_file_path!=='') { - await readAnnotationsFromMdFile(this); - } - } - } + if(forceSave || wasUpdated) { // if it requires to store the new settings, the .md file will be overwritten + await this.saveSettings(); + } else { // otherwise read from the md file + if(this.settings.markdown_file_path!=='') { + await readAnnotationsFromMdFile(this); + } + } + } - // Store the path to the vault - getVaultPath():string { - if(this.vaultPath) return this.vaultPath; + // Store the path to the vault + getVaultPath():string { + if(this.vaultPath) return this.vaultPath; - if (Platform.isDesktopApp) { - // store the vault path - const adapter = this.app.vault.adapter; - if (!(adapter instanceof FileSystemAdapter)) { - throw new Error("The vault folder could not be determined."); - } - // Normalize to POSIX-style path - this.vaultPath = adapter.getBasePath().split(path.sep).join(path.posix.sep); - - return this.vaultPath; - } else return ""; - } - - async saveSettings() { - try { - await this.saveData(this.settings); - } catch (error) { - console.error('Failed to save annotations:', error); - } - if(this.settings.markdown_file_path!=='') { - try { - await writeAnnotationsToMdFile(this); - } catch (error) { - console.error('Failed to save annotations to md file:', error); - } - } - } + if (Platform.isDesktopApp) { + // store the vault path + const adapter = this.app.vault.adapter; + if (!(adapter instanceof FileSystemAdapter)) { + throw new Error("The vault folder could not be determined."); + } + // Normalize to POSIX-style path + this.vaultPath = adapter.getBasePath().split(path.sep).join(path.posix.sep); + + return this.vaultPath; + } else return ""; + } + + async saveSettings() { + try { + await this.saveData(this.settings); + } catch (error) { + console.error('Failed to save annotations:', error); + } + if(this.settings.markdown_file_path!=='') { + try { + await writeAnnotationsToMdFile(this); + } catch (error) { + console.error('Failed to save annotations to md file:', error); + } + } + } - constructPluginNameToIdMap(): Record < string, string > { - const map: Record < string, string > = {}; - for (const pluginId in this.app.plugins.manifests) { - const plugin = this.app.plugins.manifests[pluginId]; - if (plugin) { - map[plugin.name] = plugin.id; - } - } - return map; - } + constructPluginNameToIdMap(): Record < string, string > { + const map: Record < string, string > = {}; + for (const pluginId in this.app.plugins.manifests) { + const plugin = this.app.plugins.manifests[pluginId]; + if (plugin) { + map[plugin.name] = plugin.id; + } + } + return map; + } - // Function to generate the inverted map - generateInvertedMap(originalMap: Record < string, string >) { - const invertedMap: Record < string, string > = {}; - for (const key in originalMap) { - if (originalMap.hasOwnProperty(key)) { - const value = originalMap[key]; - invertedMap[value] = key; - } - } - return invertedMap; - } + // Function to generate the inverted map + generateInvertedMap(originalMap: Record < string, string >) { + const invertedMap: Record < string, string > = {}; + for (const key in originalMap) { + if (originalMap.hasOwnProperty(key)) { + const value = originalMap[key]; + invertedMap[value] = key; + } + } + return invertedMap; + } - patchSettings() { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - - // Patch openTab to detect when a tab is opened - const removeMonkeyPatchForSetting = around(this.app.setting, { - openTab: (next: (tab: SettingTab) => void) => { - return function(this: Setting, tab: SettingTab) { - next.call(this, tab); - if (tab && tab.id === 'community-plugins') { - if(self.observedTab!==tab) - { - self.observeTab(tab); - } - } - }; - }, - onClose: (next: () => void) => { - return function (this: Setting) { - const result = next.call(this); - // closing settings pane - self.disconnectObservers(); - return result; - }; - } - }); + patchSettings() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + // Patch openTab to detect when a tab is opened + const removeMonkeyPatchForSetting = around(this.app.setting, { + openTab: (next: (tab: SettingTab) => void) => { + return function(this: Setting, tab: SettingTab) { + next.call(this, tab); + if (tab && tab.id === 'community-plugins') { + if(self.observedTab!==tab) + { + self.observeTab(tab); + } + } + }; + }, + onClose: (next: () => void) => { + return function (this: Setting) { + const result = next.call(this); + // closing settings pane + self.disconnectObservers(); + return result; + }; + } + }); - // Register the cleanup for openTab patch - this.register(removeMonkeyPatchForSetting); + // Register the cleanup for openTab patch + this.register(removeMonkeyPatchForSetting); - // Monkey patch for uninstallPlugin - const removeMonkeyPatchForPlugins = around(this.app.plugins, { - uninstallPlugin: (next: (pluginId: string) => Promise) => { - return async function (this: Plugins, pluginId: string): Promise { - await next.call(this, pluginId); - // Triggered when pluginId has been uninstalled - if (self.settings.automatic_remove && self.settings.annotations.hasOwnProperty(pluginId)) { - // If automatic_remove is enabled and there is an annotation, remove the annotation - delete self.settings.annotations[pluginId]; - self.debouncedSaveAnnotations(); - } - }; - }, - }); + // Monkey patch for uninstallPlugin + const removeMonkeyPatchForPlugins = around(this.app.plugins, { + uninstallPlugin: (next: (pluginId: string) => Promise) => { + return async function (this: Plugins, pluginId: string): Promise { + await next.call(this, pluginId); + // Triggered when pluginId has been uninstalled + if (self.settings.automatic_remove && self.settings.annotations.hasOwnProperty(pluginId)) { + // If automatic_remove is enabled and there is an annotation, remove the annotation + delete self.settings.annotations[pluginId]; + self.debouncedSaveAnnotations(); + } + }; + }, + }); - // Register the patch to ensure it gets cleaned up - this.register(removeMonkeyPatchForPlugins); - } + // Register the patch to ensure it gets cleaned up + this.register(removeMonkeyPatchForPlugins); + } - async observeTab(tab: SettingTab) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; + async observeTab(tab: SettingTab) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; - // Monkey patch for uninstallPlugin - const removeMonkeyPatchForUpdateSearch = around(tab, { - renderInstalledPlugin: (next: ( - pluginManifest: PluginManifest, - containerEl:HTMLElement, - nameMatch: boolean | null, - authorMatch: boolean | null, - descriptionMatch: boolean | null - ) => void ) => { + // Monkey patch for uninstallPlugin + const removeMonkeyPatchForUpdateSearch = around(tab, { + renderInstalledPlugin: (next: ( + pluginManifest: PluginManifest, + containerEl:HTMLElement, + nameMatch: boolean | null, + authorMatch: boolean | null, + descriptionMatch: boolean | null + ) => void ) => { - return function (this: SettingTab, - pluginManifest: PluginManifest, - containerEl: HTMLElement, - nameMatch: boolean | null, - authorMatch: boolean | null, - descriptionMatch: boolean | null - ): void { - next.call(this, pluginManifest, containerEl, nameMatch, authorMatch, descriptionMatch); + return function (this: SettingTab, + pluginManifest: PluginManifest, + containerEl: HTMLElement, + nameMatch: boolean | null, + authorMatch: boolean | null, + descriptionMatch: boolean | null + ): void { + next.call(this, pluginManifest, containerEl, nameMatch, authorMatch, descriptionMatch); - // Add your custom code for personal annotations here - const annotation = self.settings.annotations[pluginManifest.id]; - if (annotation) { - if(containerEl && containerEl.lastElementChild) - { - self.addAnnotation(containerEl.lastElementChild) - } - } - }; - } - }); + // Add your custom code for personal annotations here + const annotation = self.settings.annotations[pluginManifest.id]; + if (annotation) { + if(containerEl && containerEl.lastElementChild) + { + self.addAnnotation(containerEl.lastElementChild) + } + } + }; + } + }); - // // Register the patch to ensure it gets cleaned up - this.register(removeMonkeyPatchForUpdateSearch); + // // Register the patch to ensure it gets cleaned up + this.register(removeMonkeyPatchForUpdateSearch); - if(!this.mutationObserver) { - this.observedTab = tab; + if(!this.mutationObserver) { + this.observedTab = tab; - const observer = new MutationObserver(async () => { + const observer = new MutationObserver(async () => { // force reload - this is convenient because since the loading of the plugin // there could be changes in the settings due to synchronization among devices // which only happens after the plugin is loaded await this.loadSettings(); - this.addIcon(tab); - this.addAnnotations(tab); - }); + this.addIcon(tab); + this.addAnnotations(tab); + }); - observer.observe(tab.containerEl, { childList: true, subtree: false }); - this.mutationObserver = observer; - } + observer.observe(tab.containerEl, { childList: true, subtree: false }); + this.mutationObserver = observer; + } // force reload - this is convenient because since the loading of the plugin // there could be changes in the settings due to synchronization among devices // which only happens after the plugin is loaded await this.loadSettings(); - // Initial call to add comments to already present plugins - this.addIcon(tab); - this.addAnnotations(tab); - } + // Initial call to add comments to already present plugins + this.addIcon(tab); + this.addAnnotations(tab); + } - disconnectObservers() { - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - this.mutationObserver = null; - this.observedTab = null; - } - } + disconnectObservers() { + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + this.observedTab = null; + } + } - // Helper function to parse links and add click listeners - handleAnnotationLinks(element: HTMLElement) { - const links = element.querySelectorAll('a'); - links.forEach(link => { + // Helper function to parse links and add click listeners + handleAnnotationLinks(element: HTMLElement) { + const links = element.querySelectorAll('a'); + links.forEach(link => { console.log(element); - link.addEventListener('click', (event) => { - event.preventDefault(); - const href = link.getAttribute('href'); + link.addEventListener('click', (event) => { + event.preventDefault(); + const href = link.getAttribute('href'); if (href) { - this.app.workspace.openLinkText(href, '', false); - this.app.setting.close(); // Close the settings pane when a link is clicked - } - }); - }); - } + this.app.workspace.openLinkText(href, '', false); + this.app.setting.close(); // Close the settings pane when a link is clicked + } + }); + }); + } - create_label(): HTMLSpanElement | null { - const label = Platform.isMobile ? this.settings.label_mobile : this.settings.label_desktop; - if(label.trim() === "") { - return null; - } else { - const span = document.createElement('span'); - span.innerHTML = label; - span.classList.add('plugin-comment-label'); - return span; - } - } + create_label(): HTMLSpanElement | null { + const label = Platform.isMobile ? this.settings.label_mobile : this.settings.label_desktop; + if(label.trim() === "") { + return null; + } else { + const span = document.createElement('span'); + span.innerHTML = label; + span.classList.add('plugin-comment-label'); + return span; + } + } - async renderAnnotation(annotation_div: HTMLElement, annoType:AnnotationType, desc:string) { - annotation_div.innerText = ''; - switch(annoType) { - case AnnotationType.text: { - const p = document.createElement('p'); - p.dir = 'auto'; - const label = this.create_label(); - if(label) { - p.appendChild(label); - p.appendText(desc); - } - else { - p.innerText = desc; - } - annotation_div.appendChild(p); - break; - } - case AnnotationType.html: { - const label = Platform.isMobile ? this.settings.label_mobile : this.settings.label_desktop; - const desc_with_label = desc.replace(/\$\{label\}/g, label); - annotation_div.innerHTML = desc_with_label; - this.handleAnnotationLinks(annotation_div); - break; - } - case AnnotationType.markdown: { - const label = Platform.isMobile ? this.settings.label_mobile : this.settings.label_desktop; - const desc_with_label = label + desc; - await MarkdownRenderer.renderMarkdown(desc_with_label, annotation_div, '', this); - this.handleAnnotationLinks(annotation_div); - break; - } - } - } + async renderAnnotation(annotation_div: HTMLElement, annoType:AnnotationType, desc:string) { + annotation_div.innerText = ''; + switch(annoType) { + case AnnotationType.text: { + const p = document.createElement('p'); + p.dir = 'auto'; + const label = this.create_label(); + if(label) { + p.appendChild(label); + p.appendText(desc); + } + else { + p.innerText = desc; + } + annotation_div.appendChild(p); + break; + } + case AnnotationType.html: { + const label = Platform.isMobile ? this.settings.label_mobile : this.settings.label_desktop; + const desc_with_label = desc.replace(/\$\{label\}/g, label); + annotation_div.innerHTML = desc_with_label; + this.handleAnnotationLinks(annotation_div); + break; + } + case AnnotationType.markdown: { + const label = Platform.isMobile ? this.settings.label_mobile : this.settings.label_desktop; + const desc_with_label = label + desc; + await MarkdownRenderer.renderMarkdown(desc_with_label, annotation_div, '', this); + this.handleAnnotationLinks(annotation_div); + break; + } + } + } - configureAnnotation(annotation_container:HTMLDivElement,annotation_div:HTMLDivElement,pluginId:string,pluginName:string) { - - if(this.settings.editable) { - annotation_div.contentEditable = 'true'; - annotation_div.classList.add('plugin-comment-annotation-editable'); - } else { - annotation_div.contentEditable = 'false'; - annotation_div.classList.remove('plugin-comment-annotation-editable'); - } + configureAnnotation(annotation_container:HTMLDivElement,annotation_div:HTMLDivElement,pluginId:string,pluginName:string) { + + if(this.settings.editable) { + annotation_div.contentEditable = 'true'; + annotation_div.classList.add('plugin-comment-annotation-editable'); + } else { + annotation_div.contentEditable = 'false'; + annotation_div.classList.remove('plugin-comment-annotation-editable'); + } - const placeholder = (this.settings.label_placeholder).replace(/\$\{plugin_name\}/g, pluginName); + const placeholder = (this.settings.label_placeholder).replace(/\$\{plugin_name\}/g, pluginName); - let isPlaceholder = this.settings.annotations[pluginId] ? false : true; - let annotationDesc:string; - let annoType:AnnotationType; - - if(!isPlaceholder && isPluginAnnotation(this.settings.annotations[pluginId])) { - const annotation = this.settings.annotations[pluginId]; - annotationDesc = annotation.desc; - annoType = annotation.type; - } else { - annotationDesc = placeholder.trim(); - annoType = AnnotationType.html; - - annotation_div.classList.add('plugin-comment-placeholder'); - if (this.settings.hide_placeholders) { // if it is a placeholder - if(this.settings.editable) { // if fields can be edited, set the placeholder tag - annotation_container.classList.add('plugin-comment-placeholder'); - } else { // if fields cannot be edited, just simply hide placeholders - annotation_container.classList.add('plugin-comment-hidden'); - } - } - } + let isPlaceholder = this.settings.annotations[pluginId] ? false : true; + let annotationDesc:string; + let annoType:AnnotationType; + + if(!isPlaceholder && isPluginAnnotation(this.settings.annotations[pluginId])) { + const annotation = this.settings.annotations[pluginId]; + annotationDesc = annotation.desc; + annoType = annotation.type; + } else { + annotationDesc = placeholder.trim(); + annoType = AnnotationType.html; + + annotation_div.classList.add('plugin-comment-placeholder'); + if (this.settings.hide_placeholders) { // if it is a placeholder + if(this.settings.editable) { // if fields can be edited, set the placeholder tag + annotation_container.classList.add('plugin-comment-placeholder'); + } else { // if fields cannot be edited, just simply hide placeholders + annotation_container.classList.add('plugin-comment-hidden'); + } + } + } - // Initial render - this.renderAnnotation(annotation_div,annoType,annotationDesc); + // Initial render + this.renderAnnotation(annotation_div,annoType,annotationDesc); - // Handle mousedown event to check if a link was clicked + // Handle mousedown event to check if a link was clicked let clickedLink = false; - annotation_div.addEventListener('mousedown', (event:MouseEvent) => { + annotation_div.addEventListener('mousedown', (event:MouseEvent) => { console.log("MOUSE DOWN"); if(!this.settings.editable) { return; } if (event.target && (event.target as HTMLElement).tagName === 'A') { @@ -516,258 +516,258 @@ export default class PluginsAnnotations extends Plugin { event.stopPropagation(); }); - // Remove placeholder class when user starts typing - annotation_div.addEventListener('focus', (event:FocusEvent) => { + // Remove placeholder class when user starts typing + annotation_div.addEventListener('focus', (event:FocusEvent) => { console.log("FOCUS"); - if(!this.settings.editable) { return; } - if (isPlaceholder) { - if (this.settings.delete_placeholder_string_on_insertion) { - annotation_div.innerText = ''; - } - annotation_div.classList.remove('plugin-comment-placeholder'); - if (this.settings.hide_placeholders) { - // we remove 'plugin-comment-placeholder' only when 'this.settings.hide_placeholders' is true - // when 'this.settings.hide_placeholders' is false, the class is not set and does not need to be removed. - annotation_container.classList.remove('plugin-comment-placeholder'); - } - - const text = annotation_div.innerText; // text without html markup - annotation_div.innerText = text; // this removes all html markup for editing + if(!this.settings.editable) { return; } + if (isPlaceholder) { + if (this.settings.delete_placeholder_string_on_insertion) { + annotation_div.innerText = ''; + } + annotation_div.classList.remove('plugin-comment-placeholder'); + if (this.settings.hide_placeholders) { + // we remove 'plugin-comment-placeholder' only when 'this.settings.hide_placeholders' is true + // when 'this.settings.hide_placeholders' is false, the class is not set and does not need to be removed. + annotation_container.classList.remove('plugin-comment-placeholder'); + } + + const text = annotation_div.innerText; // text without html markup + annotation_div.innerText = text; // this removes all html markup for editing - // Force a DOM reflow by reading the offsetHeight (or another property) - annotation_div.offsetHeight; + // Force a DOM reflow by reading the offsetHeight (or another property) + annotation_div.offsetHeight; - const range = document.createRange(); - range.selectNodeContents(annotation_div); - const selection = window.getSelection(); - if (selection) { - selection.removeAllRanges(); - selection.addRange(range); - } - } else { - // Only update annotation_div.innerText if not clicking on a link - if (!clickedLink) { - let preamble; - switch(annoType) { - case AnnotationType.html: - preamble = 'html:'; - break; - case AnnotationType.markdown: - preamble = 'markdown:'; - break; - case AnnotationType.text: - preamble = 'text:'; - break; - default: - preamble = 'markdown:'; - } + const range = document.createRange(); + range.selectNodeContents(annotation_div); + const selection = window.getSelection(); + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + } else { + // Only update annotation_div.innerText if not clicking on a link + if (!clickedLink) { + let preamble; + switch(annoType) { + case AnnotationType.html: + preamble = 'html:'; + break; + case AnnotationType.markdown: + preamble = 'markdown:'; + break; + case AnnotationType.text: + preamble = 'text:'; + break; + default: + preamble = 'markdown:'; + } - annotation_div.innerText = preamble + '\n' + annotationDesc; - } - } - }); + annotation_div.innerText = preamble + '\n' + annotationDesc; + } + } + }); - // Save the comment on input change and update inputTriggered status - annotation_div.addEventListener('input', (event: Event) => { - if(!this.settings.editable) return; - isPlaceholder = false; - }); + // Save the comment on input change and update inputTriggered status + annotation_div.addEventListener('input', (event: Event) => { + if(!this.settings.editable) return; + isPlaceholder = false; + }); - // Add placeholder class back if no changes are made - annotation_div.addEventListener('blur', (event:FocusEvent) => { - if(!this.settings.editable) { return; } + // Add placeholder class back if no changes are made + annotation_div.addEventListener('blur', (event:FocusEvent) => { + if(!this.settings.editable) { return; } - const {annoType: type, annoDesc: content} = parseAnnotation(annotation_div.innerText.trim()); + const {annoType: type, annoDesc: content} = parseAnnotation(annotation_div.innerText.trim()); - if (isPlaceholder || content === '') { // placeholder - annotation_div.innerHTML = placeholder; - delete this.settings.annotations[pluginId]; - annotation_div.classList.add('plugin-comment-placeholder'); - if (this.settings.hide_placeholders) { - annotation_container.classList.add('plugin-comment-placeholder'); - } - isPlaceholder = true; - annotationDesc = ''; - annoType = AnnotationType.html; - } else { - isPlaceholder = false; + if (isPlaceholder || content === '') { // placeholder + annotation_div.innerHTML = placeholder; + delete this.settings.annotations[pluginId]; + annotation_div.classList.add('plugin-comment-placeholder'); + if (this.settings.hide_placeholders) { + annotation_container.classList.add('plugin-comment-placeholder'); + } + isPlaceholder = true; + annotationDesc = ''; + annoType = AnnotationType.html; + } else { + isPlaceholder = false; - annotationDesc = content.trim(); - annoType = type; - - this.settings.annotations[pluginId] = { - desc: annotationDesc, - name: pluginName, - type: type, - }; - annotation_div.classList.remove('plugin-comment-placeholder'); + annotationDesc = content.trim(); + annoType = type; + + this.settings.annotations[pluginId] = { + desc: annotationDesc, + name: pluginName, + type: type, + }; + annotation_div.classList.remove('plugin-comment-placeholder'); - this.renderAnnotation(annotation_div,type,content); - } - this.debouncedSaveAnnotations(); - }); - } + this.renderAnnotation(annotation_div,type,content); + } + this.debouncedSaveAnnotations(); + }); + } - async addIcon(tab: SettingTab) { - // Add new icon to the existing icons container - const headingContainer = tab.containerEl.querySelector('.setting-item-heading .setting-item-control'); - if (headingContainer) { - const svg_unlocked = '\ - \ - \ - '; - const svg_locked ='\ - \ - \ - '; + async addIcon(tab: SettingTab) { + // Add new icon to the existing icons container + const headingContainer = tab.containerEl.querySelector('.setting-item-heading .setting-item-control'); + if (headingContainer) { + const svg_unlocked = '\ + \ + \ + '; + const svg_locked ='\ + \ + \ + '; - const newIcon = document.createElement('div'); - newIcon.classList.add('clickable-icon', 'extra-setting-button'); + const newIcon = document.createElement('div'); + newIcon.classList.add('clickable-icon', 'extra-setting-button'); console.log('EDITABLE',this.settings.editable); - if(this.settings.editable) { - newIcon.setAttribute('aria-label', 'Click to lock personal annotations'); - newIcon.innerHTML = svg_unlocked; - } else { - newIcon.setAttribute('aria-label', 'Click to be able to edit personal annotations'); - newIcon.innerHTML = svg_locked; - } + if(this.settings.editable) { + newIcon.setAttribute('aria-label', 'Click to lock personal annotations'); + newIcon.innerHTML = svg_unlocked; + } else { + newIcon.setAttribute('aria-label', 'Click to be able to edit personal annotations'); + newIcon.innerHTML = svg_locked; + } - newIcon.addEventListener('click', (event:MouseEvent) => { + newIcon.addEventListener('click', (event:MouseEvent) => { this.settings.editable = !this.settings.editable; this.debouncedSaveAnnotations(); - if(this.settings.editable) { - newIcon.setAttribute('aria-label', 'Click to lock personal annotations'); - newIcon.innerHTML = svg_unlocked; - } else { - newIcon.setAttribute('aria-label', 'Click to unlock personal annotations'); - newIcon.innerHTML = svg_locked; + if(this.settings.editable) { + newIcon.setAttribute('aria-label', 'Click to lock personal annotations'); + newIcon.innerHTML = svg_unlocked; + } else { + newIcon.setAttribute('aria-label', 'Click to unlock personal annotations'); + newIcon.innerHTML = svg_locked; - } - const plugins = tab.containerEl.querySelectorAll('.plugin-comment-annotation'); - plugins.forEach((div:Element) => { - if (div instanceof HTMLDivElement) { - if(this.settings.editable) { - div.contentEditable = 'true'; - div.classList.add('plugin-comment-annotation-editable'); - } else { - div.contentEditable = 'false'; - div.classList.remove('plugin-comment-annotation-editable'); - } - } - }); + } + const plugins = tab.containerEl.querySelectorAll('.plugin-comment-annotation'); + plugins.forEach((div:Element) => { + if (div instanceof HTMLDivElement) { + if(this.settings.editable) { + div.contentEditable = 'true'; + div.classList.add('plugin-comment-annotation-editable'); + } else { + div.contentEditable = 'false'; + div.classList.remove('plugin-comment-annotation-editable'); + } + } + }); - // Select all div elements that have both 'plugin-comment' and 'plugin-comment-placeholder' classes - const placeholders = document.querySelectorAll(!this.settings.editable ? 'div.plugin-comment.plugin-comment-placeholder' : 'div.plugin-comment.plugin-comment-hidden'); + // Select all div elements that have both 'plugin-comment' and 'plugin-comment-placeholder' classes + const placeholders = document.querySelectorAll(!this.settings.editable ? 'div.plugin-comment.plugin-comment-placeholder' : 'div.plugin-comment.plugin-comment-hidden'); - // Loop through each plugin for which the placeholder is shown - placeholders.forEach((el) => { - if(this.settings.editable) { - // Add the 'plugin-comment-placeholder' class - el.classList.add('plugin-comment-placeholder'); - // Remove the 'plugin-comment-hidden' class - el.classList.remove('plugin-comment-hidden'); - } else { - // Add the 'plugin-comment-hidden' class - el.classList.add('plugin-comment-hidden'); - // Remove the 'plugin-comment-placeholder' class - el.classList.remove('plugin-comment-placeholder'); - } - - }); - }); + // Loop through each plugin for which the placeholder is shown + placeholders.forEach((el) => { + if(this.settings.editable) { + // Add the 'plugin-comment-placeholder' class + el.classList.add('plugin-comment-placeholder'); + // Remove the 'plugin-comment-hidden' class + el.classList.remove('plugin-comment-hidden'); + } else { + // Add the 'plugin-comment-hidden' class + el.classList.add('plugin-comment-hidden'); + // Remove the 'plugin-comment-placeholder' class + el.classList.remove('plugin-comment-placeholder'); + } + + }); + }); - headingContainer.appendChild(newIcon); - } - } + headingContainer.appendChild(newIcon); + } + } - addAnnotation(plugin: Element) { - const settingItemInfo = plugin.querySelector('.setting-item-info'); - if (settingItemInfo) { - const pluginNameDiv = plugin.querySelector('.setting-item-name'); - const pluginName = pluginNameDiv ? pluginNameDiv.textContent : null; + addAnnotation(plugin: Element) { + const settingItemInfo = plugin.querySelector('.setting-item-info'); + if (settingItemInfo) { + const pluginNameDiv = plugin.querySelector('.setting-item-name'); + const pluginName = pluginNameDiv ? pluginNameDiv.textContent : null; - if (!pluginName) { - console.warn('Plugin name not found'); - return; - } + if (!pluginName) { + console.warn('Plugin name not found'); + return; + } - const pluginId = this.pluginNameToIdMap[pluginName]; - if (!pluginId) { - console.warn(`Plugin ID not found for plugin name: ${pluginName}`); - return; - } + const pluginId = this.pluginNameToIdMap[pluginName]; + if (!pluginId) { + console.warn(`Plugin ID not found for plugin name: ${pluginName}`); + return; + } - 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 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 annotation_div = document.createElement('div'); - annotation_div.className = 'plugin-comment-annotation'; + const annotation_div = document.createElement('div'); + annotation_div.className = 'plugin-comment-annotation'; - this.configureAnnotation(annotation_container,annotation_div,pluginId,pluginName); + this.configureAnnotation(annotation_container,annotation_div,pluginId,pluginName); - annotation_container.appendChild(annotation_div); - descriptionDiv.appendChild(annotation_container); - } - } - } - } + annotation_container.appendChild(annotation_div); + descriptionDiv.appendChild(annotation_container); + } + } + } + } - addAnnotations(tab: SettingTab) { - const pluginsContainer = tab.containerEl.querySelector('.installed-plugins-container'); - if (!pluginsContainer) return; + addAnnotations(tab: SettingTab) { + const pluginsContainer = tab.containerEl.querySelector('.installed-plugins-container'); + if (!pluginsContainer) return; - const plugins = pluginsContainer.querySelectorAll('.setting-item'); - plugins.forEach(plugin => { - this.addAnnotation(plugin); - }); - } + const plugins = pluginsContainer.querySelectorAll('.setting-item'); + plugins.forEach(plugin => { + this.addAnnotation(plugin); + }); + } - debouncedSaveAnnotations(timeout_ms = 250) { - if (this.saveTimeout) { - clearTimeout(this.saveTimeout); - } - - this.saveTimeout = window.setTimeout(() => { - this.saveSettings(); - this.saveTimeout = null; - }, timeout_ms); - } + debouncedSaveAnnotations(timeout_ms = 250) { + if (this.saveTimeout) { + clearTimeout(this.saveTimeout); + } + + this.saveTimeout = window.setTimeout(() => { + this.saveSettings(); + this.saveTimeout = null; + }, timeout_ms); + } - removeCommentsFromTab() { - if (this.observedTab) { - const commentElements = this.observedTab.containerEl.querySelectorAll('.plugin-comment'); - commentElements.forEach(element => { - element.remove(); - }); - } - } + removeCommentsFromTab() { + if (this.observedTab) { + const commentElements = this.observedTab.containerEl.querySelectorAll('.plugin-comment'); + commentElements.forEach(element => { + element.remove(); + }); + } + } - onunload() { - // console.log('Unloading Plugins Annotations'); + onunload() { + // console.log('Unloading Plugins Annotations'); - // Remove all comments - this.removeCommentsFromTab(); + // Remove all comments + this.removeCommentsFromTab(); - // Just in case, disconnect observers if they still exist - this.disconnectObservers(); - } + // Just in case, disconnect observers if they still exist + this.disconnectObservers(); + } - getUninstalledPlugins(): PluginAnnotationDict { - const installedPluginIds = new Set(Object.keys(this.app.plugins.manifests)); - const uninstalledPlugins: PluginAnnotationDict = {}; + getUninstalledPlugins(): PluginAnnotationDict { + const installedPluginIds = new Set(Object.keys(this.app.plugins.manifests)); + const uninstalledPlugins: PluginAnnotationDict = {}; - for (const pluginId of sortPluginAnnotationsByName(this.settings.annotations)) { - if (!installedPluginIds.has(pluginId)) { - uninstalledPlugins[pluginId] = this.settings.annotations[pluginId]; - } - } - return uninstalledPlugins; - } + for (const pluginId of sortPluginAnnotationsByName(this.settings.annotations)) { + if (!installedPluginIds.has(pluginId)) { + uninstalledPlugins[pluginId] = this.settings.annotations[pluginId]; + } + } + return uninstalledPlugins; + } } diff --git a/src/manageAnnotations.ts b/src/manageAnnotations.ts index b6baeab..673ec83 100644 --- a/src/manageAnnotations.ts +++ b/src/manageAnnotations.ts @@ -9,133 +9,133 @@ import { PluginAnnotationDict } from "types"; let isWriting = false; export async function handleMarkdownFilePathChange(plugin: PluginsAnnotations, filepath: string): Promise { - const file = plugin.app.vault.getAbstractFileByPath(filepath); + const file = plugin.app.vault.getAbstractFileByPath(filepath); - const {base} = parseFilePath(filepath); + const {base} = parseFilePath(filepath); - if (!file) { - const message = createFragment((frag) => { - frag.appendText('The file '); + if (!file) { + const message = createFragment((frag) => { + frag.appendText('The file '); - frag.createEl('strong', { - text: base - }); + frag.createEl('strong', { + text: base + }); - frag.appendText(' does not exist. Do you want to create it?'); - }); + frag.appendText(' does not exist. Do you want to create it?'); + }); - // File doesn't exist, ask user if they want to create it - const createFile = await showConfirmationDialog(plugin.app, 'Create file', message); - if (!createFile) return false; - } else { - // File exists, ask user if they want to overwrite it + // File doesn't exist, ask user if they want to create it + const createFile = await showConfirmationDialog(plugin.app, 'Create file', message); + if (!createFile) return false; + } else { + // File exists, ask user if they want to overwrite it - const {base} = parseFilePath(filepath); - - const message = createFragment((frag) => { - frag.appendText('The file '); + const {base} = parseFilePath(filepath); + + const message = createFragment((frag) => { + frag.appendText('The file '); - if (Platform.isDesktopApp) { - const fileLink = frag.createEl('a', { - text: base, - href: '#', - }); - fileLink.addEventListener('click', (e) => { - e.preventDefault(); // Prevent the default anchor behavior - // Open the folder in the system's default file explorer + if (Platform.isDesktopApp) { + const fileLink = frag.createEl('a', { + text: base, + href: '#', + }); + fileLink.addEventListener('click', (e) => { + e.preventDefault(); // Prevent the default anchor behavior + // Open the folder in the system's default file explorer - window.require('electron').remote.shell.showItemInFolder(makePosixPathOScompatible(joinPaths(plugin.getVaultPath(),filepath))); // Adjust as necessary - }); - } else { - frag.createEl('strong', { - text: base - }); - } - frag.appendText(' already exists. Do you want to replace the file with your personal annotations about the installed plugins? If you reply yes, the existing file is moved to the trash.'); - }); - - const overwriteFile = await showConfirmationDialog(plugin.app, 'Overwrite file', message); - if (!overwriteFile) return false; - await plugin.app.vault.adapter.trashSystem(file.path); - } - return true; + window.require('electron').remote.shell.showItemInFolder(makePosixPathOScompatible(joinPaths(plugin.getVaultPath(),filepath))); // Adjust as necessary + }); + } else { + frag.createEl('strong', { + text: base + }); + } + frag.appendText(' already exists. Do you want to replace the file with your personal annotations about the installed plugins? If you reply yes, the existing file is moved to the trash.'); + }); + + const overwriteFile = await showConfirmationDialog(plugin.app, 'Overwrite file', message); + if (!overwriteFile) return false; + await plugin.app.vault.adapter.trashSystem(file.path); + } + return true; } export async function readAnnotationsFromMdFile(plugin: PluginsAnnotations): Promise { - if(isWriting) return; + if(isWriting) return; - const filePath = plugin.settings.markdown_file_path; + const filePath = plugin.settings.markdown_file_path; - const file = plugin.app.vault.getFileByPath(filePath); + const file = plugin.app.vault.getFileByPath(filePath); - if(!file) { - // If the file does not exist but we have annotation in memory, write them down - writeAnnotationsToMdFile(plugin); - return; - } + if(!file) { + // If the file does not exist but we have annotation in memory, write them down + writeAnnotationsToMdFile(plugin); + return; + } - try { - const md_content = await plugin.app.vault.read(file); + try { + const md_content = await plugin.app.vault.read(file); - try { - const md_content_parsed = parse(md_content) as PluginAnnotationDict; - plugin.settings.annotations = md_content_parsed; - } catch(error) { - if (error instanceof SyntaxError) { - console.error("Syntax error:", error); - } else { - console.error("Unexpected error:", error); - } - } - } catch (error) { - console.error('Failed to read annotations from file:', error); - return; - } + try { + const md_content_parsed = parse(md_content) as PluginAnnotationDict; + plugin.settings.annotations = md_content_parsed; + } catch(error) { + if (error instanceof SyntaxError) { + console.error("Syntax error:", error); + } else { + console.error("Unexpected error:", error); + } + } + } catch (error) { + console.error('Failed to read annotations from file:', error); + return; + } } export async function writeAnnotationsToMdFile(plugin: PluginsAnnotations) { - if(!plugin.pluginNameToIdMap) return; - const filePath = plugin.settings.markdown_file_path; - if(filePath === "") return; - const annotations = plugin.settings.annotations; - // if(Object.keys(annotations).length === 0) return; + if(!plugin.pluginNameToIdMap) return; + const filePath = plugin.settings.markdown_file_path; + if(filePath === "") return; + const annotations = plugin.settings.annotations; + // if(Object.keys(annotations).length === 0) return; - const header = 'Make changes only within the annotation blocks marked by and . Changes made anywhere else will be overwritten.\n' + const header = 'Make changes only within the annotation blocks marked by and . Changes made anywhere else will be overwritten.\n' - const content: string[] = [header]; - for (const pluginId in annotations) { - content.push(`# ${annotations[pluginId].name}\n\n\n\n\n${annotations[pluginId].desc}\n\n`); - } - const content_concatenated = content.join('\n'); + const content: string[] = [header]; + for (const pluginId in annotations) { + content.push(`# ${annotations[pluginId].name}\n\n\n\n\n${annotations[pluginId].desc}\n\n`); + } + const content_concatenated = content.join('\n'); - try { - let file = await getFileCaseInsensitive(plugin.app.vault,filePath); - - if (file!==null && file.path !== filePath) { - // remove the existing file, whose name is written with different case - // the md file will be recreated with the correct name - await plugin.app.vault.adapter.remove(file.path); - file = null; - } + try { + let file = await getFileCaseInsensitive(plugin.app.vault,filePath); + + if (file!==null && file.path !== filePath) { + // remove the existing file, whose name is written with different case + // the md file will be recreated with the correct name + await plugin.app.vault.adapter.remove(file.path); + file = null; + } - if (!file) { - try { - const {dir} = parseFilePath(filePath); - await createFolderIfNotExists(plugin.app.vault,dir); - } catch (error) { - console.error('Failed to create folder for Markdown file with annotations:', error); - return; - } - isWriting = true; - file = await plugin.app.vault.create(filePath, content_concatenated); - isWriting = false; - } else { - isWriting = true; - await plugin.app.vault.modify(file as TFile, content_concatenated); - isWriting = false; - } - } catch (error) { - console.error('Failed to write Markdown file with annotations:', error); - return; - } + if (!file) { + try { + const {dir} = parseFilePath(filePath); + await createFolderIfNotExists(plugin.app.vault,dir); + } catch (error) { + console.error('Failed to create folder for Markdown file with annotations:', error); + return; + } + isWriting = true; + file = await plugin.app.vault.create(filePath, content_concatenated); + isWriting = false; + } else { + isWriting = true; + await plugin.app.vault.modify(file as TFile, content_concatenated); + isWriting = false; + } + } catch (error) { + console.error('Failed to write Markdown file with annotations:', error); + return; + } } diff --git a/src/settings_tab.ts b/src/settings_tab.ts index c0ab1e1..9d223de 100644 --- a/src/settings_tab.ts +++ b/src/settings_tab.ts @@ -9,501 +9,501 @@ import { parseFilePath, FileSuggestion, downloadJson, showConfirmationDialog } f declare const moment: typeof import('moment'); export class PluginsAnnotationsSettingTab extends PluginSettingTab { - plugin: PluginsAnnotations; - - constructor(app: App, plugin: PluginsAnnotations) { - super(app, plugin); - this.plugin = plugin; - } - - createUninstalledPluginSettings(containerEl: HTMLElement) { - const uninstalledPlugins:PluginAnnotationDict = this.plugin.getUninstalledPlugins(); - - const heading = new Setting(containerEl).setName('Personal annotations of no longer installed community plugins').setHeading(); - const headingEl = heading.settingEl; - - new Setting(containerEl) - .setName('Automatically remove personal annotations of uninstalled plugins:') - .setDesc('If this option is enabled, whenever a plugin is uninstalled, the \ - attached personal annotation is automatically removed. \ - If this option is disabled, you can still manually remove the personal \ - annotations of any plugin that is no longer installed. \ - The list of the no longer installed plugins is shown below, when the list is not empty.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.automatic_remove) - .onChange(async (value: boolean) => { - this.plugin.settings.automatic_remove = value; - this.plugin.debouncedSaveAnnotations(); - })); - - // Check if uninstalledPlugins is empty - if (Object.keys(uninstalledPlugins).length === 0) return; - - const list_uninstalled_label = new Setting(containerEl) - .setName('List of no longer installed plugins:') - .setDesc('If you plan to reinstall the plugin in the future, \ - it is recommended not to remove your annotations, as you can reuse them later.'); - - // Iterate over uninstalled plugins and add settings to the new subcontainer - Object.keys({...uninstalledPlugins}).forEach(pluginId => { - const pluginSetting = new Setting(containerEl) - .setName(`Plugin ${uninstalledPlugins[pluginId].name}`) - .addButton(button => button - .setButtonText('Delete') - .setCta() - .onClick(async () => { - delete this.plugin.settings.annotations[pluginId]; - delete uninstalledPlugins[pluginId]; - pluginSetting.settingEl.remove(); - this.plugin.debouncedSaveAnnotations(); - - // If no more uninstalled plugins, remove the section container - if (Object.keys(uninstalledPlugins).length === 0) { - headingEl.remove(); - list_uninstalled_label.settingEl.remove(); - } - })); - // Render the annotation inside the temporary div - this.plugin.renderAnnotation(pluginSetting.descEl, uninstalledPlugins[pluginId].type, uninstalledPlugins[pluginId].desc); - pluginSetting.descEl.classList.add('plugin-comment-annotation'); - pluginSetting.settingEl.classList.add('plugin-comment-uninstalled'); - }); - } - - async display(): Promise { - const createPluginsPaneFragment = (): DocumentFragment => { - return createFragment((frag) => { - const em = frag.createEl('em'); - const link = frag.createEl('a', { href: '#', text: 'Community plugins'}); - link.onclick = () => { - this.app.setting.openTabById('community-plugins'); - }; - em.appendChild(link); - }); - }; - - // Load annotations first - await this.plugin.loadSettings(); - - // Clean container in the preference pane - const containerEl = this.containerEl; - containerEl.empty(); - - /* ====== Instructions ====== */ - - new Setting(containerEl).setName('Instructions').setHeading(); - - const instructions_frag = createFragment((frag) => { - const div = document.createElement('div'); - div.classList.add('plugin-comment-instructions'); - - const p1 = document.createElement('p'); - p1.appendText('To add or edit your personal annotations for the installed plugins, go to the '); - p1.appendChild(createPluginsPaneFragment()); - p1.appendText(' pane and click over the annotation fields to edit their content'); - div.appendChild(p1); - - const p2 = document.createElement('p2'); - p2.innerHTML = "You can enter rich text notes using Markdown (recommended) and HTML. \ - Markdown annotations will be displayed as Obsidian renders Markdown text. \ - The annotation type can be selected by starting the annotation text with a line containing one \ - of the following options:\ -
    \ -
  • markdown:
  • \ -
  • html:
  • \ -
  • text:
  • \ -
\ - If the first line of annotation text contains none of the options above, \ - the default markdown: is assumed."; - div.appendChild(p2); - - const p3 = document.createElement('p'); - p3.innerHTML = "In Markdown annotations, you can directly link notes inside your \ - vault by adding links such as [[My notes/Review of plugin XYZ|my plugin note]]."; - div.appendChild(p3); - - const p4 = document.createElement('p'); - p4.innerHTML = "When editing HTML annotations, use the placeholder ${label} to \ - display the annotation label at the chosen location." - div.appendChild(p4); - - frag.appendChild(div); - }); - - new Setting(containerEl).setName(instructions_frag); - - /* ==== Storage ==== */ - - new Setting(containerEl).setName('Storage').setHeading(); - - // Add new setting for storing annotations in a Markdown file - const toggle_md_file = new Setting(containerEl) - .setName('Store annotations in a Markdown file:') - .setDesc('With this option enabled, you can select a Markdown file in your vault to \ - contain your personal annotations for the installed plugins. This feature is intended \ - for power users who prefer to edit annotations directly from a Markdown file. \ - A second advantage of this mode is that if you use links to some of your notes in \ - the vault, those links will be automatically updated if your notes are later renamed.'); - - let file_path_field_control: TextComponent; - let md_filepath_error_div: HTMLDivElement; - // Add new setting for markdown file path - const file_path_field = new Setting(containerEl) - .setName('Markdown File Path') - .setDesc(createFragment((frag) => { - frag.appendText('Markdown file where the plugins\' annotations are stored (e.g, 00 Meta/Misc/Plugins annotations.md).'); - md_filepath_error_div = frag.createDiv({text: 'Error: the filename must end with .md extension.', cls: "mod-warning" }); - md_filepath_error_div.style.display = 'none'; - })) - .addText(text => { - - let processingChange = false; - - file_path_field_control = text; - - text.setPlaceholder('E.g.: 00 Meta/Plugins annotations.md'); - text.setValue(this.plugin.settings.markdown_file_path); - - const inputEl = text.inputEl; - const fileSuggestion = new FileSuggestion(this.app, inputEl); - - const updateVaultFiles = () => { - if(fileSuggestion) { - fileSuggestion.setSuggestions(this.app.vault.getFiles().filter((f) => f.extension === "md")); - } - } - - updateVaultFiles(); - - const onChangeHandler = async (event: Event) => { - if(processingChange) { - return; - } else { - processingChange = true; - } - - let filepath = inputEl.value; - - if(filepath!==this.plugin.settings.markdown_file_path) { // if the path has changed - - if(filepath.trim()==='') { - this.plugin.settings.markdown_file_path = ''; - text.setValue(this.plugin.settings.markdown_file_path); - processingChange = false; - this.plugin.debouncedSaveAnnotations(); - return; - } - - filepath = normalizePath(filepath); - - if (parseFilePath(filepath).ext !== '.md') { - md_filepath_error_div.style.display = ''; - text.setValue(this.plugin.settings.markdown_file_path); - processingChange = false; - return; - } - - md_filepath_error_div.style.display = 'none'; - const answer = await handleMarkdownFilePathChange(this.plugin, filepath); - if(answer) { - this.plugin.settings.markdown_file_path = filepath; - await this.plugin.saveSettings(); - updateVaultFiles(); - } - text.setValue(this.plugin.settings.markdown_file_path); - } - - processingChange = false; - }; - - inputEl.addEventListener('keydown', (event) => { - if (event.key === 'Enter') { - event.preventDefault(); - onChangeHandler(event); - } - }); - - inputEl.addEventListener('blur', onChangeHandler); - - // Use change explicitly instead of onChange because onChange - // reacts to events of type `input` instead of `change` - inputEl.addEventListener('change', onChangeHandler); - }); - - file_path_field.settingEl.style.display = this.plugin.settings.markdown_file_path === '' ? 'none' : ''; - - toggle_md_file.addToggle(toggle => toggle - .setValue(this.plugin.settings.markdown_file_path !== '') - .onChange(async (value: boolean) => { - if (value) { - file_path_field.settingEl.style.display = ''; - this.plugin.settings.markdown_file_path = file_path_field_control.getValue(); - } else { - file_path_field.settingEl.style.display = 'none'; - this.plugin.settings.markdown_file_path = ''; - } - this.plugin.debouncedSaveAnnotations(); - })); - - // Append the settings - containerEl.appendChild(toggle_md_file.settingEl); - containerEl.appendChild(file_path_field.settingEl); - - /* ==== Display heading ==== */ - - new Setting(containerEl).setName('Display').setHeading(); - - if (Platform.isMobile) { - new Setting(containerEl) - .setName('Annotation label:') - .setDesc('Choose the annotation label for the mobile version of Obsidian. \ - Use HTML code if you want to format it. Enter an empty string if \ - you want to hide the label.') - .addText(text => { - text.setPlaceholder('Annotation label'); - text.setValue(this.plugin.settings.label_mobile); - text.onChange(async (value: string) => { - this.plugin.settings.label_mobile = value; - this.plugin.debouncedSaveAnnotations(); - })}); - } else { - new Setting(containerEl) - .setName('Annotation label:') - .setDesc('Choose the annotation label for the desktop version of Obsidian. \ - Use HTML code if you want to format it. Enter an empty string if you want \ - to hide the label.') - .addText(text => { - text.setPlaceholder('Annotation label'); - text.setValue(this.plugin.settings.label_desktop); - text.onChange(async (value: string) => { - this.plugin.settings.label_desktop = value; - this.plugin.debouncedSaveAnnotations(); - })}); - } - - new Setting(containerEl) - .setName('Placeholder label:') - .setDesc(createFragment((frag) => { - frag.appendText('Choose the label appearing where no user annotation is provied yet. Use '); - frag.createEl('em').appendText('${plugin_name}'); - frag.appendText(' to refer to the plugin name.')})) - .addText(text => { - text.setPlaceholder('Annotation label'); - text.setValue(this.plugin.settings.label_placeholder); - text.onChange(async (value: string) => { - this.plugin.settings.label_placeholder = value; - this.plugin.debouncedSaveAnnotations(); - })}); - - new Setting(containerEl) - .setName('Hide empty annotations:') - .setDesc(createFragment((frag) => { - frag.appendText('If this option is enabled, only annotations set by the user \ - will be shown. If you want to insert an annotation to a plugin for the first \ - time, hover with the mouse over the chosen plugin in the '); - frag.appendChild(createPluginsPaneFragment()); - frag.appendText(' pane. The annotation field will appear automatically.'); - - if (Platform.isMobile) { - const p = frag.createEl('p'); - const warning = p.createEl('span', { - text: 'On mobile devices, you can hover over plugins with your finger instead of using the mouse.', - }); - warning.classList.add('mod-warning'); - frag.appendChild(p); - } - })) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.hide_placeholders) - .onChange(async (value: boolean) => { - this.plugin.settings.hide_placeholders = value; - this.plugin.debouncedSaveAnnotations(); - })); - - new Setting(containerEl) - .setName('Delete placeholder text when inserting a new annotation:') - .setDesc('If this option is enabled, the placeholder text will be deleted \ - automatically when you start typing a new annotation. If disabled, \ - the placeholder text will be selected for easier replacement. \ - This is a minor customization.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.delete_placeholder_string_on_insertion) - .onChange(async (value: boolean) => { - this.plugin.settings.delete_placeholder_string_on_insertion = value; - this.plugin.debouncedSaveAnnotations(); - })); - - - /* ====== Backups ====== */ - this.createBackupManager(containerEl); - - /* ====== Personal annotations of no longer installed community plugins ====== */ - this.createUninstalledPluginSettings(containerEl); - } - - createBackupManager(containerEl: HTMLElement) { - new Setting(containerEl) - .setName('Backups') - .setHeading(); - - const export_label = (Platform.isDesktopApp) ? ' Use the export and import buttons \ - to copy the current settings and annnotations to an external file and, vicevera, \ - to restore them from an external file.' : ''; - - const backup_settings = new Setting(containerEl) - .setName('Create a backup copy of your current settings and annotations:') - .setDesc('Use the backup button to create an internal backup copy. \ - You can customize the names of existing backups by clicking on their names once you have created them.' - + export_label) - .addButton(button => button - .setButtonText('Create Backup') - .setCta() - .onClick(async () => { - const backupName = 'Untitled backup'; - await this.plugin.backupSettings(backupName,this.plugin.settings); - this.display(); - }) - ); - - backup_settings.controlEl.classList.add('plugin-comment-export-buttons'); - - if (Platform.isDesktopApp) { - backup_settings.addButton(button => button - .setButtonText('Export') - .setCta() - .onClick(async () => { - downloadJson({...this.plugin.settings, backups: []}); - }) - ); - - backup_settings.addButton(button => button - .setButtonText('Import') - .setCta() - .onClick(async () => { - // Create an input element to upload a file - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; // Only allow JSON files - - input.onchange = async () => { - const file = input.files?.[0]; - if (file) { - // Read the file as text - const reader = new FileReader(); - reader.onload = async (event) => { - try { - // Parse the JSON file - const importedData = JSON.parse(event.target?.result as string); - - // Validate and merge the imported settings - if (importedData && typeof importedData === 'object') { - const forceSave = true; - await this.plugin.loadSettings(importedData,forceSave); - new Notice('Settings successfully imported.'); - this.display(); // Refresh the display to reflect the imported annotations - } else { - alert('Invalid settings file.'); - } - } catch (error) { - console.error('Error importing settings:', error); - alert('Failed to import settings. Please ensure the file is valid.'); - } - }; - reader.readAsText(file); - } - }; - - // Trigger the file input click - input.click(); - }) - ); - } - - // List Existing Backups - if (this.plugin.settings.backups.length > 0) { - - // Sort the backups by date (most recent first) - this.plugin.settings.backups.sort((a, b) => b.date.getTime() - a.date.getTime()); - - // Create a wrapper div for the table - const backupTableContainer = containerEl.createDiv({ cls: 'setting-item' }); - - const tableDiv = backupTableContainer.createDiv({ cls: 'plugin-comment-backup-table' }); - - // Create the header row - const headerRow = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row header' }); - headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Backup name' + (Platform.isMobileApp ? '' : ' (click to edit)') }); - headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Created on' }); - headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: '' }); - - this.plugin.settings.backups.forEach((backup, index) => { - const rowDiv = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row' }); - - // Backup name cell - const nameCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-name' }); - const nameDiv = nameCell.createDiv({ text: backup.name, attr: { contenteditable: 'true' } }); - - // Handle saving the updated name when editing is complete - nameDiv.addEventListener('blur', async () => { - const newName = nameDiv.textContent?.trim() || 'Unnamed Backup'; - this.plugin.settings.backups[index].name = newName; - this.plugin.debouncedSaveAnnotations(); - }); - - // Handle the Enter key to finish editing - nameDiv.addEventListener('keydown', (event) => { - if (event.key === 'Enter') { - event.preventDefault(); - nameDiv.blur(); // Trigger the blur event to save the name - } - }); - - // Created on cell - const dateCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell' }); - dateCell.setText(moment(backup.date).format('YYYY-MM-DD HH:mm:ss')); - - // Add Restore and Delete buttons to the last cell - const actionCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-buttons' }); - actionCell.createEl('button', { text: 'Restore', cls: 'mod-cta' }) - .addEventListener('click', async () => { - const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', - createFragment((frag) => { - frag.appendText('You are about to restore the settings from the backup named '); - frag.createEl('strong',{text: this.plugin.settings.backups[index].name}); - frag.appendText(' created on '); - frag.createEl('strong',{text: moment(this.plugin.settings.backups[index].date).format('YYYY-MM-DD HH:mm:ss')}); - frag.appendText('. If you proceed, the current settings will be overwritten with those from the backup. \ - If you want to keep a copy of the current settings, make a backup before proceeding.\ - Do you want to proceed restoring the seettings from the backup?'); - })); - if(answer) { - const backups = [...this.plugin.settings.backups]; // store a copy of the backups before restoring the old settings - await this.plugin.loadSettings(structuredClone(this.plugin.settings.backups[index].settings)); - this.plugin.settings.backups = backups; // restore the copy of the backups - await this.plugin.saveSettings(); // save the restored settings with the backups - new Notice(`Annotations restored from backup "${backup.name}"`); - this.display(); // Refresh the display to reflect the restored annotations - } - }); - - actionCell.createEl('button', { text: 'Delete', cls: 'mod-cta' }) - .addEventListener('click', async () => { - const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', - createFragment((frag) => { - frag.appendText('You are about to delete the backup named '); - frag.createEl('strong',{text: this.plugin.settings.backups[index].name}); - frag.appendText(' created on '); - frag.createEl('strong',{text: moment(this.plugin.settings.backups[index].date).format('YYYY-MM-DD HH:mm:ss')}); - frag.appendText('. Do you want to continue?'); - })); - if(answer) { - this.plugin.settings.backups.splice(index, 1); - await this.plugin.saveSettings(); - this.display(); // Refresh the display to remove the deleted backup - } - }); - }); - } - } + plugin: PluginsAnnotations; + + constructor(app: App, plugin: PluginsAnnotations) { + super(app, plugin); + this.plugin = plugin; + } + + createUninstalledPluginSettings(containerEl: HTMLElement) { + const uninstalledPlugins:PluginAnnotationDict = this.plugin.getUninstalledPlugins(); + + const heading = new Setting(containerEl).setName('Personal annotations of no longer installed community plugins').setHeading(); + const headingEl = heading.settingEl; + + new Setting(containerEl) + .setName('Automatically remove personal annotations of uninstalled plugins:') + .setDesc('If this option is enabled, whenever a plugin is uninstalled, the \ + attached personal annotation is automatically removed. \ + If this option is disabled, you can still manually remove the personal \ + annotations of any plugin that is no longer installed. \ + The list of the no longer installed plugins is shown below, when the list is not empty.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.automatic_remove) + .onChange(async (value: boolean) => { + this.plugin.settings.automatic_remove = value; + this.plugin.debouncedSaveAnnotations(); + })); + + // Check if uninstalledPlugins is empty + if (Object.keys(uninstalledPlugins).length === 0) return; + + const list_uninstalled_label = new Setting(containerEl) + .setName('List of no longer installed plugins:') + .setDesc('If you plan to reinstall the plugin in the future, \ + it is recommended not to remove your annotations, as you can reuse them later.'); + + // Iterate over uninstalled plugins and add settings to the new subcontainer + Object.keys({...uninstalledPlugins}).forEach(pluginId => { + const pluginSetting = new Setting(containerEl) + .setName(`Plugin ${uninstalledPlugins[pluginId].name}`) + .addButton(button => button + .setButtonText('Delete') + .setCta() + .onClick(async () => { + delete this.plugin.settings.annotations[pluginId]; + delete uninstalledPlugins[pluginId]; + pluginSetting.settingEl.remove(); + this.plugin.debouncedSaveAnnotations(); + + // If no more uninstalled plugins, remove the section container + if (Object.keys(uninstalledPlugins).length === 0) { + headingEl.remove(); + list_uninstalled_label.settingEl.remove(); + } + })); + // Render the annotation inside the temporary div + this.plugin.renderAnnotation(pluginSetting.descEl, uninstalledPlugins[pluginId].type, uninstalledPlugins[pluginId].desc); + pluginSetting.descEl.classList.add('plugin-comment-annotation'); + pluginSetting.settingEl.classList.add('plugin-comment-uninstalled'); + }); + } + + async display(): Promise { + const createPluginsPaneFragment = (): DocumentFragment => { + return createFragment((frag) => { + const em = frag.createEl('em'); + const link = frag.createEl('a', { href: '#', text: 'Community plugins'}); + link.onclick = () => { + this.app.setting.openTabById('community-plugins'); + }; + em.appendChild(link); + }); + }; + + // Load annotations first + await this.plugin.loadSettings(); + + // Clean container in the preference pane + const containerEl = this.containerEl; + containerEl.empty(); + + /* ====== Instructions ====== */ + + new Setting(containerEl).setName('Instructions').setHeading(); + + const instructions_frag = createFragment((frag) => { + const div = document.createElement('div'); + div.classList.add('plugin-comment-instructions'); + + const p1 = document.createElement('p'); + p1.appendText('To add or edit your personal annotations for the installed plugins, go to the '); + p1.appendChild(createPluginsPaneFragment()); + p1.appendText(' pane and click over the annotation fields to edit their content'); + div.appendChild(p1); + + const p2 = document.createElement('p2'); + p2.innerHTML = "You can enter rich text notes using Markdown (recommended) and HTML. \ + Markdown annotations will be displayed as Obsidian renders Markdown text. \ + The annotation type can be selected by starting the annotation text with a line containing one \ + of the following options:\ +
    \ +
  • markdown:
  • \ +
  • html:
  • \ +
  • text:
  • \ +
\ + If the first line of annotation text contains none of the options above, \ + the default markdown: is assumed."; + div.appendChild(p2); + + const p3 = document.createElement('p'); + p3.innerHTML = "In Markdown annotations, you can directly link notes inside your \ + vault by adding links such as [[My notes/Review of plugin XYZ|my plugin note]]."; + div.appendChild(p3); + + const p4 = document.createElement('p'); + p4.innerHTML = "When editing HTML annotations, use the placeholder ${label} to \ + display the annotation label at the chosen location." + div.appendChild(p4); + + frag.appendChild(div); + }); + + new Setting(containerEl).setName(instructions_frag); + + /* ==== Storage ==== */ + + new Setting(containerEl).setName('Storage').setHeading(); + + // Add new setting for storing annotations in a Markdown file + const toggle_md_file = new Setting(containerEl) + .setName('Store annotations in a Markdown file:') + .setDesc('With this option enabled, you can select a Markdown file in your vault to \ + contain your personal annotations for the installed plugins. This feature is intended \ + for power users who prefer to edit annotations directly from a Markdown file. \ + A second advantage of this mode is that if you use links to some of your notes in \ + the vault, those links will be automatically updated if your notes are later renamed.'); + + let file_path_field_control: TextComponent; + let md_filepath_error_div: HTMLDivElement; + // Add new setting for markdown file path + const file_path_field = new Setting(containerEl) + .setName('Markdown File Path') + .setDesc(createFragment((frag) => { + frag.appendText('Markdown file where the plugins\' annotations are stored (e.g, 00 Meta/Misc/Plugins annotations.md).'); + md_filepath_error_div = frag.createDiv({text: 'Error: the filename must end with .md extension.', cls: "mod-warning" }); + md_filepath_error_div.style.display = 'none'; + })) + .addText(text => { + + let processingChange = false; + + file_path_field_control = text; + + text.setPlaceholder('E.g.: 00 Meta/Plugins annotations.md'); + text.setValue(this.plugin.settings.markdown_file_path); + + const inputEl = text.inputEl; + const fileSuggestion = new FileSuggestion(this.app, inputEl); + + const updateVaultFiles = () => { + if(fileSuggestion) { + fileSuggestion.setSuggestions(this.app.vault.getFiles().filter((f) => f.extension === "md")); + } + } + + updateVaultFiles(); + + const onChangeHandler = async (event: Event) => { + if(processingChange) { + return; + } else { + processingChange = true; + } + + let filepath = inputEl.value; + + if(filepath!==this.plugin.settings.markdown_file_path) { // if the path has changed + + if(filepath.trim()==='') { + this.plugin.settings.markdown_file_path = ''; + text.setValue(this.plugin.settings.markdown_file_path); + processingChange = false; + this.plugin.debouncedSaveAnnotations(); + return; + } + + filepath = normalizePath(filepath); + + if (parseFilePath(filepath).ext !== '.md') { + md_filepath_error_div.style.display = ''; + text.setValue(this.plugin.settings.markdown_file_path); + processingChange = false; + return; + } + + md_filepath_error_div.style.display = 'none'; + const answer = await handleMarkdownFilePathChange(this.plugin, filepath); + if(answer) { + this.plugin.settings.markdown_file_path = filepath; + await this.plugin.saveSettings(); + updateVaultFiles(); + } + text.setValue(this.plugin.settings.markdown_file_path); + } + + processingChange = false; + }; + + inputEl.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + onChangeHandler(event); + } + }); + + inputEl.addEventListener('blur', onChangeHandler); + + // Use change explicitly instead of onChange because onChange + // reacts to events of type `input` instead of `change` + inputEl.addEventListener('change', onChangeHandler); + }); + + file_path_field.settingEl.style.display = this.plugin.settings.markdown_file_path === '' ? 'none' : ''; + + toggle_md_file.addToggle(toggle => toggle + .setValue(this.plugin.settings.markdown_file_path !== '') + .onChange(async (value: boolean) => { + if (value) { + file_path_field.settingEl.style.display = ''; + this.plugin.settings.markdown_file_path = file_path_field_control.getValue(); + } else { + file_path_field.settingEl.style.display = 'none'; + this.plugin.settings.markdown_file_path = ''; + } + this.plugin.debouncedSaveAnnotations(); + })); + + // Append the settings + containerEl.appendChild(toggle_md_file.settingEl); + containerEl.appendChild(file_path_field.settingEl); + + /* ==== Display heading ==== */ + + new Setting(containerEl).setName('Display').setHeading(); + + if (Platform.isMobile) { + new Setting(containerEl) + .setName('Annotation label:') + .setDesc('Choose the annotation label for the mobile version of Obsidian. \ + Use HTML code if you want to format it. Enter an empty string if \ + you want to hide the label.') + .addText(text => { + text.setPlaceholder('Annotation label'); + text.setValue(this.plugin.settings.label_mobile); + text.onChange(async (value: string) => { + this.plugin.settings.label_mobile = value; + this.plugin.debouncedSaveAnnotations(); + })}); + } else { + new Setting(containerEl) + .setName('Annotation label:') + .setDesc('Choose the annotation label for the desktop version of Obsidian. \ + Use HTML code if you want to format it. Enter an empty string if you want \ + to hide the label.') + .addText(text => { + text.setPlaceholder('Annotation label'); + text.setValue(this.plugin.settings.label_desktop); + text.onChange(async (value: string) => { + this.plugin.settings.label_desktop = value; + this.plugin.debouncedSaveAnnotations(); + })}); + } + + new Setting(containerEl) + .setName('Placeholder label:') + .setDesc(createFragment((frag) => { + frag.appendText('Choose the label appearing where no user annotation is provied yet. Use '); + frag.createEl('em').appendText('${plugin_name}'); + frag.appendText(' to refer to the plugin name.')})) + .addText(text => { + text.setPlaceholder('Annotation label'); + text.setValue(this.plugin.settings.label_placeholder); + text.onChange(async (value: string) => { + this.plugin.settings.label_placeholder = value; + this.plugin.debouncedSaveAnnotations(); + })}); + + new Setting(containerEl) + .setName('Hide empty annotations:') + .setDesc(createFragment((frag) => { + frag.appendText('If this option is enabled, only annotations set by the user \ + will be shown. If you want to insert an annotation to a plugin for the first \ + time, hover with the mouse over the chosen plugin in the '); + frag.appendChild(createPluginsPaneFragment()); + frag.appendText(' pane. The annotation field will appear automatically.'); + + if (Platform.isMobile) { + const p = frag.createEl('p'); + const warning = p.createEl('span', { + text: 'On mobile devices, you can hover over plugins with your finger instead of using the mouse.', + }); + warning.classList.add('mod-warning'); + frag.appendChild(p); + } + })) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.hide_placeholders) + .onChange(async (value: boolean) => { + this.plugin.settings.hide_placeholders = value; + this.plugin.debouncedSaveAnnotations(); + })); + + new Setting(containerEl) + .setName('Delete placeholder text when inserting a new annotation:') + .setDesc('If this option is enabled, the placeholder text will be deleted \ + automatically when you start typing a new annotation. If disabled, \ + the placeholder text will be selected for easier replacement. \ + This is a minor customization.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.delete_placeholder_string_on_insertion) + .onChange(async (value: boolean) => { + this.plugin.settings.delete_placeholder_string_on_insertion = value; + this.plugin.debouncedSaveAnnotations(); + })); + + + /* ====== Backups ====== */ + this.createBackupManager(containerEl); + + /* ====== Personal annotations of no longer installed community plugins ====== */ + this.createUninstalledPluginSettings(containerEl); + } + + createBackupManager(containerEl: HTMLElement) { + new Setting(containerEl) + .setName('Backups') + .setHeading(); + + const export_label = (Platform.isDesktopApp) ? ' Use the export and import buttons \ + to copy the current settings and annnotations to an external file and, vicevera, \ + to restore them from an external file.' : ''; + + const backup_settings = new Setting(containerEl) + .setName('Create a backup copy of your current settings and annotations:') + .setDesc('Use the backup button to create an internal backup copy. \ + You can customize the names of existing backups by clicking on their names once you have created them.' + + export_label) + .addButton(button => button + .setButtonText('Create Backup') + .setCta() + .onClick(async () => { + const backupName = 'Untitled backup'; + await this.plugin.backupSettings(backupName,this.plugin.settings); + this.display(); + }) + ); + + backup_settings.controlEl.classList.add('plugin-comment-export-buttons'); + + if (Platform.isDesktopApp) { + backup_settings.addButton(button => button + .setButtonText('Export') + .setCta() + .onClick(async () => { + downloadJson({...this.plugin.settings, backups: []}); + }) + ); + + backup_settings.addButton(button => button + .setButtonText('Import') + .setCta() + .onClick(async () => { + // Create an input element to upload a file + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; // Only allow JSON files + + input.onchange = async () => { + const file = input.files?.[0]; + if (file) { + // Read the file as text + const reader = new FileReader(); + reader.onload = async (event) => { + try { + // Parse the JSON file + const importedData = JSON.parse(event.target?.result as string); + + // Validate and merge the imported settings + if (importedData && typeof importedData === 'object') { + const forceSave = true; + await this.plugin.loadSettings(importedData,forceSave); + new Notice('Settings successfully imported.'); + this.display(); // Refresh the display to reflect the imported annotations + } else { + alert('Invalid settings file.'); + } + } catch (error) { + console.error('Error importing settings:', error); + alert('Failed to import settings. Please ensure the file is valid.'); + } + }; + reader.readAsText(file); + } + }; + + // Trigger the file input click + input.click(); + }) + ); + } + + // List Existing Backups + if (this.plugin.settings.backups.length > 0) { + + // Sort the backups by date (most recent first) + this.plugin.settings.backups.sort((a, b) => b.date.getTime() - a.date.getTime()); + + // Create a wrapper div for the table + const backupTableContainer = containerEl.createDiv({ cls: 'setting-item' }); + + const tableDiv = backupTableContainer.createDiv({ cls: 'plugin-comment-backup-table' }); + + // Create the header row + const headerRow = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row header' }); + headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Backup name' + (Platform.isMobileApp ? '' : ' (click to edit)') }); + headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Created on' }); + headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: '' }); + + this.plugin.settings.backups.forEach((backup, index) => { + const rowDiv = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row' }); + + // Backup name cell + const nameCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-name' }); + const nameDiv = nameCell.createDiv({ text: backup.name, attr: { contenteditable: 'true' } }); + + // Handle saving the updated name when editing is complete + nameDiv.addEventListener('blur', async () => { + const newName = nameDiv.textContent?.trim() || 'Unnamed Backup'; + this.plugin.settings.backups[index].name = newName; + this.plugin.debouncedSaveAnnotations(); + }); + + // Handle the Enter key to finish editing + nameDiv.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + nameDiv.blur(); // Trigger the blur event to save the name + } + }); + + // Created on cell + const dateCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell' }); + dateCell.setText(moment(backup.date).format('YYYY-MM-DD HH:mm:ss')); + + // Add Restore and Delete buttons to the last cell + const actionCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-buttons' }); + actionCell.createEl('button', { text: 'Restore', cls: 'mod-cta' }) + .addEventListener('click', async () => { + const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', + createFragment((frag) => { + frag.appendText('You are about to restore the settings from the backup named '); + frag.createEl('strong',{text: this.plugin.settings.backups[index].name}); + frag.appendText(' created on '); + frag.createEl('strong',{text: moment(this.plugin.settings.backups[index].date).format('YYYY-MM-DD HH:mm:ss')}); + frag.appendText('. If you proceed, the current settings will be overwritten with those from the backup. \ + If you want to keep a copy of the current settings, make a backup before proceeding.\ + Do you want to proceed restoring the seettings from the backup?'); + })); + if(answer) { + const backups = [...this.plugin.settings.backups]; // store a copy of the backups before restoring the old settings + await this.plugin.loadSettings(structuredClone(this.plugin.settings.backups[index].settings)); + this.plugin.settings.backups = backups; // restore the copy of the backups + await this.plugin.saveSettings(); // save the restored settings with the backups + new Notice(`Annotations restored from backup "${backup.name}"`); + this.display(); // Refresh the display to reflect the restored annotations + } + }); + + actionCell.createEl('button', { text: 'Delete', cls: 'mod-cta' }) + .addEventListener('click', async () => { + const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', + createFragment((frag) => { + frag.appendText('You are about to delete the backup named '); + frag.createEl('strong',{text: this.plugin.settings.backups[index].name}); + frag.appendText(' created on '); + frag.createEl('strong',{text: moment(this.plugin.settings.backups[index].date).format('YYYY-MM-DD HH:mm:ss')}); + frag.appendText('. Do you want to continue?'); + })); + if(answer) { + this.plugin.settings.backups.splice(index, 1); + await this.plugin.saveSettings(); + this.display(); // Refresh the display to remove the deleted backup + } + }); + }); + } + } } diff --git a/src/types.ts b/src/types.ts index fecf7e2..244ee48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,13 +3,13 @@ import { DEFAULT_SETTINGS } from "defaults"; export interface PluginAnnotation { - name: string; // extended name of the plugin - desc: string; // personal annontation - type: AnnotationType; // annotation type + name: string; // extended name of the plugin + desc: string; // personal annontation + type: AnnotationType; // annotation type } export interface PluginAnnotationDict { - [pluginId: string]: PluginAnnotation; + [pluginId: string]: PluginAnnotation; } export interface PluginBackup { @@ -19,41 +19,41 @@ export interface PluginBackup { } export interface PluginsAnnotationsSettings { - annotations: PluginAnnotationDict; - plugins_annotations_uuid: string; - hide_placeholders: boolean; - delete_placeholder_string_on_insertion: boolean; - label_mobile: string; - label_desktop: string; - label_placeholder: string; - editable: boolean; - automatic_remove: boolean; - markdown_file_path: string; - compatibility: string; - backups: PluginBackup[]; + annotations: PluginAnnotationDict; + plugins_annotations_uuid: string; + hide_placeholders: boolean; + delete_placeholder_string_on_insertion: boolean; + label_mobile: string; + label_desktop: string; + label_placeholder: string; + editable: boolean; + automatic_remove: boolean; + markdown_file_path: string; + compatibility: string; + backups: PluginBackup[]; } export function isPluginsAnnotationsSettings(s:unknown): s is PluginsAnnotationsSettings { - if (typeof s !== 'object' || s === null) { - return false; - } - return 'annotations' in s - && 'compatibility' in s && (s as PluginsAnnotationsSettings).compatibility === '1.5.0' - && 'plugins_annotations_uuid' in s - && (s as PluginsAnnotationsSettings).plugins_annotations_uuid === DEFAULT_SETTINGS.plugins_annotations_uuid; + if (typeof s !== 'object' || s === null) { + return false; + } + return 'annotations' in s + && 'compatibility' in s && (s as PluginsAnnotationsSettings).compatibility === '1.5.0' + && 'plugins_annotations_uuid' in s + && (s as PluginsAnnotationsSettings).plugins_annotations_uuid === DEFAULT_SETTINGS.plugins_annotations_uuid; } export function isPluginAnnotation(anno:unknown): anno is PluginAnnotation { - if (typeof anno !== 'object' || anno === null) { - return false; - } - const obj = anno as Record; + if (typeof anno !== 'object' || anno === null) { + return false; + } + const obj = anno as Record; - const hasName = typeof obj.name === 'string'; - const hasDesc = typeof obj.desc === 'string'; - const hasType = typeof obj.type === 'string' && Object.values(AnnotationType).includes(obj.type as AnnotationType); + const hasName = typeof obj.name === 'string'; + const hasDesc = typeof obj.desc === 'string'; + const hasType = typeof obj.type === 'string' && Object.values(AnnotationType).includes(obj.type as AnnotationType); - return hasName && hasDesc && hasType; + return hasName && hasDesc && hasType; } // Function to render the annotation based on preamble @@ -80,15 +80,15 @@ export function parseAnnotation(text: string): {annoType: AnnotationType, annoDe } export enum AnnotationType { - text = 'text', - html = 'html', - markdown = 'markdown', + text = 'text', + html = 'html', + markdown = 'markdown', } export interface ParsedPath { - dir: string, - base: string, - filename: string, - ext: string, - path: string + dir: string, + base: string, + filename: string, + ext: string, + path: string } diff --git a/src/types/obsidian-augment.d.ts b/src/types/obsidian-augment.d.ts index 2cf25d4..ce9d060 100644 --- a/src/types/obsidian-augment.d.ts +++ b/src/types/obsidian-augment.d.ts @@ -3,69 +3,69 @@ import 'obsidian'; declare module "obsidian" { - interface App { - internalPlugins: InternalPlugins; - plugins: Plugins; - setting: Setting; - } - interface Plugin { - _loaded: boolean; - } - interface PluginSettingTab { - name: string; - } - interface SettingTab { - id: string; - name: string; - navEl: HTMLElement; - // updateSearch(e: string): void; - renderInstalledPlugin( - pluginManifest: PluginManifest, - containerEl: HTMLElement, - nameMatch: boolean | null, - authorMatch: boolean | null, - descriptionMatch: boolean | null - ): void; - } - interface Setting { - onOpen(): void; - onClose(): void; + interface App { + internalPlugins: InternalPlugins; + plugins: Plugins; + setting: Setting; + } + interface Plugin { + _loaded: boolean; + } + interface PluginSettingTab { + name: string; + } + interface SettingTab { + id: string; + name: string; + navEl: HTMLElement; + // updateSearch(e: string): void; + renderInstalledPlugin( + pluginManifest: PluginManifest, + containerEl: HTMLElement, + nameMatch: boolean | null, + authorMatch: boolean | null, + descriptionMatch: boolean | null + ): void; + } + interface Setting { + onOpen(): void; + onClose(): void; - openTabById(id: string): void; - openTab(tab: SettingTab): void; + openTabById(id: string): void; + openTab(tab: SettingTab): void; - closeActiveTab(tab: SettingTab): void; + closeActiveTab(tab: SettingTab): void; - isPluginSettingTab(tab: SettingTab): boolean; - addSettingTab(tab: SettingTab): void; - removeSettingTab(tab: SettingTab): void; + isPluginSettingTab(tab: SettingTab): boolean; + addSettingTab(tab: SettingTab): void; + removeSettingTab(tab: SettingTab): void; - activeTab: SettingTab; - lastTabId: string; + activeTab: SettingTab; + lastTabId: string; - pluginTabs: PluginSettingTab[]; - settingTabs: SettingTab[]; + pluginTabs: PluginSettingTab[]; + settingTabs: SettingTab[]; - tabContentContainer: HTMLDivElement; - tabHeadersEl: HTMLDivElement; + tabContentContainer: HTMLDivElement; + tabHeadersEl: HTMLDivElement; - close(): void; - } - - interface Plugins { - manifests: Record; - plugins: Record; - getPlugin(id: string): Plugin; - uninstallPlugin(pluginId: string): Promise; + close(): void; } - interface InternalPlugins { - plugins: Record; - getPluginById(id: string): Plugin; - } + interface Plugins { + manifests: Record; + plugins: Record; + getPlugin(id: string): Plugin; + uninstallPlugin(pluginId: string): Promise; + } + + interface InternalPlugins { + plugins: Record; + getPluginById(id: string): Plugin; + } - interface AbstractInputSuggest extends PopoverSuggest { - textInputEl: HTMLInputElement; - } + interface AbstractInputSuggest extends PopoverSuggest { + textInputEl: HTMLInputElement; + } } \ No newline at end of file diff --git a/src/types_legacy.ts b/src/types_legacy.ts index acff747..f5de2e8 100644 --- a/src/types_legacy.ts +++ b/src/types_legacy.ts @@ -6,57 +6,57 @@ import { AnnotationType, PluginAnnotation, PluginsAnnotationsSettings } from "ty /* VERSION 1.4 */ export interface PluginAnnotation_1_4_0 extends Omit { - anno: string; // personal annontation + anno: string; // personal annontation } export type PluginAnnotationDict_1_4_0 = { - [pluginId: string]: PluginAnnotation_1_4_0; + [pluginId: string]: PluginAnnotation_1_4_0; } // Extend the original interface and override the annotations property export interface PluginsAnnotationsSettings_1_4_0 extends Omit { - annotations: PluginAnnotationDict_1_4_0; + annotations: PluginAnnotationDict_1_4_0; } export function isSettingsFormat_1_4_0(s:unknown): s is PluginsAnnotationsSettings_1_4_0 { - if (typeof s !== 'object' || s === null) { - return false; - } - return 'annotations' in s - && 'plugins_annotations_uuid' in s - && (s as PluginsAnnotationsSettings_1_4_0).plugins_annotations_uuid === DEFAULT_SETTINGS_1_4_0.plugins_annotations_uuid; + if (typeof s !== 'object' || s === null) { + return false; + } + return 'annotations' in s + && 'plugins_annotations_uuid' in s + && (s as PluginsAnnotationsSettings_1_4_0).plugins_annotations_uuid === DEFAULT_SETTINGS_1_4_0.plugins_annotations_uuid; } // Function to render the annotation based on preamble export function parseAnnotation_1_4_0(text: string): {type:AnnotationType,content:string} { - const lines = text.split('\n'); - const preamble = lines[0].toLowerCase(); - const sliced = lines.slice(1).join('\n'); - - // annotation_div.innerHTML = ''; - if (preamble.startsWith('html:')) { - return {type: AnnotationType.html, content: sliced}; - } else if (preamble.startsWith('markdown:')) { - return {type: AnnotationType.markdown, content: sliced.replace(/\$\{label\}/g, '')}; - } else if (preamble.startsWith('text:')) { - return {type: AnnotationType.text, content: sliced}; - } else { - return {type: AnnotationType.text, content: text}; - } + const lines = text.split('\n'); + const preamble = lines[0].toLowerCase(); + const sliced = lines.slice(1).join('\n'); + + // annotation_div.innerHTML = ''; + if (preamble.startsWith('html:')) { + return {type: AnnotationType.html, content: sliced}; + } else if (preamble.startsWith('markdown:')) { + return {type: AnnotationType.markdown, content: sliced.replace(/\$\{label\}/g, '')}; + } else if (preamble.startsWith('text:')) { + return {type: AnnotationType.text, content: sliced}; + } else { + return {type: AnnotationType.text, content: text}; + } } /* VERSION 1.3 */ // For backward compatibility only with version 1.3.0 'FAA70013-38E9-4FDF-B06A-F899F6487C19' export function isPluginAnnotationDictFormat_1_3_0(d: unknown): d is PluginAnnotationDict_1_3_0 { - if (typeof d !== 'object' || d === null) { - return false; - } - return Object.values(d).every(value => typeof value === 'string'); + if (typeof d !== 'object' || d === null) { + return false; + } + return Object.values(d).every(value => typeof value === 'string'); } export interface PluginAnnotationDict_1_3_0 { - [pluginId: string]: string; + [pluginId: string]: string; } // Extend the original interface and override the annotations property @@ -65,13 +65,13 @@ export interface PluginsAnnotationsSettings_1_3_0 extends Omit { - return new Promise((resolve) => { - const modal = new Modal(app); - modal.titleEl.setText(title); + return new Promise((resolve) => { + const modal = new Modal(app); + modal.titleEl.setText(title); - if (typeof message === 'string') { - modal.contentEl.setText(message); - } else { - modal.contentEl.appendChild(message); - } + if (typeof message === 'string') { + modal.contentEl.setText(message); + } else { + modal.contentEl.appendChild(message); + } - const buttonContainer = modal.contentEl.createDiv({ cls: 'modal-button-container' }); - buttonContainer.createEl('button', { text: 'Yes', cls: 'mod-cta' }).addEventListener('click', () => { - resolve(true); - modal.close(); - }); - buttonContainer.createEl('button', { text: 'No' }).addEventListener('click', () => { - resolve(false); - modal.close(); - }); + const buttonContainer = modal.contentEl.createDiv({ cls: 'modal-button-container' }); + buttonContainer.createEl('button', { text: 'Yes', cls: 'mod-cta' }).addEventListener('click', () => { + resolve(true); + modal.close(); + }); + buttonContainer.createEl('button', { text: 'No' }).addEventListener('click', () => { + resolve(false); + modal.close(); + }); - modal.open(); - }); + modal.open(); + }); } export function makePosixPathOScompatible(posixPath:string): string { - return posixPath.split(path.posix.sep).join(path.sep); + return posixPath.split(path.posix.sep).join(path.sep); } // Joins multiple path segments into a single normalized path. export function joinPaths(...paths: string[]): string { - return paths.join('/'); + return paths.join('/'); } export function isInstanceOfFolder(file: TAbstractFile): file is TFolder { - return file instanceof TFolder; + return file instanceof TFolder; } export function isInstanceOfFile(file: TAbstractFile): file is TFile { - return file instanceof TFile; + return file instanceof TFile; } export function doesFolderExist(vault: Vault, relativePath: string): boolean { - const file: TAbstractFile | null = vault.getAbstractFileByPath(relativePath); - return !!file && isInstanceOfFolder(file); + const file: TAbstractFile | null = vault.getAbstractFileByPath(relativePath); + return !!file && isInstanceOfFolder(file); } export function doesFileExist(vault: Vault, relativePath: string): boolean { - const file: TAbstractFile | null = vault.getAbstractFileByPath(relativePath); - return !!file && isInstanceOfFile(file); + const file: TAbstractFile | null = vault.getAbstractFileByPath(relativePath); + return !!file && isInstanceOfFile(file); } export async function getFileCaseInsensitive(vault: Vault, filePath: string): Promise { - // Check if the file exists (case-insensitive check) - const fileExists = await vault.adapter.exists(filePath); - - if (!fileExists) { - return null; // File does not exist - } + // Check if the file exists (case-insensitive check) + const fileExists = await vault.adapter.exists(filePath); + + if (!fileExists) { + return null; // File does not exist + } - // Iterate over all files and find the one with a case-insensitive match - const normalizedFilePath = filePath.toLowerCase(); + // Iterate over all files and find the one with a case-insensitive match + const normalizedFilePath = filePath.toLowerCase(); - for (const file of vault.getFiles()) { - if (file.path.toLowerCase() === normalizedFilePath) { - return file; // Return the matched TFile object with the correct case - } - } + for (const file of vault.getFiles()) { + if (file.path.toLowerCase() === normalizedFilePath) { + return file; // Return the matched TFile object with the correct case + } + } - return null; // No matching file found, this shouldn't happen if exists() returned true + return null; // No matching file found, this shouldn't happen if exists() returned true } export async function createFolderIfNotExists(vault: Vault, folderPath: string) { - if(doesFolderExist(vault,folderPath)) return; + if(doesFolderExist(vault,folderPath)) return; - try { - await vault.createFolder(folderPath); - } catch (error) { - throw new Error(`Failed to create folder at ${folderPath}: ${error}`); - } + try { + await vault.createFolder(folderPath); + } catch (error) { + throw new Error(`Failed to create folder at ${folderPath}: ${error}`); + } } /* File suggestions */ export class FileSuggestion extends AbstractInputSuggest { - private files:TFile[] = []; + private files:TFile[] = []; - constructor(app: App, inputEl: HTMLInputElement, private onSelectCallback: (file: TFile) => void = (v: TFile) => {}) { - super(app, inputEl); - } + constructor(app: App, inputEl: HTMLInputElement, private onSelectCallback: (file: TFile) => void = (v: TFile) => {}) { + super(app, inputEl); + } - doFuzzySearch(target: string, maxResults = 20, minScore = -2): TFile[] { - if (!target || target.length < 2) return []; - const fuzzy = prepareFuzzySearch(target); - const matches: [TFile, SearchResult | null][] = this.files.map((c) => [c, fuzzy(c.path)]); - // Filter out the null matches - const validMatches = matches.filter(([, result]) => result !== null && result.score > minScore); - // Sort the valid matches by score - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - validMatches.sort(([, a], [, b]) => b!.score - a!.score); - return validMatches.map((c) => c[0]).slice(0, maxResults); - } + doFuzzySearch(target: string, maxResults = 20, minScore = -2): TFile[] { + if (!target || target.length < 2) return []; + const fuzzy = prepareFuzzySearch(target); + const matches: [TFile, SearchResult | null][] = this.files.map((c) => [c, fuzzy(c.path)]); + // Filter out the null matches + const validMatches = matches.filter(([, result]) => result !== null && result.score > minScore); + // Sort the valid matches by score + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + validMatches.sort(([, a], [, b]) => b!.score - a!.score); + return validMatches.map((c) => c[0]).slice(0, maxResults); + } - getSuggestions(inputStr: string): TFile[] { - return this.doFuzzySearch(inputStr); - } + getSuggestions(inputStr: string): TFile[] { + return this.doFuzzySearch(inputStr); + } - renderSuggestion(file: TFile, el: HTMLElement): void { - el.setText(file.path); - } + renderSuggestion(file: TFile, el: HTMLElement): void { + el.setText(file.path); + } - selectSuggestion(selection: TFile, evt: MouseEvent | KeyboardEvent): void { - this.onSelectCallback(selection); - this.textInputEl.value = selection.path; - - // Create a custom event with additional data - this.textInputEl.dispatchEvent(new Event('change')); - this.textInputEl.setSelectionRange(0, 1) - this.textInputEl.setSelectionRange(this.textInputEl.value.length,this.textInputEl.value.length) - this.textInputEl.focus() - this.close(); - } + selectSuggestion(selection: TFile, evt: MouseEvent | KeyboardEvent): void { + this.onSelectCallback(selection); + this.textInputEl.value = selection.path; + + // Create a custom event with additional data + this.textInputEl.dispatchEvent(new Event('change')); + this.textInputEl.setSelectionRange(0, 1) + this.textInputEl.setSelectionRange(this.textInputEl.value.length,this.textInputEl.value.length) + this.textInputEl.focus() + this.close(); + } - setSuggestions(files:TFile[]) { - this.files = files; - } + setSuggestions(files:TFile[]) { + this.files = files; + } } @@ -151,24 +151,24 @@ export class FileSuggestion extends AbstractInputSuggest { // Function to sort PluginAnnotationDict based on the name field export function sortPluginAnnotationsByName(annotations: PluginAnnotationDict): string[] { - // Create an array of pairs [pluginId, name] - const pluginArray = Object.entries(annotations).map(([pluginId, annotation]) => { - return { pluginId, name: annotation.name }; - }); + // Create an array of pairs [pluginId, name] + const pluginArray = Object.entries(annotations).map(([pluginId, annotation]) => { + return { pluginId, name: annotation.name }; + }); - // Sort the array based on the 'name' field - pluginArray.sort((a, b) => a.name.localeCompare(b.name)); + // Sort the array based on the 'name' field + pluginArray.sort((a, b) => a.name.localeCompare(b.name)); - return pluginArray.map(item => item.pluginId); + return pluginArray.map(item => item.pluginId); } /* Download json settings */ export function downloadJson(data: unknown, filename = 'data.json') { - if (typeof data !== 'object' || data === null) { - return false; - } + if (typeof data !== 'object' || data === null) { + return false; + } // Step Convert data to JSON string const jsonStr = JSON.stringify(data, null, 2); // Pretty print with 2-space indentation