commit
5b79a8665c
44 changed files with 8121 additions and 659 deletions
32
__mocks__/obsidian.ts
Normal file
32
__mocks__/obsidian.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
|
||||
export function requestUrl(request: RequestUrlParam): Promise<RequestUrlResponse> {
|
||||
return fetch(request.url, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
}).then(async (response) => {
|
||||
if (response.status >= 400 && request.throw) {
|
||||
throw new Error(`Request failed, ${response.status}`);
|
||||
}
|
||||
|
||||
// Turn response headers into Record<string, string> object
|
||||
const headers: Record<string, string> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
const arraybuffer = await response.arrayBuffer();
|
||||
const text = arraybuffer ? new TextDecoder().decode(arraybuffer) : '';
|
||||
const json = text ? JSON.parse(text) : {};
|
||||
|
||||
let response_body: RequestUrlResponse = {
|
||||
status: response.status,
|
||||
headers: headers,
|
||||
arrayBuffer: arraybuffer,
|
||||
json: json,
|
||||
text: text,
|
||||
};
|
||||
return response_body;
|
||||
});
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from 'builtin-modules'
|
||||
import esbuild from 'esbuild';
|
||||
import process from 'process';
|
||||
import builtins from 'builtin-modules';
|
||||
import esbuildSvelte from 'esbuild-svelte';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
|
|
@ -45,8 +47,14 @@ esbuild.build({
|
|||
format: 'cjs',
|
||||
watch: !prod,
|
||||
target: 'es2016',
|
||||
logLevel: "info",
|
||||
logLevel: 'info',
|
||||
sourcemap: prod ? false : 'inline',
|
||||
treeShaking: true,
|
||||
outfile: 'main.js',
|
||||
plugins: [
|
||||
esbuildSvelte({
|
||||
compilerOptions: { css: true },
|
||||
preprocess: sveltePreprocess(),
|
||||
}),
|
||||
],
|
||||
}).catch(() => process.exit(1));
|
||||
|
|
|
|||
13
jest.config.js
Normal file
13
jest.config.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
"<rootDir>"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/__tests__/**/*.+(ts|tsx|js)",
|
||||
"**/?(*.)+(spec|test).+(ts|tsx|js)"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
},
|
||||
}
|
||||
6189
package-lock.json
generated
6189
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
|
@ -1,25 +1,38 @@
|
|||
{
|
||||
"name": "obsidian-media-db-plugin",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"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",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Moritz Jung",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.2.0",
|
||||
"@typescript-eslint/parser": "^5.2.0",
|
||||
"builtin-modules": "^3.2.0",
|
||||
"esbuild": "0.13.12",
|
||||
"esbuild-svelte": "^0.7.1",
|
||||
"jest": "^28.1.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"obsidian": "latest",
|
||||
"svelte": "^3.50.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"ts-jest": "^28.0.5",
|
||||
"tslib": "2.3.1",
|
||||
"typescript": "4.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"ts-node": "^10.8.1",
|
||||
"yarn": "^1.22.19"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {APIModel} from './APIModel';
|
||||
import {MediaTypeModel} from '../models/MediaTypeModel';
|
||||
import {debugLog} from '../utils/Utils';
|
||||
|
||||
export class APIManager {
|
||||
apis: APIModel[];
|
||||
|
|
@ -10,7 +9,7 @@ export class APIManager {
|
|||
}
|
||||
|
||||
async query(query: string, apisToQuery: string[]): Promise<MediaTypeModel[]> {
|
||||
debugLog(`MDB | api manager queried with "${query}"`);
|
||||
console.debug(`MDB | api manager queried with "${query}"`);
|
||||
|
||||
let res: MediaTypeModel[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -32,16 +31,16 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
}
|
||||
|
||||
const data = fetchData.text;
|
||||
const response = new window.DOMParser().parseFromString(data, "text/xml")
|
||||
const response = new window.DOMParser().parseFromString(data, 'text/xml');
|
||||
|
||||
debugLog(response);
|
||||
|
||||
let ret: MediaTypeModel[] = [];
|
||||
|
||||
for (const boardgame of Array.from(response.querySelectorAll("boardgame"))) {
|
||||
const id = boardgame.attributes.getNamedItem("objectid")!.value;
|
||||
const title = boardgame.querySelector("name")!.textContent!;
|
||||
const year = boardgame.querySelector("yearpublished")?.textContent ?? "";
|
||||
for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) {
|
||||
const id = boardgame.attributes.getNamedItem('objectid')!.value;
|
||||
const title = boardgame.querySelector('name')!.textContent!;
|
||||
const year = boardgame.querySelector('yearpublished')?.textContent ?? '';
|
||||
|
||||
ret.push(new BoardGameModel({
|
||||
dataSource: this.apiName,
|
||||
|
|
@ -68,28 +67,27 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
}
|
||||
|
||||
const data = fetchData.text;
|
||||
const response = new window.DOMParser().parseFromString(data, "text/xml")
|
||||
const response = new window.DOMParser().parseFromString(data, 'text/xml');
|
||||
debugLog(response);
|
||||
|
||||
const boardgame = response.querySelector("boardgame")!;
|
||||
const title = boardgame.querySelector("name")!.textContent!;
|
||||
const year = boardgame.querySelector("yearpublished")?.textContent ?? "";
|
||||
const image = boardgame.querySelector("image")?.textContent ?? undefined;
|
||||
const onlineRating = Number.parseFloat(boardgame.querySelector("statistics ratings average")?.textContent ?? "");
|
||||
const genres = Array.from(boardgame.querySelectorAll("boardgamecategory")).map(n => n!.textContent!);
|
||||
const boardgame = response.querySelector('boardgame')!;
|
||||
const title = boardgame.querySelector('name')!.textContent!;
|
||||
const year = boardgame.querySelector('yearpublished')?.textContent ?? '';
|
||||
const image = boardgame.querySelector('image')?.textContent ?? undefined;
|
||||
const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '');
|
||||
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,
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
373
src/main.ts
373
src/main.ts
|
|
@ -1,36 +1,31 @@
|
|||
import {Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder} from 'obsidian';
|
||||
import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/Settings';
|
||||
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, debugLog, 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';
|
||||
import {SteamAPI} from './api/apis/SteamAPI';
|
||||
import {BoardGameGeekAPI} from './api/apis/BoardGameGeekAPI';
|
||||
import {ModelPropertyMapper} from './settings/ModelPropertyMapper';
|
||||
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: ModelPropertyMapper;
|
||||
modelPropertyMapper: PropertyMapper;
|
||||
modalHelper: ModalHelper;
|
||||
|
||||
frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---';
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
// register the settings tab
|
||||
this.addSettingTab(new MediaDbSettingTab(this.app, this));
|
||||
|
||||
|
||||
this.apiManager = new APIManager();
|
||||
// register APIs
|
||||
this.apiManager.registerAPI(new OMDbAPI(this));
|
||||
|
|
@ -41,9 +36,15 @@ export default class MediaDbPlugin extends Plugin {
|
|||
this.apiManager.registerAPI(new BoardGameGeekAPI(this));
|
||||
// this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data
|
||||
|
||||
this.mediaTypeManager = new MediaTypeManager(this.settings);
|
||||
this.modelPropertyMapper = new ModelPropertyMapper(this.settings);
|
||||
this.mediaTypeManager = new MediaTypeManager();
|
||||
this.modelPropertyMapper = new PropertyMapper(this);
|
||||
this.modalHelper = new ModalHelper(this);
|
||||
|
||||
await this.loadSettings();
|
||||
// register the settings tab
|
||||
this.addSettingTab(new MediaDbSettingTab(this.app, this));
|
||||
|
||||
this.mediaTypeManager.updateTemplates(this.settings);
|
||||
|
||||
// add icon to the left ribbon
|
||||
const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', (evt: MouseEvent) =>
|
||||
|
|
@ -56,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));
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
|
@ -76,62 +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[] = [];
|
||||
try {
|
||||
const {query, apis} = await this.openMediaDbAdvancedSearchModal();
|
||||
let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async (advancedSearchModalData) => {
|
||||
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);
|
||||
});
|
||||
|
||||
new Notice('MediaDB Searching...');
|
||||
|
||||
const apiSearchResults = await this.apiManager.query(query, apis);
|
||||
const selectResults = await this.openMediaDbSelectModal(apiSearchResults, false);
|
||||
results = await this.queryDetails(selectResults);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
new Notice(e.toString());
|
||||
if (!apiSearchResults) {
|
||||
// TODO: add new notice saying no results found?
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog(results);
|
||||
await this.createMediaDbNotes(results);
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
await this.createMediaDbNotes(selectResults);
|
||||
}
|
||||
|
||||
async createEntryWithIdSearchModal() {
|
||||
let result: MediaTypeModel = undefined;
|
||||
try {
|
||||
const {query, api} = await this.openMediaDbIdSearchModal();
|
||||
async createEntryWithIdSearchModal(): Promise<void> {
|
||||
let idSearchResult: MediaTypeModel;
|
||||
let proceed: boolean;
|
||||
|
||||
new Notice('MediaDB Searching...');
|
||||
while (!proceed) {
|
||||
idSearchResult = await this.modalHelper.openIdSearchModal({}, async (idSearchModalData) => {
|
||||
return await this.apiManager.queryDetailedInfoById(idSearchModalData.query, idSearchModalData.api);
|
||||
});
|
||||
if (!idSearchResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
result = await this.apiManager.queryDetailedInfoById(query, api);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
new Notice(e.toString());
|
||||
proceed = await this.modalHelper.openPreviewModal({elements: [idSearchResult]}, async (previewModalData) => {
|
||||
return previewModalData.confirmed;
|
||||
});
|
||||
}
|
||||
|
||||
debugLog(result);
|
||||
await this.createMediaDbNoteFromModel(result);
|
||||
await this.createMediaDbNoteFromModel(idSearchResult, {attachTemplate: true, openNote: true});
|
||||
}
|
||||
|
||||
async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise<void> {
|
||||
for (const model of models) {
|
||||
await this.createMediaDbNoteFromModel(model, attachFile);
|
||||
await this.createMediaDbNoteFromModel(model, {attachTemplate: true, attachFile: attachFile});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,44 +226,44 @@ export default class MediaDbPlugin extends Plugin {
|
|||
return detailModels;
|
||||
}
|
||||
|
||||
async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, attachFile?: TFile): Promise<void> {
|
||||
async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise<void> {
|
||||
try {
|
||||
console.log('MDB | Creating new note...');
|
||||
// console.log(mediaTypeModel);
|
||||
console.debug('MDB | creating new note');
|
||||
|
||||
let fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject());
|
||||
let fileContent = '';
|
||||
let fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options);
|
||||
|
||||
({fileMetadata, fileContent} = await this.attachFile(fileMetadata, fileContent, attachFile));
|
||||
({fileMetadata, fileContent} = await this.attachTemplate(fileMetadata, fileContent, await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app)));
|
||||
|
||||
fileContent = `---\n${this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(fileMetadata) : stringifyYaml(fileMetadata)}---\n` + fileContent;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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, options.attachFile));
|
||||
({fileMetadata, fileContent} = await this.attachTemplate(fileMetadata, fileContent, template));
|
||||
|
||||
fileContent = `---\n${this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(fileMetadata) : stringifyYaml(fileMetadata)}---\n` + fileContent;
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
async attachFile(fileMetadata: any, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: any, fileContent: string }> {
|
||||
if (!fileToAttach) {
|
||||
return {fileMetadata: fileMetadata, fileContent: fileContent};
|
||||
}
|
||||
|
||||
let attachFileMetadata: any = this.app.metadataCache.getFileCache(fileToAttach).frontmatter;
|
||||
if (attachFileMetadata) {
|
||||
attachFileMetadata = JSON.parse(JSON.stringify(attachFileMetadata)); // deep copy
|
||||
delete attachFileMetadata.position;
|
||||
} else {
|
||||
attachFileMetadata = {};
|
||||
}
|
||||
let attachFileMetadata: any = this.getMetadataFromFileCache(fileToAttach);
|
||||
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, '');
|
||||
fileContent += '\n' + attachFileContent;
|
||||
attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent;
|
||||
fileContent += attachFileContent;
|
||||
|
||||
return {fileMetadata: fileMetadata, fileContent: fileContent};
|
||||
}
|
||||
|
|
@ -198,9 +276,9 @@ export default class MediaDbPlugin extends Plugin {
|
|||
let templateMetadata: any = this.getMetaDataFromFileContent(template);
|
||||
fileMetadata = Object.assign(templateMetadata, fileMetadata);
|
||||
|
||||
const regExp = new RegExp('^(---)\\n[\\s\\S]*\\n---');
|
||||
const regExp = new RegExp(this.frontMatterRexExpPattern);
|
||||
const attachFileContent = template.replace(regExp, '');
|
||||
fileContent += '\n' + attachFileContent;
|
||||
fileContent += attachFileContent;
|
||||
|
||||
return {fileMetadata: fileMetadata, fileContent: fileContent};
|
||||
}
|
||||
|
|
@ -208,7 +286,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
getMetaDataFromFileContent(fileContent: string): any {
|
||||
let metadata: any;
|
||||
|
||||
const regExp = new RegExp('^(---)\\n[\\s\\S]*\\n---');
|
||||
const regExp = new RegExp(this.frontMatterRexExpPattern);
|
||||
const frontMatterRegExpResult = regExp.exec(fileContent);
|
||||
if (!frontMatterRegExpResult) {
|
||||
return {};
|
||||
|
|
@ -226,6 +304,19 @@ export default class MediaDbPlugin extends Plugin {
|
|||
metadata = {};
|
||||
}
|
||||
|
||||
console.debug(`MDB | metadata read from file content`, metadata);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
getMetadataFromFileCache(file: TFile) {
|
||||
let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter;
|
||||
if (metadata) {
|
||||
metadata = Object.assign({}, metadata); // copy
|
||||
delete metadata.position;
|
||||
} else {
|
||||
metadata = {};
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
|
@ -254,12 +345,13 @@ export default class MediaDbPlugin extends Plugin {
|
|||
|
||||
// create the file
|
||||
const targetFile = await this.app.vault.create(filePath, fileContent);
|
||||
console.debug(`MDB | created new file at ${filePath}`);
|
||||
|
||||
// open newly crated file
|
||||
if (openFile) {
|
||||
const activeLeaf = this.app.workspace.getUnpinnedLeaf();
|
||||
if (!activeLeaf) {
|
||||
console.warn('MDB | no active leaf, not opening media db note');
|
||||
console.warn('MDB | no active leaf, not opening newly created note');
|
||||
return;
|
||||
}
|
||||
await activeLeaf.openFile(targetFile, {state: {mode: 'source'}});
|
||||
|
|
@ -270,18 +362,16 @@ 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');
|
||||
}
|
||||
|
||||
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
|
||||
let metadata: any = this.getMetadataFromFileCache(activeFile);
|
||||
metadata = this.modelPropertyMapper.convertObjectBack(metadata);
|
||||
|
||||
debugLog(metadata);
|
||||
console.debug(`MDB | read metadata`, metadata);
|
||||
|
||||
if (!metadata?.type || !metadata?.dataSource || !metadata?.id) {
|
||||
throw new Error('MDB | active note is not a Media DB entry or is missing metadata');
|
||||
|
|
@ -296,16 +386,21 @@ export default class MediaDbPlugin extends Plugin {
|
|||
|
||||
newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData());
|
||||
|
||||
console.log('MDB | deleting old entry');
|
||||
await this.app.vault.delete(activeFile);
|
||||
await this.createMediaDbNoteFromModel(newMediaTypeModel);
|
||||
// deletion not happening anymore why is this log statement still here
|
||||
console.debug('MDB | deleting old entry');
|
||||
if (onlyMetadata) {
|
||||
await this.createMediaDbNoteFromModel(newMediaTypeModel, {attachFile: activeFile, openNote: true});
|
||||
} else {
|
||||
await this.createMediaDbNoteFromModel(newMediaTypeModel, {attachTemplate: true, openNote: true});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async createEntriesFromFolder(folder: TFolder) {
|
||||
const erroredFiles: { filePath: string, error: string }[] = [];
|
||||
let canceled: boolean = false;
|
||||
|
||||
const {selectedAPI, titleFieldName, appendContent} = await new Promise<{selectedAPI: string, titleFieldName: string, appendContent: boolean}>((resolve, reject) => {
|
||||
const {selectedAPI, titleFieldName, appendContent} = await new Promise<{ selectedAPI: string, titleFieldName: string, appendContent: boolean }>((resolve, reject) => {
|
||||
new MediaDbFolderImportModal(this.app, this, ((selectedAPI: string, titleFieldName: string, appendContent: boolean) => {
|
||||
resolve({selectedAPI, titleFieldName, appendContent});
|
||||
})).open();
|
||||
|
|
@ -313,13 +408,13 @@ export default class MediaDbPlugin extends Plugin {
|
|||
|
||||
for (const child of folder.children) {
|
||||
if (child instanceof TFile) {
|
||||
const file = child as TFile;
|
||||
const file: TFile = child;
|
||||
if (canceled) {
|
||||
erroredFiles.push({filePath: file.path, error: 'user canceled'});
|
||||
continue;
|
||||
}
|
||||
|
||||
let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter;
|
||||
let metadata: any = this.getMetadataFromFileCache(file);
|
||||
|
||||
let title = metadata[titleFieldName];
|
||||
if (!title) {
|
||||
|
|
@ -339,44 +434,36 @@ export default class MediaDbPlugin extends Plugin {
|
|||
continue;
|
||||
}
|
||||
|
||||
let selectedResults: MediaTypeModel[] = [];
|
||||
try {
|
||||
selectedResults = await new Promise((resolve, reject) => {
|
||||
const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, true, (res, err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(res);
|
||||
}, () => {
|
||||
reject(new UserCancelError('user canceled'));
|
||||
}, () => {
|
||||
reject(new UserSkipError('user skipped'));
|
||||
});
|
||||
let {selectModalResult, selectModal} = await this.modalHelper.createSelectModal({elements: results, skipButton: true, modalTitle: `Results for \'${title}\'`});
|
||||
|
||||
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 (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);
|
||||
|
||||
selectModal.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -390,54 +477,48 @@ export default class MediaDbPlugin extends Plugin {
|
|||
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<{ query: string, apis: string[] }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
new MediaDbAdvancedSearchModal(this.app, this, (res, err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(res)
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
|
||||
async openMediaDbIdSearchModal(): Promise<{ query: string, api: string }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
new MediaDbIdSearchModal(this.app, this, (res, err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(res)
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
|
||||
async openMediaDbSelectModal(resultsToDisplay: MediaTypeModel[], skipButton: boolean = false): Promise<MediaTypeModel[]> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
new MediaDbSearchResultModal(this.app, this, resultsToDisplay, skipButton, (res, err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(res);
|
||||
}, () => {
|
||||
resolve([])
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
// console.log(DEFAULT_SETTINGS);
|
||||
const diskSettings: MediaDbPluginSettings = await this.loadData();
|
||||
const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this);
|
||||
const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings);
|
||||
|
||||
// migrate the settings loaded from the disk to match the structure of the default settings
|
||||
let newPropertyMappings: PropertyMappingModel[] = [];
|
||||
for (const defaultPropertyMappingModel of defaultSettings.propertyMappingModels) {
|
||||
let newPropertyMappingModel: PropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type);
|
||||
if (newPropertyMappingModel === undefined) { // if the propertyMappingModel exists in the default settings but not the loaded settings, add it
|
||||
newPropertyMappings.push(defaultPropertyMappingModel);
|
||||
} else { // if the propertyMappingModel also exists in the loaded settings, add it from there
|
||||
let newProperties: PropertyMapping[] = [];
|
||||
|
||||
for (const defaultProperty of defaultPropertyMappingModel.properties) {
|
||||
let newProperty = newPropertyMappingModel.properties.find(x => x.property === defaultProperty.property);
|
||||
if (newProperty === undefined) {
|
||||
// default property is an instance
|
||||
newProperties.push(defaultProperty);
|
||||
} else {
|
||||
// newProperty is just an object and take locked status from default property
|
||||
newProperties.push(new PropertyMapping(newProperty.property, newProperty.newProperty, newProperty.mapping, defaultProperty.locked));
|
||||
}
|
||||
}
|
||||
|
||||
newPropertyMappings.push(new PropertyMappingModel(newPropertyMappingModel.type, newProperties));
|
||||
}
|
||||
}
|
||||
loadedSettings.propertyMappingModels = newPropertyMappings;
|
||||
|
||||
this.settings = loadedSettings;
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
this.mediaTypeManager.updateTemplates(this.settings);
|
||||
this.modelPropertyMapper.updateConversionRules(this.settings);
|
||||
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,53 @@
|
|||
import {App, ButtonComponent, Component, Modal, Notice, Setting, TextComponent, ToggleComponent} from 'obsidian';
|
||||
import {ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent} from 'obsidian';
|
||||
import {MediaTypeModel} from '../models/MediaTypeModel';
|
||||
import {debugLog} from '../utils/Utils';
|
||||
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;
|
||||
selectedApis: {name: string, selected: boolean}[];
|
||||
onSubmit: (res: {query: string, apis: string[]}, err?: Error) => void;
|
||||
title: string;
|
||||
selectedApis: { name: string, selected: boolean }[];
|
||||
|
||||
searchBtn: ButtonComponent;
|
||||
|
||||
submitCallback?: (res: AdvancedSearchModalData) => void;
|
||||
closeCallback?: (err?: Error) => void;
|
||||
|
||||
|
||||
constructor(plugin: MediaDbPlugin, advancedSearchModalOptions: AdvancedSearchModalOptions) {
|
||||
advancedSearchModalOptions = Object.assign({}, ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, advancedSearchModalOptions);
|
||||
super(plugin.app);
|
||||
|
||||
constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (res: {query: string, apis: string[]}, err?: Error) => void) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.onSubmit = onSubmit;
|
||||
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)});
|
||||
}
|
||||
}
|
||||
|
||||
submitCallback(event: KeyboardEvent) {
|
||||
setSubmitCallback(submitCallback: (res: AdvancedSearchModalData) => void): void {
|
||||
this.submitCallback = submitCallback;
|
||||
}
|
||||
|
||||
setCloseCallback(closeCallback: (err?: Error) => void): void {
|
||||
this.closeCallback = closeCallback;
|
||||
}
|
||||
|
||||
keyPressCallback(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
async search(): Promise<MediaTypeModel[]> {
|
||||
|
||||
debugLog(this.selectedApis);
|
||||
|
||||
if (!this.query || this.query.length < 3) {
|
||||
new Notice('MDB | Query to short');
|
||||
new Notice('MDB | Query too short');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -44,31 +59,26 @@ export class MediaDbAdvancedSearchModal extends Modal {
|
|||
}
|
||||
|
||||
if (!this.isBusy) {
|
||||
try {
|
||||
this.isBusy = true;
|
||||
this.searchBtn.setDisabled(false);
|
||||
this.searchBtn.setButtonText('Searching...');
|
||||
this.isBusy = true;
|
||||
this.searchBtn.setDisabled(false);
|
||||
this.searchBtn.setButtonText('Searching...');
|
||||
|
||||
this.onSubmit({query: this.query, apis: apis});
|
||||
} catch (e) {
|
||||
this.onSubmit(null, e);
|
||||
} finally {
|
||||
this.close();
|
||||
}
|
||||
this.submitCallback({query: this.query, apis: apis});
|
||||
}
|
||||
}
|
||||
|
||||
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.submitCallback.bind(this));
|
||||
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
|
||||
|
||||
contentEl.appendChild(searchComponent.inputEl);
|
||||
searchComponent.inputEl.focus();
|
||||
|
|
@ -76,7 +86,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
|
|||
contentEl.createDiv({cls: 'media-db-plugin-spacer'});
|
||||
contentEl.createEl('h3', {text: 'APIs to search'});
|
||||
|
||||
const apiToggleComponents: Component[] = [];
|
||||
// const apiToggleComponents: Component[] = [];
|
||||
for (const api of this.plugin.apiManager.apis) {
|
||||
const apiToggleListElementWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'});
|
||||
|
||||
|
|
@ -98,21 +108,26 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.closeCallback();
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,46 @@
|
|||
import {App, ButtonComponent, DropdownComponent, Modal, Notice, Setting, TextComponent} from 'obsidian';
|
||||
import {ButtonComponent, DropdownComponent, Modal, Notice, Setting, TextComponent} from 'obsidian';
|
||||
import {MediaTypeModel} from '../models/MediaTypeModel';
|
||||
import {debugLog} from '../utils/Utils';
|
||||
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;
|
||||
onSubmit: (res: {query: string, api: string}, err?: Error) => void;
|
||||
|
||||
constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (res: {query: string, api: string}, err?: Error) => void) {
|
||||
super(app);
|
||||
searchBtn: ButtonComponent;
|
||||
|
||||
submitCallback?: (res: IdSearchModalData, err?: Error) => void;
|
||||
closeCallback?: (err?: Error) => void;
|
||||
|
||||
|
||||
constructor(plugin: MediaDbPlugin, idSearchModalOptions: IdSearchModalOptions) {
|
||||
idSearchModalOptions = Object.assign({}, ID_SEARCH_MODAL_DEFAULT_OPTIONS, idSearchModalOptions);
|
||||
super(plugin.app);
|
||||
|
||||
this.plugin = plugin;
|
||||
this.onSubmit = onSubmit;
|
||||
this.selectedApi = plugin.apiManager.apis[0].apiName;
|
||||
this.title = idSearchModalOptions.modalTitle;
|
||||
this.selectedApi = idSearchModalOptions.preselectedAPI || plugin.apiManager.apis[0].apiName;
|
||||
}
|
||||
|
||||
submitCallback(event: KeyboardEvent) {
|
||||
setSubmitCallback(submitCallback: (res: IdSearchModalData, err?: Error) => void): void {
|
||||
this.submitCallback = submitCallback;
|
||||
}
|
||||
|
||||
setCloseCallback(closeCallback: (err?: Error) => void): void {
|
||||
this.closeCallback = closeCallback;
|
||||
}
|
||||
|
||||
keyPressCallback(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
async search(): Promise<MediaTypeModel> {
|
||||
|
||||
debugLog(this.selectedApi);
|
||||
|
||||
if (!this.query) {
|
||||
new Notice('MDB | no Id entered');
|
||||
return;
|
||||
|
|
@ -39,31 +52,25 @@ export class MediaDbIdSearchModal extends Modal {
|
|||
}
|
||||
|
||||
if (!this.isBusy) {
|
||||
try {
|
||||
this.isBusy = true;
|
||||
this.searchBtn.setDisabled(false);
|
||||
this.searchBtn.setButtonText('Searching...');
|
||||
this.isBusy = true;
|
||||
this.searchBtn.setDisabled(false);
|
||||
this.searchBtn.setButtonText('Searching...');
|
||||
|
||||
this.onSubmit({query: this.query, api: this.selectedApi});
|
||||
} catch (e) {
|
||||
this.onSubmit(null, e);
|
||||
} finally {
|
||||
this.close();
|
||||
}
|
||||
this.submitCallback({query: this.query, api: this.selectedApi});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
searchComponent.inputEl.style.width = '100%';
|
||||
searchComponent.setPlaceholder(placeholder);
|
||||
searchComponent.onChange(value => (this.query = value));
|
||||
searchComponent.inputEl.addEventListener('keydown', this.submitCallback.bind(this));
|
||||
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
|
||||
|
||||
contentEl.appendChild(searchComponent.inputEl);
|
||||
searchComponent.inputEl.focus();
|
||||
|
|
@ -86,21 +93,26 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.closeCallback();
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
82
src/modals/MediaDbPreviewModal.ts
Normal file
82
src/modals/MediaDbPreviewModal.ts
Normal file
|
|
@ -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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,45 @@
|
|||
import {App} from 'obsidian';
|
||||
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<MediaTypeModel> {
|
||||
plugin: MediaDbPlugin;
|
||||
heading: string;
|
||||
onSubmit: (res: MediaTypeModel[], err?: Error) => void;
|
||||
onCancel: () => void;
|
||||
onSkip: () => void;
|
||||
|
||||
busy: boolean;
|
||||
sendCallback: boolean;
|
||||
|
||||
constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], skipButton: boolean, onSubmit: (res: MediaTypeModel[], err?: Error) => void, onCancel: () => void, onSkip?: () => void) {
|
||||
super(app, elements);
|
||||
this.plugin = plugin;
|
||||
this.onSubmit = onSubmit;
|
||||
this.onCancel = onCancel;
|
||||
this.onSkip = onSkip;
|
||||
submitCallback: (res: SelectModalData) => void;
|
||||
closeCallback: (err?: Error) => void;
|
||||
skipCallback: () => void;
|
||||
|
||||
this.title = 'Search Results';
|
||||
|
||||
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 = selectModalOptions.modalTitle;
|
||||
this.description = 'Select one or multiple search results.';
|
||||
this.skipButton = skipButton;
|
||||
this.addSkipButton = selectModalOptions.skipButton;
|
||||
|
||||
this.busy = false;
|
||||
|
||||
this.sendCallback = false;
|
||||
}
|
||||
|
||||
setSubmitCallback(submitCallback: (res: SelectModalData) => void): void {
|
||||
this.submitCallback = submitCallback;
|
||||
}
|
||||
|
||||
setCloseCallback(closeCallback: (err?: Error) => void): void {
|
||||
this.closeCallback = closeCallback;
|
||||
}
|
||||
|
||||
setSkipCallback(skipCallback: () => void): void {
|
||||
this.skipCallback = skipCallback;
|
||||
}
|
||||
|
||||
// Renders each suggestion item.
|
||||
renderElement(item: MediaTypeModel, el: HTMLElement) {
|
||||
el.createEl('div', {text: this.plugin.mediaTypeManager.getFileName(item)});
|
||||
|
|
@ -35,20 +49,20 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
|
|||
|
||||
// Perform action on the selected suggestion.
|
||||
submit() {
|
||||
this.onSubmit(this.selectModalElements.filter(x => x.isActive()).map(x => x.value));
|
||||
this.sendCallback = true;
|
||||
this.close();
|
||||
if (!this.busy) {
|
||||
this.busy = true;
|
||||
this.submitButton.setButtonText('Creating entry...');
|
||||
this.submitCallback({selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value)});
|
||||
}
|
||||
}
|
||||
|
||||
skip() {
|
||||
this.onSkip();
|
||||
this.sendCallback = true;
|
||||
this.close();
|
||||
this.skipButton.setButtonText('Skipping...');
|
||||
this.skipCallback();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (!this.sendCallback) {
|
||||
this.onCancel();
|
||||
}
|
||||
console.log('close');
|
||||
this.closeCallback();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {App, Modal, Setting} from 'obsidian';
|
||||
import {App, ButtonComponent, Modal, Setting} from 'obsidian';
|
||||
import {SelectModalElement} from './SelectModalElement';
|
||||
import {mod} from '../utils/Utils';
|
||||
|
||||
|
|
@ -7,32 +7,50 @@ export abstract class SelectModal<T> extends Modal {
|
|||
|
||||
title: string;
|
||||
description: string;
|
||||
skipButton: boolean;
|
||||
addSkipButton: boolean;
|
||||
cancelButton?: ButtonComponent;
|
||||
skipButton?: ButtonComponent;
|
||||
submitButton?: ButtonComponent;
|
||||
|
||||
elementWrapper?: HTMLDivElement;
|
||||
|
||||
elements: T[];
|
||||
selectModalElements: SelectModalElement<T>[];
|
||||
|
||||
|
||||
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 = '';
|
||||
this.skipButton = false;
|
||||
this.addSkipButton = false;
|
||||
this.cancelButton = undefined;
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
@ -59,26 +77,18 @@ export abstract class SelectModal<T> 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);
|
||||
|
||||
|
|
@ -89,12 +99,28 @@ export abstract class SelectModal<T> extends Modal {
|
|||
|
||||
this.selectModalElements.first()?.element.scrollIntoView();
|
||||
|
||||
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()));
|
||||
const bottomSettingRow = new Setting(contentEl);
|
||||
bottomSettingRow.addButton(btn => {
|
||||
btn.setButtonText('Cancel');
|
||||
btn.onClick(() => this.close());
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
this.cancelButton = btn;
|
||||
});
|
||||
if (this.addSkipButton) {
|
||||
bottomSettingRow.addButton(btn => {
|
||||
btn.setButtonText('Skip');
|
||||
btn.onClick(() => this.skip());
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
this.skipButton = btn;
|
||||
});
|
||||
}
|
||||
bottomSetting.addButton(btn => 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() {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ export class BoardGameModel extends MediaTypeModel {
|
|||
constructor(obj: any = {}) {
|
||||
super();
|
||||
|
||||
this.genres = undefined;
|
||||
this.onlineRating = undefined;
|
||||
this.image = undefined;
|
||||
this.released = undefined;
|
||||
this.userData = {
|
||||
played: undefined,
|
||||
personalRating: undefined,
|
||||
};
|
||||
|
||||
Object.assign(this, obj);
|
||||
|
||||
this.type = this.getMediaType();
|
||||
|
|
|
|||
|
|
@ -4,15 +4,6 @@ import {MediaType} from '../utils/MediaType';
|
|||
|
||||
|
||||
export class GameModel extends MediaTypeModel {
|
||||
type: string;
|
||||
subType: string;
|
||||
title: string;
|
||||
englishTitle: string;
|
||||
year: string;
|
||||
dataSource: string;
|
||||
url: string;
|
||||
id: string;
|
||||
|
||||
genres: string[];
|
||||
onlineRating: number;
|
||||
image: string;
|
||||
|
|
@ -29,6 +20,16 @@ export class GameModel extends MediaTypeModel {
|
|||
constructor(obj: any = {}) {
|
||||
super();
|
||||
|
||||
this.genres = undefined;
|
||||
this.onlineRating = undefined;
|
||||
this.image = undefined;
|
||||
this.released = undefined;
|
||||
this.releaseDate = undefined;
|
||||
this.userData = {
|
||||
played: undefined,
|
||||
personalRating: undefined,
|
||||
};
|
||||
|
||||
Object.assign(this, obj);
|
||||
|
||||
this.type = this.getMediaType();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ export abstract class MediaTypeModel {
|
|||
|
||||
userData: object;
|
||||
|
||||
|
||||
protected constructor() {
|
||||
this.type = undefined;
|
||||
this.subType = undefined;
|
||||
this.title = undefined;
|
||||
this.englishTitle = undefined;
|
||||
this.year = undefined;
|
||||
this.dataSource = undefined;
|
||||
this.url = undefined;
|
||||
this.id = undefined;
|
||||
this.userData = {};
|
||||
}
|
||||
|
||||
abstract getMediaType(): MediaType;
|
||||
|
||||
//a string that contains enough info to disambiguate from similar media
|
||||
|
|
@ -24,7 +37,7 @@ export abstract class MediaTypeModel {
|
|||
}
|
||||
|
||||
getWithOutUserData(): object {
|
||||
const copy = JSON.parse(JSON.stringify(this));
|
||||
const copy = Object.assign({}, this);
|
||||
delete copy.userData;
|
||||
return copy;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,6 @@ import {MediaType} from '../utils/MediaType';
|
|||
|
||||
|
||||
export class MovieModel extends MediaTypeModel {
|
||||
type: string;
|
||||
subType: string;
|
||||
title: string;
|
||||
englishTitle: string;
|
||||
year: string;
|
||||
dataSource: string;
|
||||
url: string;
|
||||
id: string;
|
||||
|
||||
genres: string[];
|
||||
producer: string;
|
||||
duration: string;
|
||||
|
|
@ -31,6 +22,19 @@ export class MovieModel extends MediaTypeModel {
|
|||
constructor(obj: any = {}) {
|
||||
super();
|
||||
|
||||
this.genres = undefined;
|
||||
this.producer = undefined;
|
||||
this.duration = undefined;
|
||||
this.onlineRating = undefined;
|
||||
this.image = undefined;
|
||||
this.released = undefined;
|
||||
this.premiere = undefined;
|
||||
this.userData = {
|
||||
watched: undefined,
|
||||
lastWatched: undefined,
|
||||
personalRating: undefined,
|
||||
};
|
||||
|
||||
Object.assign(this, obj);
|
||||
|
||||
this.type = this.getMediaType();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ export class MusicReleaseModel extends MediaTypeModel {
|
|||
constructor(obj: any = {}) {
|
||||
super();
|
||||
|
||||
this.genres = undefined;
|
||||
this.artists = undefined;
|
||||
this.rating = undefined;
|
||||
this.userData = {
|
||||
personalRating: undefined,
|
||||
};
|
||||
|
||||
Object.assign(this, obj);
|
||||
|
||||
this.type = this.getMediaType();
|
||||
|
|
@ -38,9 +45,9 @@ export class MusicReleaseModel extends MediaTypeModel {
|
|||
}
|
||||
|
||||
getSummary(): string {
|
||||
var summary = this.title + ' (' + this.year + ')';
|
||||
if(this.artists.length > 0)
|
||||
summary += ' - ' + this.artists.join(', ')
|
||||
let summary = this.title + ' (' + this.year + ')';
|
||||
if (this.artists.length > 0)
|
||||
summary += ' - ' + this.artists.join(', ');
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,22 @@ export class SeriesModel extends MediaTypeModel {
|
|||
constructor(obj: any = {}) {
|
||||
super();
|
||||
|
||||
this.genres = undefined;
|
||||
this.studios = undefined;
|
||||
this.episodes = undefined;
|
||||
this.duration = undefined;
|
||||
this.onlineRating = undefined;
|
||||
this.image = undefined;
|
||||
this.released = undefined;
|
||||
this.airing = undefined;
|
||||
this.airedFrom = undefined;
|
||||
this.airedTo = undefined;
|
||||
this.userData = {
|
||||
watched: undefined,
|
||||
lastWatched: undefined,
|
||||
personalRating: undefined,
|
||||
};
|
||||
|
||||
Object.assign(this, obj);
|
||||
|
||||
this.type = this.getMediaType();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ export class WikiModel extends MediaTypeModel {
|
|||
constructor(obj: any = {}) {
|
||||
super();
|
||||
|
||||
this.wikiUrl = undefined;
|
||||
this.lastUpdated = undefined;
|
||||
this.length = undefined;
|
||||
this.article = undefined;
|
||||
this.userData = {};
|
||||
|
||||
Object.assign(this, obj);
|
||||
|
||||
this.type = this.getMediaType();
|
||||
|
|
@ -37,7 +43,7 @@ export class WikiModel extends MediaTypeModel {
|
|||
}
|
||||
|
||||
override getWithOutUserData(): object {
|
||||
const copy = JSON.parse(JSON.stringify(this));
|
||||
const copy = Object.assign({}, this);
|
||||
delete copy.userData;
|
||||
delete copy.article;
|
||||
return copy;
|
||||
|
|
|
|||
36
src/settings/Icon.svelte
Normal file
36
src/settings/Icon.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!--adapted from @joethei's code: https://github.com/joethei/obsidian-rss/blob/master/src/view/IconComponent.svelte-->
|
||||
<!--adapted from @javalent's code: https://discord.com/channels/686053708261228577/840286264964022302/902949764209987654-->
|
||||
<script lang="ts">
|
||||
import {setIcon} from 'obsidian';
|
||||
import {onMount} from 'svelte';
|
||||
|
||||
export let iconName: string = '';
|
||||
export let iconSize: number = 20;
|
||||
|
||||
let iconEl: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
setIcon(iconEl, iconName, iconSize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.icon-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
top: calc(50% - 10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if iconName.length > 0}
|
||||
<div class="icon-wrapper">
|
||||
<div bind:this={iconEl} class="icon"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import {containsOnlyLettersAndUnderscores} from '../utils/Utils';
|
||||
|
||||
export class ModelPropertyConversionRule {
|
||||
property: string;
|
||||
newProperty: string;
|
||||
|
||||
constructor(conversionRule: string) {
|
||||
const conversionRuleParts = conversionRule.split('->');
|
||||
if (conversionRuleParts.length !== 2) {
|
||||
throw Error(`Conversion rule "${conversionRule}" may only have exactly one "->"`);
|
||||
}
|
||||
|
||||
let property = conversionRuleParts[0].trim();
|
||||
let newProperty = conversionRuleParts[1].trim();
|
||||
|
||||
if (!property || !containsOnlyLettersAndUnderscores(property)) {
|
||||
throw Error(`Error in conversion rule "${conversionRule}": property may not be empty and only contain letters and underscores.`);
|
||||
}
|
||||
|
||||
if (!newProperty || !containsOnlyLettersAndUnderscores(newProperty)) {
|
||||
throw Error(`Error in conversion rule "${conversionRule}": new property may not be empty and only contain letters and underscores.`);
|
||||
}
|
||||
|
||||
this.property = property;
|
||||
this.newProperty = newProperty;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import {MediaType} from '../utils/MediaType';
|
||||
import {MediaDbPluginSettings} from './Settings';
|
||||
import {ModelPropertyConversionRule} from './ModelPropertyConversionRule';
|
||||
|
||||
export class ModelPropertyMapper {
|
||||
conversionRulesMap: Map<MediaType, string>;
|
||||
|
||||
constructor(settings: MediaDbPluginSettings) {
|
||||
this.updateConversionRules(settings);
|
||||
}
|
||||
|
||||
updateConversionRules(settings: MediaDbPluginSettings) {
|
||||
this.conversionRulesMap = new Map<MediaType, string>();
|
||||
this.conversionRulesMap.set(MediaType.Movie, settings.moviePropertyConversionRules);
|
||||
this.conversionRulesMap.set(MediaType.Series, settings.seriesPropertyConversionRules);
|
||||
this.conversionRulesMap.set(MediaType.Game, settings.gamePropertyConversionRules);
|
||||
this.conversionRulesMap.set(MediaType.Wiki, settings.wikiPropertyConversionRules);
|
||||
this.conversionRulesMap.set(MediaType.MusicRelease, settings.musicReleasePropertyConversionRules);
|
||||
this.conversionRulesMap.set(MediaType.BoardGame, settings.boardgamePropertyConversionRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object using the conversion rules for its type.
|
||||
* Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
convertObject(obj: object): object {
|
||||
if (!obj.hasOwnProperty('type')) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// get conversion rules from settings corresponding to the object type
|
||||
const conversionRulesString: string = this.conversionRulesMap.get(obj['type']);
|
||||
if (!conversionRulesString) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// parse the conversion rules
|
||||
const conversionRules: ModelPropertyConversionRule[] = [];
|
||||
for (const conversionRuleString of conversionRulesString.split('\n')) {
|
||||
if (conversionRuleString) {
|
||||
conversionRules.push(new ModelPropertyConversionRule(conversionRuleString));
|
||||
}
|
||||
}
|
||||
|
||||
const newObj: object = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
// property 'type' can not be remapped
|
||||
if (key === 'type') {
|
||||
// @ts-ignore
|
||||
newObj[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
let hasConversionRule = false;
|
||||
for (const conversionRule of conversionRules) {
|
||||
if (conversionRule.property === key) {
|
||||
hasConversionRule = true;
|
||||
// if the conversion rule maps to 'x', then that means it should be ignored
|
||||
if (conversionRule.newProperty.toLowerCase() !== 'x') {
|
||||
// @ts-ignore
|
||||
newObj[conversionRule.newProperty] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasConversionRule) {
|
||||
// @ts-ignore
|
||||
newObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object back using the conversion rules for its type.
|
||||
* Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
convertObjectBack(obj: object): object {
|
||||
if (!obj.hasOwnProperty('type')) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// get conversion rules from settings corresponding to the object type
|
||||
const conversionRulesString: string = this.conversionRulesMap.get(obj['type']);
|
||||
if (!conversionRulesString) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const conversionRules: ModelPropertyConversionRule[] = [];
|
||||
// parse the conversion rules
|
||||
for (const conversionRuleString of conversionRulesString.split('\n')) {
|
||||
if (conversionRuleString) {
|
||||
conversionRules.push(new ModelPropertyConversionRule(conversionRuleString));
|
||||
}
|
||||
}
|
||||
|
||||
const originalObj: object = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
// property 'type' can not be remapped
|
||||
if (key === 'type') {
|
||||
// @ts-ignore
|
||||
originalObj[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
let hasConversionRule = false;
|
||||
for (const conversionRule of conversionRules) {
|
||||
if (conversionRule.newProperty === key) {
|
||||
hasConversionRule = true;
|
||||
// @ts-ignore
|
||||
originalObj[conversionRule.property] = value;
|
||||
}
|
||||
}
|
||||
if (!hasConversionRule) {
|
||||
// @ts-ignore
|
||||
originalObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return originalObj;
|
||||
}
|
||||
}
|
||||
106
src/settings/PropertyMapper.ts
Normal file
106
src/settings/PropertyMapper.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import {PropertyMappingOption} from './PropertyMapping';
|
||||
import {MEDIA_TYPES} from '../utils/MediaTypeManager';
|
||||
import MediaDbPlugin from '../main';
|
||||
|
||||
export class PropertyMapper {
|
||||
plugin: MediaDbPlugin;
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object using the conversion rules for its type.
|
||||
* Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
convertObject(obj: object): object {
|
||||
console.log('test1');
|
||||
|
||||
if (!obj.hasOwnProperty('type')) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
console.log('test2');
|
||||
// @ts-ignore
|
||||
console.log(obj.type);
|
||||
|
||||
// @ts-ignore
|
||||
if (MEDIA_TYPES.filter(x => x.toString() == obj.type).length < 1) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
console.log('test3');
|
||||
|
||||
// @ts-ignore
|
||||
const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties;
|
||||
|
||||
const newObj: object = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.property === key) {
|
||||
if (propertyMapping.mapping === PropertyMappingOption.Map) {
|
||||
// @ts-ignore
|
||||
newObj[propertyMapping.newProperty] = value;
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Remove) {
|
||||
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Default) {
|
||||
// @ts-ignore
|
||||
newObj[key] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object back using the conversion rules for its type.
|
||||
* Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
convertObjectBack(obj: object): object {
|
||||
if (!obj.hasOwnProperty('type')) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (MEDIA_TYPES.contains(obj.type)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties;
|
||||
|
||||
const originalObj: object = {};
|
||||
|
||||
objLoop: for (const [key, value] of Object.entries(obj)) {
|
||||
// first try if it is a normal property
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.property === key) {
|
||||
// @ts-ignore
|
||||
originalObj[key] = value;
|
||||
|
||||
continue objLoop;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise see if it is a mapped property
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.newProperty === key) {
|
||||
// @ts-ignore
|
||||
originalObj[propertyMapping.property] = value;
|
||||
|
||||
continue objLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return originalObj;
|
||||
}
|
||||
}
|
||||
150
src/settings/PropertyMapping.ts
Normal file
150
src/settings/PropertyMapping.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import {containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError} from '../utils/Utils';
|
||||
import {MediaType} from '../utils/MediaType';
|
||||
|
||||
export enum PropertyMappingOption {
|
||||
Default = 'default',
|
||||
Map = 'remap',
|
||||
Remove = 'remove',
|
||||
}
|
||||
|
||||
export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove];
|
||||
|
||||
export class PropertyMappingModel {
|
||||
type: MediaType;
|
||||
properties: PropertyMapping[];
|
||||
|
||||
constructor(type: MediaType, properties?: PropertyMapping[]) {
|
||||
this.type = type;
|
||||
this.properties = properties ?? [];
|
||||
}
|
||||
|
||||
validate(): { res: boolean, err?: Error } {
|
||||
console.debug(`MDB | validated property mappings for ${this.type}`);
|
||||
|
||||
// check properties
|
||||
for (const property of this.properties) {
|
||||
const propertyValidation = property.validate();
|
||||
if (!propertyValidation.res) {
|
||||
return {
|
||||
res: false,
|
||||
err: propertyValidation.err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// check for name collisions
|
||||
for (const property of this.getMappedProperties()) {
|
||||
const propertiesWithSameTarget = this.getMappedProperties().filter(x => x.newProperty === property.newProperty);
|
||||
if (propertiesWithSameTarget.length === 0) {
|
||||
// if we get there, then something in this code is wrong
|
||||
} else if (propertiesWithSameTarget.length === 1) {
|
||||
// all good
|
||||
} else {
|
||||
// two or more properties are mapped to the same property
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingNameConflictError(`Multiple remapped properties (${propertiesWithSameTarget.map(x => x.toString()).toString()}) may not share the same name.`),
|
||||
};
|
||||
}
|
||||
}
|
||||
// remapped properties may not have the same name as any original property
|
||||
for (const property of this.getMappedProperties()) {
|
||||
const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property);
|
||||
if (propertiesWithSameTarget.length === 0) {
|
||||
// all good
|
||||
} else {
|
||||
// a mapped property shares the same name with an original property
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingNameConflictError(`Remapped property (${property}) may not share it's new name with an existing property.`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
res: true,
|
||||
};
|
||||
}
|
||||
|
||||
getMappedProperties() {
|
||||
return this.properties.filter(x => x.mapping === PropertyMappingOption.Map);
|
||||
}
|
||||
|
||||
copy(): PropertyMappingModel {
|
||||
const copy = new PropertyMappingModel(this.type);
|
||||
for (const property of this.properties) {
|
||||
const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked);
|
||||
copy.properties.push(propertyCopy);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
export class PropertyMapping {
|
||||
property: string;
|
||||
newProperty: string;
|
||||
locked: boolean;
|
||||
mapping: PropertyMappingOption;
|
||||
|
||||
constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean) {
|
||||
this.property = property;
|
||||
this.newProperty = newProperty;
|
||||
this.mapping = mapping;
|
||||
this.locked = locked ?? false;
|
||||
}
|
||||
|
||||
validate(): { res: boolean, err?: Error } {
|
||||
// locked property may only be default
|
||||
if (this.locked) {
|
||||
if (this.mapping === PropertyMappingOption.Remove) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be removed.`),
|
||||
};
|
||||
}
|
||||
if (this.mapping === PropertyMappingOption.Map) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be remapped.`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mapping === PropertyMappingOption.Default) {
|
||||
return {res: true};
|
||||
}
|
||||
if (this.mapping === PropertyMappingOption.Remove) {
|
||||
return {res: true};
|
||||
}
|
||||
|
||||
if (!this.property || !containsOnlyLettersAndUnderscores(this.property)) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": property may not be empty and only contain letters and underscores.`),
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.newProperty || !containsOnlyLettersAndUnderscores(this.newProperty)) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": new property may not be empty and only contain letters and underscores.`),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
res: true,
|
||||
};
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.mapping === PropertyMappingOption.Default) {
|
||||
return this.property;
|
||||
} else if (this.mapping === PropertyMappingOption.Map) {
|
||||
return `${this.property} -> ${this.newProperty}`;
|
||||
} else if (this.mapping === PropertyMappingOption.Remove) {
|
||||
return `remove ${this.property}`;
|
||||
}
|
||||
|
||||
return this.property;
|
||||
}
|
||||
}
|
||||
63
src/settings/PropertyMappingModelComponent.svelte
Normal file
63
src/settings/PropertyMappingModelComponent.svelte
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts">
|
||||
import {PropertyMappingModel, PropertyMappingOption, propertyMappingOptions} from './PropertyMapping';
|
||||
import {capitalizeFirstLetter} from '../utils/Utils';
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
export let model: PropertyMappingModel;
|
||||
export let save: (model: PropertyMappingModel) => void;
|
||||
|
||||
let validationResult: { res: boolean, err?: Error };
|
||||
|
||||
$: modelChanged(model);
|
||||
|
||||
function modelChanged(model: PropertyMappingModel) {
|
||||
validationResult = model.validate();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<div class="media-db-plugin-property-mappings-model-container">
|
||||
<div class="setting-item-name">{capitalizeFirstLetter(model.type)}</div>
|
||||
<div class="media-db-plugin-property-mappings-container">
|
||||
{ #each model.properties as property }
|
||||
<div class="media-db-plugin-property-mapping-element">
|
||||
<div class="media-db-plugin-property-mapping-element-property-name-wrapper">
|
||||
<pre
|
||||
class="media-db-plugin-property-mapping-element-property-name"><code>{property.property}</code></pre>
|
||||
</div>
|
||||
{ #if property.locked }
|
||||
<div class="media-db-plugin-property-binding-text">
|
||||
property can not be remapped
|
||||
</div>
|
||||
{ :else }
|
||||
<select class="dropdown" bind:value={property.mapping}>
|
||||
{#each propertyMappingOptions as remappingOption}
|
||||
<option value={remappingOption}>
|
||||
{remappingOption}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{ #if property.mapping === PropertyMappingOption.Map }
|
||||
<Icon iconName="arrow-right"/>
|
||||
<div class="media-db-plugin-property-mapping-to">
|
||||
<input type="text" spellcheck="false" bind:value="{property.newProperty}">
|
||||
</div>
|
||||
{ /if }
|
||||
{ /if }
|
||||
</div>
|
||||
{ /each }
|
||||
</div>
|
||||
{ #if !validationResult?.res }
|
||||
<div class="media-db-plugin-property-mapping-validation">
|
||||
{validationResult?.err?.message}
|
||||
</div>
|
||||
{ /if }
|
||||
<button
|
||||
class="media-db-plugin-property-mappings-save-button {validationResult?.res ? 'mod-cta' : 'mod-muted'}"
|
||||
on:click={() => { if(model.validate().res) save(model) }}>Save
|
||||
</button>
|
||||
</div>
|
||||
27
src/settings/PropertyMappingModelsComponent.svelte
Normal file
27
src/settings/PropertyMappingModelsComponent.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import {PropertyMappingModel} from './PropertyMapping';
|
||||
import PropertyMappingModelComponent from './PropertyMappingModelComponent.svelte';
|
||||
|
||||
export let models: PropertyMappingModel[] = [];
|
||||
export let save: (model: PropertyMappingModel) => void;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<div class="setting-item" style="display: flex; gap: 10px; flex-direction: column; align-items: stretch;">
|
||||
{ #each models as model }
|
||||
<PropertyMappingModelComponent model={model} save={save}></PropertyMappingModelComponent>
|
||||
{ /each }
|
||||
|
||||
<!--
|
||||
<pre>{JSON.stringify(models, null, 4)}</pre>
|
||||
|
||||
{ #each ICON_LIST as icon }
|
||||
<p>
|
||||
{icon} <Icon iconName="{icon}"/>
|
||||
</p>
|
||||
{/each}
|
||||
-->
|
||||
</div>
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
import {App, PluginSettingTab, Setting} from 'obsidian';
|
||||
import {App, Notice, PluginSettingTab, Setting} from 'obsidian';
|
||||
|
||||
import MediaDbPlugin from '../main';
|
||||
import {FolderSuggest} from './suggesters/FolderSuggest';
|
||||
import {FileSuggest} from './suggesters/FileSuggest';
|
||||
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte';
|
||||
import {PropertyMapping, PropertyMappingModel, PropertyMappingOption} from './PropertyMapping';
|
||||
import {MEDIA_TYPES} from '../utils/MediaTypeManager';
|
||||
import {MediaTypeModel} from '../models/MediaTypeModel';
|
||||
|
||||
|
||||
export interface MediaDbPluginSettings {
|
||||
|
|
@ -34,9 +38,11 @@ export interface MediaDbPluginSettings {
|
|||
musicReleasePropertyConversionRules: string,
|
||||
boardgamePropertyConversionRules: string,
|
||||
|
||||
propertyMappingModels: PropertyMappingModel[],
|
||||
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
||||
const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
||||
folder: 'Media DB',
|
||||
OMDbKey: '',
|
||||
sfwFilter: true,
|
||||
|
|
@ -64,8 +70,37 @@ export const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
|||
musicReleasePropertyConversionRules: '',
|
||||
boardgamePropertyConversionRules: '',
|
||||
|
||||
propertyMappingModels: [],
|
||||
};
|
||||
|
||||
export const lockedPropertyMappings: string[] = ['type', 'id', 'dataSource'];
|
||||
|
||||
export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings {
|
||||
let defaultSettings = DEFAULT_SETTINGS;
|
||||
|
||||
// construct property mapping defaults
|
||||
const propertyMappingModels: PropertyMappingModel[] = [];
|
||||
for (const mediaType of MEDIA_TYPES) {
|
||||
const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType);
|
||||
const metadataObj = model.toMetaDataObject();
|
||||
// console.log(metadataObj);
|
||||
// console.log(model);
|
||||
|
||||
const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType);
|
||||
|
||||
for (const key of Object.keys(metadataObj)) {
|
||||
propertyMappingModel.properties.push(
|
||||
new PropertyMapping(key, '', PropertyMappingOption.Default, lockedPropertyMappings.contains(key)),
|
||||
);
|
||||
}
|
||||
|
||||
propertyMappingModels.push(propertyMappingModel);
|
||||
}
|
||||
|
||||
defaultSettings.propertyMappingModels = propertyMappingModels;
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
export class MediaDbSettingTab extends PluginSettingTab {
|
||||
plugin: MediaDbPlugin;
|
||||
|
||||
|
|
@ -296,79 +331,48 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
});
|
||||
// endregion
|
||||
|
||||
containerEl.createEl('h3', {text: 'Property Mappings'});
|
||||
// region Property Mappings
|
||||
new Setting(containerEl)
|
||||
.setName('Movie model property mappings')
|
||||
.setDesc('Mappings for the property names of a movie.')
|
||||
.addTextArea(cb => {
|
||||
cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`)
|
||||
.setValue(this.plugin.settings.moviePropertyConversionRules)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.moviePropertyConversionRules = data;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Series model property mappings')
|
||||
.setDesc('Mappings for the property names of a series.')
|
||||
.addTextArea(cb => {
|
||||
cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`)
|
||||
.setValue(this.plugin.settings.seriesPropertyConversionRules)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.seriesPropertyConversionRules = data;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
containerEl.createEl('h3', {text: 'Property Mappings'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Game model property mappings')
|
||||
.setDesc('Mappings for the property names of a game.')
|
||||
.addTextArea(cb => {
|
||||
cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`)
|
||||
.setValue(this.plugin.settings.gamePropertyConversionRules)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.gamePropertyConversionRules = data;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
let propertyMappingExplanation = containerEl.createEl('div');
|
||||
propertyMappingExplanation.innerHTML = `
|
||||
<p>Allow you to remap the metadata fields of newly created media db entries.</p>
|
||||
<p>
|
||||
The different options are:
|
||||
<lu>
|
||||
<li>"default": does no remapping and keeps the metadata field as it is</li>
|
||||
<li>"remap": renames the metadata field to what ever you specify</li>
|
||||
<li>"remove": removes the metadata field entirely</li>
|
||||
</lu>
|
||||
</p>
|
||||
<p>
|
||||
Don't forget to save your changes using the save button for each individual category.
|
||||
</p>`;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Wiki model property mappings')
|
||||
.setDesc('Mappings for the property names of a wiki entry.')
|
||||
.addTextArea(cb => {
|
||||
cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`)
|
||||
.setValue(this.plugin.settings.wikiPropertyConversionRules)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.wikiPropertyConversionRules = data;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Music Release model property mappings')
|
||||
.setDesc('Mappings for the property names of a music release.')
|
||||
.addTextArea(cb => {
|
||||
cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`)
|
||||
.setValue(this.plugin.settings.musicReleasePropertyConversionRules)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.musicReleasePropertyConversionRules = data;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new PropertyMappingModelsComponent({
|
||||
target: this.containerEl,
|
||||
props: {
|
||||
models: this.plugin.settings.propertyMappingModels.map(x => x.copy()),
|
||||
save: (model: PropertyMappingModel) => {
|
||||
let propertyMappingModels: PropertyMappingModel[] = [];
|
||||
|
||||
for (const model2 of this.plugin.settings.propertyMappingModels) {
|
||||
if (model2.type === model.type) {
|
||||
propertyMappingModels.push(model);
|
||||
} else {
|
||||
propertyMappingModels.push(model2);
|
||||
}
|
||||
}
|
||||
|
||||
this.plugin.settings.propertyMappingModels = propertyMappingModels;
|
||||
new Notice(`MDB: Property Mappings for ${model.type} saved successfully.`);
|
||||
this.plugin.saveSettings();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Board Game model property mappings')
|
||||
.setDesc('Mappings for the property names of a boardgame.')
|
||||
.addTextArea(cb => {
|
||||
cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`)
|
||||
.setValue(this.plugin.settings.boardgamePropertyConversionRules)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.boardgamePropertyConversionRules = data;
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
// endregion
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class Suggest<T> {
|
|||
}
|
||||
|
||||
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
|
||||
const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length);
|
||||
const normalizedIndex = this.suggestions.length > 0 ? wrapAround(selectedIndex, this.suggestions.length) : 0;
|
||||
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
|
||||
const selectedSuggestion = this.suggestions[normalizedIndex];
|
||||
|
||||
|
|
|
|||
110
src/tests/ParameterizedAPI.test.ts
Normal file
110
src/tests/ParameterizedAPI.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import fetchMock, {enableFetchMocks} from 'jest-fetch-mock';
|
||||
import {MediaDbPluginSettings} from 'src/settings/Settings';
|
||||
import {LocGovAPI} from '../api/apis/LocGovAPI';
|
||||
import {MALAPI} from '../api/apis/MALAPI';
|
||||
import {MusicBrainzAPI} from '../api/apis/MusicBrainzAPI';
|
||||
import {OMDbAPI} from '../api/apis/OMDbAPI';
|
||||
import {SteamAPI} from '../api/apis/SteamAPI';
|
||||
import {WikipediaAPI} from '../api/apis/WikipediaAPI';
|
||||
import MediaDbPlugin from '../main';
|
||||
import {setMALResponseMock, setMusicBrainzResponseMock, setOMDbResponseMock, setSteamResponseMock, setWikipediaResponseMock} from './mockHelpers';
|
||||
import MALMockMovie from './ResponseMocks/MALMockMovie.json';
|
||||
import MusicBrainzResponseMock from './ResponseMocks/MusicBrainzMockResponse.json';
|
||||
import OMDBMockMovie from './ResponseMocks/OMDBMockResponse.json';
|
||||
import SteamAPIResponseMock from './ResponseMocks/SteamAPIMockResponse.json';
|
||||
import WikipediaMockResponse from './ResponseMocks/WikipediaMockResponse.json';
|
||||
|
||||
enableFetchMocks();
|
||||
export let apiMock: OMDbAPI | MALAPI | LocGovAPI | MusicBrainzAPI | SteamAPI | WikipediaAPI;
|
||||
|
||||
describe.each(
|
||||
[
|
||||
{name: OMDbAPI},
|
||||
{name: MALAPI},
|
||||
{name: LocGovAPI},
|
||||
{name: MusicBrainzAPI},
|
||||
{name: SteamAPI},
|
||||
{name: WikipediaAPI},
|
||||
],
|
||||
)('$name.name', ({name: parameterizedApi}) => {
|
||||
beforeAll(() => {
|
||||
let settingsMock: MediaDbPluginSettings = {} as MediaDbPluginSettings;
|
||||
let pluginMock = {} as MediaDbPlugin;
|
||||
pluginMock.settings = settingsMock;
|
||||
// TODO: add fake API key?
|
||||
apiMock = new parameterizedApi(pluginMock);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
|
||||
test('searchByTitle behavior when API returns garbage data', async () => {
|
||||
const garbageResponse = JSON.stringify({
|
||||
data: 'string',
|
||||
});
|
||||
fetchMock.mockResponseOnce(garbageResponse);
|
||||
await expect(apiMock.searchByTitle('sample')).resolves.toEqual([]);
|
||||
// }
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('searchByTitle behavior when requestUrl/fetch returns 401', async () => {
|
||||
let sampleResponse = {
|
||||
data: 'string',
|
||||
};
|
||||
fetchMock.mockResponse(JSON.stringify(sampleResponse), {status: 401});
|
||||
// TODO: Check API name and fix message
|
||||
// TODO: Externalize string
|
||||
await expect(apiMock.searchByTitle('sample')).rejects.toThrow(`MDB | Received status code ${401} from an API.`);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('searchByTitle behavior when requestUrl/fetch returns 403', async () => {
|
||||
let sampleResponse = {
|
||||
data: 'string',
|
||||
};
|
||||
fetchMock.mockResponse(JSON.stringify(sampleResponse), {status: 403});
|
||||
// TODO: Check API name and fix message
|
||||
// TODO: Externalize string/import?
|
||||
await expect(apiMock.searchByTitle('sample')).rejects.toThrow(`MDB | Received status code ${403} from an API.`);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('searchByTitle behavior when requestUrl/fetch returns 200', async () => {
|
||||
let sampleResponse;
|
||||
let ret;
|
||||
switch (parameterizedApi) {
|
||||
case OMDbAPI:
|
||||
ret = setOMDbResponseMock();
|
||||
sampleResponse = OMDBMockMovie;
|
||||
break;
|
||||
case WikipediaAPI:
|
||||
ret = setWikipediaResponseMock();
|
||||
sampleResponse = WikipediaMockResponse;
|
||||
break;
|
||||
case MALAPI:
|
||||
// TODO: MAL needs more tests for different types of content
|
||||
ret = setMALResponseMock();
|
||||
sampleResponse = MALMockMovie;
|
||||
case LocGovAPI:
|
||||
// TODO: Add soon
|
||||
break;
|
||||
case SteamAPI:
|
||||
sampleResponse = SteamAPIResponseMock;
|
||||
ret = setSteamResponseMock();
|
||||
break;
|
||||
case MusicBrainzAPI:
|
||||
sampleResponse = MusicBrainzResponseMock;
|
||||
ret = setMusicBrainzResponseMock();
|
||||
break;
|
||||
default:
|
||||
throw Error();
|
||||
}
|
||||
fetchMock.mockResponse(JSON.stringify(sampleResponse), {status: 200});
|
||||
// TODO: Check API name and fix message
|
||||
// TODO: Externalize string
|
||||
await expect(apiMock.searchByTitle('Hooking Season Playtest')).resolves.toEqual(ret);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
30
src/tests/ResponseMocks/MALMockMovie.json
Normal file
30
src/tests/ResponseMocks/MALMockMovie.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"data": [
|
||||
{
|
||||
"mal_id": 2890,
|
||||
"url": "https://myanimelist.net/anime/2890/Gake_no_Ue_no_Ponyo",
|
||||
"title": "Gake no Ue no Ponyo",
|
||||
"title_english": "Ponyo",
|
||||
"type": "Movie",
|
||||
"source": "Original",
|
||||
"episodes": 1,
|
||||
"aired": {
|
||||
"from": "2008-07-19T00:00:00+00:00",
|
||||
"to": null,
|
||||
"prop": {
|
||||
"from": {
|
||||
"day": 19,
|
||||
"month": 7,
|
||||
"year": 2008
|
||||
},
|
||||
"to": {
|
||||
"day": null,
|
||||
"month": null,
|
||||
"year": null
|
||||
}
|
||||
},
|
||||
"string": "Jul 19, 2008"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
src/tests/ResponseMocks/MusicBrainzMockResponse.json
Normal file
25
src/tests/ResponseMocks/MusicBrainzMockResponse.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"release-groups": [
|
||||
{
|
||||
"id": "9cf08bf9-1948-4087-abe1-783210ea1fae",
|
||||
"primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc",
|
||||
"title": "Halo Halo",
|
||||
"first-release-date": "2013-07-08",
|
||||
"primary-type": "Album",
|
||||
"artist-credit": [
|
||||
{
|
||||
"name": "Halo Halo",
|
||||
"artist": {
|
||||
"name": "Halo Halo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"releases": [
|
||||
{
|
||||
"id": "58dd1d57-2201-472e-9e36-5d497dcedb6f",
|
||||
"title": "Halo Halo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
src/tests/ResponseMocks/OMDBMockResponse.json
Normal file
13
src/tests/ResponseMocks/OMDBMockResponse.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"Search": [
|
||||
{
|
||||
"Title": "Guardians of the Galaxy",
|
||||
"Year": "2014",
|
||||
"imdbID": "tt2015381",
|
||||
"Type": "movie",
|
||||
"Poster": "https://m.media-amazon.com/images/M/MV5BMTAwMjU5OTgxNjZeQTJeQWpwZ15BbWU4MDUxNDYxODEx._V1_SX300.jpg"
|
||||
}
|
||||
],
|
||||
"totalResults": "1",
|
||||
"Response": "True"
|
||||
}
|
||||
14
src/tests/ResponseMocks/SteamAPIMockResponse.json
Normal file
14
src/tests/ResponseMocks/SteamAPIMockResponse.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"applist": {
|
||||
"apps": [
|
||||
{
|
||||
"appid": 2076590,
|
||||
"name": "Hooking Season Playtest"
|
||||
},
|
||||
{
|
||||
"appid": 2076600,
|
||||
"name": "MonsterTamer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
20
src/tests/ResponseMocks/WikipediaMockResponse.json
Normal file
20
src/tests/ResponseMocks/WikipediaMockResponse.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"query": {
|
||||
"searchinfo": {
|
||||
"totalhits": 1199001,
|
||||
"suggestion": "book",
|
||||
"suggestionsnippet": "book"
|
||||
},
|
||||
"search": [
|
||||
{
|
||||
"ns": 0,
|
||||
"title": "Book",
|
||||
"pageid": 3778,
|
||||
"size": 68829,
|
||||
"wordcount": 8821,
|
||||
"snippet": "called <span class=\"searchmatch\">books</span> or chapters or parts, are parts. The intellectual content in a physical book need not be a composition, nor even be called a book. <span class=\"searchmatch\">Books</span> can",
|
||||
"timestamp": "2022-08-19T19:13:56Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
85
src/tests/mockHelpers.ts
Normal file
85
src/tests/mockHelpers.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import {GameModel} from '../models/GameModel';
|
||||
import {MovieModel} from '../models/MovieModel';
|
||||
import {MusicReleaseModel} from '../models/MusicReleaseModel';
|
||||
import {WikiModel} from '../models/WikiModel';
|
||||
import {MediaType} from '../utils/MediaType';
|
||||
import {apiMock} from './ParameterizedAPI.test';
|
||||
import MALMockMovie from './ResponseMocks/MALMockMovie.json';
|
||||
import MusicBrainzResponseMock from './ResponseMocks/MusicBrainzMockResponse.json';
|
||||
import OMDBMockMovie from './ResponseMocks/OMDBMockResponse.json';
|
||||
import SteamAPIResponseMock from './ResponseMocks/SteamAPIMockResponse.json';
|
||||
import WikipediaMockResponse from './ResponseMocks/WikipediaMockResponse.json';
|
||||
|
||||
export function setWikipediaResponseMock() {
|
||||
let ret = [];
|
||||
let wikiresponse = WikipediaMockResponse.query.search[0];
|
||||
ret.push(new WikiModel({
|
||||
type: 'wiki',
|
||||
title: wikiresponse.title,
|
||||
englishTitle: wikiresponse.title,
|
||||
year: '',
|
||||
dataSource: apiMock.apiName,
|
||||
id: wikiresponse.pageid,
|
||||
}));
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function setOMDbResponseMock() {
|
||||
let ret = [];
|
||||
let omdbresponse = OMDBMockMovie.Search[0];
|
||||
ret.push(new MovieModel({
|
||||
type: 'wiki',
|
||||
title: omdbresponse.Title,
|
||||
englishTitle: omdbresponse.Title,
|
||||
year: omdbresponse.Year,
|
||||
dataSource: apiMock.apiName,
|
||||
id: omdbresponse.imdbID,
|
||||
}));
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function setMALResponseMock() {
|
||||
let ret = [];
|
||||
let result = MALMockMovie.data[0];
|
||||
ret.push(new MovieModel({
|
||||
type: result.type,
|
||||
title: result.title,
|
||||
englishTitle: result.title_english,
|
||||
year: result.aired.prop.from.year,
|
||||
dataSource: apiMock.apiName,
|
||||
id: result.mal_id,
|
||||
}));
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function setSteamResponseMock() {
|
||||
let ret = [];
|
||||
let steamResponse = SteamAPIResponseMock.applist.apps[0];
|
||||
ret.push(new GameModel({
|
||||
type: MediaType.Game,
|
||||
title: steamResponse.name,
|
||||
englishTitle: steamResponse.name,
|
||||
year: '',
|
||||
dataSource: apiMock.apiName,
|
||||
id: steamResponse.appid,
|
||||
}));
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function setMusicBrainzResponseMock() {
|
||||
let ret = [];
|
||||
let result = MusicBrainzResponseMock['release-groups'][0];
|
||||
ret.push(new MusicReleaseModel({
|
||||
type: 'musicRelease',
|
||||
title: result.title,
|
||||
englishTitle: result.title,
|
||||
year: (new Date(result['first-release-date'])).getFullYear().toString(),
|
||||
dataSource: apiMock.apiName,
|
||||
url: '',
|
||||
id: result.id,
|
||||
|
||||
artists: result['artist-credit'].map((a: any) => a.name),
|
||||
subType: result['primary-type'],
|
||||
} as MusicReleaseModel));
|
||||
return ret;
|
||||
}
|
||||
29
src/tests/utils.test.ts
Normal file
29
src/tests/utils.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import {containsOnlyLettersAndUnderscores, replaceIllegalFileNameCharactersInString, wrapAround} from '../utils/Utils';
|
||||
|
||||
test('If wrapAround wraps correctly', () => {
|
||||
expect(wrapAround(100, 5)).toBe(0);
|
||||
expect(wrapAround(100, 7)).toBe(2);
|
||||
});
|
||||
|
||||
test('If wrapAround errors out when dividing by zero', () => {
|
||||
expect(wrapAround(100, 0)).toThrow();
|
||||
});
|
||||
|
||||
test('If wrapAround errors out when size is negative', () => {
|
||||
expect(wrapAround(100, -5)).toThrow();
|
||||
});
|
||||
|
||||
test('Letter and underscore string validity', () => {
|
||||
expect(containsOnlyLettersAndUnderscores('asdkfj_')).toBe(true);
|
||||
expect(containsOnlyLettersAndUnderscores('asdkfj0')).toBe(false);
|
||||
});
|
||||
|
||||
// since this is used to check if a string is a valid name for an object property, unicode characters shouldn't be allowed, thus the name of the function is misleading
|
||||
test('Letter and underscore unicode char test', () => {
|
||||
expect(containsOnlyLettersAndUnderscores('asdkaÈj')).toBe(true);
|
||||
expect(containsOnlyLettersAndUnderscores('asdkaÈj0')).toBe(false);
|
||||
});
|
||||
|
||||
test('Valid filename test', () => {
|
||||
expect(replaceIllegalFileNameCharactersInString('what?is\\this:')).toBe('whatisthis -');
|
||||
});
|
||||
|
|
@ -10,12 +10,13 @@ import {WikiModel} from '../models/WikiModel';
|
|||
import {MusicReleaseModel} from '../models/MusicReleaseModel';
|
||||
import {BoardGameModel} from '../models/BoardGameModel';
|
||||
|
||||
export const MEDIA_TYPES: MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Game, MediaType.Wiki, MediaType.MusicRelease, MediaType.BoardGame];
|
||||
|
||||
export class MediaTypeManager {
|
||||
mediaFileNameTemplateMap: Map<MediaType, string>;
|
||||
mediaTemplateMap: Map<MediaType, string>;
|
||||
|
||||
constructor(settings: MediaDbPluginSettings) {
|
||||
this.updateTemplates(settings);
|
||||
constructor() {
|
||||
}
|
||||
|
||||
updateTemplates(settings: MediaDbPluginSettings) {
|
||||
|
|
|
|||
418
src/utils/ModalHelper.ts
Normal file
418
src/utils/ModalHelper.ts
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
import {MediaDbAdvancedSearchModal} from '../modals/MediaDbAdvancedSearchModal';
|
||||
import {MediaDbIdSearchModal} from '../modals/MediaDbIdSearchModal';
|
||||
import {MediaTypeModel} from '../models/MediaTypeModel';
|
||||
import {MediaDbSearchResultModal} from '../modals/MediaDbSearchResultModal';
|
||||
import {Notice} from 'obsidian';
|
||||
import MediaDbPlugin from '../main';
|
||||
import {MediaDbPreviewModal} from 'src/modals/MediaDbPreviewModal';
|
||||
import {CreateNoteOptions} from './Utils';
|
||||
|
||||
|
||||
export enum ModalResultCode {
|
||||
SUCCESS,
|
||||
SKIP,
|
||||
CLOSE,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns.
|
||||
* On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}.
|
||||
* On {@link ModalResultCode.ERROR} this contains a reference to that error.
|
||||
*/
|
||||
export interface AdvancedSearchModalResult {
|
||||
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR,
|
||||
data?: AdvancedSearchModalData,
|
||||
error?: Error,
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing the data {@link ModalHelper.createIdSearchModal} returns.
|
||||
* On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}.
|
||||
* On {@link ModalResultCode.ERROR} this contains a reference to that error.
|
||||
*/
|
||||
export interface IdSearchModalResult {
|
||||
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR,
|
||||
data?: IdSearchModalData,
|
||||
error?: Error,
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing the data {@link ModalHelper.createSelectModal} returns.
|
||||
* On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}.
|
||||
* On {@link ModalResultCode.ERROR} this contains a reference to that error.
|
||||
*/
|
||||
export interface SelectModalResult {
|
||||
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.SKIP | ModalResultCode.ERROR,
|
||||
data?: SelectModalData,
|
||||
error?: Error,
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing the data {@link ModalHelper.createPreviewModal} returns.
|
||||
* On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}.
|
||||
* On {@link ModalResultCode.ERROR} this contains a reference to that error.
|
||||
*/
|
||||
export interface PreviewModalResult {
|
||||
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR,
|
||||
data?: PreviewModalData,
|
||||
error?: Error,
|
||||
}
|
||||
|
||||
/**
|
||||
* The data the advanced search modal returns.
|
||||
* - query: the query string
|
||||
* - apis: the selected APIs
|
||||
*/
|
||||
export interface AdvancedSearchModalData {
|
||||
query: string,
|
||||
apis: string[],
|
||||
}
|
||||
|
||||
/**
|
||||
* The data the id search modal returns.
|
||||
* - query: the query string
|
||||
* - apis: the selected APIs
|
||||
*/
|
||||
export interface IdSearchModalData {
|
||||
query: string,
|
||||
api: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* The data the select modal returns.
|
||||
* - selected: the selected items
|
||||
*/
|
||||
export interface SelectModalData {
|
||||
selected: MediaTypeModel[],
|
||||
}
|
||||
|
||||
/**
|
||||
* The data the preview modal returns.
|
||||
* - confirmed: whether the selected element has been confirmed
|
||||
*/
|
||||
export interface PreviewModalData {
|
||||
confirmed: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the advanced search modal.
|
||||
* - modalTitle: the title of the modal
|
||||
* - preselectedAPIs: a list of preselected APIs
|
||||
* - prefilledSearchString: prefilled query
|
||||
*/
|
||||
export interface AdvancedSearchModalOptions {
|
||||
modalTitle?: string,
|
||||
preselectedAPIs?: string[],
|
||||
prefilledSearchString?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the id search modal.
|
||||
* - modalTitle: the title of the modal
|
||||
* - preselectedAPIs: a list of preselected APIs
|
||||
* - prefilledSearchString: prefilled query
|
||||
*/
|
||||
export interface IdSearchModalOptions {
|
||||
modalTitle?: string,
|
||||
preselectedAPI?: string,
|
||||
prefilledSearchString?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the select modal.
|
||||
* - modalTitle: the title of the modal
|
||||
* - elements: the elements the user can select from
|
||||
* - multiSelect: whether to allow multiselect
|
||||
* - skipButton: whether to add a skip button to the modal
|
||||
*/
|
||||
export interface SelectModalOptions {
|
||||
modalTitle?: string,
|
||||
elements?: MediaTypeModel[],
|
||||
multiSelect?: boolean,
|
||||
skipButton?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the preview modal.
|
||||
* - modalTitle: the title of the modal
|
||||
* - elements: the elements to preview
|
||||
*/
|
||||
export interface PreviewModalOptions {
|
||||
modalTitle?: string,
|
||||
elements?: MediaTypeModel[],
|
||||
createNoteOptions?: CreateNoteOptions,
|
||||
}
|
||||
|
||||
export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = {
|
||||
modalTitle: 'Media DB Advanced Search',
|
||||
preselectedAPIs: [],
|
||||
prefilledSearchString: '',
|
||||
};
|
||||
|
||||
export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = {
|
||||
modalTitle: 'Media DB Id Search',
|
||||
preselectedAPI: '',
|
||||
prefilledSearchString: '',
|
||||
};
|
||||
|
||||
export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = {
|
||||
modalTitle: 'Media DB Search Results',
|
||||
elements: [],
|
||||
multiSelect: true,
|
||||
skipButton: false,
|
||||
};
|
||||
|
||||
export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = {
|
||||
modalTitle: 'Media DB Preview',
|
||||
elements: [],
|
||||
createNoteOptions: {attachTemplate: true},
|
||||
};
|
||||
|
||||
/**
|
||||
* A class providing multiple usefull functions for dealing with the plugins modals.
|
||||
*/
|
||||
export class ModalHelper {
|
||||
plugin: MediaDbPlugin;
|
||||
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link MediaDbAdvancedSearchModal}, then sets callbacks and awaits them,
|
||||
* returning either the user input once submitted or nothing once closed.
|
||||
* The modal needs ot be manually closed by calling `close()` on the modal reference.
|
||||
*
|
||||
* @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS}
|
||||
* @returns the user input or nothing and a reference to the modal.
|
||||
*/
|
||||
async createAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise<{ advancedSearchModalResult: AdvancedSearchModalResult, advancedSearchModal: MediaDbAdvancedSearchModal }> {
|
||||
const modal = new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions);
|
||||
const res: AdvancedSearchModalResult = await new Promise((resolve, reject) => {
|
||||
modal.setSubmitCallback(res => resolve({code: ModalResultCode.SUCCESS, data: res}));
|
||||
modal.setCloseCallback(err => {
|
||||
if (err) {
|
||||
resolve({code: ModalResultCode.ERROR, error: err});
|
||||
}
|
||||
resolve({code: ModalResultCode.CLOSE});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
});
|
||||
return {advancedSearchModalResult: res, advancedSearchModal: modal};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an {@link MediaDbAdvancedSearchModal} and awaits its result,
|
||||
* then executes the `submitCallback` returning the callbacks result and closing the modal.
|
||||
*
|
||||
* @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS}
|
||||
* @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed
|
||||
* @returns the user input or nothing and a reference to the modal.
|
||||
*/
|
||||
async openAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions, submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise<MediaTypeModel[]>): Promise<MediaTypeModel[]> {
|
||||
const {advancedSearchModalResult, advancedSearchModal} = await this.createAdvancedSearchModal(advancedSearchModalOptions);
|
||||
|
||||
if (advancedSearchModalResult.code === ModalResultCode.ERROR) {
|
||||
// there was an error in the modal itself
|
||||
console.warn(advancedSearchModalResult.error);
|
||||
new Notice(advancedSearchModalResult.error.toString());
|
||||
advancedSearchModal.close();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (advancedSearchModalResult.code === ModalResultCode.CLOSE) {
|
||||
// modal is already being closed
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
let callbackRes: MediaTypeModel[];
|
||||
callbackRes = await submitCallback(advancedSearchModalResult.data);
|
||||
advancedSearchModal.close();
|
||||
return callbackRes;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
new Notice(e.toString());
|
||||
advancedSearchModal.close();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link MediaDbIdSearchModal}, then sets callbacks and awaits them,
|
||||
* returning either the user input once submitted or nothing once closed.
|
||||
* The modal needs ot be manually closed by calling `close()` on the modal reference.
|
||||
*
|
||||
* @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS}
|
||||
* @returns the user input or nothing and a reference to the modal.
|
||||
*/
|
||||
async createIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise<{ idSearchModalResult: IdSearchModalResult, idSearchModal: MediaDbIdSearchModal }> {
|
||||
const modal = new MediaDbIdSearchModal(this.plugin, idSearchModalOptions);
|
||||
const res: IdSearchModalResult = await new Promise((resolve, reject) => {
|
||||
modal.setSubmitCallback(res => resolve({code: ModalResultCode.SUCCESS, data: res}));
|
||||
modal.setCloseCallback(err => {
|
||||
if (err) {
|
||||
resolve({code: ModalResultCode.ERROR, error: err});
|
||||
}
|
||||
resolve({code: ModalResultCode.CLOSE});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
});
|
||||
return {idSearchModalResult: res, idSearchModal: modal};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an {@link MediaDbIdSearchModal} and awaits its result,
|
||||
* then executes the `submitCallback` returning the callbacks result and closing the modal.
|
||||
*
|
||||
* @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS}
|
||||
* @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed
|
||||
* @returns the user input or nothing and a reference to the modal.
|
||||
*/
|
||||
async openIdSearchModal(idSearchModalOptions: IdSearchModalOptions, submitCallback: (idSearchModalData: IdSearchModalData) => Promise<MediaTypeModel>): Promise<MediaTypeModel> {
|
||||
const {idSearchModalResult, idSearchModal} = await this.createIdSearchModal(idSearchModalOptions);
|
||||
|
||||
if (idSearchModalResult.code === ModalResultCode.ERROR) {
|
||||
// there was an error in the modal itself
|
||||
console.warn(idSearchModalResult.error);
|
||||
new Notice(idSearchModalResult.error.toString());
|
||||
idSearchModal.close();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (idSearchModalResult.code === ModalResultCode.CLOSE) {
|
||||
// modal is already being closed
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
let callbackRes: MediaTypeModel;
|
||||
callbackRes = await submitCallback(idSearchModalResult.data);
|
||||
idSearchModal.close();
|
||||
return callbackRes;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
new Notice(e.toString());
|
||||
idSearchModal.close();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link MediaDbSearchResultModal}, then sets callbacks and awaits them,
|
||||
* returning either the user input once submitted or nothing once closed.
|
||||
* The modal needs ot be manually closed by calling `close()` on the modal reference.
|
||||
*
|
||||
* @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT}
|
||||
* @returns the user input or nothing and a reference to the modal.
|
||||
*/
|
||||
async createSelectModal(selectModalOptions: SelectModalOptions): Promise<{ selectModalResult: SelectModalResult, selectModal: MediaDbSearchResultModal }> {
|
||||
const modal = new MediaDbSearchResultModal(this.plugin, selectModalOptions);
|
||||
const res: SelectModalResult = await new Promise((resolve, reject) => {
|
||||
modal.setSubmitCallback(res => resolve({code: ModalResultCode.SUCCESS, data: res}));
|
||||
modal.setSkipCallback(() => resolve({code: ModalResultCode.SKIP}));
|
||||
modal.setCloseCallback(err => {
|
||||
if (err) {
|
||||
resolve({code: ModalResultCode.ERROR, error: err});
|
||||
}
|
||||
resolve({code: ModalResultCode.CLOSE});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
});
|
||||
return {selectModalResult: res, selectModal: modal};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an {@link MediaDbSearchResultModal} and awaits its result,
|
||||
* then executes the `submitCallback` returning the callbacks result and closing the modal.
|
||||
*
|
||||
* @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT}
|
||||
* @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed
|
||||
* @returns the user input or nothing and a reference to the modal.
|
||||
*/
|
||||
async openSelectModal(selectModalOptions: SelectModalOptions, submitCallback: (selectModalData: SelectModalData) => Promise<MediaTypeModel[]>): Promise<MediaTypeModel[]> {
|
||||
const {selectModalResult, selectModal} = await this.createSelectModal(selectModalOptions);
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.ERROR) {
|
||||
// there was an error in the modal itself
|
||||
console.warn(selectModalResult.error);
|
||||
new Notice(selectModalResult.error.toString());
|
||||
selectModal.close();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.CLOSE) {
|
||||
// modal is already being closed
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.SKIP) {
|
||||
// selection was skipped
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
let callbackRes: MediaTypeModel[];
|
||||
callbackRes = await submitCallback(selectModalResult.data);
|
||||
selectModal.close();
|
||||
return callbackRes;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
new Notice(e.toString());
|
||||
selectModal.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async createPreviewModal(previewModalOptions: PreviewModalOptions): Promise<{ previewModalResult: PreviewModalResult, previewModal: MediaDbPreviewModal }> {
|
||||
//todo: handle attachFile for existing files
|
||||
const modal = new MediaDbPreviewModal(this.plugin, previewModalOptions);
|
||||
const res: PreviewModalResult = await new Promise((resolve, reject) => {
|
||||
modal.setSubmitCallback(res => resolve({code: ModalResultCode.SUCCESS, data: res}));
|
||||
modal.setCloseCallback(err => {
|
||||
if (err) {
|
||||
resolve({code: ModalResultCode.ERROR, error: err});
|
||||
}
|
||||
resolve({code: ModalResultCode.CLOSE});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
});
|
||||
return {previewModalResult: res, previewModal: modal};
|
||||
}
|
||||
|
||||
async openPreviewModal(previewModalOptions: PreviewModalOptions, submitCallback: (previewModalData: PreviewModalData) => Promise<boolean>): Promise<boolean> {
|
||||
const {previewModalResult, previewModal} = await this.createPreviewModal(previewModalOptions);
|
||||
|
||||
if (previewModalResult.code === ModalResultCode.ERROR) {
|
||||
// there was an error in the modal itself
|
||||
console.warn(previewModalResult.error);
|
||||
new Notice(previewModalResult.error.toString());
|
||||
previewModal.close();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (previewModalResult.code === ModalResultCode.CLOSE) {
|
||||
// modal is already being closed
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
let callbackRes: boolean;
|
||||
callbackRes = await submitCallback(previewModalResult.data);
|
||||
previewModal.close();
|
||||
return callbackRes;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
new Notice(e.toString());
|
||||
previewModal.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
71
styles.css
71
styles.css
|
|
@ -23,13 +23,13 @@ small.media-db-plugin-list-text{
|
|||
}
|
||||
|
||||
.media-db-plugin-select-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.media-db-plugin-select-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 5px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +52,71 @@ small.media-db-plugin-list-text{
|
|||
background: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.media-db-plugin-preview-modal {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.media-db-plugin-preview-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.media-db-plugin-spacer {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* region property mappings */
|
||||
.media-db-plugin-property-mappings-model-container {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mappings-container {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-element {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-element-property-name-wrapper {
|
||||
min-width: 160px;
|
||||
background: var(--background-modifier-form-field);
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-element-property-name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mappings-save-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-to {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-validation {
|
||||
color: var(--text-error);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.media-db-plugin-button:focus {
|
||||
/*outline: 1px solid white;*/
|
||||
}
|
||||
|
||||
/* endregion */
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"svelte",
|
||||
"node",
|
||||
"jest"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue