From bbc4b57092954a4b6e787e1ea8f1a4b56380944f Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Fri, 10 Jun 2022 15:55:08 +0200 Subject: [PATCH 1/6] Bulk import #26 --- src/main.ts | 145 ++++++++++++++++++++----- src/modals/MediaDbFolderImportModal.ts | 59 ++++++++++ src/modals/MediaDbSearchResultModal.ts | 29 +++-- src/modals/SelectModal.ts | 72 ++++++++++++ src/modals/SelectModalElement.ts | 72 ++++++++++++ src/models/MediaTypeModel.ts | 1 - src/settings/ModelPropertyMapper.ts | 4 +- src/utils/Utils.ts | 66 ++++++++++- styles.css | 23 ++++ 9 files changed, 421 insertions(+), 50 deletions(-) create mode 100644 src/modals/MediaDbFolderImportModal.ts create mode 100644 src/modals/SelectModal.ts create mode 100644 src/modals/SelectModalElement.ts diff --git a/src/main.ts b/src/main.ts index eae5fef..e101a16 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ -import {FrontMatterCache, Notice, Plugin, TFile} from 'obsidian'; +import {Notice, Plugin, TFile, TFolder} from 'obsidian'; import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/Settings'; import {APIManager} from './api/APIManager'; import {MediaTypeModel} from './models/MediaTypeModel'; -import {replaceIllegalFileNameCharactersInString} from './utils/Utils'; +import {dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString} from './utils/Utils'; import {OMDbAPI} from './api/apis/OMDbAPI'; import {MediaDbAdvancedSearchModal} from './modals/MediaDbAdvancedSearchModal'; import {MediaDbSearchResultModal} from './modals/MediaDbSearchResultModal'; @@ -14,6 +14,7 @@ import {MediaTypeManager} from './utils/MediaTypeManager'; import {SteamAPI} from './api/apis/SteamAPI'; import {ModelPropertyMapper} from './settings/ModelPropertyMapper'; import {YAMLConverter} from './utils/YAMLConverter'; +import {MediaDbFolderImportModal} from './modals/MediaDbFolderImportModal'; export default class MediaDbPlugin extends Plugin { settings: MediaDbPluginSettings; @@ -30,6 +31,16 @@ export default class MediaDbPlugin extends Plugin { ); ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); + this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => { + if (file instanceof TFolder) { + menu.addItem(item => { + item.setTitle('Create Media DB entries from folder') + .setIcon('database') + .onClick(() => this.createEntriesFromFolder(file as TFolder)); + }); + } + })); + // register command to open search modal this.addCommand({ id: 'open-media-db-search-modal', @@ -74,16 +85,23 @@ export default class MediaDbPlugin extends Plugin { this.modelPropertyMapper = new ModelPropertyMapper(this.settings); } - async createMediaDbNote(modal: () => Promise): Promise { + async createMediaDbNote(modal: () => Promise): Promise { + let models: MediaTypeModel[] = []; try { - let data: MediaTypeModel = await modal(); - data = await this.apiManager.queryDetailedInfo(data); - - await this.createMediaDbNoteFromModel(data); + models = await modal(); } catch (e) { console.warn(e); new Notice(e.toString()); } + + for (const model of models) { + try { + await this.createMediaDbNoteFromModel(await this.apiManager.queryDetailedInfo(model)); + } catch (e) { + console.warn(e); + new Notice(e.toString()); + } + } } async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel): Promise { @@ -126,27 +144,6 @@ export default class MediaDbPlugin extends Plugin { } } - async openMediaDbAdvancedSearchModal(): Promise { - return new Promise(((resolve, reject) => { - new MediaDbAdvancedSearchModal(this.app, this, (err, results) => { - if (err) return reject(err); - new MediaDbSearchResultModal(this.app, this, results, (err2, res) => { - if (err2) return reject(err2); - resolve(res); - }).open(); - }).open(); - })); - } - - async openMediaDbIdSearchModal(): Promise { - return new Promise(((resolve, reject) => { - new MediaDbIdSearchModal(this.app, this, (err, res) => { - if (err) return reject(err); - resolve(res); - }).open(); - })); - } - async updateActiveNote() { const activeFile: TFile = this.app.workspace.getActiveFile(); if (!activeFile) { @@ -154,16 +151,16 @@ export default class MediaDbPlugin extends Plugin { } let metadata: any = this.app.metadataCache.getFileCache(activeFile).frontmatter; + metadata = JSON.parse(JSON.stringify(metadata)); // deep copy delete metadata.position; // remove unnecessary data from the FrontMatterCache metadata = this.modelPropertyMapper.convertObjectBack(metadata); - console.log(metadata) + console.log(metadata); if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); } - let oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type); let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(metadata.id, metadata.dataSource); @@ -178,6 +175,94 @@ export default class MediaDbPlugin extends Plugin { await this.createMediaDbNoteFromModel(newMediaTypeModel); } + async createEntriesFromFolder(folder: TFolder) { + const erroredFiles: { filePath: string, error: string }[] = []; + + const {selectedAPI, titleFieldName} = await new Promise((resolve, reject) => { + new MediaDbFolderImportModal(this.app, this, ((selectedAPI, titleFieldName) => { + resolve({selectedAPI, titleFieldName}); + })).open(); + }); + + const selectedAPIs = {}; + for (const api of this.apiManager.apis) { + // @ts-ignore + selectedAPIs[api.apiName] = api.apiName === selectedAPI; + } + + for (const child of folder.children) { + if (child instanceof TFile) { + const file = child as TFile; + let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter; + + let title = metadata[titleFieldName]; + if (!title) { + erroredFiles.push({filePath: file.path, error: `metadata field \'${titleFieldName}\' not found or empty`}); + continue; + } + + let results: MediaTypeModel[] = []; + try { + results = await this.apiManager.query(title, selectedAPIs); + } catch (e) { + erroredFiles.push({filePath: file.path, error: e.toString()}); + continue; + } + if (!results || results.length === 0) { + erroredFiles.push({filePath: file.path, error: `no search results`}); + continue; + } + + let selectedResults: MediaTypeModel[] = await new Promise((resolve, reject) => { + const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, (err, res) => { + if (err) return reject(err); + resolve(res); + }); + searchResultModal.title = `Results for \'${title}\'`; + searchResultModal.open(); + }); + + await this.createMediaDbNote(async () => selectedResults); + } + } + + if (erroredFiles.length > 0) { + const title = `bulk import error report ${dateTimeToString(new Date())}`; + const filePath = `${this.settings.folder.replace(/\/$/, '')}/${title}.md`; + + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + // console.log(table) + let fileContent = `# ${title}\n\n${markdownTable(table)}`; + + const targetFile = await this.app.vault.create(filePath, fileContent); + } + } + + async openMediaDbAdvancedSearchModal(): Promise { + return new Promise(((resolve, reject) => { + new MediaDbAdvancedSearchModal(this.app, this, (err, results) => { + if (err) { + return reject(err); + } + new MediaDbSearchResultModal(this.app, this, results, (err2, res) => { + if (err2) { + return reject(err2); + } + resolve(res); + }).open(); + }).open(); + })); + } + + async openMediaDbIdSearchModal(): Promise { + return new Promise(((resolve, reject) => { + new MediaDbIdSearchModal(this.app, this, (err, res) => { + if (err) return reject(err); + resolve(res); + }).open(); + })); + } + async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbFolderImportModal.ts new file mode 100644 index 0000000..69b579f --- /dev/null +++ b/src/modals/MediaDbFolderImportModal.ts @@ -0,0 +1,59 @@ +import {App, ButtonComponent, DropdownComponent, Modal, Setting, TextComponent} from 'obsidian'; +import MediaDbPlugin from '../main'; + +export class MediaDbFolderImportModal extends Modal { + plugin: MediaDbPlugin; + onSubmit: (selectedAPI: string, titleFieldName: string) => void; + selectedApi: string; + searchBtn: ButtonComponent; + titleFieldName: string; + + constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string) => void) { + super(app); + this.plugin = plugin; + this.onSubmit = onSubmit; + this.selectedApi = plugin.apiManager.apis[0].apiName; + } + + submit() { + this.onSubmit(this.selectedApi, this.titleFieldName); + this.close(); + } + + onOpen() { + const {contentEl} = this; + + contentEl.createEl('h2', {text: 'Create Media DB entries from folder'}); + + const apiSelectorWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'}); + const apiSelectorTextWrapper = apiSelectorWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'}); + apiSelectorTextWrapper.createEl('span', {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); + + + const placeholder = 'Title metadata field name'; + const titleFieldNameComponent = new TextComponent(contentEl); + titleFieldNameComponent.inputEl.style.width = '100%'; + titleFieldNameComponent.setPlaceholder(placeholder); + titleFieldNameComponent.onChange(value => this.titleFieldName = value); + + contentEl.appendChild(titleFieldNameComponent.inputEl); + + new Setting(contentEl) + .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) + .addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit())); + } + + onClose() { + const {contentEl} = this; + contentEl.empty(); + } +} diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 7cab85f..2306e77 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -1,35 +1,32 @@ -import {App, SuggestModal} from 'obsidian'; +import {App} from 'obsidian'; import {MediaTypeModel} from '../models/MediaTypeModel'; import MediaDbPlugin from '../main'; +import {SelectModal} from './SelectModal'; -export class MediaDbSearchResultModal extends SuggestModal { - suggestion: MediaTypeModel[]; +export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; - onChoose: (error: Error, result?: MediaTypeModel) => void; + heading: string; + onChoose: (error: Error, result: MediaTypeModel[]) => void; - constructor(app: App, plugin: MediaDbPlugin, suggestion: MediaTypeModel[], onChoose: (error: Error, result?: MediaTypeModel) => void) { - super(app); + constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], onChoose: (error: Error, result: MediaTypeModel[]) => void) { + super(app, elements); this.plugin = plugin; - this.suggestion = suggestion; this.onChoose = onChoose; - } - getSuggestions(query: string): MediaTypeModel[] { - return this.suggestion.filter(item => { - const searchQuery = query.toLowerCase(); - return item.title.toLowerCase().includes(searchQuery); - }); + this.title = 'Search Results'; + this.description = 'Select one or multiple search results.'; } // Renders each suggestion item. - renderSuggestion(item: MediaTypeModel, el: HTMLElement) { + renderElement(item: MediaTypeModel, el: HTMLElement) { el.createEl('div', {text: this.plugin.mediaTypeManager.getFileName(item)}); el.createEl('small', {text: `${item.englishTitle}\n`}); el.createEl('small', {text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`}); } // Perform action on the selected suggestion. - onChooseSuggestion(item: MediaTypeModel, evt: MouseEvent | KeyboardEvent) { - this.onChoose(null, item); + submit() { + this.onChoose(null, this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); + this.close(); } } diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts new file mode 100644 index 0000000..6b11073 --- /dev/null +++ b/src/modals/SelectModal.ts @@ -0,0 +1,72 @@ +import {App, Modal, Setting} from 'obsidian'; +import {SelectModalElement} from './SelectModalElement'; + +export abstract class SelectModal extends Modal { + multiSelect: boolean; + allowMultiSelect: boolean; + + title: string; + description: string; + + elements: T[]; + selectModalElements: SelectModalElement[]; + + + constructor(app: App, elements: T[]) { + super(app); + this.elements = elements; + this.allowMultiSelect = true; + + this.selectModalElements = []; + } + + abstract renderElement(value: T, el: HTMLElement): any; + + abstract submit(): void; + + disableAllOtherElements(elementId: number) { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.id !== elementId) { + selectModalElement.setActive(false); + } + } + } + + async onOpen() { + const {contentEl} = this; + + contentEl.createEl('h2', {text: this.title}); + contentEl.createEl('p', {text: this.description}); + + if (this.allowMultiSelect) { + new Setting(contentEl) + .setName('Select Multiple') + .addToggle(cb => { + cb.setValue(this.multiSelect); + cb.onChange(value => { + this.multiSelect = value; + for (const selectModalElement of this.selectModalElements) { + selectModalElement.setActive(false); + } + }); + }); + } + + const elementWrapper = contentEl.createDiv({cls: 'media-db-plugin-select-wrapper'}); + + let i = 0; + for (const element of this.elements) { + const selectModalElement = new SelectModalElement(element, contentEl, i, this, false); + + this.selectModalElements.push(selectModalElement); + + this.renderElement(element, selectModalElement.element); + + i += 1; + } + + new Setting(contentEl) + .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) + .addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit())); + } +} diff --git a/src/modals/SelectModalElement.ts b/src/modals/SelectModalElement.ts new file mode 100644 index 0000000..40c983f --- /dev/null +++ b/src/modals/SelectModalElement.ts @@ -0,0 +1,72 @@ +import {SelectModal} from './SelectModal'; + +export class SelectModalElement { + selectModal: SelectModal; + value: T; + readonly id: number; + element: HTMLDivElement; + cssClass: string; + activeClass: string; + hoverClass: string; + private active: boolean; + + constructor(value: T, parentElement: HTMLElement, id: number, selectModal: SelectModal, 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.multiSelect) { + this.selectModal.disableAllOtherElements(this.id); + } + }); + this.element.on('mouseenter', '#' + this.getHTMLId(), () => { + this.addClass(this.hoverClass); + }); + this.element.on('mouseleave', '#' + this.getHTMLId(), () => { + this.removeClass(this.hoverClass); + }); + } + + isActive(): boolean { + return this.active; + } + + getHTMLId(): string { + return `media-db-plugin-select-element-${this.id}`; + } + + 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); + } + } + +} diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index d8fbd4a..d82216d 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -1,5 +1,4 @@ import {MediaType} from '../utils/MediaType'; -import {YAMLConverter} from '../utils/YAMLConverter'; export abstract class MediaTypeModel { type: string; diff --git a/src/settings/ModelPropertyMapper.ts b/src/settings/ModelPropertyMapper.ts index a97ed44..f9dc6ad 100644 --- a/src/settings/ModelPropertyMapper.ts +++ b/src/settings/ModelPropertyMapper.ts @@ -29,7 +29,7 @@ export class ModelPropertyMapper { return obj; } - const conversionRules: ModelPropertyConversionRule[] = [] + const conversionRules: ModelPropertyConversionRule[] = []; for (const conversionRuleString of conversionRulesString.split('\n')) { if (conversionRuleString) { conversionRules.push(new ModelPropertyConversionRule(conversionRuleString)); @@ -74,7 +74,7 @@ export class ModelPropertyMapper { return obj; } - const conversionRules: ModelPropertyConversionRule[] = [] + const conversionRules: ModelPropertyConversionRule[] = []; for (const conversionRuleString of conversionRulesString.split('\n')) { if (conversionRuleString) { conversionRules.push(new ModelPropertyConversionRule(conversionRuleString)); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 0c8334f..847e3cb 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -5,7 +5,7 @@ export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; export const mediaDbTag: string = 'mediaDB'; export const mediaDbVersion: string = '0.2.1'; -export const debug: boolean = false; +export const debug: boolean = true; export function wrapAround(value: number, size: number): number { return ((value % size) + size) % size; @@ -88,3 +88,67 @@ function traverseMetaData(path: Array, mediaTypeModel: MediaTypeModel): return o; } + +export function markdownTable(content: string[][]): string { + let rows = content.length; + if (rows === 0) { + return ''; + } + + let columns = content[0].length; + if (columns === 0) { + return ''; + } + for (const row of content) { + if (row.length !== columns) { + return ''; + } + } + + let longestStringInColumns: number[] = []; + + for (let i = 0; i < columns; i++) { + let longestStringInColumn = 0; + for (const row of content) { + if (row[i].length > longestStringInColumn) { + longestStringInColumn = row[i].length; + } + } + + longestStringInColumns.push(longestStringInColumn); + } + + let table = ''; + + for (let i = 0; i < rows; i++) { + table += '|'; + for (let j = 0; j < columns; j++) { + let element = content[i][j]; + element += ' '.repeat(longestStringInColumns[j] - element.length); + table += ' ' + element + ' |'; + } + table += '\n'; + if (i === 0) { + table += '|'; + for (let j = 0; j < columns; j++) { + table += ' ' + '-'.repeat(longestStringInColumns[j]) + ' |'; + } + table += '\n'; + } + } + + return table; +} + +export function dateToString(date: Date) { + return `${date.getMonth()}-${date.getDay()}-${date.getFullYear()}`; +} + +export function timeToString(time: Date) { + return `${time.getHours()}-${time.getMinutes()}-${time.getSeconds()}`; +} + +export function dateTimeToString(dateTime: Date) { + return `${dateToString(dateTime)} ${timeToString(dateTime)}`; +} + diff --git a/styles.css b/styles.css index 272d000..221a228 100644 --- a/styles.css +++ b/styles.css @@ -21,3 +21,26 @@ small.media-db-plugin-list-text{ color: var(--text-muted); } + +.media-db-plugin-select-wrapper { + margin: 5px; +} + +.media-db-plugin-select-element { + cursor: pointer; + border-left: 5px solid transparent; + padding: 5px; + margin: 5px 0 5px 0; + border-radius: 5px; + white-space: pre-wrap; + font-size: 16px; +} + +.media-db-plugin-select-element-selected { + border-left: 5px solid var(--interactive-accent) !important; + background: var(--background-secondary-alt); +} + +.media-db-plugin-select-element-hover { + background: var(--background-secondary-alt); +} From 038d473a4f4d9b04b8d5e7ed1fe70bc3ebd570c7 Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Fri, 10 Jun 2022 20:54:21 +0200 Subject: [PATCH 2/6] cancel shows in error report --- src/main.ts | 17 ++++++++++++++--- src/modals/MediaDbSearchResultModal.ts | 10 ++++++++-- src/modals/SelectModal.ts | 4 ++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index e101a16..e08b3b1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -215,13 +215,22 @@ export default class MediaDbPlugin extends Plugin { let selectedResults: MediaTypeModel[] = await new Promise((resolve, reject) => { const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, (err, res) => { - if (err) return reject(err); + if (err) { + return reject(err); + } resolve(res); + }, () => { + resolve([]); }); + searchResultModal.title = `Results for \'${title}\'`; searchResultModal.open(); }); + if (selectedResults.length === 0) { + erroredFiles.push({filePath: file.path, error: `no search results selected`}); + } + await this.createMediaDbNote(async () => selectedResults); } } @@ -249,7 +258,7 @@ export default class MediaDbPlugin extends Plugin { return reject(err2); } resolve(res); - }).open(); + }, () => resolve([])).open(); }).open(); })); } @@ -257,7 +266,9 @@ export default class MediaDbPlugin extends Plugin { async openMediaDbIdSearchModal(): Promise { return new Promise(((resolve, reject) => { new MediaDbIdSearchModal(this.app, this, (err, res) => { - if (err) return reject(err); + if (err) { + return reject(err); + } resolve(res); }).open(); })); diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 2306e77..5bddc99 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -7,11 +7,13 @@ export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; heading: string; onChoose: (error: Error, result: MediaTypeModel[]) => void; + onCancel: () => void; - constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], onChoose: (error: Error, result: MediaTypeModel[]) => void) { + constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], onChoose: (error: Error, result: MediaTypeModel[]) => void, onCancel: () => void) { super(app, elements); this.plugin = plugin; this.onChoose = onChoose; + this.onCancel = onCancel; this.title = 'Search Results'; this.description = 'Select one or multiple search results.'; @@ -25,8 +27,12 @@ export class MediaDbSearchResultModal extends SelectModal { } // Perform action on the selected suggestion. - submit() { + onSubmit() { this.onChoose(null, this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); this.close(); } + + onClose() { + this.onCancel(); + } } diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts index 6b11073..c6423b8 100644 --- a/src/modals/SelectModal.ts +++ b/src/modals/SelectModal.ts @@ -22,7 +22,7 @@ export abstract class SelectModal extends Modal { abstract renderElement(value: T, el: HTMLElement): any; - abstract submit(): void; + abstract onSubmit(): void; disableAllOtherElements(elementId: number) { for (const selectModalElement of this.selectModalElements) { @@ -67,6 +67,6 @@ export abstract class SelectModal extends Modal { new Setting(contentEl) .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) - .addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit())); + .addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.onSubmit())); } } From aa7659ee1f83692ffde5f6dc1058ff369dbbfa76 Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Sat, 11 Jun 2022 15:12:24 +0200 Subject: [PATCH 3/6] cancel and skip buttons #26 --- src/main.ts | 58 +++++++++++++++++------- src/modals/MediaDbAdvancedSearchModal.ts | 2 + src/modals/MediaDbFolderImportModal.ts | 40 +++++++++++++--- src/modals/MediaDbIdSearchModal.ts | 4 ++ src/modals/MediaDbSearchResultModal.ts | 28 +++++++++--- src/modals/SelectModal.ts | 45 +++++++++--------- src/modals/SelectModalElement.ts | 2 +- src/utils/Utils.ts | 12 +++++ styles.css | 4 ++ 9 files changed, 144 insertions(+), 51 deletions(-) diff --git a/src/main.ts b/src/main.ts index e08b3b1..3bbf6aa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import {Notice, Plugin, TFile, TFolder} from 'obsidian'; import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/Settings'; import {APIManager} from './api/APIManager'; import {MediaTypeModel} from './models/MediaTypeModel'; -import {dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString} from './utils/Utils'; +import {dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, UserCancelError, UserSkipError} from './utils/Utils'; import {OMDbAPI} from './api/apis/OMDbAPI'; import {MediaDbAdvancedSearchModal} from './modals/MediaDbAdvancedSearchModal'; import {MediaDbSearchResultModal} from './modals/MediaDbSearchResultModal'; @@ -177,10 +177,11 @@ export default class MediaDbPlugin extends Plugin { async createEntriesFromFolder(folder: TFolder) { const erroredFiles: { filePath: string, error: string }[] = []; + let canceled: boolean = false; - const {selectedAPI, titleFieldName} = await new Promise((resolve, reject) => { - new MediaDbFolderImportModal(this.app, this, ((selectedAPI, titleFieldName) => { - resolve({selectedAPI, titleFieldName}); + const {selectedAPI, titleFieldName, appendContent} = await new Promise((resolve, reject) => { + new MediaDbFolderImportModal(this.app, this, ((selectedAPI, titleFieldName, appendContent) => { + resolve({selectedAPI, titleFieldName, appendContent}); })).open(); }); @@ -193,6 +194,11 @@ export default class MediaDbPlugin extends Plugin { for (const child of folder.children) { if (child instanceof TFile) { const file = child as TFile; + if (canceled) { + erroredFiles.push({filePath: file.path, error: 'user canceled'}); + continue; + } + let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter; let title = metadata[titleFieldName]; @@ -213,22 +219,40 @@ export default class MediaDbPlugin extends Plugin { continue; } - let selectedResults: MediaTypeModel[] = await new Promise((resolve, reject) => { - const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, (err, res) => { - if (err) { - return reject(err); - } - resolve(res); - }, () => { - resolve([]); - }); + let selectedResults: MediaTypeModel[] = []; + try { + selectedResults = await new Promise((resolve, reject) => { + const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, true, (err, res) => { + if (err) { + return reject(err); + } + resolve(res); + }, () => { + reject(new UserCancelError('user canceled')); + }, () => { + reject(new UserSkipError('user skipped')); + }); - searchResultModal.title = `Results for \'${title}\'`; - searchResultModal.open(); - }); + searchResultModal.title = `Results for \'${title}\'`; + searchResultModal.open(); + }); + } catch (e) { + 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 (selectedResults.length === 0) { erroredFiles.push({filePath: file.path, error: `no search results selected`}); + continue; } await this.createMediaDbNote(async () => selectedResults); @@ -253,7 +277,7 @@ export default class MediaDbPlugin extends Plugin { if (err) { return reject(err); } - new MediaDbSearchResultModal(this.app, this, results, (err2, res) => { + new MediaDbSearchResultModal(this.app, this, results, false, (err2, res) => { if (err2) { return reject(err2); } diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/src/modals/MediaDbAdvancedSearchModal.ts index c878e61..5b28370 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/src/modals/MediaDbAdvancedSearchModal.ts @@ -81,6 +81,7 @@ export class MediaDbAdvancedSearchModal extends Modal { contentEl.appendChild(searchComponent.inputEl); searchComponent.inputEl.focus(); + contentEl.createDiv({cls: 'media-db-plugin-spacer'}); contentEl.createEl('h3', {text: 'APIs to search'}); const apiToggleComponents: Component[] = []; @@ -102,6 +103,7 @@ export class MediaDbAdvancedSearchModal extends Modal { apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); } + contentEl.createDiv({cls: 'media-db-plugin-spacer'}); new Setting(contentEl) .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbFolderImportModal.ts index 69b579f..ed8f675 100644 --- a/src/modals/MediaDbFolderImportModal.ts +++ b/src/modals/MediaDbFolderImportModal.ts @@ -1,14 +1,15 @@ -import {App, ButtonComponent, DropdownComponent, Modal, Setting, TextComponent} from 'obsidian'; +import {App, ButtonComponent, DropdownComponent, Modal, Setting, TextComponent, ToggleComponent} from 'obsidian'; import MediaDbPlugin from '../main'; export class MediaDbFolderImportModal extends Modal { plugin: MediaDbPlugin; - onSubmit: (selectedAPI: string, titleFieldName: string) => void; + onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void; selectedApi: string; searchBtn: ButtonComponent; titleFieldName: string; + appendContent: boolean; - constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string) => void) { + constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void) { super(app); this.plugin = plugin; this.onSubmit = onSubmit; @@ -16,7 +17,7 @@ export class MediaDbFolderImportModal extends Modal { } submit() { - this.onSubmit(this.selectedApi, this.titleFieldName); + this.onSubmit(this.selectedApi, this.titleFieldName, this.appendContent); this.close(); } @@ -39,14 +40,41 @@ export class MediaDbFolderImportModal extends Modal { apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); - const placeholder = 'Title metadata field name'; + contentEl.createDiv({cls: 'media-db-plugin-spacer'}); + contentEl.createEl('h3', {text: 'Append note content to Media DB entry.'}); + + const appendContentToggleElementWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'}); + const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'}); + appendContentToggleTextWrapper.createEl('span', { + text: 'If this is enabled, the plugin will override meta data fields with the same name.', + cls: 'media-db-plugin-list-text', + }); + + const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', {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: 'The name of the mata data field that should be used as the title to query'}); + + const placeholder = 'title'; const titleFieldNameComponent = new TextComponent(contentEl); titleFieldNameComponent.inputEl.style.width = '100%'; titleFieldNameComponent.setPlaceholder(placeholder); titleFieldNameComponent.onChange(value => this.titleFieldName = value); - + titleFieldNameComponent.inputEl.addEventListener('keydown', (ke) => { + if (ke.key === 'Enter') { + this.submit(); + } + }); contentEl.appendChild(titleFieldNameComponent.inputEl); + 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())); diff --git a/src/modals/MediaDbIdSearchModal.ts b/src/modals/MediaDbIdSearchModal.ts index 035e8fb..31c74d6 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/src/modals/MediaDbIdSearchModal.ts @@ -75,6 +75,8 @@ export class MediaDbIdSearchModal extends Modal { contentEl.appendChild(searchComponent.inputEl); searchComponent.inputEl.focus(); + contentEl.createDiv({cls: 'media-db-plugin-spacer'}); + const apiSelectorWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'}); const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'}); apiSelectorTExtWrapper.createEl('span', {text: 'API to search', cls: 'media-db-plugin-list-text'}); @@ -88,6 +90,8 @@ export class MediaDbIdSearchModal extends Modal { } apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); + contentEl.createDiv({cls: 'media-db-plugin-spacer'}); + new Setting(contentEl) .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) .addButton(btn => { diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 5bddc99..ec7015a 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -6,17 +6,24 @@ import {SelectModal} from './SelectModal'; export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; heading: string; - onChoose: (error: Error, result: MediaTypeModel[]) => void; + onSubmit: (error: Error, result: MediaTypeModel[]) => void; onCancel: () => void; + onSkip: () => void; - constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], onChoose: (error: Error, result: MediaTypeModel[]) => void, onCancel: () => void) { + sendCallback: boolean; + + constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], skipButton: boolean, onSubmit: (error: Error, result: MediaTypeModel[]) => void, onCancel: () => void, onSkip?: () => void) { super(app, elements); this.plugin = plugin; - this.onChoose = onChoose; + this.onSubmit = onSubmit; this.onCancel = onCancel; + this.onSkip = onSkip; this.title = 'Search Results'; this.description = 'Select one or multiple search results.'; + this.skipButton = skipButton; + + this.sendCallback = false; } // Renders each suggestion item. @@ -27,12 +34,21 @@ export class MediaDbSearchResultModal extends SelectModal { } // Perform action on the selected suggestion. - onSubmit() { - this.onChoose(null, this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); + submit() { + this.onSubmit(null, this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); + this.sendCallback = true; + this.close(); + } + + skip() { + this.onSkip(); + this.sendCallback = true; this.close(); } onClose() { - this.onCancel(); + if (!this.sendCallback) { + this.onCancel(); + } } } diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts index c6423b8..35b278b 100644 --- a/src/modals/SelectModal.ts +++ b/src/modals/SelectModal.ts @@ -2,27 +2,33 @@ import {App, Modal, Setting} from 'obsidian'; import {SelectModalElement} from './SelectModalElement'; export abstract class SelectModal extends Modal { - multiSelect: boolean; allowMultiSelect: boolean; title: string; description: string; + skipButton: boolean; elements: T[]; selectModalElements: SelectModalElement[]; - constructor(app: App, elements: T[]) { + protected constructor(app: App, elements: T[]) { super(app); - this.elements = elements; this.allowMultiSelect = true; + this.title = ''; + this.description = ''; + this.skipButton = false; + + this.elements = elements; this.selectModalElements = []; } abstract renderElement(value: T, el: HTMLElement): any; - abstract onSubmit(): void; + abstract submit(): void; + + abstract skip(): void; disableAllOtherElements(elementId: number) { for (const selectModalElement of this.selectModalElements) { @@ -35,23 +41,17 @@ export abstract class SelectModal extends Modal { async onOpen() { const {contentEl} = this; + /* + contentEl.id = 'media-db-plugin-modal' + + contentEl.on('keydown', '#' + contentEl.id, (ev, delegateTarget) => { + console.log(ev.key); + }); + */ + contentEl.createEl('h2', {text: this.title}); contentEl.createEl('p', {text: this.description}); - if (this.allowMultiSelect) { - new Setting(contentEl) - .setName('Select Multiple') - .addToggle(cb => { - cb.setValue(this.multiSelect); - cb.onChange(value => { - this.multiSelect = value; - for (const selectModalElement of this.selectModalElements) { - selectModalElement.setActive(false); - } - }); - }); - } - const elementWrapper = contentEl.createDiv({cls: 'media-db-plugin-select-wrapper'}); let i = 0; @@ -65,8 +65,11 @@ export abstract class SelectModal extends Modal { i += 1; } - new Setting(contentEl) - .addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())) - .addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.onSubmit())); + const bottomSetting = new Setting(contentEl); + bottomSetting.addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close())); + if (this.skipButton) { + bottomSetting.addButton(btn => btn.setButtonText('Skip').onClick(() => this.skip())); + } + bottomSetting.addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit())); } } diff --git a/src/modals/SelectModalElement.ts b/src/modals/SelectModalElement.ts index 40c983f..8ef9a74 100644 --- a/src/modals/SelectModalElement.ts +++ b/src/modals/SelectModalElement.ts @@ -24,7 +24,7 @@ export class SelectModalElement { this.element.id = this.getHTMLId(); this.element.on('click', '#' + this.getHTMLId(), () => { this.setActive(!this.active); - if (!this.selectModal.allowMultiSelect || !this.selectModal.multiSelect) { + if (!this.selectModal.allowMultiSelect) { this.selectModal.disableAllOtherElements(this.id); } }); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 847e3cb..621d414 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -152,3 +152,15 @@ export function dateTimeToString(dateTime: Date) { return `${dateToString(dateTime)} ${timeToString(dateTime)}`; } +export class UserCancelError extends Error { + constructor(message: string) { + super(message); + } +} + +export class UserSkipError extends Error { + constructor(message: string) { + super(message); + } +} + diff --git a/styles.css b/styles.css index 221a228..7befc32 100644 --- a/styles.css +++ b/styles.css @@ -44,3 +44,7 @@ small.media-db-plugin-list-text{ .media-db-plugin-select-element-hover { background: var(--background-secondary-alt); } + +.media-db-plugin-spacer { + margin-bottom: 10px; +} From 68654d97e796045742a9a84ca51829e72e2cab30 Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Mon, 13 Jun 2022 11:02:46 +0200 Subject: [PATCH 4/6] Append original file on bulk import #26 --- src/main.ts | 79 +++++++++++++++++++++++++++++++--------------- src/utils/Utils.ts | 2 +- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/main.ts b/src/main.ts index 3bbf6aa..ae684be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import {Notice, Plugin, TFile, TFolder} from 'obsidian'; import {DEFAULT_SETTINGS, 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 {dateTimeToString, debugLog, markdownTable, replaceIllegalFileNameCharactersInString, UserCancelError, UserSkipError} from './utils/Utils'; import {OMDbAPI} from './api/apis/OMDbAPI'; import {MediaDbAdvancedSearchModal} from './modals/MediaDbAdvancedSearchModal'; import {MediaDbSearchResultModal} from './modals/MediaDbSearchResultModal'; @@ -27,7 +27,7 @@ export default class MediaDbPlugin extends Plugin { // add icon to the left ribbon const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', (evt: MouseEvent) => - this.createMediaDbNote(this.openMediaDbAdvancedSearchModal.bind(this)), + this.createMediaDbNotes(this.openMediaDbAdvancedSearchModal.bind(this)), ); ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); @@ -45,13 +45,13 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'open-media-db-search-modal', name: 'Add new Media DB entry', - callback: () => this.createMediaDbNote(this.openMediaDbAdvancedSearchModal.bind(this)), + callback: () => this.createMediaDbNotes(this.openMediaDbAdvancedSearchModal.bind(this)), }); // register command to open id search modal this.addCommand({ id: 'open-media-db-id-search-modal', name: 'Add new Media DB entry by id', - callback: () => this.createMediaDbNote(this.openMediaDbIdSearchModal.bind(this)), + callback: () => this.createMediaDbNotes(this.openMediaDbIdSearchModal.bind(this)), }); // register command to update the open note this.addCommand({ @@ -85,7 +85,7 @@ export default class MediaDbPlugin extends Plugin { this.modelPropertyMapper = new ModelPropertyMapper(this.settings); } - async createMediaDbNote(modal: () => Promise): Promise { + async createMediaDbNotes(modal: () => Promise, attachFile?: TFile): Promise { let models: MediaTypeModel[] = []; try { models = await modal(); @@ -96,7 +96,7 @@ export default class MediaDbPlugin extends Plugin { for (const model of models) { try { - await this.createMediaDbNoteFromModel(await this.apiManager.queryDetailedInfo(model)); + await this.createMediaDbNoteFromModel(await this.apiManager.queryDetailedInfo(model), attachFile); } catch (e) { console.warn(e); new Notice(e.toString()); @@ -104,43 +104,70 @@ export default class MediaDbPlugin extends Plugin { } } - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel): Promise { + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, attachFile?: TFile): Promise { try { console.log('MDB | Creating new note...'); // console.log(mediaTypeModel); - let fileContent = `---\n${YAMLConverter.toYaml(this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()))}---\n`; + let metadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + if (attachFile) { + let attachFileMetadata: any = this.app.metadataCache.getFileCache(attachFile).frontmatter; + if (attachFileMetadata) { + attachFileMetadata = JSON.parse(JSON.stringify(attachFileMetadata)); // deep copy + delete attachFileMetadata.position; + } else { + attachFileMetadata = {}; + } + + metadata = Object.assign(attachFileMetadata, metadata); + } + + debugLog(metadata); + + let fileContent = `---\n${YAMLConverter.toYaml(metadata)}---\n`; if (this.settings.templates) { fileContent += await this.mediaTypeManager.getContent(mediaTypeModel, this.app); } - const fileName = replaceIllegalFileNameCharactersInString(this.mediaTypeManager.getFileName(mediaTypeModel)); - const filePath = `${this.settings.folder.replace(/\/$/, '')}/${fileName}.md`; - - const folder = this.app.vault.getAbstractFileByPath(this.settings.folder); - if (!folder) { - await this.app.vault.createFolder(this.settings.folder.replace(/\/$/, '')); + if (attachFile) { + let attachFileContent: string = await this.app.vault.read(attachFile); + const regExp = new RegExp('^(---)\\n[\\s\\S]*\\n---'); + attachFileContent = attachFileContent.replace(regExp, ''); + fileContent += '\n\n' + attachFileContent; } - const file = this.app.vault.getAbstractFileByPath(filePath); - if (file) { - await this.app.vault.delete(file); - } + await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent); + } catch (e) { + console.warn(e); + new Notice(e.toString()); + } + } - const targetFile = await this.app.vault.create(filePath, fileContent); + async createNote(fileName: string, fileContent: string, openFile: boolean = false) { + fileName = replaceIllegalFileNameCharactersInString(fileName); + const filePath = `${this.settings.folder.replace(/\/$/, '')}/${fileName}.md`; - // open file + const folder = this.app.vault.getAbstractFileByPath(this.settings.folder); + if (!folder) { + await this.app.vault.createFolder(this.settings.folder.replace(/\/$/, '')); + } + + const file = this.app.vault.getAbstractFileByPath(filePath); + if (file) { + await this.app.vault.delete(file); + } + + const targetFile = await this.app.vault.create(filePath, fileContent); + + // open file + if (openFile) { const activeLeaf = this.app.workspace.getUnpinnedLeaf(); if (!activeLeaf) { console.warn('MDB | no active leaf, not opening media db note'); return; } await activeLeaf.openFile(targetFile, {state: {mode: 'source'}}); - - } catch (e) { - console.warn(e); - new Notice(e.toString()); } } @@ -155,7 +182,7 @@ export default class MediaDbPlugin extends Plugin { delete metadata.position; // remove unnecessary data from the FrontMatterCache metadata = this.modelPropertyMapper.convertObjectBack(metadata); - console.log(metadata); + debugLog(metadata); if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); @@ -255,7 +282,7 @@ export default class MediaDbPlugin extends Plugin { continue; } - await this.createMediaDbNote(async () => selectedResults); + await this.createMediaDbNotes(async () => selectedResults, appendContent ? file : null); } } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 621d414..a4b3cdb 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -141,7 +141,7 @@ export function markdownTable(content: string[][]): string { } export function dateToString(date: Date) { - return `${date.getMonth()}-${date.getDay()}-${date.getFullYear()}`; + return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; } export function timeToString(time: Date) { From 115728fb7b4daa122a71721f35736fc277b54cc2 Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Tue, 14 Jun 2022 17:47:36 +0200 Subject: [PATCH 5/6] Key binds for #26 --- src/modals/SelectModal.ts | 70 ++++++++++++++++++++++++++++++++ src/modals/SelectModalElement.ts | 27 +++++++++--- src/utils/Utils.ts | 5 +++ src/utils/YAMLConverter.ts | 4 ++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts index 35b278b..de2cdc3 100644 --- a/src/modals/SelectModal.ts +++ b/src/modals/SelectModal.ts @@ -1,5 +1,6 @@ import {App, Modal, Setting} from 'obsidian'; import {SelectModalElement} from './SelectModalElement'; +import {mod} from '../utils/Utils'; export abstract class SelectModal extends Modal { allowMultiSelect: boolean; @@ -22,6 +23,17 @@ export abstract class SelectModal extends Modal { this.elements = elements; this.selectModalElements = []; + + this.scope.register([], 'ArrowUp', () => { + this.highlightUp(); + }); + this.scope.register([], 'ArrowDown', () => { + this.highlightDown(); + }); + this.scope.register([], 'ArrowRight', () => { + this.activateHighlighted(); + }); + this.scope.register([], 'Enter', () => this.submit()); } abstract renderElement(value: T, el: HTMLElement): any; @@ -38,6 +50,14 @@ export abstract class SelectModal extends Modal { } } + deHighlightAllOtherElements(elementId: number) { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.id !== elementId) { + selectModalElement.setHighlighted(false); + } + } + } + async onOpen() { const {contentEl} = this; @@ -72,4 +92,54 @@ export abstract class SelectModal extends Modal { } bottomSetting.addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit())); } + + activateHighlighted() { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.isHighlighted()) { + selectModalElement.setActive(!selectModalElement.isActive()); + if (!this.allowMultiSelect) { + this.disableAllOtherElements(selectModalElement.id); + } + } + } + } + + highlightUp() { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.isHighlighted()) { + this.getPreviousSelectModalElement(selectModalElement).setHighlighted(true); + return; + } + } + + // nothing is highlighted + this.selectModalElements.last().setHighlighted(true); + } + + highlightDown() { + 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): SelectModalElement { + let nextId = selectModalElement.id + 1; + nextId = mod(nextId, this.selectModalElements.length); + + return this.selectModalElements.filter(x => x.id === nextId).first(); + } + + private getPreviousSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { + let nextId = selectModalElement.id - 1; + nextId = mod(nextId, this.selectModalElements.length); + + return this.selectModalElements.filter(x => x.id === nextId).first(); + } + } diff --git a/src/modals/SelectModalElement.ts b/src/modals/SelectModalElement.ts index 8ef9a74..4aadb9e 100644 --- a/src/modals/SelectModalElement.ts +++ b/src/modals/SelectModalElement.ts @@ -9,6 +9,7 @@ export class SelectModalElement { activeClass: string; hoverClass: string; private active: boolean; + private highlighted: boolean; constructor(value: T, parentElement: HTMLElement, id: number, selectModal: SelectModal, active: boolean = false) { this.value = value; @@ -29,21 +30,35 @@ export class SelectModalElement { } }); this.element.on('mouseenter', '#' + this.getHTMLId(), () => { - this.addClass(this.hoverClass); + this.setHighlighted(true); }); this.element.on('mouseleave', '#' + this.getHTMLId(), () => { - this.removeClass(this.hoverClass); + this.setHighlighted(false); }); } - isActive(): boolean { - return this.active; - } - getHTMLId(): string { return `media-db-plugin-select-element-${this.id}`; } + isHighlighted(): boolean { + return this.highlighted; + } + + setHighlighted(value: boolean) { + 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(); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index a4b3cdb..d6768a4 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -164,3 +164,8 @@ export class UserSkipError extends Error { } } +// js can't even implement modulo correctly... +export function mod(n: number, m: number): number { + return ((n % m) + m) % m; +} + diff --git a/src/utils/YAMLConverter.ts b/src/utils/YAMLConverter.ts index 7cec238..f9e013f 100644 --- a/src/utils/YAMLConverter.ts +++ b/src/utils/YAMLConverter.ts @@ -10,6 +10,10 @@ export class YAMLConverter { } private static toYamlString(value: any, indentation: number): string { + if (value == null) { + return 'null'; + } + if (typeof value === 'boolean') { return value ? 'true' : 'false'; } else if (typeof value === 'number') { From 61ebbc859916481b94f28c114c227d3000943053 Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Tue, 14 Jun 2022 19:52:47 +0200 Subject: [PATCH 6/6] bump version to 0.3.0 --- README.md | 25 +++++++++++++++++++++++++ manifest.json | 4 ++-- package.json | 8 ++++---- src/main.ts | 2 +- src/modals/MediaDbFolderImportModal.ts | 2 +- src/utils/Utils.ts | 4 ++-- 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1bf3b3c..bf1c232 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,23 @@ title -> name year -> releaseYear ``` +#### Bulk Import +The plugin allows you to import your preexisting media collection and upgrade them to Media DB entries. + +##### Prerequisites +The preexisting media notes must be inside a folder in your vault. +For the plugin to be able to query them they need one metadata field that is used as the title the piece of media is searched by. +This can be achieved by for example using a `csv` import plugin to import an existing list from outside of obsidian. + +##### Importing +To start the import process, right-click on the folder and select the `Import folder as Media DB entries` option. +Then specify the API to search, if the current note content and metadata should be appended to the Media DB entry and the name of the metadata field that contains the title of the piece of media. + +Then the plugin will go through every file in the folder and prompt you to select from the search results. + +##### Post import +After all files have been imported or the import was canceled, you will find the new entries as well as an error report that contains any errors or skipped/canceled files in the folder specified in the setting of the plugin. + ### How to install **The plugin is now released, so it can be installed directly through obsidian's plugin installer.** @@ -113,6 +130,14 @@ Now you select the result you want and the plugin will cast it's magic and creat You are more than welcome to open an issue on [GitHub](https://github.com/mProjectsCode/obsidian-media-db-plugin/issues). ### Changelog +#### 0.3.0 +- Added bulk import. Import a folder of media notes as Media DB entries (thanks to [PaperOrb](https://github.com/PaperOrb) on GitHub for their input and for helping me test this feature) +- Added a custom result select modal that allows you to select multiple results at once +- Fixed a bug where the note creation would fail when the metadata included a field with the values `null` or `undefined` + +#### 0.2.1 +- fixed a small bug with the initial selection of an API in the ID search modal + #### 0.2.0 - Added the option to rename metadata fields through property mappings - fixed note creation falling, when the folder set in the settings did not exist diff --git a/manifest.json b/manifest.json index c0f6377..ecbf5cd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB Plugin", - "version": "0.2.1", + "version": "0.3.0", "minAppVersion": "0.14.0", - "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault. ", + "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", "authorUrl": "https://mprojectscode.github.io/", "isDesktopOnly": false diff --git a/package.json b/package.json index 7611c0e..b627065 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "obsidian-media-db-plugin", - "version": "0.1.0", - "description": "Coming soon TM", + "version": "0.3.0", + "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", @@ -9,8 +9,8 @@ "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], - "author": "", - "license": "MIT", + "author": "Moritz Jung", + "license": "GPL-3.0", "devDependencies": { "@popperjs/core": "^2.11.5", "@types/node": "^16.11.6", diff --git a/src/main.ts b/src/main.ts index ae684be..2583bfc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,7 +34,7 @@ export default class MediaDbPlugin extends Plugin { this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => { if (file instanceof TFolder) { menu.addItem(item => { - item.setTitle('Create Media DB entries from folder') + item.setTitle('Import folder as Media DB entries') .setIcon('database') .onClick(() => this.createEntriesFromFolder(file as TFolder)); }); diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbFolderImportModal.ts index ed8f675..39a7fdc 100644 --- a/src/modals/MediaDbFolderImportModal.ts +++ b/src/modals/MediaDbFolderImportModal.ts @@ -24,7 +24,7 @@ export class MediaDbFolderImportModal extends Modal { onOpen() { const {contentEl} = this; - contentEl.createEl('h2', {text: 'Create Media DB entries from folder'}); + contentEl.createEl('h2', {text: 'Import folder as Media DB entries'}); const apiSelectorWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'}); const apiSelectorTextWrapper = apiSelectorWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'}); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index d6768a4..a855b23 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -4,8 +4,8 @@ import {MediaTypeModel} from '../models/MediaTypeModel'; export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; export const mediaDbTag: string = 'mediaDB'; -export const mediaDbVersion: string = '0.2.1'; -export const debug: boolean = true; +export const mediaDbVersion: string = '0.3.0'; +export const debug: boolean = false; export function wrapAround(value: number, size: number): number { return ((value % size) + size) % size;