diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 4996eb6..4198720 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -4,7 +4,6 @@ import MediaDbPlugin from '../../main'; import {BoardGameModel} from 'src/models/BoardGameModel'; import {debugLog} from '../../utils/Utils'; import {requestUrl} from 'obsidian'; -import {MediaType} from '../../utils/MediaType'; export class BoardGameGeekAPI extends APIModel { plugin: MediaDbPlugin; @@ -79,17 +78,16 @@ export class BoardGameGeekAPI extends APIModel { const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')).map(n => n!.textContent!); const model = new BoardGameModel({ - type: MediaType.BoardGame, title, englishTitle: title, year: year === '0' ? '' : year, dataSource: this.apiName, url: `https://boardgamegeek.com/boardgame/${id}`, - id, + id: id, - genres, - onlineRating, - image, + genres: genres, + onlineRating: onlineRating, + image: image, released: true, userData: { diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts index 835e225..cd73890 100644 --- a/src/api/apis/WikipediaAPI.ts +++ b/src/api/apis/WikipediaAPI.ts @@ -66,6 +66,7 @@ export class WikipediaAPI extends APIModel { englishTitle: result.title, year: '', dataSource: this.apiName, + url: result.fullurl, id: result.pageid, wikiUrl: result.fullurl, diff --git a/src/main.ts b/src/main.ts index c6f62a9..5f87f9f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,10 @@ -import {Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder} from 'obsidian'; +import {MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder} from 'obsidian'; import {getDefaultSettings, MediaDbPluginSettings, MediaDbSettingTab} from './settings/Settings'; import {APIManager} from './api/APIManager'; import {MediaTypeModel} from './models/MediaTypeModel'; -import {dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, UserCancelError, UserSkipError} from './utils/Utils'; +import {CreateNoteOptions, dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString} from './utils/Utils'; import {OMDbAPI} from './api/apis/OMDbAPI'; -import {MediaDbAdvancedSearchModal} from './modals/MediaDbAdvancedSearchModal'; -import {MediaDbSearchResultModal} from './modals/MediaDbSearchResultModal'; import {MALAPI} from './api/apis/MALAPI'; -import {MediaDbIdSearchModal} from './modals/MediaDbIdSearchModal'; import {WikipediaAPI} from './api/apis/WikipediaAPI'; import {MusicBrainzAPI} from './api/apis/MusicBrainzAPI'; import {MediaTypeManager} from './utils/MediaTypeManager'; @@ -17,12 +14,14 @@ import {PropertyMapper} from './settings/PropertyMapper'; import {YAMLConverter} from './utils/YAMLConverter'; import {MediaDbFolderImportModal} from './modals/MediaDbFolderImportModal'; import {PropertyMapping, PropertyMappingModel} from './settings/PropertyMapping'; +import {ModalHelper, ModalResultCode} from './utils/ModalHelper'; export default class MediaDbPlugin extends Plugin { settings: MediaDbPluginSettings; apiManager: APIManager; mediaTypeManager: MediaTypeManager; modelPropertyMapper: PropertyMapper; + modalHelper: ModalHelper; frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; @@ -39,6 +38,7 @@ export default class MediaDbPlugin extends Plugin { this.mediaTypeManager = new MediaTypeManager(); this.modelPropertyMapper = new PropertyMapper(this); + this.modalHelper = new ModalHelper(this); await this.loadSettings(); // register the settings tab @@ -57,7 +57,7 @@ export default class MediaDbPlugin extends Plugin { menu.addItem(item => { item.setTitle('Import folder as Media DB entries') .setIcon('database') - .onClick(() => this.createEntriesFromFolder(file as TFolder)); + .onClick(() => this.createEntriesFromFolder(file)); }); } })); @@ -77,94 +77,139 @@ export default class MediaDbPlugin extends Plugin { // register command to update the open note this.addCommand({ id: 'update-media-db-note', - name: 'Update the open note, if it is a Media DB entry.', + name: 'Update open note (this will recreate the note)', checkCallback: (checking: boolean) => { if (!this.app.workspace.getActiveFile()) { return false; } if (!checking) { - this.updateActiveNote(); + this.updateActiveNote(false); + } + return true; + }, + }); + this.addCommand({ + id: 'update-media-db-note-metadata', + name: 'Update metadata', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + this.updateActiveNote(true); + } + return true; + }, + }); + // register link insert command + this.addCommand({ + id: 'add-media-db-link', + name: 'Insert link', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + this.createLinkWithSearchModal(); } return true; }, }); } + /** + * first very simple approach + * TODO: + * - replace the detail query + * - maybe custom link syntax + */ + async createLinkWithSearchModal() { + + let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async (advancedSearchModalData) => { + return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); + }); + + if (!apiSearchResults) { + return; + } + + const selectResults: MediaTypeModel[] = await this.modalHelper.openSelectModal({elements: apiSearchResults, multiSelect: false}, async (selectModalData) => { + return await this.queryDetails(selectModalData.selected); + }); + + if (!selectResults || selectResults.length < 1) { + return; + } + + const link = `[${selectResults[0].title}](${selectResults[0].url})`; + + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + + // Make sure the user is editing a Markdown file. + if (view) { + view.editor.replaceRange(link, view.editor.getCursor()); + } + } + async createEntryWithSearchModal() { } + /** + * TODO: further refactor: extract it into own method, pass the action (api query) as lambda as well as an options object + */ async createEntryWithAdvancedSearchModal() { - let results: MediaTypeModel[] = []; + let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async (advancedSearchModalData) => { + return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); + }); - const {advancedSearchOptions, advancedSearchModal} = await this.openMediaDbAdvancedSearchModal(); - if (!advancedSearchOptions) { - advancedSearchModal.close(); + if (!apiSearchResults) { + // TODO: add new notice saying no results found? return; } - let apiSearchResults: MediaTypeModel[] = undefined; - try { - apiSearchResults = await this.apiManager.query(advancedSearchOptions.query, advancedSearchOptions.apis); - } catch (e) { - console.warn(e); - new Notice(e.toString()); - advancedSearchModal.close(); - return; + let selectResults: MediaTypeModel[]; + let proceed: boolean; + + while (!proceed) { + selectResults = await this.modalHelper.openSelectModal({elements: apiSearchResults}, async (selectModalData) => { + return await this.queryDetails(selectModalData.selected); + }); + if (!selectResults) { + return; + } + + proceed = await this.modalHelper.openPreviewModal({elements: selectResults}, async (previewModalData) => { + return previewModalData.confirmed; + }); } - advancedSearchModal.close(); - - const {selectRes, selectModal} = await this.openMediaDbSelectModal(apiSearchResults, false); - if (!selectRes) { - selectModal.close(); - return; - } - - try { - results = await this.queryDetails(selectRes); - } catch (e) { - console.warn(e); - new Notice(e.toString()); - selectModal.close(); - return; - } - - selectModal.close(); - - if (results) { - await this.createMediaDbNotes(results); - } + await this.createMediaDbNotes(selectResults); } - async createEntryWithIdSearchModal() { - let result: MediaTypeModel = undefined; + async createEntryWithIdSearchModal(): Promise { + let idSearchResult: MediaTypeModel; + let proceed: boolean; - const {idSearchOptions, idSearchModal} = await this.openMediaDbIdSearchModal(); - if (!idSearchOptions) { - idSearchModal.close(); - return; + while (!proceed) { + idSearchResult = await this.modalHelper.openIdSearchModal({}, async (idSearchModalData) => { + return await this.apiManager.queryDetailedInfoById(idSearchModalData.query, idSearchModalData.api); + }); + if (!idSearchResult) { + return; + } + + proceed = await this.modalHelper.openPreviewModal({elements: [idSearchResult]}, async (previewModalData) => { + return previewModalData.confirmed; + }); } - try { - result = await this.apiManager.queryDetailedInfoById(idSearchOptions.query, idSearchOptions.api); - } catch (e) { - console.warn(e); - new Notice(e.toString()); - idSearchModal.close(); - return; - } - - idSearchModal.close(); - - if (result) { - await this.createMediaDbNoteFromModel(result); - } + await this.createMediaDbNoteFromModel(idSearchResult, {attachTemplate: true, openNote: true}); } async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { for (const model of models) { - await this.createMediaDbNoteFromModel(model, attachFile); + await this.createMediaDbNoteFromModel(model, {attachTemplate: true, attachFile: attachFile}); } } @@ -181,27 +226,28 @@ export default class MediaDbPlugin extends Plugin { return detailModels; } - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, attachFile?: TFile): Promise { + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { try { console.debug('MDB | creating new note'); - let fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, attachFile); + let fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); - await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent); + await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options.openNote); } catch (e) { console.warn(e); new Notice(e.toString()); } } - private async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, attachFile: TFile) { + async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions) { let fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); let fileContent = ''; + const template = options.attachTemplate ? await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app) : ''; - ({fileMetadata, fileContent} = await this.attachFile(fileMetadata, fileContent, attachFile)); - ({fileMetadata, fileContent} = await this.attachTemplate(fileMetadata, fileContent, await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app))); + ({fileMetadata, fileContent} = await this.attachFile(fileMetadata, fileContent, options.attachFile)); + ({fileMetadata, fileContent} = await this.attachTemplate(fileMetadata, fileContent, template)); - fileContent = `---\n${this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(fileMetadata) : stringifyYaml(fileMetadata)}---` + fileContent; + fileContent = `---\n${this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(fileMetadata) : stringifyYaml(fileMetadata)}---\n` + fileContent; return fileContent; } @@ -214,8 +260,9 @@ export default class MediaDbPlugin extends Plugin { fileMetadata = Object.assign(attachFileMetadata, fileMetadata); let attachFileContent: string = await this.app.vault.read(fileToAttach); - const regExp = new RegExp('^(---)\\n[\\s\\S]*\\n---'); + const regExp = new RegExp(this.frontMatterRexExpPattern); attachFileContent = attachFileContent.replace(regExp, ''); + attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; fileContent += attachFileContent; return {fileMetadata: fileMetadata, fileContent: fileContent}; @@ -315,7 +362,7 @@ export default class MediaDbPlugin extends Plugin { * Update the active note by querying the API again. * Tries to read the type, id and dataSource of the active note. If successful it will query the api, delete the old note and create a new one. */ - async updateActiveNote() { + async updateActiveNote(onlyMetadata: boolean = false) { const activeFile: TFile = this.app.workspace.getActiveFile(); if (!activeFile) { throw new Error('MDB | there is no active note'); @@ -341,7 +388,12 @@ export default class MediaDbPlugin extends Plugin { // deletion not happening anymore why is this log statement still here console.debug('MDB | deleting old entry'); - await this.createMediaDbNoteFromModel(newMediaTypeModel, activeFile); + if (onlyMetadata) { + await this.createMediaDbNoteFromModel(newMediaTypeModel, {attachFile: activeFile, openNote: true}); + } else { + await this.createMediaDbNoteFromModel(newMediaTypeModel, {attachTemplate: true, openNote: true}); + } + } async createEntriesFromFolder(folder: TFolder) { @@ -382,46 +434,36 @@ export default class MediaDbPlugin extends Plugin { continue; } - let selectedResults: MediaTypeModel[] = []; - const modal = new MediaDbSearchResultModal(this, results, true); - try { - selectedResults = await new Promise((resolve, reject) => { - modal.title = `Results for \'${title}\'`; - modal.setSubmitCallback(res => resolve(res)); - modal.setSkipCallback(() => reject(new UserCancelError('user skipped'))); - modal.setCloseCallback(err => { - if (err) { - reject(err); - } - reject(new UserCancelError('user canceled')); - }); + let {selectModalResult, selectModal} = await this.modalHelper.createSelectModal({elements: results, skipButton: true, modalTitle: `Results for \'${title}\'`}); - modal.open(); - }); - } catch (e) { - modal.close(); - if (e instanceof UserCancelError) { - erroredFiles.push({filePath: file.path, error: e.message}); - canceled = true; - continue; - } else if (e instanceof UserSkipError) { - erroredFiles.push({filePath: file.path, error: e.message}); - continue; - } else { - erroredFiles.push({filePath: file.path, error: e.message}); - continue; - } + if (selectModalResult.code === ModalResultCode.ERROR) { + erroredFiles.push({filePath: file.path, error: selectModalResult.error.message}); + selectModal.close(); + continue; } - if (selectedResults.length === 0) { + if (selectModalResult.code === ModalResultCode.CLOSE) { + erroredFiles.push({filePath: file.path, error: 'user canceled'}); + selectModal.close(); + canceled = true; + continue; + } + + if (selectModalResult.code === ModalResultCode.SKIP) { + erroredFiles.push({filePath: file.path, error: 'user skipped'}); + selectModal.close(); + continue; + } + + if (selectModalResult.data.selected.length === 0) { erroredFiles.push({filePath: file.path, error: `no search results selected`}); continue; } - const detailedResults = await this.queryDetails(selectedResults); + const detailedResults = await this.queryDetails(selectModalResult.data.selected); await this.createMediaDbNotes(detailedResults, appendContent ? file : null); - modal.close(); + selectModal.close(); } } @@ -441,55 +483,6 @@ export default class MediaDbPlugin extends Plugin { const targetFile = await this.app.vault.create(filePath, fileContent); } - async openMediaDbAdvancedSearchModal(): Promise<{ advancedSearchOptions: { query: string, apis: string[] }, advancedSearchModal: MediaDbAdvancedSearchModal }> { - const modal = new MediaDbAdvancedSearchModal(this); - const res: { query: string, apis: string[] } = await new Promise((resolve, reject) => { - modal.setSubmitCallback(res => resolve(res)); - modal.setCloseCallback(err => { - if (err) { - reject(err); - } - resolve(undefined); - }); - - modal.open(); - }); - return {advancedSearchOptions: res, advancedSearchModal: modal}; - } - - async openMediaDbIdSearchModal(): Promise<{ idSearchOptions: { query: string, api: string }, idSearchModal: MediaDbIdSearchModal }> { - const modal = new MediaDbIdSearchModal(this); - const res: { query: string, api: string } = await new Promise((resolve, reject) => { - modal.setSubmitCallback(res => resolve(res)); - modal.setCloseCallback(err => { - if (err) { - reject(err); - } - resolve(undefined); - }); - - modal.open(); - }); - return {idSearchOptions: res, idSearchModal: modal}; - } - - async openMediaDbSelectModal(resultsToDisplay: MediaTypeModel[], skipButton: boolean = false): Promise<{ selectRes: MediaTypeModel[], selectModal: MediaDbSearchResultModal }> { - const modal = new MediaDbSearchResultModal(this, resultsToDisplay, skipButton); - const res: MediaTypeModel[] = await new Promise((resolve, reject) => { - modal.setSubmitCallback(res => resolve(res)); - modal.setSkipCallback(() => resolve([])); - modal.setCloseCallback(err => { - if (err) { - reject(err); - } - resolve(undefined); - }); - - modal.open(); - }); - return {selectRes: res, selectModal: modal}; - } - async loadSettings() { // console.log(DEFAULT_SETTINGS); const diskSettings: MediaDbPluginSettings = await this.loadData(); diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/src/modals/MediaDbAdvancedSearchModal.ts index 3711e55..fa7ac6b 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/src/modals/MediaDbAdvancedSearchModal.ts @@ -1,26 +1,37 @@ import {ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent} from 'obsidian'; import {MediaTypeModel} from '../models/MediaTypeModel'; import MediaDbPlugin from '../main'; +import {ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, AdvancedSearchModalData, AdvancedSearchModalOptions} from '../utils/ModalHelper'; export class MediaDbAdvancedSearchModal extends Modal { + plugin: MediaDbPlugin; + query: string; isBusy: boolean; - plugin: MediaDbPlugin; - searchBtn: ButtonComponent; + title: string; selectedApis: { name: string, selected: boolean }[]; - submitCallback?: (res: { query: string, apis: string[] }) => void; + + searchBtn: ButtonComponent; + + submitCallback?: (res: AdvancedSearchModalData) => void; closeCallback?: (err?: Error) => void; - constructor(plugin: MediaDbPlugin) { + + 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; + for (const api of this.plugin.apiManager.apis) { - this.selectedApis.push({name: api.apiName, selected: false}); + this.selectedApis.push({name: api.apiName, selected: advancedSearchModalOptions.preselectedAPIs.contains(api.apiName)}); } } - setSubmitCallback(submitCallback: (res: { query: string, apis: string[] }) => void): void { + setSubmitCallback(submitCallback: (res: AdvancedSearchModalData) => void): void { this.submitCallback = submitCallback; } @@ -36,7 +47,7 @@ export class MediaDbAdvancedSearchModal extends Modal { async search(): Promise { if (!this.query || this.query.length < 3) { - new Notice('MDB | Query to short'); + new Notice('MDB | Query too short'); return; } @@ -59,12 +70,13 @@ export class MediaDbAdvancedSearchModal extends Modal { onOpen() { const {contentEl} = this; - contentEl.createEl('h2', {text: 'Search media db'}); + contentEl.createEl('h2', {text: this.title}); const placeholder = 'Search by title'; const searchComponent = new TextComponent(contentEl); searchComponent.inputEl.style.width = '100%'; searchComponent.setPlaceholder(placeholder); + searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); @@ -96,14 +108,19 @@ export class MediaDbAdvancedSearchModal extends Modal { contentEl.createDiv({cls: 'media-db-plugin-spacer'}); new Setting(contentEl) - .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) .addButton(btn => { - return (this.searchBtn = btn - .setButtonText('Ok') - .setCta() - .onClick(() => { - this.search(); - })); + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }) + .addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => { + this.search(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + this.searchBtn = btn; }); } diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbFolderImportModal.ts index 415e4df..9c1f64a 100644 --- a/src/modals/MediaDbFolderImportModal.ts +++ b/src/modals/MediaDbFolderImportModal.ts @@ -76,8 +76,20 @@ export class MediaDbFolderImportModal extends Modal { contentEl.createDiv({cls: 'media-db-plugin-spacer'}); new Setting(contentEl) - .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) - .addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit())); + .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; + }); } onClose() { diff --git a/src/modals/MediaDbIdSearchModal.ts b/src/modals/MediaDbIdSearchModal.ts index 9437e2a..ec29cac 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/src/modals/MediaDbIdSearchModal.ts @@ -1,23 +1,32 @@ import {ButtonComponent, DropdownComponent, Modal, Notice, Setting, TextComponent} from 'obsidian'; import {MediaTypeModel} from '../models/MediaTypeModel'; import MediaDbPlugin from '../main'; +import {ID_SEARCH_MODAL_DEFAULT_OPTIONS, IdSearchModalData, IdSearchModalOptions} from '../utils/ModalHelper'; export class MediaDbIdSearchModal extends Modal { + plugin: MediaDbPlugin; + query: string; isBusy: boolean; - plugin: MediaDbPlugin; - searchBtn: ButtonComponent; + title: string; selectedApi: string; - submitCallback?: (res: { query: string, api: string }, err?: Error) => void; + + searchBtn: ButtonComponent; + + submitCallback?: (res: IdSearchModalData, err?: Error) => void; closeCallback?: (err?: Error) => void; - constructor(plugin: MediaDbPlugin) { + + constructor(plugin: MediaDbPlugin, idSearchModalOptions: IdSearchModalOptions) { + idSearchModalOptions = Object.assign({}, ID_SEARCH_MODAL_DEFAULT_OPTIONS, idSearchModalOptions); super(plugin.app); + this.plugin = plugin; - this.selectedApi = plugin.apiManager.apis[0].apiName; + this.title = idSearchModalOptions.modalTitle; + this.selectedApi = idSearchModalOptions.preselectedAPI || plugin.apiManager.apis[0].apiName; } - setSubmitCallback(submitCallback: (res: { query: string, api: string }, err?: Error) => void): void { + setSubmitCallback(submitCallback: (res: IdSearchModalData, err?: Error) => void): void { this.submitCallback = submitCallback; } @@ -54,7 +63,7 @@ export class MediaDbIdSearchModal extends Modal { onOpen() { const {contentEl} = this; - contentEl.createEl('h2', {text: 'Search media db by id'}); + contentEl.createEl('h2', {text: this.title}); const placeholder = 'Search by id'; const searchComponent = new TextComponent(contentEl); @@ -84,14 +93,19 @@ export class MediaDbIdSearchModal extends Modal { contentEl.createDiv({cls: 'media-db-plugin-spacer'}); new Setting(contentEl) - .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) .addButton(btn => { - return (this.searchBtn = btn - .setButtonText('Ok') - .setCta() - .onClick(() => { - this.search(); - })); + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }) + .addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => { + this.search(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + this.searchBtn = btn; }); } diff --git a/src/modals/MediaDbPreviewModal.ts b/src/modals/MediaDbPreviewModal.ts new file mode 100644 index 0000000..ec20a28 --- /dev/null +++ b/src/modals/MediaDbPreviewModal.ts @@ -0,0 +1,82 @@ +import {ButtonComponent, MarkdownRenderer, Modal, Setting} from 'obsidian'; +import MediaDbPlugin from 'src/main'; +import {MediaTypeModel} from 'src/models/MediaTypeModel'; +import {PREVIEW_MODAL_DEFAULT_OPTIONS, PreviewModalData, PreviewModalOptions} from '../utils/ModalHelper'; +import {CreateNoteOptions} from '../utils/Utils'; + +export class MediaDbPreviewModal extends Modal { + plugin: MediaDbPlugin; + + createNoteOptions: CreateNoteOptions; + elements: MediaTypeModel[]; + isBusy: boolean; + title: string; + cancelButton: ButtonComponent; + submitButton: ButtonComponent; + + 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.createNoteOptions = previewModalOptions.createNoteOptions; + } + + setSubmitCallback(submitCallback: (previewModalData: PreviewModalData) => void): void { + this.submitCallback = submitCallback; + } + + setCloseCallback(closeCallback: (err?: Error) => void): void { + this.closeCallback = closeCallback; + } + + async preview(): Promise { + let {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'}); + + for (let result of this.elements) { + previewWrapper.createEl('h3', {text: result.englishTitle}); + const fileDiv = previewWrapper.createDiv(); + + let fileContent = await this.plugin.generateMediaDbNoteContents(result, this.createNoteOptions); + fileContent = `\n${fileContent}\n`; + + MarkdownRenderer.renderMarkdown(fileContent, fileDiv, null, null); + } + + contentEl.createDiv({cls: 'media-db-plugin-spacer'}); + + const bottomSettingRow = new Setting(contentEl); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.closeCallback()); + btn.buttonEl.addClass('media-db-plugin-button'); + this.cancelButton = btn; + }); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => this.submitCallback({confirmed: true})); + btn.buttonEl.addClass('media-db-plugin-button'); + this.submitButton = btn; + }); + } + + onOpen(): void { + this.preview(); + } + + onClose(): void { + this.closeCallback(); + } +} diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 53e19bc..c44a91f 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -1,31 +1,34 @@ import {MediaTypeModel} from '../models/MediaTypeModel'; import MediaDbPlugin from '../main'; import {SelectModal} from './SelectModal'; +import {SELECT_MODAL_OPTIONS_DEFAULT, SelectModalData, SelectModalOptions} from '../utils/ModalHelper'; export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; - heading: string; + busy: boolean; - submitCallback: (res: MediaTypeModel[]) => void; + sendCallback: boolean; + + submitCallback: (res: SelectModalData) => void; closeCallback: (err?: Error) => void; skipCallback: () => void; - sendCallback: boolean; - constructor(plugin: MediaDbPlugin, elements: MediaTypeModel[], skipButton: boolean) { - super(plugin.app, elements); + constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) { + selectModalOptions = Object.assign({}, SELECT_MODAL_OPTIONS_DEFAULT, selectModalOptions); + super(plugin.app, selectModalOptions.elements, selectModalOptions.multiSelect); this.plugin = plugin; - this.title = 'Search Results'; + this.title = selectModalOptions.modalTitle; this.description = 'Select one or multiple search results.'; - this.addSkipButton = skipButton; + this.addSkipButton = selectModalOptions.skipButton; this.busy = false; this.sendCallback = false; } - setSubmitCallback(submitCallback: (res: MediaTypeModel[]) => void): void { + setSubmitCallback(submitCallback: (res: SelectModalData) => void): void { this.submitCallback = submitCallback; } @@ -49,7 +52,7 @@ export class MediaDbSearchResultModal extends SelectModal { if (!this.busy) { this.busy = true; this.submitButton.setButtonText('Creating entry...'); - this.submitCallback(this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); + this.submitCallback({selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value)}); } } @@ -59,6 +62,7 @@ export class MediaDbSearchResultModal extends SelectModal { } onClose() { + console.log('close'); this.closeCallback(); } } diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts index 761f3b8..82bcccd 100644 --- a/src/modals/SelectModal.ts +++ b/src/modals/SelectModal.ts @@ -12,13 +12,15 @@ export abstract class SelectModal extends Modal { skipButton?: ButtonComponent; submitButton?: ButtonComponent; + elementWrapper?: HTMLDivElement; + elements: T[]; selectModalElements: SelectModalElement[]; - protected constructor(app: App, elements: T[]) { + protected constructor(app: App, elements: T[], allowMultiSelect: boolean = true) { super(app); - this.allowMultiSelect = true; + this.allowMultiSelect = allowMultiSelect; this.title = ''; this.description = ''; @@ -27,18 +29,28 @@ export abstract class SelectModal extends Modal { this.skipButton = undefined; this.submitButton = undefined; + this.elementWrapper = undefined; + this.elements = elements; this.selectModalElements = []; - this.scope.register([], 'ArrowUp', () => { + this.scope.register([], 'ArrowUp', (evt) => { this.highlightUp(); + evt.preventDefault(); }); - this.scope.register([], 'ArrowDown', () => { + this.scope.register([], 'ArrowDown', (evt) => { this.highlightDown(); + evt.preventDefault(); }); this.scope.register([], 'ArrowRight', () => { this.activateHighlighted(); }); + this.scope.register([], ' ', (evt) => { + if (this.elementWrapper && this.elementWrapper === document.activeElement) { + this.activateHighlighted(); + evt.preventDefault(); + } + }); this.scope.register([], 'Enter', () => this.submit()); } @@ -65,26 +77,18 @@ export abstract class SelectModal extends Modal { } async onOpen() { - const {contentEl} = this; + const {contentEl, titleEl} = this; - /* - contentEl.id = 'media-db-plugin-modal' - - contentEl.on('keydown', '#' + contentEl.id, (ev, delegateTarget) => { - console.log(ev.key); - }); - */ - - contentEl.createEl('h2', {text: this.title}); + titleEl.createEl('h2', {text: this.title}); + contentEl.addClass('media-db-plugin-select-modal'); contentEl.createEl('p', {text: this.description}); - contentEl.addClass('media-db-plugin-select-modal'); - - const elementWrapper = contentEl.createDiv({cls: 'media-db-plugin-select-wrapper'}); + 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, elementWrapper, i, this, false); + const selectModalElement = new SelectModalElement(element, this.elementWrapper, i, this, false); this.selectModalElements.push(selectModalElement); @@ -96,11 +100,27 @@ export abstract class SelectModal extends Modal { this.selectModalElements.first()?.element.scrollIntoView(); const bottomSettingRow = new Setting(contentEl); - bottomSettingRow.addButton(btn => this.cancelButton = btn.setButtonText('Cancel').onClick(() => this.close())); + 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 => this.skipButton = btn.setButtonText('Skip').onClick(() => this.skip())); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Skip'); + btn.onClick(() => this.skip()); + btn.buttonEl.addClass('media-db-plugin-button'); + this.skipButton = btn; + }); } - bottomSettingRow.addButton(btn => this.submitButton = btn.setButtonText('Ok').setCta().onClick(() => this.submit())); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => this.submit()); + btn.buttonEl.addClass('media-db-plugin-button'); + this.submitButton = btn; + }); } activateHighlighted() { diff --git a/src/settings/PropertyMappingModelComponent.svelte b/src/settings/PropertyMappingModelComponent.svelte index da8797b..6fe5227 100644 --- a/src/settings/PropertyMappingModelComponent.svelte +++ b/src/settings/PropertyMappingModelComponent.svelte @@ -46,16 +46,16 @@
- { /if } - { /if } + { /if } + { /if } - { /each } + { /each } { #if !validationResult?.res }
{validationResult?.err?.message}
- { /if } + { /if }