big changes; we have a good TS config now

This commit is contained in:
Moritz Jung 2025-01-14 22:30:28 +01:00
parent cbbaf54b33
commit 4497991344
54 changed files with 750 additions and 671 deletions

View file

@ -1,2 +0,0 @@
npm node_modules
build

View file

@ -1,20 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": ["@typescript-eslint", "only-warn"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-function-return-type": ["warn"]
}
}

View file

@ -112,7 +112,7 @@ Now you select the result you want and the plugin will cast it's magic and creat
### Currently supported APIs:
| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes |
| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movie, series and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No |
| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |

View file

@ -1,7 +1,7 @@
import builtins from 'builtin-modules';
import esbuild from 'esbuild';
import esbuildSvelte from 'esbuild-svelte';
import sveltePreprocess from 'svelte-preprocess';
import { sveltePreprocess } from 'svelte-preprocess';
import { getBuildBanner } from 'build/buildBanner';
const banner = getBuildBanner('Release Build', version => version);
@ -41,7 +41,7 @@ const build = await esbuild.build({
},
plugins: [
esbuildSvelte({
compilerOptions: { css: 'injected', dev: false, sveltePath: 'svelte' },
compilerOptions: { css: 'injected', dev: false },
preprocess: sveltePreprocess(),
filterWarnings: warning => {
// we don't want warnings from node modules that we can do nothing about

View file

@ -1,7 +1,7 @@
import esbuild from 'esbuild';
import copy from 'esbuild-plugin-copy-watch';
import esbuildSvelte from 'esbuild-svelte';
import sveltePreprocess from 'svelte-preprocess';
import { sveltePreprocess } from 'svelte-preprocess';
import manifest from '../../manifest.json' assert { type: 'json' };
import { getBuildBanner } from 'build/buildBanner';
@ -52,7 +52,7 @@ const context = await esbuild.context({
],
}),
esbuildSvelte({
compilerOptions: { css: 'injected', dev: true, sveltePath: 'svelte' },
compilerOptions: { css: 'injected', dev: true },
preprocess: sveltePreprocess(),
filterWarnings: warning => {
// we don't want warnings from node modules that we can do nothing about

BIN
bun.lockb

Binary file not shown.

54
eslint.config.mjs Normal file
View file

@ -0,0 +1,54 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import only_warn from 'eslint-plugin-only-warn';
import * as plugin_import from 'eslint-plugin-import';
export default tseslint.config(
{
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.svelte'],
},
{
files: ['src/**/*.ts'],
extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: true,
},
},
plugins: {
// @ts-ignore
'only-warn': only_warn,
import: plugin_import,
},
rules: {
'@typescript-eslint/no-explicit-any': ['warn'],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'separate-type-imports' }],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/order': [
'error',
{
'newlines-between': 'never',
alphabetize: { order: 'asc', orderImportKind: 'asc', caseInsensitive: true },
},
],
'@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/explicit-function-return-type': ['warn'],
'@typescript-eslint/require-await': 'off',
},
},
);

View file

@ -14,8 +14,8 @@
"lint": "eslint --max-warnings=0 src/**",
"lint:fix": "eslint --max-warnings=0 --fix src/**",
"svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"",
"check": "bun run format:check && bun run tsc && bun run lint && bun run test",
"check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run test",
"check": "bun run format:check && bun run tsc && bun run test",
"check:fix": "bun run format && bun run tsc && bun run test",
"release": "bun run automation/release.ts",
"stats": "bun run automation/stats.ts"
},
@ -25,27 +25,24 @@
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@lemons_dev/parsinom": "^0.0.12",
"@happy-dom/global-registrator": "^14.3.6",
"@tsconfig/svelte": "^5.0.3",
"@types/bun": "^1.0.10",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"builtin-modules": "^3.3.0",
"esbuild": "^0.20.2",
"esbuild-plugin-copy-watch": "^2.1.0",
"esbuild-svelte": "^0.8.0",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-isaacscript": "^3.12.2",
"@happy-dom/global-registrator": "^14.12.3",
"@types/bun": "^1.1.16",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.2",
"esbuild-plugin-copy-watch": "^2.3.1",
"esbuild-svelte": "^0.8.2",
"eslint": "^9.18.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-only-warn": "^1.1.0",
"obsidian": "latest",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"string-argv": "^0.3.2",
"svelte": "^4.2.12",
"svelte-check": "^3.6.8",
"svelte-preprocess": "^5.1.3",
"tslib": "^2.6.2",
"typescript": "^5.4.3"
"svelte": "^5.17.5",
"svelte-check": "^4.1.4",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
}
}

View file

@ -1,5 +1,6 @@
import { APIModel } from './APIModel';
import { MediaTypeModel } from '../models/MediaTypeModel';
import { Notice } from 'obsidian';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { APIModel } from './APIModel';
export class APIManager {
apis: APIModel[];
@ -23,7 +24,10 @@ export class APIManager {
try {
return await api.searchByTitle(query);
} catch (e) {
new Notice(`Error querying ${api.apiName}: ${e}`);
console.warn(e);
return [];
}
});
@ -35,7 +39,7 @@ export class APIManager {
*
* @param item
*/
async queryDetailedInfo(item: MediaTypeModel): Promise<MediaTypeModel> {
async queryDetailedInfo(item: MediaTypeModel): Promise<MediaTypeModel | undefined> {
return await this.queryDetailedInfoById(item.id, item.dataSource);
}
@ -45,22 +49,31 @@ export class APIManager {
* @param id
* @param apiName
*/
async queryDetailedInfoById(id: string, apiName: string): Promise<MediaTypeModel> {
async queryDetailedInfoById(id: string, apiName: string): Promise<MediaTypeModel | undefined> {
for (const api of this.apis) {
if (api.apiName === apiName) {
try {
return api.getById(id);
} catch (e) {
new Notice(`Error querying ${api.apiName}: ${e}`);
console.warn(e);
return undefined;
}
}
}
getApiByName(name: string): APIModel {
return undefined;
}
getApiByName(name: string): APIModel | undefined {
for (const api of this.apis) {
if (api.apiName === name) {
return api;
}
}
return null;
return undefined;
}
registerAPI(api: APIModel): void {

View file

@ -1,13 +1,13 @@
import { MediaTypeModel } from '../models/MediaTypeModel';
import { MediaType } from '../utils/MediaType';
import MediaDbPlugin from '../main';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { MediaType } from '../utils/MediaType';
export abstract class APIModel {
apiName: string;
apiUrl: string;
apiDescription: string;
types: MediaType[];
plugin: MediaDbPlugin;
apiName!: string;
apiUrl!: string;
apiDescription!: string;
types!: MediaType[];
plugin!: MediaDbPlugin;
/**
* This function should query the api and return a list of matches. The matches should be caped at 20.

View file

@ -1,9 +1,9 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { BoardGameModel } from 'src/models/BoardGameModel';
import { requestUrl } from 'obsidian';
import { BoardGameModel } from 'src/models/BoardGameModel';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class BoardGameGeekAPI extends APIModel {
plugin: MediaDbPlugin;
@ -38,8 +38,8 @@ export class BoardGameGeekAPI extends APIModel {
const ret: MediaTypeModel[] = [];
for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) {
const id = boardgame.attributes.getNamedItem('objectid')!.value;
const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')!.textContent!;
const id = boardgame.attributes.getNamedItem('objectid')?.value;
const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined;
const year = boardgame.querySelector('yearpublished')?.textContent ?? '';
ret.push(
@ -49,7 +49,7 @@ export class BoardGameGeekAPI extends APIModel {
title,
englishTitle: title,
year,
} as BoardGameModel),
}),
);
}
@ -72,21 +72,29 @@ export class BoardGameGeekAPI extends APIModel {
const response = new window.DOMParser().parseFromString(data, 'text/xml');
// console.debug(response);
const boardgame = response.querySelector('boardgame')!;
const title = boardgame.querySelector('name[primary=true]')!.textContent!;
const boardgame = response.querySelector('boardgame');
if (!boardgame) {
throw Error(`MDB | Received invalid data from ${this.apiName}.`);
}
const title = boardgame.querySelector('name[primary=true]')?.textContent;
const year = boardgame.querySelector('yearpublished')?.textContent ?? '';
const image = boardgame.querySelector('image')?.textContent ?? undefined;
const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0');
const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')).map(n => n!.textContent!);
const genres = Array.from(boardgame.querySelectorAll('boardgamecategory'))
.map(n => n.textContent)
.filter(n => n !== null);
const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0');
const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0');
const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0');
const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes';
const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')).map(n => n!.textContent!);
const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher'))
.map(n => n.textContent)
.filter(n => n !== null);
return new BoardGameModel({
title: title,
englishTitle: title,
title: title ?? undefined,
englishTitle: title ?? undefined,
year: year === '0' ? '' : year,
dataSource: this.apiName,
url: `https://boardgamegeek.com/boardgame/${id}`,
@ -107,6 +115,6 @@ export class BoardGameGeekAPI extends APIModel {
played: false,
personalRating: 0,
},
} as BoardGameModel);
});
}
}

View file

@ -1,9 +1,9 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import { requestUrl } from 'obsidian';
import type MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class GiantBombAPI extends APIModel {
plugin: MediaDbPlugin;
@ -54,7 +54,7 @@ export class GiantBombAPI extends APIModel {
year: new Date(result.original_release_date).getFullYear().toString(),
dataSource: this.apiName,
id: result.guid,
} as GameModel),
}),
);
}
@ -104,6 +104,6 @@ export class GiantBombAPI extends APIModel {
personalRating: 0,
},
} as GameModel);
});
}
}

View file

@ -1,9 +1,9 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MovieModel } from '../../models/MovieModel';
import MediaDbPlugin from '../../main';
import { SeriesModel } from '../../models/SeriesModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class MALAPI extends APIModel {
plugin: MediaDbPlugin;
@ -52,7 +52,7 @@ export class MALAPI extends APIModel {
year: result.year ?? result.aired?.prop?.from?.year ?? '',
dataSource: this.apiName,
id: result.mal_id,
} as MovieModel),
}),
);
}
if (type === 'movie' || type === 'special') {
@ -64,7 +64,7 @@ export class MALAPI extends APIModel {
year: result.year ?? result.aired?.prop?.from?.year ?? '',
dataSource: this.apiName,
id: result.mal_id,
} as MovieModel),
}),
);
} else if (type === 'series' || type === 'ova') {
ret.push(
@ -75,7 +75,7 @@ export class MALAPI extends APIModel {
year: result.year ?? result.aired?.prop?.from?.year ?? '',
dataSource: this.apiName,
id: result.mal_id,
} as SeriesModel),
}),
);
}
}
@ -127,7 +127,7 @@ export class MALAPI extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as MovieModel);
});
}
if (type === 'movie' || type === 'special') {
@ -159,7 +159,7 @@ export class MALAPI extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as MovieModel);
});
} else if (type === 'series' || type === 'ova') {
return new SeriesModel({
subType: type,
@ -190,9 +190,9 @@ export class MALAPI extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as SeriesModel);
});
}
return;
throw new Error(`MDB | Unknown media type for id ${id}`);
}
}

View file

@ -1,8 +1,8 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import type MediaDbPlugin from '../../main';
import { MangaModel } from '../../models/MangaModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class MALAPIManga extends APIModel {
plugin: MediaDbPlugin;
@ -73,7 +73,7 @@ export class MALAPIManga extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as MangaModel),
}),
);
}
@ -123,6 +123,6 @@ export class MALAPIManga extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as MangaModel);
});
}
}

View file

@ -1,10 +1,10 @@
import { APIModel } from '../APIModel';
import { Notice } from 'obsidian';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import { requestUrl } from 'obsidian';
import type MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class MobyGamesAPI extends APIModel {
plugin: MediaDbPlugin;
@ -23,9 +23,7 @@ export class MobyGamesAPI extends APIModel {
console.log(`MDB | api "${this.apiName}" queried by Title`);
if (!this.plugin.settings.MobyGamesKey) {
console.error(new Error(`MDB | API key for ${this.apiName} missing.`));
new Notice(`MediaDB | API key for ${this.apiName} missing.`);
return [];
throw new Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`;
@ -68,7 +66,6 @@ export class MobyGamesAPI extends APIModel {
console.log(`MDB | api "${this.apiName}" queried by ID`);
if (!this.plugin.settings.MobyGamesKey) {
new Notice(`MediaDB | API key for ${this.apiName} missing.`);
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
@ -79,7 +76,6 @@ export class MobyGamesAPI extends APIModel {
console.debug(fetchData);
if (fetchData.status !== 200) {
new Notice(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
@ -109,6 +105,6 @@ export class MobyGamesAPI extends APIModel {
personalRating: 0,
},
} as GameModel);
});
}
}

View file

@ -1,10 +1,10 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { requestUrl } from 'obsidian';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MusicReleaseModel } from '../../models/MusicReleaseModel';
import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils';
import { MediaType } from '../../utils/MediaType';
import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils';
import { APIModel } from '../APIModel';
export class MusicBrainzAPI extends APIModel {
plugin: MediaDbPlugin;
@ -55,7 +55,7 @@ export class MusicBrainzAPI extends APIModel {
artists: result['artist-credit'].map((a: any) => a.name),
subType: result['primary-type'],
} as MusicReleaseModel),
}),
);
}
@ -97,6 +97,6 @@ export class MusicBrainzAPI extends APIModel {
userData: {
personalRating: 0,
},
} as MusicReleaseModel);
});
}
}

View file

@ -1,11 +1,11 @@
import { APIModel } from '../APIModel';
import { Notice } from 'obsidian';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import { MovieModel } from '../../models/MovieModel';
import MediaDbPlugin from '../../main';
import { SeriesModel } from '../../models/SeriesModel';
import type MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MovieModel } from '../../models/MovieModel';
import { SeriesModel } from '../../models/SeriesModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class OMDbAPI extends APIModel {
plugin: MediaDbPlugin;
@ -30,9 +30,7 @@ export class OMDbAPI extends APIModel {
console.log(`MDB | api "${this.apiName}" queried by Title`);
if (!this.plugin.settings.OMDbKey) {
console.error(new Error(`MDB | API key for ${this.apiName} missing.`));
new Notice(`MediaDB | API key for ${this.apiName} missing.`);
return [];
throw new Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`;
@ -76,7 +74,7 @@ export class OMDbAPI extends APIModel {
year: result.Year,
dataSource: this.apiName,
id: result.imdbID,
} as MovieModel),
}),
);
} else if (type === 'series') {
ret.push(
@ -87,7 +85,7 @@ export class OMDbAPI extends APIModel {
year: result.Year,
dataSource: this.apiName,
id: result.imdbID,
} as SeriesModel),
}),
);
} else if (type === 'game') {
ret.push(
@ -98,7 +96,7 @@ export class OMDbAPI extends APIModel {
year: result.Year,
dataSource: this.apiName,
id: result.imdbID,
} as GameModel),
}),
);
}
}
@ -110,7 +108,6 @@ export class OMDbAPI extends APIModel {
console.log(`MDB | api "${this.apiName}" queried by ID`);
if (!this.plugin.settings.OMDbKey) {
new Notice(`MediaDB | API key for ${this.apiName} missing.`);
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
@ -118,11 +115,9 @@ export class OMDbAPI extends APIModel {
const fetchData = await fetch(searchUrl);
if (fetchData.status === 401) {
new Notice(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (fetchData.status !== 200) {
new Notice(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
@ -130,7 +125,6 @@ export class OMDbAPI extends APIModel {
// console.debug(result);
if (result.Response === 'False') {
new Notice(`MDB | Received error from ${this.apiName}: ${result.Error}`);
throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`);
}
@ -168,7 +162,7 @@ export class OMDbAPI extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as MovieModel);
});
} else if (type === 'series') {
return new SeriesModel({
type: type,
@ -200,7 +194,7 @@ export class OMDbAPI extends APIModel {
lastWatched: '',
personalRating: 0,
},
} as SeriesModel);
});
} else if (type === 'game') {
return new GameModel({
type: type,
@ -224,9 +218,9 @@ export class OMDbAPI extends APIModel {
played: false,
personalRating: 0,
},
} as GameModel);
});
}
return;
throw new Error(`MDB | Unknown media type for id ${id}`);
}
}

View file

@ -1,8 +1,8 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { BookModel } from 'src/models/BookModel';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class OpenLibraryAPI extends APIModel {
plugin: MediaDbPlugin;
@ -42,7 +42,7 @@ export class OpenLibraryAPI extends APIModel {
dataSource: this.apiName,
id: result.key,
author: result.author_name ?? 'unknown',
} as BookModel),
}),
);
}
@ -87,6 +87,6 @@ export class OpenLibraryAPI extends APIModel {
lastRead: '',
personalRating: 0,
},
} as BookModel);
});
}
}

View file

@ -1,9 +1,9 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import { requestUrl } from 'obsidian';
import type MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class SteamAPI extends APIModel {
plugin: MediaDbPlugin;
@ -49,7 +49,7 @@ export class SteamAPI extends APIModel {
year: '',
dataSource: this.apiName,
id: result.appid,
} as GameModel),
}),
);
}
@ -94,8 +94,8 @@ export class SteamAPI extends APIModel {
url: `https://store.steampowered.com/app/${result.steam_appid}`,
id: result.steam_appid,
developers: result['developers'],
publishers: result['publishers'],
developers: result.developers,
publishers: result.publishers,
genres: result.genres?.map((x: any) => x.description) ?? [],
onlineRating: Number.parseFloat(result.metacritic?.score ?? 0),
image: result.header_image ?? '',
@ -107,6 +107,6 @@ export class SteamAPI extends APIModel {
played: false,
personalRating: 0,
},
} as GameModel);
});
}
}

View file

@ -1,8 +1,8 @@
import { APIModel } from '../APIModel';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { WikiModel } from '../../models/WikiModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class WikipediaAPI extends APIModel {
plugin: MediaDbPlugin;
@ -42,7 +42,7 @@ export class WikipediaAPI extends APIModel {
year: '',
dataSource: this.apiName,
id: result.pageid,
} as WikiModel),
}),
);
}
@ -73,10 +73,10 @@ export class WikipediaAPI extends APIModel {
id: result.pageid,
wikiUrl: result.fullurl,
lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat),
lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat) ?? undefined,
length: result.length,
userData: {},
} as WikiModel);
});
}
}

View file

@ -1,33 +1,28 @@
import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder } from 'obsidian';
import { getDefaultSettings, MediaDbPluginSettings, MediaDbSettingTab } from './settings/Settings';
import type { MediaType } from 'src/utils/MediaType';
import { APIManager } from './api/APIManager';
import { MediaTypeModel } from './models/MediaTypeModel';
import {
CreateNoteOptions,
dateTimeToString,
markdownTable,
replaceIllegalFileNameCharactersInString,
unCamelCase,
hasTemplaterPlugin,
useTemplaterPluginInFile,
} from './utils/Utils';
import { OMDbAPI } from './api/apis/OMDbAPI';
import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI';
import { GiantBombAPI } from './api/apis/GiantBombAPI';
import { MALAPI } from './api/apis/MALAPI';
import { MALAPIManga } from './api/apis/MALAPIManga';
import { WikipediaAPI } from './api/apis/WikipediaAPI';
import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI';
import { OMDbAPI } from './api/apis/OMDbAPI';
import type { MediaTypeModel } from './models/MediaTypeModel';
import type { MediaDbPluginSettings } from './settings/Settings';
import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings';
import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager';
import type { SearchModalOptions } from './utils/ModalHelper';
import type { CreateNoteOptions } from './utils/Utils';
import { dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils';
import { WikipediaAPI } from './api/apis/WikipediaAPI';
import { SteamAPI } from './api/apis/SteamAPI';
import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI';
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
import { MobyGamesAPI } from './api/apis/MobyGamesAPI';
import { GiantBombAPI } from './api/apis/GiantBombAPI';
import { PropertyMapper } from './settings/PropertyMapper';
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping';
import { ModalHelper, ModalResultCode, SearchModalOptions } from './utils/ModalHelper';
import { ModalHelper, ModalResultCode } from './utils/ModalHelper';
import { DateFormatter } from './utils/DateFormatter';
import { MediaType } from 'src/utils/MediaType';
export type Metadata = Record<string, unknown>;
@ -38,12 +33,12 @@ export interface MediaTypeModelObj {
}
export default class MediaDbPlugin extends Plugin {
settings: MediaDbPluginSettings;
apiManager: APIManager;
mediaTypeManager: MediaTypeManager;
modelPropertyMapper: PropertyMapper;
modalHelper: ModalHelper;
dateFormatter: DateFormatter;
settings!: MediaDbPluginSettings;
apiManager!: APIManager;
mediaTypeManager!: MediaTypeManager;
modelPropertyMapper!: PropertyMapper;
modalHelper!: ModalHelper;
dateFormatter!: DateFormatter;
frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---';
@ -165,7 +160,7 @@ export default class MediaDbPlugin extends Plugin {
* - maybe custom link syntax
*/
async createLinkWithSearchModal(): Promise<void> {
const apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => {
const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => {
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);
});
@ -173,7 +168,7 @@ export default class MediaDbPlugin extends Plugin {
return;
}
const selectResults: MediaTypeModel[] = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => {
const selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => {
return await this.queryDetails(selectModalData.selected);
});
@ -193,7 +188,7 @@ export default class MediaDbPlugin extends Plugin {
async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise<void> {
let types: string[] = [];
let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => {
let apiSearchResults = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => {
types = searchModalData.types;
const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName);
try {
@ -214,12 +209,13 @@ export default class MediaDbPlugin extends Plugin {
apiSearchResults = apiSearchResults.filter(x => types.contains(x.type));
let selectResults: MediaTypeModel[];
let proceed: boolean;
let proceed: boolean = false;
while (!proceed) {
selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
selectResults =
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
return await this.queryDetails(selectModalData.selected);
});
})) ?? [];
if (!selectResults) {
return;
}
@ -229,11 +225,11 @@ export default class MediaDbPlugin extends Plugin {
});
}
await this.createMediaDbNotes(selectResults);
await this.createMediaDbNotes(selectResults!);
}
async createEntryWithAdvancedSearchModal(): Promise<void> {
const apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => {
const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => {
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);
});
@ -243,12 +239,13 @@ export default class MediaDbPlugin extends Plugin {
}
let selectResults: MediaTypeModel[];
let proceed: boolean;
let proceed: boolean = false;
while (!proceed) {
selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
selectResults =
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
return await this.queryDetails(selectModalData.selected);
});
})) ?? [];
if (!selectResults) {
return;
}
@ -258,12 +255,12 @@ export default class MediaDbPlugin extends Plugin {
});
}
await this.createMediaDbNotes(selectResults);
await this.createMediaDbNotes(selectResults!);
}
async createEntryWithIdSearchModal(): Promise<void> {
let idSearchResult: MediaTypeModel;
let proceed: boolean;
let idSearchResult: MediaTypeModel | undefined = undefined;
let proceed: boolean = false;
while (!proceed) {
idSearchResult = await this.modalHelper.openIdSearchModal({}, async idSearchModalData => {
@ -278,6 +275,9 @@ export default class MediaDbPlugin extends Plugin {
});
}
if (!idSearchResult) {
return;
}
await this.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true });
}
@ -290,11 +290,9 @@ export default class MediaDbPlugin extends Plugin {
async queryDetails(models: MediaTypeModel[]): Promise<MediaTypeModel[]> {
const detailModels: MediaTypeModel[] = [];
for (const model of models) {
try {
detailModels.push(await this.apiManager.queryDetailedInfo(model));
} catch (e) {
console.warn(e);
new Notice(e.toString());
const res = await this.apiManager.queryDetailedInfo(model);
if (res) {
detailModels.push(res);
}
}
return detailModels;
@ -319,7 +317,7 @@ export default class MediaDbPlugin extends Plugin {
}
} catch (e) {
console.warn(e);
new Notice(e.toString());
new Notice(`${e}`);
}
}
@ -383,7 +381,7 @@ export default class MediaDbPlugin extends Plugin {
// Updating a previous file
if (options.attachFile) {
const previousMetadata = this.app.metadataCache.getFileCache(options.attachFile).frontmatter;
const previousMetadata = this.app.metadataCache.getFileCache(options.attachFile)?.frontmatter ?? {};
// Use contents (below front matter) from previous file
fileContent = await this.app.vault.read(options.attachFile);
@ -443,7 +441,7 @@ export default class MediaDbPlugin extends Plugin {
return { fileMetadata: fileMetadata, fileContent: fileContent };
}
async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string): Promise<{ fileMetadata: Metadata; fileContent: string }> {
async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> {
if (!template) {
return { fileMetadata: fileMetadata, fileContent: fileContent };
}
@ -486,7 +484,7 @@ export default class MediaDbPlugin extends Plugin {
}
getMetadataFromFileCache(file: TFile): Metadata {
const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file).frontmatter;
const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter;
return structuredClone(metadata ?? {});
}
@ -501,6 +499,10 @@ export default class MediaDbPlugin extends Plugin {
// find and possibly create the folder set in settings or passed in folder
const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/');
if (!folder || !(folder instanceof TFolder)) {
throw new Error('MDB | invalid folder');
}
fileName = replaceIllegalFileNameCharactersInString(fileName);
const filePath = `${folder.path}/${fileName}.md`;
@ -519,7 +521,7 @@ export default class MediaDbPlugin extends Plugin {
const activeLeaf = this.app.workspace.getUnpinnedLeaf();
if (!activeLeaf) {
console.warn('MDB | no active leaf, not opening newly created note');
return;
return targetFile;
}
await activeLeaf.openFile(targetFile, { state: { mode: 'source' } });
}
@ -532,7 +534,7 @@ export default class MediaDbPlugin extends Plugin {
* 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(onlyMetadata: boolean = false): Promise<void> {
const activeFile: TFile = this.app.workspace.getActiveFile();
const activeFile = this.app.workspace.getActiveFile() ?? undefined;
if (!activeFile) {
throw new Error('MDB | there is no active note');
}
@ -560,9 +562,9 @@ export default class MediaDbPlugin extends Plugin {
// console.debug(newMediaTypeModel);
if (onlyMetadata) {
await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent, openNote: true });
await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true });
} else {
await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent, openNote: true });
await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true });
}
}
@ -596,7 +598,7 @@ export default class MediaDbPlugin extends Plugin {
try {
results = await this.apiManager.query(title, [selectedAPI]);
} catch (e) {
erroredFiles.push({ filePath: file.path, error: e.toString() });
erroredFiles.push({ filePath: file.path, error: `${e}` });
continue;
}
if (!results || results.length === 0) {
@ -631,7 +633,7 @@ export default class MediaDbPlugin extends Plugin {
}
const detailedResults = await this.queryDetails(selectModalResult.data.selected);
await this.createMediaDbNotes(detailedResults, appendContent ? file : null);
await this.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
selectModal.close();
}
@ -661,7 +663,7 @@ export default class MediaDbPlugin extends Plugin {
// migrate the settings loaded from the disk to match the structure of the default settings
const newPropertyMappings: PropertyMappingModel[] = [];
for (const defaultPropertyMappingModel of defaultSettings.propertyMappingModels) {
const newPropertyMappingModel: PropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type);
const newPropertyMappingModel = 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);

View file

@ -1,7 +1,9 @@
import { ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import { MediaTypeModel } from '../models/MediaTypeModel';
import MediaDbPlugin from '../main';
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper';
import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper';
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
export class MediaDbAdvancedSearchModal extends Modal {
plugin: MediaDbPlugin;
@ -9,9 +11,9 @@ export class MediaDbAdvancedSearchModal extends Modal {
query: string;
isBusy: boolean;
title: string;
selectedApis: { name: string; selected: boolean }[];
selectedApis: string[];
searchBtn: ButtonComponent;
searchBtn?: ButtonComponent;
submitCallback?: (res: AdvancedSearchModalData) => void;
closeCallback?: (err?: Error) => void;
@ -22,12 +24,9 @@ export class MediaDbAdvancedSearchModal extends Modal {
this.plugin = plugin;
this.selectedApis = [];
this.title = advancedSearchModalOptions.modalTitle;
this.query = advancedSearchModalOptions.prefilledSearchString;
for (const api of this.plugin.apiManager.apis) {
this.selectedApis.push({ name: api.apiName, selected: advancedSearchModalOptions.preselectedAPIs.contains(api.apiName) });
}
this.title = advancedSearchModalOptions.modalTitle ?? '';
this.query = advancedSearchModalOptions.prefilledSearchString ?? '';
this.isBusy = false;
}
setSubmitCallback(submitCallback: (res: AdvancedSearchModalData) => void): void {
@ -44,13 +43,13 @@ export class MediaDbAdvancedSearchModal extends Modal {
}
}
async search(): Promise<MediaTypeModel[]> {
async search(): Promise<void> {
if (!this.query || this.query.length < 3) {
new Notice('MDB | Query too short');
return;
}
const apis: string[] = this.selectedApis.filter(x => x.selected).map(x => x.name);
const apis: string[] = this.selectedApis;
if (apis.length === 0) {
new Notice('MDB | No API selected');
@ -59,10 +58,10 @@ export class MediaDbAdvancedSearchModal extends Modal {
if (!this.isBusy) {
this.isBusy = true;
this.searchBtn.setDisabled(false);
this.searchBtn.setButtonText('Searching...');
this.searchBtn?.setDisabled(false);
this.searchBtn?.setButtonText('Searching...');
this.submitCallback({ query: this.query, apis: apis });
this.submitCallback?.({ query: this.query, apis: apis });
}
}
@ -97,9 +96,13 @@ export class MediaDbAdvancedSearchModal extends Modal {
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
apiToggleComponent.setTooltip(api.apiName);
apiToggleComponent.setValue(this.selectedApis.find(x => x.name === api.apiName).selected);
apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName));
apiToggleComponent.onChange(value => {
this.selectedApis.find(x => x.name === api.apiName).selected = value;
if (value) {
this.selectedApis.push(api.apiName);
} else {
this.selectedApis = this.selectedApis.filter(x => x !== api.apiName);
}
});
apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl);
}
@ -124,7 +127,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
}
onClose(): void {
this.closeCallback();
this.closeCallback?.();
const { contentEl } = this;
contentEl.empty();
}

View file

@ -1,11 +1,12 @@
import { App, ButtonComponent, DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
import MediaDbPlugin from '../main';
import type { App, ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
export class MediaDbFolderImportModal extends Modal {
plugin: MediaDbPlugin;
onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void;
selectedApi: string;
searchBtn: ButtonComponent;
searchBtn?: ButtonComponent;
titleFieldName: string;
appendContent: boolean;
@ -14,6 +15,8 @@ export class MediaDbFolderImportModal extends Modal {
this.plugin = plugin;
this.onSubmit = onSubmit;
this.selectedApi = plugin.apiManager.apis[0].apiName;
this.titleFieldName = '';
this.appendContent = false;
}
submit(): void {

View file

@ -1,7 +1,9 @@
import { ButtonComponent, DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian';
import { MediaTypeModel } from '../models/MediaTypeModel';
import MediaDbPlugin from '../main';
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS, IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper';
import type { ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper';
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
export class MediaDbIdSearchModal extends Modal {
plugin: MediaDbPlugin;
@ -11,7 +13,7 @@ export class MediaDbIdSearchModal extends Modal {
title: string;
selectedApi: string;
searchBtn: ButtonComponent;
searchBtn?: ButtonComponent;
submitCallback?: (res: IdSearchModalData, err?: Error) => void;
closeCallback?: (err?: Error) => void;
@ -21,8 +23,10 @@ export class MediaDbIdSearchModal extends Modal {
super(plugin.app);
this.plugin = plugin;
this.title = idSearchModalOptions.modalTitle;
this.title = idSearchModalOptions.modalTitle ?? '';
this.selectedApi = idSearchModalOptions.preselectedAPI || plugin.apiManager.apis[0].apiName;
this.query = '';
this.isBusy = false;
}
setSubmitCallback(submitCallback: (res: IdSearchModalData, err?: Error) => void): void {
@ -39,7 +43,7 @@ export class MediaDbIdSearchModal extends Modal {
}
}
async search(): Promise<MediaTypeModel> {
async search(): Promise<void> {
if (!this.query) {
new Notice('MDB | no Id entered');
return;
@ -52,10 +56,10 @@ export class MediaDbIdSearchModal extends Modal {
if (!this.isBusy) {
this.isBusy = true;
this.searchBtn.setDisabled(false);
this.searchBtn.setButtonText('Searching...');
this.searchBtn?.setDisabled(false);
this.searchBtn?.setButtonText('Searching...');
this.submitCallback({ query: this.query, api: this.selectedApi });
this.submitCallback?.({ query: this.query, api: this.selectedApi });
}
}
@ -109,7 +113,7 @@ export class MediaDbIdSearchModal extends Modal {
}
onClose(): void {
this.closeCallback();
this.closeCallback?.();
const { contentEl } = this;
contentEl.empty();
}

View file

@ -1,22 +1,19 @@
import { ButtonComponent, Component, 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';
import type { ButtonComponent } from 'obsidian';
import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian';
import type MediaDbPlugin from 'src/main';
import type { MediaTypeModel } from 'src/models/MediaTypeModel';
import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper';
import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
export class MediaDbPreviewModal extends Modal {
plugin: MediaDbPlugin;
createNoteOptions: CreateNoteOptions;
elements: MediaTypeModel[];
isBusy: boolean;
title: string;
cancelButton: ButtonComponent;
submitButton: ButtonComponent;
markdownComponent: Component;
submitCallback: (previewModalData: PreviewModalData) => void;
closeCallback: (err?: Error) => void;
submitCallback?: (previewModalData: PreviewModalData) => void;
closeCallback?: (err?: Error) => void;
constructor(plugin: MediaDbPlugin, previewModalOptions: PreviewModalOptions) {
previewModalOptions = Object.assign({}, PREVIEW_MODAL_DEFAULT_OPTIONS, previewModalOptions);
@ -24,8 +21,8 @@ export class MediaDbPreviewModal extends Modal {
super(plugin.app);
this.plugin = plugin;
this.title = previewModalOptions.modalTitle;
this.elements = previewModalOptions.elements;
this.title = previewModalOptions.modalTitle ?? '';
this.elements = previewModalOptions.elements ?? [];
this.markdownComponent = new Component();
}
@ -70,14 +67,12 @@ export class MediaDbPreviewModal extends Modal {
btn.setButtonText('Cancel');
btn.onClick(() => this.close());
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.onClick(() => this.submitCallback?.({ confirmed: true }));
btn.buttonEl.addClass('media-db-plugin-button');
this.submitButton = btn;
});
}
@ -87,6 +82,6 @@ export class MediaDbPreviewModal extends Modal {
onClose(): void {
this.markdownComponent.unload();
this.closeCallback();
this.closeCallback?.();
}
}

View file

@ -1,10 +1,12 @@
import { ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import { MediaTypeModel } from '../models/MediaTypeModel';
import MediaDbPlugin from '../main';
import { SEARCH_MODAL_DEFAULT_OPTIONS, SearchModalData, SearchModalOptions } from '../utils/ModalHelper';
import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { MediaType } from '../utils/MediaType';
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper';
import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
import { unCamelCase } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
export class MediaDbSearchModal extends Modal {
plugin: MediaDbPlugin;
@ -12,9 +14,9 @@ export class MediaDbSearchModal extends Modal {
query: string;
isBusy: boolean;
title: string;
selectedTypes: { name: MediaType; selected: boolean }[];
selectedTypes: MediaType[];
searchBtn: ButtonComponent;
searchBtn?: ButtonComponent;
submitCallback?: (res: SearchModalData) => void;
closeCallback?: (err?: Error) => void;
@ -24,13 +26,10 @@ export class MediaDbSearchModal extends Modal {
super(plugin.app);
this.plugin = plugin;
this.selectedTypes = [];
this.title = searchModalOptions.modalTitle;
this.query = searchModalOptions.prefilledSearchString;
for (const mediaType of MEDIA_TYPES) {
this.selectedTypes.push({ name: mediaType, selected: searchModalOptions.preselectedTypes.contains(mediaType) });
}
this.selectedTypes = [...(searchModalOptions.preselectedTypes ?? [])];
this.title = searchModalOptions.modalTitle ?? '';
this.query = searchModalOptions.prefilledSearchString ?? '';
this.isBusy = false;
}
setSubmitCallback(submitCallback: (res: SearchModalData) => void): void {
@ -47,13 +46,13 @@ export class MediaDbSearchModal extends Modal {
}
}
async search(): Promise<MediaTypeModel[]> {
async search(): Promise<void> {
if (!this.query || this.query.length < 3) {
new Notice('MDB | Query too short');
return;
}
const types: MediaType[] = this.selectedTypes.filter(x => x.selected).map(x => x.name);
const types: MediaType[] = this.selectedTypes;
if (types.length === 0) {
new Notice('MDB | No Type selected');
@ -62,10 +61,10 @@ export class MediaDbSearchModal extends Modal {
if (!this.isBusy) {
this.isBusy = true;
this.searchBtn.setDisabled(false);
this.searchBtn.setButtonText('Searching...');
this.searchBtn?.setDisabled(false);
this.searchBtn?.setButtonText('Searching...');
this.submitCallback({ query: this.query, types: types });
this.submitCallback?.({ query: this.query, types: types });
}
}
@ -76,7 +75,7 @@ export class MediaDbSearchModal extends Modal {
const placeholder = 'Search by title';
const searchComponent = new TextComponent(contentEl);
let currentToggle: ToggleComponent = null;
let currentToggle: ToggleComponent | undefined = undefined;
searchComponent.inputEl.style.width = '100%';
searchComponent.setPlaceholder(placeholder);
@ -100,7 +99,7 @@ export class MediaDbSearchModal extends Modal {
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
apiToggleComponent.setTooltip(unCamelCase(mediaType));
apiToggleComponent.setValue(this.selectedTypes.find(x => x.name === mediaType).selected);
apiToggleComponent.setValue(this.selectedTypes.contains(mediaType));
if (apiToggleComponent.getValue()) {
currentToggle = apiToggleComponent;
}
@ -108,13 +107,13 @@ export class MediaDbSearchModal extends Modal {
if (value) {
if (currentToggle && currentToggle !== apiToggleComponent) {
currentToggle.setValue(false);
this.selectedTypes.find(x => x.name === mediaType).selected = false;
this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType);
}
currentToggle = apiToggleComponent;
this.selectedTypes.find(x => x.name === mediaType).selected = true;
this.selectedTypes.push(mediaType);
} else {
currentToggle = null;
this.selectedTypes.find(x => x.name === mediaType).selected = false;
currentToggle = undefined;
this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType);
}
});
apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl);
@ -140,7 +139,7 @@ export class MediaDbSearchModal extends Modal {
}
onClose(): void {
this.closeCallback();
this.closeCallback?.();
const { contentEl } = this;
contentEl.empty();
}

View file

@ -1,7 +1,8 @@
import { MediaTypeModel } from '../models/MediaTypeModel';
import MediaDbPlugin from '../main';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper';
import { SELECT_MODAL_OPTIONS_DEFAULT } from '../utils/ModalHelper';
import { SelectModal } from './SelectModal';
import { SELECT_MODAL_OPTIONS_DEFAULT, SelectModalData, SelectModalOptions } from '../utils/ModalHelper';
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
plugin: MediaDbPlugin;
@ -9,18 +10,18 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
busy: boolean;
sendCallback: boolean;
submitCallback: (res: SelectModalData) => void;
closeCallback: (err?: Error) => void;
skipCallback: () => void;
submitCallback?: (res: SelectModalData) => void;
closeCallback?: (err?: Error) => void;
skipCallback?: () => void;
constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) {
selectModalOptions = Object.assign({}, SELECT_MODAL_OPTIONS_DEFAULT, selectModalOptions);
super(plugin.app, selectModalOptions.elements, selectModalOptions.multiSelect);
super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect);
this.plugin = plugin;
this.title = selectModalOptions.modalTitle;
this.title = selectModalOptions.modalTitle ?? '';
this.description = 'Select one or multiple search results.';
this.addSkipButton = selectModalOptions.skipButton;
this.addSkipButton = selectModalOptions.skipButton ?? false;
this.busy = false;
@ -50,17 +51,17 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
submit(): void {
if (!this.busy) {
this.busy = true;
this.submitButton.setButtonText('Creating entry...');
this.submitCallback({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) });
this.submitButton?.setButtonText('Creating entry...');
this.submitCallback?.({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) });
}
}
skip(): void {
this.skipButton.setButtonText('Skipping...');
this.skipCallback();
this.skipButton?.setButtonText('Skipping...');
this.skipCallback?.();
}
onClose(): void {
this.closeCallback();
this.closeCallback?.();
}
}

View file

@ -1,6 +1,7 @@
import { App, ButtonComponent, Modal, Setting } from 'obsidian';
import { SelectModalElement } from './SelectModalElement';
import type { App, ButtonComponent } from 'obsidian';
import { Modal, Setting } from 'obsidian';
import { mod } from '../utils/Utils';
import { SelectModalElement } from './SelectModalElement';
export abstract class SelectModal<T> extends Modal {
allowMultiSelect: boolean;
@ -142,7 +143,7 @@ export abstract class SelectModal<T> extends Modal {
}
// nothing is highlighted
this.selectModalElements.last().setHighlighted(true);
this.selectModalElements.last()?.setHighlighted(true);
}
highlightDown(): void {
@ -154,20 +155,20 @@ export abstract class SelectModal<T> extends Modal {
}
// nothing is highlighted
this.selectModalElements.first().setHighlighted(true);
this.selectModalElements.first()?.setHighlighted(true);
}
private getNextSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id + 1;
nextId = mod(nextId, this.selectModalElements.length);
return this.selectModalElements.filter(x => x.id === nextId).first();
return this.selectModalElements.find(x => x.id === nextId)!;
}
private getPreviousSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id - 1;
nextId = mod(nextId, this.selectModalElements.length);
return this.selectModalElements.filter(x => x.id === nextId).first();
return this.selectModalElements.find(x => x.id === nextId)!;
}
}

View file

@ -1,4 +1,4 @@
import { SelectModal } from './SelectModal';
import type { SelectModal } from './SelectModal';
export class SelectModalElement<T> {
selectModal: SelectModal<T>;
@ -35,6 +35,8 @@ export class SelectModalElement<T> {
this.element.on('mouseleave', '#' + this.getHTMLId(), () => {
this.setHighlighted(false);
});
this.highlighted = false;
}
getHTMLId(): string {

View file

@ -1,6 +1,8 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type BoardGameData = ModelToData<BoardGameModel>;
export class BoardGameModel extends MediaTypeModel {
genres: string[];
@ -19,23 +21,23 @@ export class BoardGameModel extends MediaTypeModel {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: BoardGameData) {
super();
this.genres = undefined;
this.onlineRating = undefined;
this.minPlayers = undefined;
this.maxPlayers = undefined;
this.playtime = undefined;
this.publishers = undefined;
this.complexityRating = undefined;
this.image = undefined;
this.genres = [];
this.onlineRating = 0;
this.complexityRating = 0;
this.minPlayers = 0;
this.maxPlayers = 0;
this.playtime = '';
this.publishers = [];
this.image = '';
this.released = undefined;
this.released = false;
this.userData = {
played: undefined,
personalRating: undefined,
played: false,
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,6 +1,8 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type BookData = ModelToData<BookModel>;
export class BookModel extends MediaTypeModel {
author: string;
@ -8,7 +10,6 @@ export class BookModel extends MediaTypeModel {
pages: number;
image: string;
onlineRating: number;
english_title: string;
isbn: number;
isbn13: number;
@ -20,22 +21,23 @@ export class BookModel extends MediaTypeModel {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: BookData) {
super();
this.author = undefined;
this.pages = undefined;
this.image = undefined;
this.onlineRating = undefined;
this.isbn = undefined;
this.isbn13 = undefined;
this.author = '';
this.plot = '';
this.pages = 0;
this.image = '';
this.onlineRating = 0;
this.isbn = 0;
this.isbn13 = 0;
this.released = undefined;
this.released = false;
this.userData = {
read: undefined,
lastRead: undefined,
personalRating: undefined,
read: false,
lastRead: '',
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,6 +1,8 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type GameData = ModelToData<GameModel>;
export class GameModel extends MediaTypeModel {
developers: string[];
@ -17,19 +19,21 @@ export class GameModel extends MediaTypeModel {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: GameData) {
super();
this.developers = undefined;
this.publishers = undefined;
this.genres = undefined;
this.onlineRating = undefined;
this.image = undefined;
this.released = undefined;
this.releaseDate = undefined;
this.developers = [];
this.publishers = [];
this.genres = [];
this.onlineRating = 0;
this.image = '';
this.released = false;
this.releaseDate = '';
this.userData = {
played: undefined,
personalRating: undefined,
played: false,
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,19 +1,12 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type MangaData = ModelToData<MangaModel>;
export class MangaModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
plot: string;
englishTitle: string;
alternateTitles: string[];
year: string;
dataSource: string;
url: string;
id: string;
genres: string[];
authors: string[];
chapters: number;
@ -32,27 +25,27 @@ export class MangaModel extends MediaTypeModel {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: MangaData) {
super();
this.plot = undefined;
this.genres = undefined;
this.authors = undefined;
this.alternateTitles = undefined;
this.chapters = undefined;
this.volumes = undefined;
this.onlineRating = undefined;
this.image = undefined;
this.plot = '';
this.alternateTitles = [];
this.genres = [];
this.authors = [];
this.chapters = 0;
this.volumes = 0;
this.onlineRating = 0;
this.image = '';
this.released = undefined;
this.status = undefined;
this.publishedFrom = undefined;
this.publishedTo = undefined;
this.released = false;
this.status = '';
this.publishedFrom = '';
this.publishedTo = '';
this.userData = {
watched: undefined,
lastWatched: undefined,
personalRating: undefined,
watched: false,
lastWatched: '',
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,4 +1,4 @@
import { MediaType } from '../utils/MediaType';
import type { MediaType } from '../utils/MediaType';
export abstract class MediaTypeModel {
type: string;
@ -13,14 +13,14 @@ 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.type = '';
this.subType = '';
this.title = '';
this.englishTitle = '';
this.year = '';
this.dataSource = '';
this.url = '';
this.id = '';
this.userData = {};
}

View file

@ -1,6 +1,8 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type MovieData = ModelToData<MovieModel>;
export class MovieModel extends MediaTypeModel {
plot: string;
@ -23,27 +25,27 @@ export class MovieModel extends MediaTypeModel {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: MovieData) {
super();
this.plot = undefined;
this.genres = undefined;
this.director = undefined;
this.writer = undefined;
this.studio = undefined;
this.duration = undefined;
this.onlineRating = undefined;
this.actors = undefined;
this.image = undefined;
this.plot = '';
this.genres = [];
this.director = [];
this.writer = [];
this.studio = [];
this.duration = '';
this.onlineRating = 0;
this.actors = [];
this.image = '';
this.released = undefined;
this.streamingServices = undefined;
this.premiere = undefined;
this.released = false;
this.streamingServices = [];
this.premiere = '';
this.userData = {
watched: undefined,
lastWatched: undefined,
personalRating: undefined,
watched: false,
lastWatched: '',
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,35 +1,28 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type MusicReleaseData = ModelToData<MusicReleaseModel>;
export class MusicReleaseModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
dataSource: string;
url: string;
id: string;
image: string;
genres: string[];
artists: string[];
image: string;
rating: number;
userData: {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: MusicReleaseData) {
super();
this.genres = undefined;
this.artists = undefined;
this.image = undefined;
this.rating = undefined;
this.genres = [];
this.artists = [];
this.image = '';
this.rating = 0;
this.userData = {
personalRating: undefined,
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,17 +1,10 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type SeriesData = ModelToData<SeriesModel>;
export class SeriesModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
dataSource: string;
url: string;
id: string;
plot: string;
genres: string[];
writer: string[];
@ -34,29 +27,29 @@ export class SeriesModel extends MediaTypeModel {
personalRating: number;
};
constructor(obj: any = {}) {
constructor(obj: SeriesData) {
super();
this.plot = undefined;
this.genres = undefined;
this.writer = undefined;
this.studio = undefined;
this.episodes = undefined;
this.duration = undefined;
this.onlineRating = undefined;
this.actors = undefined;
this.image = undefined;
this.plot = '';
this.genres = [];
this.writer = [];
this.studio = [];
this.episodes = 0;
this.duration = '';
this.onlineRating = 0;
this.actors = [];
this.image = '';
this.released = undefined;
this.streamingServices = undefined;
this.airing = undefined;
this.airedFrom = undefined;
this.airedTo = undefined;
this.released = false;
this.streamingServices = [];
this.airing = false;
this.airedFrom = '';
this.airedTo = '';
this.userData = {
watched: undefined,
lastWatched: undefined,
personalRating: undefined,
watched: false,
lastWatched: '',
personalRating: 0,
};
migrateObject(this, obj, this);

View file

@ -1,17 +1,10 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type WikiData = ModelToData<WikiModel>;
export class WikiModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
dataSource: string;
url: string;
id: string;
wikiUrl: string;
lastUpdated: string;
length: number;
@ -19,13 +12,13 @@ export class WikiModel extends MediaTypeModel {
userData: Record<string, unknown>;
constructor(obj: any = {}) {
constructor(obj: WikiData) {
super();
this.wikiUrl = undefined;
this.lastUpdated = undefined;
this.length = undefined;
this.article = undefined;
this.wikiUrl = '';
this.lastUpdated = '';
this.length = 0;
this.article = '';
this.userData = {};
migrateObject(this, obj, this);

View file

@ -4,13 +4,16 @@
import { setIcon } from 'obsidian';
import { onMount } from 'svelte';
export let iconName: string = '';
export let iconSize: number = 20;
interface Props {
iconName?: string;
}
let iconEl: HTMLElement;
let { iconName = '' }: Props = $props();
let iconEl: HTMLElement | undefined = $state();
onMount(() => {
setIcon(iconEl, iconName, iconSize);
setIcon(iconEl!, iconName);
});
</script>

View file

@ -1,6 +1,6 @@
import { PropertyMappingOption } from './PropertyMapping';
import type MediaDbPlugin from '../main';
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import MediaDbPlugin from '../main';
import { PropertyMappingOption } from './PropertyMapping';
export class PropertyMapper {
plugin: MediaDbPlugin;
@ -66,7 +66,7 @@ export class PropertyMapper {
return obj;
}
const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties;
const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type)?.properties ?? [];
const originalObj: Record<string, unknown> = {};

View file

@ -1,5 +1,5 @@
import type { MediaType } from '../utils/MediaType';
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
export enum PropertyMappingOption {
Default = 'default',

View file

@ -1,18 +1,22 @@
<script lang="ts">
import { run } from 'svelte/legacy';
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();
interface Props {
model: PropertyMappingModel;
save: (model: PropertyMappingModel) => void;
}
let { model, save }: Props = $props();
let validationResult: { res: boolean; err?: Error } | undefined = $state();
$effect(() => {
validationResult = model.validate();
});
</script>
<div class="media-db-plugin-property-mappings-model-container">
@ -51,7 +55,7 @@
{/if}
<button
class="media-db-plugin-property-mappings-save-button {validationResult?.res ? 'mod-cta' : 'mod-muted'}"
on:click={() => {
onclick={() => {
if (model.validate().res) save(model);
}}
>Save

View file

@ -2,8 +2,12 @@
import { PropertyMappingModel } from './PropertyMapping';
import PropertyMappingModelComponent from './PropertyMappingModelComponent.svelte';
export let models: PropertyMappingModel[] = [];
export let save: (model: PropertyMappingModel) => void;
interface Props {
models?: PropertyMappingModel[];
save: (model: PropertyMappingModel) => void;
}
let { models = [], save }: Props = $props();
</script>
<div class="setting-item" style="display: flex; gap: 10px; flex-direction: column; align-items: stretch;">

View file

@ -1,13 +1,14 @@
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 type { App } from 'obsidian';
import { Notice, PluginSettingTab, Setting } from 'obsidian';
import { mount } from 'svelte';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import { MediaTypeModel } from '../models/MediaTypeModel';
import { fragWithHTML } from '../utils/Utils';
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping';
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte';
import { FileSuggest } from './suggesters/FileSuggest';
import { FolderSuggest } from './suggesters/FolderSuggest';
export interface MediaDbPluginSettings {
OMDbKey: string;
@ -254,7 +255,10 @@ export class MediaDbSettingTab extends PluginSettingTab {
.onChange(data => {
const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat;
this.plugin.settings.customDateFormat = newDateFormat;
document.getElementById('media-db-dateformat-preview').textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview
const previewEl = document.getElementById('media-db-dateformat-preview');
if (previewEl) {
previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview
}
void this.plugin.saveSettings();
});
});
@ -689,7 +693,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
Don't forget to save your changes using the save button for each individual category.
</p>`;
new PropertyMappingModelsComponent({
mount(PropertyMappingModelsComponent, {
target: this.containerEl,
props: {
models: this.plugin.settings.propertyMappingModels.map(x => x.copy()),

View file

@ -1,5 +1,6 @@
import type { TAbstractFile } from 'obsidian';
import { TFile } from 'obsidian';
import { TextInputSuggest } from './Suggest';
import { TAbstractFile, TFile } from 'obsidian';
export class FileSuggest extends TextInputSuggest<TFile> {
getSuggestions(inputStr: string): TFile[] {

View file

@ -1,6 +1,7 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { TAbstractFile, TFolder } from 'obsidian';
import type { TAbstractFile } from 'obsidian';
import { TFolder } from 'obsidian';
import { TextInputSuggest } from './Suggest';
export class FolderSuggest extends TextInputSuggest<TFolder> {

View file

@ -1,28 +1,34 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { App, ISuggestOwner, Scope } from 'obsidian';
import { createPopper, Instance as PopperInstance } from '@popperjs/core';
import type { Instance as PopperInstance } from '@popperjs/core';
import { createPopper } from '@popperjs/core';
import type { App, ISuggestOwner } from 'obsidian';
import { Scope } from 'obsidian';
import { wrapAround } from 'src/utils/Utils';
export class Suggest<T> {
private owner: ISuggestOwner<T>;
private values: T[];
private suggestions: HTMLDivElement[];
private suggestions: HTMLElement[];
private selectedItem: number;
private containerEl: HTMLElement;
constructor(owner: ISuggestOwner<T>, containerEl: HTMLElement, scope: Scope) {
this.owner = owner;
this.containerEl = containerEl;
this.values = [];
this.suggestions = [];
this.selectedItem = 0;
containerEl.on('click', '.suggestion-item', this.onSuggestionClick.bind(this));
containerEl.on('mousemove', '.suggestion-item', this.onSuggestionMouseover.bind(this));
containerEl.on('click', '.suggestion-item', (e, el) => this.onSuggestionClick(e, el));
containerEl.on('mousemove', '.suggestion-item', (e, el) => this.onSuggestionMouseover(e, el));
scope.register([], 'ArrowUp', event => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
}
return undefined;
});
scope.register([], 'ArrowDown', event => {
@ -30,6 +36,7 @@ export class Suggest<T> {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
}
return undefined;
});
scope.register([], 'Enter', event => {
@ -37,10 +44,11 @@ export class Suggest<T> {
this.useSelectedItem(event);
return false;
}
return undefined;
});
}
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
onSuggestionClick(event: MouseEvent, el: HTMLElement): void {
event.preventDefault();
const item = this.suggestions.indexOf(el);
@ -48,7 +56,7 @@ export class Suggest<T> {
this.useSelectedItem(event);
}
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
onSuggestionMouseover(_event: MouseEvent, el: HTMLElement): void {
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
}
@ -95,7 +103,7 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
protected app: App;
protected inputEl: HTMLInputElement;
private popper: PopperInstance;
private popper?: PopperInstance;
private scope: Scope;
private suggestEl: HTMLElement;
private suggest: Suggest<T>;
@ -126,13 +134,13 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
if (suggestions.length > 0) {
this.suggest.setSuggestions(suggestions);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.open((<any>this.app).dom.appContainerEl, this.inputEl);
this.open((this.app as any).dom.appContainerEl, this.inputEl);
}
}
open(container: HTMLElement, inputEl: HTMLElement): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.pushScope(this.scope);
(this.app as any).keymap.pushScope(this.scope);
container.appendChild(this.suggestEl);
this.popper = createPopper(inputEl, this.suggestEl, {
@ -162,7 +170,7 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
close(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.popScope(this.scope);
(this.app as any).keymap.popScope(this.scope);
this.suggest.setSuggestions([]);
this.popper?.destroy();

View file

@ -1,16 +1,17 @@
import { MediaDbPluginSettings } from '../settings/Settings';
import { MediaType } from './MediaType';
import { MediaTypeModel } from '../models/MediaTypeModel';
import { replaceTags } from './Utils';
import { App, TAbstractFile, TFile, TFolder } from 'obsidian';
import { MovieModel } from '../models/MovieModel';
import { SeriesModel } from '../models/SeriesModel';
import { MangaModel } from '../models/MangaModel';
import { GameModel } from '../models/GameModel';
import { WikiModel } from '../models/WikiModel';
import { MusicReleaseModel } from '../models/MusicReleaseModel';
import type { App, TAbstractFile, TFile } from 'obsidian';
import { TFolder } from 'obsidian';
import { BoardGameModel } from '../models/BoardGameModel';
import { BookModel } from '../models/BookModel';
import { GameModel } from '../models/GameModel';
import { MangaModel } from '../models/MangaModel';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import { MovieModel } from '../models/MovieModel';
import { MusicReleaseModel } from '../models/MusicReleaseModel';
import { SeriesModel } from '../models/SeriesModel';
import type { MediaDbPluginSettings } from '../settings/Settings';
import { MediaType } from './MediaType';
import { replaceTags } from './Utils';
import { WikiModel } from '../models/WikiModel';
export const MEDIA_TYPES: MediaType[] = [
MediaType.Movie,
@ -28,7 +29,11 @@ export class MediaTypeManager {
mediaTemplateMap: Map<MediaType, string>;
mediaFolderMap: Map<MediaType, string>;
constructor() {}
constructor() {
this.mediaFileNameTemplateMap = new Map<MediaType, string>();
this.mediaTemplateMap = new Map<MediaType, string>();
this.mediaFolderMap = new Map<MediaType, string>();
}
updateTemplates(settings: MediaDbPluginSettings): void {
this.mediaFileNameTemplateMap = new Map<MediaType, string>();
@ -66,7 +71,7 @@ export class MediaTypeManager {
getFileName(mediaTypeModel: MediaTypeModel): string {
// Ignore undefined tags since some search APIs do not return all properties in the model and produce clean file names even if errors occur
return replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType()), mediaTypeModel, true);
return replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true);
}
async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise<string> {
@ -76,7 +81,7 @@ export class MediaTypeManager {
return '';
}
let templateFile = app.vault.getAbstractFileByPath(templateFilePath);
let templateFile = app.vault.getAbstractFileByPath(templateFilePath) ?? undefined;
// WARNING: This was previously selected by filename, but that could lead to collisions and unwanted effects.
// This now falls back to the previous method if no file is found
@ -107,7 +112,7 @@ export class MediaTypeManager {
if (!(await app.vault.adapter.exists(folderPath))) {
await app.vault.createFolder(folderPath);
}
const folder: TAbstractFile = app.vault.getAbstractFileByPath(folderPath);
const folder = app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) {
throw Error(`Expected ${folder} to be instance of TFolder`);
@ -141,6 +146,6 @@ export class MediaTypeManager {
return new BookModel(obj);
}
return undefined;
throw new Error(`Unknown media type: ${mediaType}`);
}
}

View file

@ -1,12 +1,12 @@
import { Notice } from 'obsidian';
import { MediaDbPreviewModal } from 'src/modals/MediaDbPreviewModal';
import type MediaDbPlugin from '../main';
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 { MediaDbSearchModal } from '../modals/MediaDbSearchModal';
import { MediaType } from './MediaType';
import { MediaDbSearchResultModal } from '../modals/MediaDbSearchResultModal';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { MediaType } from './MediaType';
export enum ModalResultCode {
SUCCESS = 'SUCCESS',
@ -15,60 +15,59 @@ export enum ModalResultCode {
ERROR = 'ERROR',
}
type ModalResult<T> =
| {
code: ModalResultCode.CLOSE;
}
| {
code: ModalResultCode.ERROR;
error: Error;
}
| {
code: ModalResultCode.SUCCESS;
data: T;
};
type SkippableModalResult<T> =
| ModalResult<T>
| {
code: ModalResultCode.SKIP;
};
/**
* Object containing the data {@link ModalHelper.createSearchModal} returns.
* On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}.
* On {@link ModalResultCode.ERROR} this contains a reference to that error.
*/
export interface SearchModalResult {
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR;
data?: SearchModalData;
error?: Error;
}
export type SearchModalResult = ModalResult<SearchModalData>;
/**
* 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;
}
export type AdvancedSearchModalResult = ModalResult<AdvancedSearchModalData>;
/**
* 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;
}
export type IdSearchModalResult = ModalResult<IdSearchModalData>;
/**
* 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;
}
export type SelectModalResult = SkippableModalResult<SelectModalData>;
/**
* 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;
}
export type PreviewModalResult = ModalResult<PreviewModalData>;
/**
* The data the search modal returns.
@ -248,7 +247,10 @@ export class ModalHelper {
* @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 openSearchModal(searchModalOptions: SearchModalOptions, submitCallback: (searchModalData: SearchModalData) => Promise<MediaTypeModel[]>): Promise<MediaTypeModel[]> {
async openSearchModal(
searchModalOptions: SearchModalOptions,
submitCallback: (searchModalData: SearchModalData) => Promise<MediaTypeModel[]>,
): Promise<MediaTypeModel[] | undefined> {
const { searchModalResult, searchModal } = await this.createSearchModal(searchModalOptions);
console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`);
@ -271,7 +273,7 @@ export class ModalHelper {
return callbackRes;
} catch (e) {
console.warn(e);
new Notice(e.toString());
new Notice(`${e}`);
searchModal.close();
return undefined;
}
@ -314,7 +316,7 @@ export class ModalHelper {
async openAdvancedSearchModal(
advancedSearchModalOptions: AdvancedSearchModalOptions,
submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise<MediaTypeModel[]>,
): Promise<MediaTypeModel[]> {
): Promise<MediaTypeModel[] | undefined> {
const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions);
console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`);
@ -337,7 +339,7 @@ export class ModalHelper {
return callbackRes;
} catch (e) {
console.warn(e);
new Notice(e.toString());
new Notice(`${e}`);
advancedSearchModal.close();
return undefined;
}
@ -377,8 +379,8 @@ export class ModalHelper {
*/
async openIdSearchModal(
idSearchModalOptions: IdSearchModalOptions,
submitCallback: (idSearchModalData: IdSearchModalData) => Promise<MediaTypeModel>,
): Promise<MediaTypeModel> {
submitCallback: (idSearchModalData: IdSearchModalData) => Promise<MediaTypeModel | undefined>,
): Promise<MediaTypeModel | undefined> {
const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions);
console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`);
@ -396,12 +398,12 @@ export class ModalHelper {
}
try {
const callbackRes: MediaTypeModel = await submitCallback(idSearchModalResult.data);
const callbackRes = await submitCallback(idSearchModalResult.data);
idSearchModal.close();
return callbackRes;
} catch (e) {
console.warn(e);
new Notice(e.toString());
new Notice(`${e}`);
idSearchModal.close();
return undefined;
}
@ -440,7 +442,10 @@ export class ModalHelper {
* @param submitCallback the callback that gets executed after the modal has been submitted, but before 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[]> {
async openSelectModal(
selectModalOptions: SelectModalOptions,
submitCallback: (selectModalData: SelectModalData) => Promise<MediaTypeModel[]>,
): Promise<MediaTypeModel[] | undefined> {
const { selectModalResult, selectModal } = await this.createSelectModal(selectModalOptions);
console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`);
@ -468,7 +473,7 @@ export class ModalHelper {
return callbackRes;
} catch (e) {
console.warn(e);
new Notice(e.toString());
new Notice(`${e}`);
selectModal.close();
return;
}
@ -500,12 +505,12 @@ export class ModalHelper {
console.warn(previewModalResult.error);
new Notice(previewModalResult.error.toString());
previewModal.close();
return undefined;
return true;
}
if (previewModalResult.code === ModalResultCode.CLOSE) {
// modal is already being closed
return undefined;
return true;
}
try {
@ -514,9 +519,9 @@ export class ModalHelper {
return callbackRes;
} catch (e) {
console.warn(e);
new Notice(e.toString());
new Notice(`${e}`);
previewModal.close();
return;
return true;
}
}
}

View file

@ -1,5 +1,5 @@
import { MediaTypeModel } from '../models/MediaTypeModel';
import { TFile, TFolder, App } from 'obsidian';
import type { TFile, TFolder, App } from 'obsidian';
import type { MediaTypeModel } from '../models/MediaTypeModel';
export const pluginName: string = 'obsidian-media-db-plugin';
export const contactEmail: string = 'm.projects.code@gmail.com';
@ -82,7 +82,7 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin
return '{{ INVALID TEMPLATE TAG }}';
}
function traverseMetaData(path: Array<string>, mediaTypeModel: MediaTypeModel): any {
function traverseMetaData(path: string[], mediaTypeModel: MediaTypeModel): any {
let o: any = mediaTypeModel;
for (const part of path) {
@ -226,7 +226,11 @@ export function hasTemplaterPlugin(app: App): boolean {
export async function useTemplaterPluginInFile(app: App, file: TFile): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const templater = (app as any).plugins.plugins['templater-obsidian'];
if (templater && !templater?.settings['trigger_on_file_creation']) {
if (templater && !templater?.settings.trigger_on_file_creation) {
await templater.templater.overwrite_file_commands(file);
}
}
export type ModelToData<T> = {
[K in keyof T as T[K] extends Function ? never : K]?: T[K];
};

View file

@ -2,14 +2,22 @@
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "ES6",
"target": "ESNext",
"allowJs": true,
"checkJs": true,
"noImplicitAny": true,
"strict": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"sourceMap": true,
"lib": ["DOM", "ESNext"],
"types": ["svelte"],
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*.ts", "tests/**/*.ts"]