add obsidian eslint plugin, solve major issues

This commit is contained in:
Moritz Jung 2026-05-06 16:26:59 +02:00
parent 84214a1c3c
commit 498e2611ae
105 changed files with 5702 additions and 5308 deletions

View file

@ -0,0 +1,44 @@
import type { App } from 'obsidian';
import { Modal, Setting } from 'obsidian';
export class ConfirmOverwriteModal extends Modal {
result: boolean = false;
onSubmit: (result: boolean) => void;
fileName: string;
constructor(app: App, fileName: string, onSubmit: (result: boolean) => void) {
super(app);
this.fileName = fileName;
this.onSubmit = onSubmit;
}
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'File already exists' });
contentEl.createEl('p', { text: `The file "${this.fileName}" already exists. Do you want to overwrite it?` });
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const bottomSettingRow = new Setting(contentEl);
bottomSettingRow.addButton(btn => {
btn.setButtonText('No');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
});
bottomSettingRow.addButton(btn => {
btn.setButtonText('Yes');
btn.setCta();
btn.onClick(() => {
this.result = true;
this.close();
});
btn.buttonEl.addClass('media-db-plugin-button');
});
}
onClose(): void {
const { contentEl } = this;
contentEl.empty();
this.onSubmit(this.result);
}
}

View file

@ -0,0 +1,133 @@
import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbAdvancedSearchModal extends Modal {
plugin: MediaDbPlugin;
query: string;
isBusy: boolean;
title: string;
selectedApis: string[];
searchBtn?: ButtonComponent;
submitCallback?: (res: AdvancedSearchModalData) => void;
closeCallback?: (err?: Error) => void;
constructor(plugin: MediaDbPlugin, advancedSearchModalOptions: AdvancedSearchModalOptions) {
advancedSearchModalOptions = Object.assign({}, ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, advancedSearchModalOptions);
super(plugin.app);
this.plugin = plugin;
this.selectedApis = [];
this.title = advancedSearchModalOptions.modalTitle ?? '';
this.query = advancedSearchModalOptions.prefilledSearchString ?? '';
this.isBusy = false;
}
setSubmitCb(submitCallback: (res: AdvancedSearchModalData) => void): void {
this.submitCallback = submitCallback;
}
setCloseCb(closeCallback: (err?: Error) => void): void {
this.closeCallback = closeCallback;
}
keyPressCallback(event: KeyboardEvent): void {
if (event.key === 'Enter') {
void this.search();
}
}
async search(): Promise<void> {
if (!this.query || this.query.length < 3) {
new Notice('MDB | Query too short');
return;
}
const apis: string[] = this.selectedApis;
if (apis.length === 0) {
new Notice('MDB | No API selected');
return;
}
if (!this.isBusy) {
this.isBusy = true;
this.searchBtn?.setDisabled(false);
this.searchBtn?.setButtonText('Searching...');
this.submitCallback?.({ query: this.query, apis: apis });
}
}
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.title });
const placeholder = 'Search by title';
const searchComponent = new TextComponent(contentEl);
searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder);
searchComponent.setValue(this.query);
searchComponent.onChange(value => (this.query = value));
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
contentEl.appendChild(searchComponent.inputEl);
searchComponent.inputEl.focus();
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'APIs to search' });
// const apiToggleComponents: Component[] = [];
for (const api of this.plugin.apiManager.apis) {
const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
apiToggleTextWrapper.createSpan({ text: api.apiName, cls: 'media-db-plugin-list-text' });
apiToggleTextWrapper.createEl('small', { text: api.apiDescription, cls: 'media-db-plugin-list-text' });
const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' });
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
apiToggleComponent.setTooltip(api.apiName);
apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName));
apiToggleComponent.onChange(value => {
if (value) {
this.selectedApis.push(api.apiName);
} else {
this.selectedApis = this.selectedApis.filter(x => x !== api.apiName);
}
});
apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl);
}
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
new Setting(contentEl)
.addButton(btn => {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
})
.addButton(btn => {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
void this.search();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;
});
}
onClose(): void {
this.closeCallback?.();
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,134 @@
import type { ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type { APIModel } from 'packages/obsidian/src/api/APIModel';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import { BulkImportLookupMethod } from 'packages/obsidian/src/utils/BulkImportHelper';
export class MediaDbBulkImportModal extends Modal {
plugin: MediaDbPlugin;
onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void;
selectedApi: string;
searchBtn?: ButtonComponent;
lookupMethod: BulkImportLookupMethod;
fieldName: string;
appendContent: boolean;
constructor(plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void) {
super(plugin.app);
this.plugin = plugin;
this.onSubmit = onSubmit;
this.selectedApi = plugin.apiManager.apis[0].apiName;
this.lookupMethod = BulkImportLookupMethod.TITLE;
this.fieldName = '';
this.appendContent = false;
}
submit(): void {
this.onSubmit(this.selectedApi, this.lookupMethod, this.fieldName, this.appendContent);
this.close();
}
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Import folder as Media DB entries' });
this.createDropdownEl(
contentEl,
'API to search',
(value: string) => {
this.selectedApi = value;
},
this.plugin.apiManager.apis.map((api: APIModel) => {
return { value: api.apiName, display: api.apiName };
}),
);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' });
const appendContentToggleElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
appendContentToggleTextWrapper.createSpan({
text: 'If this is enabled, the plugin will override metadata fields with the same name.',
cls: 'media-db-plugin-list-text',
});
const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' });
const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper);
appendContentToggle.setValue(false);
appendContentToggle.onChange(value => (this.appendContent = value));
appendContentToggleComponentWrapper.appendChild(appendContentToggle.toggleEl);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'Media lookup method' });
contentEl.createEl('p', {
text: 'Choose whether to search the API by title (can return multiple results) or lookup directly using an ID (returns at most one result), and specify the name of the frontmatter property which contains the title or ID of the media.',
});
this.createDropdownEl(
contentEl,
'Lookup media by',
(value: string) => {
this.lookupMethod = value as BulkImportLookupMethod;
},
[
{ value: BulkImportLookupMethod.TITLE, display: 'Title' },
{ value: BulkImportLookupMethod.ID, display: 'ID' },
],
);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const fieldNameWrapperEl = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const fieldNameLabelWrapperEl = fieldNameWrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
fieldNameLabelWrapperEl.createSpan({ text: 'Using the property named', cls: 'media-db-plugin-list-text' });
const fieldNameComponent = new TextComponent(fieldNameWrapperEl);
fieldNameComponent.setPlaceholder('title / id');
fieldNameComponent.onChange(value => (this.fieldName = value));
fieldNameComponent.inputEl.addEventListener('keydown', ke => {
if (ke.key === 'Enter') {
this.submit();
}
});
contentEl.appendChild(fieldNameWrapperEl);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
new Setting(contentEl)
.addButton(btn => {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
})
.addButton(btn => {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
this.submit();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;
});
}
createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void {
const wrapperEl = parentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const labelWrapperEl = wrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
labelWrapperEl.createSpan({ text: label, cls: 'media-db-plugin-list-text' });
const dropDownComponent = new DropdownComponent(wrapperEl);
dropDownComponent.onChange(onChange);
for (const option of options) {
dropDownComponent.addOption(option.value, option.display);
}
wrapperEl.appendChild(dropDownComponent.selectEl);
}
onClose(): void {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,119 @@
import type { ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { IdSearchModalData, IdSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbIdSearchModal extends Modal {
plugin: MediaDbPlugin;
query: string;
isBusy: boolean;
title: string;
selectedApi: string;
searchBtn?: ButtonComponent;
submitCallback?: (res: IdSearchModalData, err?: Error) => void;
closeCallback?: (err?: Error) => void;
constructor(plugin: MediaDbPlugin, idSearchModalOptions: IdSearchModalOptions) {
idSearchModalOptions = Object.assign({}, ID_SEARCH_MODAL_DEFAULT_OPTIONS, idSearchModalOptions);
super(plugin.app);
this.plugin = plugin;
this.title = idSearchModalOptions.modalTitle ?? '';
this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName;
this.query = '';
this.isBusy = false;
}
setSubmitCb(submitCallback: (res: IdSearchModalData, err?: Error) => void): void {
this.submitCallback = submitCallback;
}
setCloseCb(closeCallback: (err?: Error) => void): void {
this.closeCallback = closeCallback;
}
keyPressCallback(event: KeyboardEvent): void {
if (event.key === 'Enter') {
void this.search();
}
}
async search(): Promise<void> {
if (!this.query) {
new Notice('MDB | no Id entered');
return;
}
if (!this.selectedApi) {
new Notice('MDB | No API selected');
return;
}
if (!this.isBusy) {
this.isBusy = true;
this.searchBtn?.setDisabled(false);
this.searchBtn?.setButtonText('Searching...');
this.submitCallback?.({ query: this.query, api: this.selectedApi });
}
}
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.title });
const placeholder = 'Search by id';
const searchComponent = new TextComponent(contentEl);
searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder);
searchComponent.onChange(value => (this.query = value));
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
contentEl.appendChild(searchComponent.inputEl);
searchComponent.inputEl.focus();
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const apiSelectorWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const apiSelectorTExtWrapper = apiSelectorWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
apiSelectorTExtWrapper.createSpan({ text: 'API to search', cls: 'media-db-plugin-list-text' });
const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper);
apiSelectorComponent.onChange((value: string) => {
this.selectedApi = value;
});
for (const api of this.plugin.apiManager.apis) {
apiSelectorComponent.addOption(api.apiName, api.apiName);
}
apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
new Setting(contentEl)
.addButton(btn => {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
})
.addButton(btn => {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
void this.search();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;
});
}
onClose(): void {
this.closeCallback?.();
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,87 @@
import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { Logger } from 'packages/obsidian/src/utils/Logger';
import type { PreviewModalData, PreviewModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { PREVIEW_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbPreviewModal extends Modal {
plugin: MediaDbPlugin;
elements: MediaTypeModel[];
title: string;
markdownComponent: Component;
submitCallback?: (previewModalData: PreviewModalData) => void;
closeCallback?: (err?: Error) => void;
constructor(plugin: MediaDbPlugin, previewModalOptions: PreviewModalOptions) {
previewModalOptions = Object.assign({}, PREVIEW_MODAL_DEFAULT_OPTIONS, previewModalOptions);
super(plugin.app);
this.plugin = plugin;
this.title = previewModalOptions.modalTitle ?? '';
this.elements = previewModalOptions.elements ?? [];
this.markdownComponent = new Component();
}
setSubmitCb(submitCallback: (previewModalData: PreviewModalData) => void): void {
this.submitCallback = submitCallback;
}
setCloseCb(closeCallback: (err?: Error) => void): void {
this.closeCallback = closeCallback;
}
async preview(): Promise<void> {
const { contentEl } = this;
contentEl.addClass('media-db-plugin-preview-modal');
contentEl.createEl('h2', { text: this.title });
const previewWrapper = contentEl.createDiv({ cls: 'media-db-plugin-preview-wrapper' });
this.markdownComponent.load();
for (const result of this.elements) {
previewWrapper.createEl('h3', { text: result.englishTitle });
const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' });
let fileContent = this.plugin.fileHelper.generateMediaDbNoteFrontmatterPreview(result);
fileContent = `\`\`\`yaml\n${fileContent}\`\`\``;
try {
// TODO: fix this not rendering the frontmatter any more
await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent);
} catch (e) {
Logger.warn(`mdb | error during rendering of preview`, e);
}
}
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const bottomSettingRow = new Setting(contentEl);
bottomSettingRow.addButton(btn => {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
});
bottomSettingRow.addButton(btn => {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => this.submitCallback?.({ confirmed: true }));
btn.buttonEl.addClass('media-db-plugin-button');
});
}
onOpen(): void {
void this.preview();
}
onClose(): void {
this.markdownComponent.unload();
this.closeCallback?.();
}
}

View file

@ -0,0 +1,145 @@
import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
import type { SearchModalData, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
import { unCamelCase } from 'packages/obsidian/src/utils/Utils';
export class MediaDbSearchModal extends Modal {
plugin: MediaDbPlugin;
query: string;
isBusy: boolean;
title: string;
selectedTypes: MediaType[];
searchBtn?: ButtonComponent;
submitCallback?: (res: SearchModalData) => void;
closeCallback?: (err?: Error) => void;
constructor(plugin: MediaDbPlugin, searchModalOptions: SearchModalOptions) {
searchModalOptions = Object.assign({}, SEARCH_MODAL_DEFAULT_OPTIONS, searchModalOptions);
super(plugin.app);
this.plugin = plugin;
this.selectedTypes = [...(searchModalOptions.preselectedTypes ?? [])];
this.title = searchModalOptions.modalTitle ?? '';
this.query = searchModalOptions.prefilledSearchString ?? '';
this.isBusy = false;
}
setSubmitCb(submitCallback: (res: SearchModalData) => void): void {
this.submitCallback = submitCallback;
}
setCloseCb(closeCallback: (err?: Error) => void): void {
this.closeCallback = closeCallback;
}
keyPressCallback(event: KeyboardEvent): void {
if (event.key === 'Enter') {
void this.search();
}
}
async search(): Promise<void> {
if (!this.query || this.query.length < 3) {
new Notice('MDB | Query too short');
return;
}
const types: MediaType[] = this.selectedTypes;
if (types.length === 0) {
new Notice('MDB | No Type selected');
return;
}
if (!this.isBusy) {
this.isBusy = true;
this.searchBtn?.setDisabled(false);
this.searchBtn?.setButtonText('Searching...');
this.submitCallback?.({ query: this.query, types: types });
}
}
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.title });
const placeholder = 'Search by title';
const searchComponent = new TextComponent(contentEl);
let currentToggle: ToggleComponent | undefined = undefined;
searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder);
searchComponent.setValue(this.query);
searchComponent.onChange(value => (this.query = value));
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
contentEl.appendChild(searchComponent.inputEl);
searchComponent.inputEl.focus();
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'APIs to search' });
for (const mediaType of MEDIA_TYPES) {
const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
apiToggleTextWrapper.createSpan({ text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' });
const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' });
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
apiToggleComponent.setTooltip(unCamelCase(mediaType));
apiToggleComponent.setValue(this.selectedTypes.includes(mediaType));
if (apiToggleComponent.getValue()) {
currentToggle = apiToggleComponent;
}
apiToggleComponent.onChange(value => {
if (value) {
if (currentToggle && currentToggle !== apiToggleComponent) {
currentToggle.setValue(false);
this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType);
}
currentToggle = apiToggleComponent;
this.selectedTypes.push(mediaType);
} else {
currentToggle = undefined;
this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType);
}
});
apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl);
}
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
new Setting(contentEl)
.addButton(btn => {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
})
.addButton(btn => {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
void this.search();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;
});
}
onClose(): void {
this.closeCallback?.();
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,66 @@
import type MediaDbPlugin from 'packages/obsidian/src/main';
import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
plugin: MediaDbPlugin;
busy: boolean;
sendCallback: boolean;
submitCallback?: (res: SelectModalData) => void;
closeCallback?: (err?: Error) => void;
skipCallback?: () => void;
submitButtonText: string;
constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) {
selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions);
super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect);
this.plugin = plugin;
this.title = selectModalOptions.modalTitle ?? '';
this.description = selectModalOptions.description ?? 'Select one or multiple search results.';
this.addSkipButton = selectModalOptions.skipButton ?? false;
this.submitButtonText = selectModalOptions.submitButtonText ?? 'Ok';
this.busy = false;
this.sendCallback = false;
}
setSubmitCb(submitCallback: (res: SelectModalData) => void): void {
this.submitCallback = submitCallback;
}
setCloseCb(closeCallback: (err?: Error) => void): void {
this.closeCallback = closeCallback;
}
setSkipCallback(skipCallback: () => void): void {
this.skipCallback = skipCallback;
}
// Renders each suggestion item.
renderElement(item: MediaTypeModel, el: HTMLElement): void {
el.createDiv({ text: this.plugin.mediaTypeManager.getFileName(item) });
el.createEl('small', { text: `${item.getSummary()}\n` });
el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` });
}
// Perform action on the selected suggestion.
submit(): void {
if (!this.busy) {
this.busy = true;
this.submitButton?.setButtonText('Creating entry...');
this.submitCallback?.({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) });
}
}
skip(): void {
this.skipButton?.setButtonText('Skipping...');
this.skipCallback?.();
}
onClose(): void {
this.closeCallback?.();
}
}

View file

@ -0,0 +1,50 @@
import type MediaDbPlugin from 'packages/obsidian/src/main';
import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
export interface SeasonSelectModalElement {
season_number: number;
name: string;
air_date?: string;
poster_path?: string;
}
export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalElement> {
plugin: MediaDbPlugin;
submitCallback?: (selectedSeasons: SeasonSelectModalElement[]) => void;
closeCallback?: (err?: Error) => void;
seriesName?: string;
constructor(plugin: MediaDbPlugin, seasons: SeasonSelectModalElement[], multiSelect = true, seriesName?: string) {
super(plugin.app, seasons, multiSelect);
this.plugin = plugin;
this.seriesName = seriesName;
this.title = `Select seasons for${seriesName ? ` ${seriesName}` : ''}`;
this.description = 'Select one or more seasons to create notes for.';
this.submitButtonText = 'Create Entry';
}
renderElement(season: SeasonSelectModalElement, el: HTMLElement): void {
el.createDiv({ text: `${season.name}` });
if (season.air_date) {
el.createEl('small', { text: `Air date: ${season.air_date}` });
}
}
submit(): void {
const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value);
this.submitCallback?.(selected);
this.close();
}
skip(): void {
this.close();
}
setSubmitCb(cb: (selectedSeasons: SeasonSelectModalElement[]) => void): void {
this.submitCallback = cb;
}
setCloseCb(cb: (err?: Error) => void): void {
this.closeCallback = cb;
}
}

View file

@ -0,0 +1,176 @@
import type { App, ButtonComponent } from 'obsidian';
import { Modal, Setting } from 'obsidian';
import { SelectModalElement } from 'packages/obsidian/src/modals/SelectModalElement';
import { mod } from 'packages/obsidian/src/utils/Utils';
export abstract class SelectModal<T> extends Modal {
allowMultiSelect: boolean;
title: string;
description: string;
addSkipButton: boolean;
cancelButton?: ButtonComponent;
skipButton?: ButtonComponent;
submitButton?: ButtonComponent;
submitButtonText: string;
elementWrapper?: HTMLDivElement;
elements: T[];
selectModalElements: SelectModalElement<T>[];
protected constructor(app: App, elements: T[], allowMultiSelect: boolean = true) {
super(app);
this.allowMultiSelect = allowMultiSelect;
this.title = '';
this.description = '';
this.addSkipButton = false;
this.submitButtonText = 'Ok';
this.cancelButton = undefined;
this.skipButton = undefined;
this.submitButton = undefined;
this.elementWrapper = undefined;
this.elements = elements;
this.selectModalElements = [];
this.scope.register([], 'ArrowUp', evt => {
this.highlightUp();
evt.preventDefault();
});
this.scope.register([], 'ArrowDown', evt => {
this.highlightDown();
evt.preventDefault();
});
this.scope.register([], 'ArrowRight', () => {
this.activateHighlighted();
});
this.scope.register([], ' ', evt => {
if (this.elementWrapper && this.elementWrapper === activeDocument.activeElement) {
this.activateHighlighted();
evt.preventDefault();
}
});
this.scope.register([], 'Enter', () => this.submit());
}
abstract renderElement(value: T, el: HTMLElement): void;
abstract submit(): void;
abstract skip(): void;
disableAllOtherElements(elementId: number): void {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.id !== elementId) {
selectModalElement.setActive(false);
}
}
}
deHighlightAllOtherElements(elementId: number): void {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.id !== elementId) {
selectModalElement.setHighlighted(false);
}
}
}
onOpen(): void {
const { contentEl, titleEl } = this;
titleEl.createEl('h2', { text: this.title });
contentEl.addClass('media-db-plugin-select-modal');
contentEl.createEl('p', { text: this.description });
this.elementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-select-wrapper' });
this.elementWrapper.tabIndex = 0;
let i = 0;
for (const element of this.elements) {
const selectModalElement = new SelectModalElement(element, this.elementWrapper, i, this, false);
this.selectModalElements.push(selectModalElement);
this.renderElement(element, selectModalElement.element);
i += 1;
}
this.selectModalElements.first()?.element.scrollIntoView();
const bottomSettingRow = new Setting(contentEl);
bottomSettingRow.addButton(btn => {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
btn.buttonEl.addClass('media-db-plugin-button');
this.cancelButton = btn;
});
if (this.addSkipButton) {
bottomSettingRow.addButton(btn => {
btn.setButtonText('Skip');
btn.onClick(() => this.skip());
btn.buttonEl.addClass('media-db-plugin-button');
this.skipButton = btn;
});
}
bottomSettingRow.addButton(btn => {
btn.setButtonText(this.submitButtonText);
btn.setCta();
btn.onClick(() => this.submit());
btn.buttonEl.addClass('media-db-plugin-button');
this.submitButton = btn;
});
}
activateHighlighted(): void {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.isHighlighted()) {
selectModalElement.setActive(!selectModalElement.isActive());
if (!this.allowMultiSelect) {
this.disableAllOtherElements(selectModalElement.id);
}
}
}
}
highlightUp(): void {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.isHighlighted()) {
this.getPreviousSelectModalElement(selectModalElement).setHighlighted(true);
return;
}
}
// nothing is highlighted
this.selectModalElements.last()?.setHighlighted(true);
}
highlightDown(): void {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.isHighlighted()) {
this.getNextSelectModalElement(selectModalElement).setHighlighted(true);
return;
}
}
// nothing is highlighted
this.selectModalElements.first()?.setHighlighted(true);
}
private getNextSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id + 1;
nextId = mod(nextId, this.selectModalElements.length);
return this.selectModalElements.find(x => x.id === nextId)!;
}
private getPreviousSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id - 1;
nextId = mod(nextId, this.selectModalElements.length);
return this.selectModalElements.find(x => x.id === nextId)!;
}
}

View file

@ -0,0 +1,88 @@
import type { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
export class SelectModalElement<T> {
selectModal: SelectModal<T>;
value: T;
readonly id: number;
element: HTMLDivElement;
cssClass: string;
activeClass: string;
hoverClass: string;
private active: boolean;
private highlighted: boolean;
constructor(value: T, parentElement: HTMLElement, id: number, selectModal: SelectModal<T>, active: boolean = false) {
this.value = value;
this.id = id;
this.active = active;
this.selectModal = selectModal;
this.cssClass = 'media-db-plugin-select-element';
this.activeClass = 'media-db-plugin-select-element-selected';
this.hoverClass = 'media-db-plugin-select-element-hover';
this.element = parentElement.createDiv({ cls: this.cssClass });
this.element.id = this.getHTMLId();
this.element.on('click', '#' + this.getHTMLId(), () => {
this.setActive(!this.active);
if (!this.selectModal.allowMultiSelect) {
this.selectModal.disableAllOtherElements(this.id);
}
});
this.element.on('mouseenter', '#' + this.getHTMLId(), () => {
this.setHighlighted(true);
});
this.element.on('mouseleave', '#' + this.getHTMLId(), () => {
this.setHighlighted(false);
});
this.highlighted = false;
}
getHTMLId(): string {
return `media-db-plugin-select-element-${this.id}`;
}
isHighlighted(): boolean {
return this.highlighted;
}
setHighlighted(value: boolean): void {
this.highlighted = value;
if (this.highlighted) {
this.addClass(this.hoverClass);
this.selectModal.deHighlightAllOtherElements(this.id);
} else {
this.removeClass(this.hoverClass);
}
}
isActive(): boolean {
return this.active;
}
setActive(active: boolean): void {
this.active = active;
this.update();
}
update(): void {
if (this.active) {
this.addClass(this.activeClass);
} else {
this.removeClass(this.activeClass);
}
}
addClass(cssClass: string): void {
if (!this.element.hasClass(cssClass)) {
this.element.addClass(cssClass);
}
}
removeClass(cssClass: string): void {
if (this.element.hasClass(cssClass)) {
this.element.removeClass(cssClass);
}
}
}