mirror of
https://github.com/alberti42/obsidian-plugins-annotations.git
synced 2026-07-22 10:10:24 +00:00
Converted tabs to spaces
This commit is contained in:
parent
740be86c23
commit
400e4033e1
9 changed files with 1482 additions and 1482 deletions
|
|
@ -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: </b>',
|
||||
label_desktop: '<b>Personal annotation: </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: </b>',
|
||||
label_desktop: '<b>Personal annotation: </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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
1262
src/main.ts
1262
src/main.ts
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
78
src/types.ts
78
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<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
|
||||
}
|
||||
|
|
|
|||
108
src/types/obsidian-augment.d.ts
vendored
108
src/types/obsidian-augment.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
202
src/utils.ts
202
src/utils.ts
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue