mirror of
https://github.com/alberti42/obsidian-plugins-annotations.git
synced 2026-07-22 10:10:24 +00:00
510 lines
25 KiB
TypeScript
510 lines
25 KiB
TypeScript
// settings_tab.ts
|
|
|
|
import PluginsAnnotations from "main";
|
|
import { handleMarkdownFilePathChange } from "manageAnnotations";
|
|
import { App, normalizePath, Notice, Platform, PluginSettingTab, Setting, TextComponent } from "obsidian";
|
|
import { PluginAnnotationDict } from "types";
|
|
import { parseFilePath, FileSuggestion, downloadJson, showConfirmationDialog } from "utils";
|
|
|
|
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
|
|
// FIXME
|
|
// 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
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|