Merge branch 'master' into release

This commit is contained in:
mProjectsCode 2022-06-14 19:53:09 +02:00
commit 9779ad359c
15 changed files with 713 additions and 86 deletions

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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, debugLog, markdownTable, replaceIllegalFileNameCharactersInString, UserCancelError, UserSkipError} 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;
@ -26,21 +27,31 @@ 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');
this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => {
if (file instanceof TFolder) {
menu.addItem(item => {
item.setTitle('Import folder as Media DB entries')
.setIcon('database')
.onClick(() => this.createEntriesFromFolder(file as TFolder));
});
}
}));
// register command to open search modal
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({
@ -74,79 +85,92 @@ export default class MediaDbPlugin extends Plugin {
this.modelPropertyMapper = new ModelPropertyMapper(this.settings);
}
async createMediaDbNote(modal: () => Promise<MediaTypeModel>): Promise<void> {
async createMediaDbNotes(modal: () => Promise<MediaTypeModel[]>, attachFile?: TFile): Promise<void> {
let models: MediaTypeModel[] = [];
try {
let data: MediaTypeModel = await modal();
data = await this.apiManager.queryDetailedInfo(data);
models = await modal();
} catch (e) {
console.warn(e);
new Notice(e.toString());
}
await this.createMediaDbNoteFromModel(data);
for (const model of models) {
try {
await this.createMediaDbNoteFromModel(await this.apiManager.queryDetailedInfo(model), attachFile);
} catch (e) {
console.warn(e);
new Notice(e.toString());
}
}
}
async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, attachFile?: TFile): Promise<void> {
try {
console.log('MDB | Creating new note...');
// console.log(mediaTypeModel);
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);
}
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;
}
await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent);
} catch (e) {
console.warn(e);
new Notice(e.toString());
}
}
async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel): Promise<void> {
try {
console.log('MDB | Creating new note...');
// console.log(mediaTypeModel);
async createNote(fileName: string, fileContent: string, openFile: boolean = false) {
fileName = replaceIllegalFileNameCharactersInString(fileName);
const filePath = `${this.settings.folder.replace(/\/$/, '')}/${fileName}.md`;
let fileContent = `---\n${YAMLConverter.toYaml(this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()))}---\n`;
const folder = this.app.vault.getAbstractFileByPath(this.settings.folder);
if (!folder) {
await this.app.vault.createFolder(this.settings.folder.replace(/\/$/, ''));
}
if (this.settings.templates) {
fileContent += await this.mediaTypeManager.getContent(mediaTypeModel, this.app);
}
const file = this.app.vault.getAbstractFileByPath(filePath);
if (file) {
await this.app.vault.delete(file);
}
const fileName = replaceIllegalFileNameCharactersInString(this.mediaTypeManager.getFileName(mediaTypeModel));
const filePath = `${this.settings.folder.replace(/\/$/, '')}/${fileName}.md`;
const targetFile = await this.app.vault.create(filePath, fileContent);
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
// 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());
}
}
async openMediaDbAdvancedSearchModal(): Promise<MediaTypeModel> {
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<MediaTypeModel> {
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 +178,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)
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');
}
let oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type);
let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(metadata.id, metadata.dataSource);
@ -178,6 +202,129 @@ export default class MediaDbPlugin extends Plugin {
await this.createMediaDbNoteFromModel(newMediaTypeModel);
}
async createEntriesFromFolder(folder: TFolder) {
const erroredFiles: { filePath: string, error: string }[] = [];
let canceled: boolean = false;
const {selectedAPI, titleFieldName, appendContent} = await new Promise((resolve, reject) => {
new MediaDbFolderImportModal(this.app, this, ((selectedAPI, titleFieldName, appendContent) => {
resolve({selectedAPI, titleFieldName, appendContent});
})).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;
if (canceled) {
erroredFiles.push({filePath: file.path, error: 'user canceled'});
continue;
}
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[] = [];
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();
});
} 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.createMediaDbNotes(async () => selectedResults, appendContent ? file : null);
}
}
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<MediaTypeModel[]> {
return new Promise(((resolve, reject) => {
new MediaDbAdvancedSearchModal(this.app, this, (err, results) => {
if (err) {
return reject(err);
}
new MediaDbSearchResultModal(this.app, this, results, false, (err2, res) => {
if (err2) {
return reject(err2);
}
resolve(res);
}, () => resolve([])).open();
}).open();
}));
}
async openMediaDbIdSearchModal(): Promise<MediaTypeModel> {
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());
}

View file

@ -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()))

View file

@ -0,0 +1,87 @@
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, appendContent: boolean) => void;
selectedApi: string;
searchBtn: ButtonComponent;
titleFieldName: string;
appendContent: boolean;
constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void) {
super(app);
this.plugin = plugin;
this.onSubmit = onSubmit;
this.selectedApi = plugin.apiManager.apis[0].apiName;
}
submit() {
this.onSubmit(this.selectedApi, this.titleFieldName, this.appendContent);
this.close();
}
onOpen() {
const {contentEl} = this;
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'});
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);
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()));
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}

View file

@ -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 => {

View file

@ -1,35 +1,54 @@
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<MediaTypeModel> {
suggestion: MediaTypeModel[];
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
plugin: MediaDbPlugin;
onChoose: (error: Error, result?: MediaTypeModel) => void;
heading: string;
onSubmit: (error: Error, result: MediaTypeModel[]) => void;
onCancel: () => void;
onSkip: () => void;
constructor(app: App, plugin: MediaDbPlugin, suggestion: MediaTypeModel[], onChoose: (error: Error, result?: MediaTypeModel) => void) {
super(app);
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.suggestion = suggestion;
this.onChoose = onChoose;
}
this.onSubmit = onSubmit;
this.onCancel = onCancel;
this.onSkip = onSkip;
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.';
this.skipButton = skipButton;
this.sendCallback = false;
}
// 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.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() {
if (!this.sendCallback) {
this.onCancel();
}
}
}

145
src/modals/SelectModal.ts Normal file
View file

@ -0,0 +1,145 @@
import {App, Modal, Setting} from 'obsidian';
import {SelectModalElement} from './SelectModalElement';
import {mod} from '../utils/Utils';
export abstract class SelectModal<T> extends Modal {
allowMultiSelect: boolean;
title: string;
description: string;
skipButton: boolean;
elements: T[];
selectModalElements: SelectModalElement<T>[];
protected constructor(app: App, elements: T[]) {
super(app);
this.allowMultiSelect = true;
this.title = '';
this.description = '';
this.skipButton = false;
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;
abstract submit(): void;
abstract skip(): void;
disableAllOtherElements(elementId: number) {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.id !== elementId) {
selectModalElement.setActive(false);
}
}
}
deHighlightAllOtherElements(elementId: number) {
for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.id !== elementId) {
selectModalElement.setHighlighted(false);
}
}
}
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});
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;
}
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()));
}
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<T>): SelectModalElement<T> {
let nextId = selectModalElement.id + 1;
nextId = mod(nextId, this.selectModalElements.length);
return this.selectModalElements.filter(x => x.id === nextId).first();
}
private getPreviousSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id - 1;
nextId = mod(nextId, this.selectModalElements.length);
return this.selectModalElements.filter(x => x.id === nextId).first();
}
}

View file

@ -0,0 +1,87 @@
import {SelectModal} from './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);
});
}
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();
}
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);
}
}
}

View file

@ -1,5 +1,4 @@
import {MediaType} from '../utils/MediaType';
import {YAMLConverter} from '../utils/YAMLConverter';
export abstract class MediaTypeModel {
type: string;

View file

@ -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));

View file

@ -4,7 +4,7 @@ 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 mediaDbVersion: string = '0.3.0';
export const debug: boolean = false;
export function wrapAround(value: number, size: number): number {
@ -88,3 +88,84 @@ function traverseMetaData(path: Array<string>, 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() + 1}-${date.getDate()}-${date.getFullYear()}`;
}
export function timeToString(time: Date) {
return `${time.getHours()}-${time.getMinutes()}-${time.getSeconds()}`;
}
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);
}
}
// js can't even implement modulo correctly...
export function mod(n: number, m: number): number {
return ((n % m) + m) % m;
}

View file

@ -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') {

View file

@ -21,3 +21,30 @@
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);
}
.media-db-plugin-spacer {
margin-bottom: 10px;
}