Converted tabs to spaces

This commit is contained in:
Andrea Alberti 2024-08-17 22:02:13 +02:00
parent 740be86c23
commit 400e4033e1
9 changed files with 1482 additions and 1482 deletions

View file

@ -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: '<b>Annotation:&nbsp;</b>',
label_desktop: '<b>Personal annotation:&nbsp;</b>',
label_placeholder : "<em>Add your personal comment about <strong>${plugin_name}</strong> here...</em>",
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: '<b>Annotation:&nbsp;</b>',
label_desktop: '<b>Personal annotation:&nbsp;</b>',
label_placeholder : "<em>Add your personal comment about <strong>${plugin_name}</strong> here...</em>",
editable: true,
automatic_remove: false,
markdown_file_path: '',
backups: [],
compatibility: '1.5.0',
}

View file

@ -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',
}

File diff suppressed because it is too large Load diff

View file

@ -9,133 +9,133 @@ import { PluginAnnotationDict } from "types";
let isWriting = false;
export async function handleMarkdownFilePathChange(plugin: PluginsAnnotations, filepath: string): Promise<boolean> {
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<void> {
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 <!-- BEGIN ANNOTATION --> and <!-- END ANNOTATION -->. Changes made anywhere else will be overwritten.\n'
const header = 'Make changes only within the annotation blocks marked by <!-- BEGIN ANNOTATION --> and <!-- END ANNOTATION -->. Changes made anywhere else will be overwritten.\n'
const content: string[] = [header];
for (const pluginId in annotations) {
content.push(`# ${annotations[pluginId].name}\n\n<!-- id: ${pluginId} -->\n<!-- type: ${annotations[pluginId].type} -->\n<!-- BEGIN ANNOTATION -->\n${annotations[pluginId].desc}\n<!-- END ANNOTATION -->\n`);
}
const content_concatenated = content.join('\n');
const content: string[] = [header];
for (const pluginId in annotations) {
content.push(`# ${annotations[pluginId].name}\n\n<!-- id: ${pluginId} -->\n<!-- type: ${annotations[pluginId].type} -->\n<!-- BEGIN ANNOTATION -->\n${annotations[pluginId].desc}\n<!-- END ANNOTATION -->\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;
}
}

View file

@ -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<void> {
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:\
<ul>\
<li>markdown:</li>\
<li>html:</li>\
<li>text:</li>\
</ul>\
If the first line of annotation text contains none of the options above, \
the default <em>markdown:</em> 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 <em>${label}</em> to \
display the <em>annotation label</em> 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<void> {
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:\
<ul>\
<li>markdown:</li>\
<li>html:</li>\
<li>text:</li>\
</ul>\
If the first line of annotation text contains none of the options above, \
the default <em>markdown:</em> 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 <em>${label}</em> to \
display the <em>annotation label</em> 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
}
});
});
}
}
}

View file

@ -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<string, unknown>;
if (typeof anno !== 'object' || anno === null) {
return false;
}
const obj = anno as Record<string, unknown>;
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
}

View file

@ -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<string, PluginManifest>;
plugins: Record<string, Plugin>;
getPlugin(id: string): Plugin;
uninstallPlugin(pluginId: string): Promise<void>;
close(): void;
}
interface InternalPlugins {
plugins: Record<string, Plugin>;
getPluginById(id: string): Plugin;
}
interface Plugins {
manifests: Record<string, PluginManifest>;
plugins: Record<string, Plugin>;
getPlugin(id: string): Plugin;
uninstallPlugin(pluginId: string): Promise<void>;
}
interface InternalPlugins {
plugins: Record<string, Plugin>;
getPluginById(id: string): Plugin;
}
interface AbstractInputSuggest<T> extends PopoverSuggest<T> {
textInputEl: HTMLInputElement;
}
interface AbstractInputSuggest<T> extends PopoverSuggest<T> {
textInputEl: HTMLInputElement;
}
}

View file

@ -6,57 +6,57 @@ import { AnnotationType, PluginAnnotation, PluginsAnnotationsSettings } from "ty
/* VERSION 1.4 */
export interface PluginAnnotation_1_4_0 extends Omit<PluginAnnotation, 'type' | 'desc'> {
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<PluginsAnnotationsSettings, 'annotations' | 'markdown_file_path' | 'compatibility' | 'backups' > {
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<PluginsAnnotation
}
export function isSettingsFormat_1_3_0(s:unknown): s is PluginsAnnotationsSettings_1_3_0 {
if (typeof s !== 'object' || s === null) {
return false;
}
if (typeof s !== 'object' || s === null) {
return false;
}
return 'annotations' in s
&& 'plugins_annotations_uuid' in s
&& (s as PluginsAnnotationsSettings_1_3_0).plugins_annotations_uuid === DEFAULT_SETTINGS_1_3_0.plugins_annotations_uuid;
return 'annotations' in s
&& 'plugins_annotations_uuid' in s
&& (s as PluginsAnnotationsSettings_1_3_0).plugins_annotations_uuid === DEFAULT_SETTINGS_1_3_0.plugins_annotations_uuid;
}
/* VERSION 1.0 */

View file

@ -1,149 +1,149 @@
// utils.ts
import { App, Modal, normalizePath, TAbstractFile, TFile, TFolder, Vault,
AbstractInputSuggest, prepareFuzzySearch, SearchResult } from "obsidian";
AbstractInputSuggest, prepareFuzzySearch, SearchResult } from "obsidian";
import * as path from "path";
import { ParsedPath, PluginAnnotationDict } from "types";
export function parseFilePath(filePath: string): ParsedPath {
filePath = normalizePath(filePath);
const lastSlashIndex = filePath.lastIndexOf('/');
filePath = normalizePath(filePath);
const lastSlashIndex = filePath.lastIndexOf('/');
const dir = lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex) : '';
const base = lastSlashIndex !== -1 ? filePath.substring(lastSlashIndex + 1) : filePath;
const extIndex = base.lastIndexOf('.');
const filename = extIndex !== -1 ? base.substring(0, extIndex) : base;
const ext = extIndex !== -1 ? base.substring(extIndex) : '';
const dir = lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex) : '';
const base = lastSlashIndex !== -1 ? filePath.substring(lastSlashIndex + 1) : filePath;
const extIndex = base.lastIndexOf('.');
const filename = extIndex !== -1 ? base.substring(0, extIndex) : base;
const ext = extIndex !== -1 ? base.substring(extIndex) : '';
return { dir: normalizePath(dir), base, filename, ext, path: filePath };
return { dir: normalizePath(dir), base, filename, ext, path: filePath };
}
// Helper function to show a confirmation dialog
export function showConfirmationDialog(app: App, title: string, message: DocumentFragment | string): Promise<boolean> {
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<TFile | null> {
// 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<TFile> {
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<TFile> {
// 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