Merge branch 'master' into feat/vndb

This commit is contained in:
Senyksia 2025-02-20 01:36:07 +11:00 committed by GitHub
commit 98619fb75e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 927 additions and 724 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

@ -1,6 +1,4 @@
name: Build Obsidian Plugin name: Create Plugin Release
# adapted from https://github.com/argenos/nldates-obsidian/blob/master/.github/workflows/release.yml
on: on:
push: push:
@ -16,10 +14,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: Checkout
- uses: oven-sh/setup-bun@v1 uses: actions/checkout@v4
- name: Determine prerelease status
id: status
run: |
if [[ "${{ github.ref }}" == *"canary"* ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Install Bun
uses: oven-sh/setup-bun@v1
with: with:
bun-version: latest bun-version: latest
- name: Build - name: Build
id: build id: build
run: | run: |
@ -27,62 +38,17 @@ jobs:
bun run build bun run build
mkdir ${{ env.PLUGIN_NAME }} mkdir ${{ env.PLUGIN_NAME }}
cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }}
ls ls
echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT
- name: Create Release - name: Release
id: create_release id: release
uses: actions/create-release@v1 uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.ref }}
with: with:
tag_name: ${{ github.ref }} prerelease: ${{ steps.status.outputs.prerelease }}
release_name: ${{ github.ref }} token: ${{ secrets.GITHUB_TOKEN }}
draft: false files: |
prerelease: false ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip
main.js
- name: Upload zip file manifest.json
id: upload-zip styles.css
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.PLUGIN_NAME }}.zip
asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
asset_content_type: application/zip
- name: Upload main.js
id: upload-main
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./main.js
asset_name: main.js
asset_content_type: text/javascript
- name: Upload manifest.json
id: upload-manifest
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./manifest.json
asset_name: manifest.json
asset_content_type: application/json
- name: Upload styles.css
id: upload-css
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./styles.css
asset_name: styles.css
asset_content_type: text/css

View file

@ -1,5 +1,13 @@
# Changelog # Changelog
# 0.8.0
- Fixed bugs when API keys for certain APIs were missing [#161](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/161) (thanks ltctceplrm)
- Added support for other languages when remapping fields [#162](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/162) (thanks ltctceplrm)
- Added support for the `Giant Bomb` API [#166](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/166) (thanks ltctceplrm)
- Migration to Svelte 5
- Some internal changes and improved error handling
# 0.7.2 # 0.7.2
- Improvements to UI text to match the Obsidian plugin guidelines [#153](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/153) (thanks kepano) - Improvements to UI text to match the Obsidian plugin guidelines [#153](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/153) (thanks kepano)

View file

@ -112,14 +112,15 @@ Now you select the result you want and the plugin will cast it's magic and creat
### Currently supported APIs: ### Currently supported APIs:
| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support | | 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 | | [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 | | [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 | | [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No | | [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No | | [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No |
| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID so there shouldn't be a rate limit. | No | | [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID so there shouldn't be a rate limit. | No |
| [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/) | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No | | [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/). NOTE: As of September 2024 the API key is no longer free so consider using Giant Bomb or steam instead | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No |
| [Giant Bomb](https://www.giantbomb.com) | The Giant Bomb API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.giantbomb.com/login-signup/) | API requests are limited to 200 requests per resource, per hour. In addition, they implement velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No |
| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | | [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes |
#### Notes #### Notes
@ -157,6 +158,9 @@ Now you select the result you want and the plugin will cast it's magic and creat
- [Moby Games](https://www.mobygames.com) - [Moby Games](https://www.mobygames.com)
- you can find this ID in the URL - you can find this ID in the URL
- e.g. for "Bioshock 2" the URL looks like this `https://www.mobygames.com/game/45089/bioshock-2/` so the ID is `45089` - e.g. for "Bioshock 2" the URL looks like this `https://www.mobygames.com/game/45089/bioshock-2/` so the ID is `45089`
- [Giant Bomb](https://www.giantbomb.com)
- you can find this ID in the URL
- e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887`
- [VNDB](https://vndb.org/) - [VNDB](https://vndb.org/)
- Located in the novel's VNDB URL path - Located in the novel's VNDB URL path
- e.g. The ID for [Katawa Shoujo](https://vndb.org/v945) (`https://vndb.org/v945`) is `v945` - e.g. The ID for [Katawa Shoujo](https://vndb.org/v945) (`https://vndb.org/v945`) is `v945`

View file

@ -1,7 +1,7 @@
import builtins from 'builtin-modules'; import builtins from 'builtin-modules';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import esbuildSvelte from 'esbuild-svelte'; import esbuildSvelte from 'esbuild-svelte';
import sveltePreprocess from 'svelte-preprocess'; import { sveltePreprocess } from 'svelte-preprocess';
import { getBuildBanner } from 'build/buildBanner'; import { getBuildBanner } from 'build/buildBanner';
const banner = getBuildBanner('Release Build', version => version); const banner = getBuildBanner('Release Build', version => version);
@ -41,7 +41,7 @@ const build = await esbuild.build({
}, },
plugins: [ plugins: [
esbuildSvelte({ esbuildSvelte({
compilerOptions: { css: 'injected', dev: false, sveltePath: 'svelte' }, compilerOptions: { css: 'injected', dev: false },
preprocess: sveltePreprocess(), preprocess: sveltePreprocess(),
filterWarnings: warning => { filterWarnings: warning => {
// we don't want warnings from node modules that we can do nothing about // 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 esbuild from 'esbuild';
import copy from 'esbuild-plugin-copy-watch'; import copy from 'esbuild-plugin-copy-watch';
import esbuildSvelte from 'esbuild-svelte'; import esbuildSvelte from 'esbuild-svelte';
import sveltePreprocess from 'svelte-preprocess'; import { sveltePreprocess } from 'svelte-preprocess';
import manifest from '../../manifest.json' assert { type: 'json' }; import manifest from '../../manifest.json' assert { type: 'json' };
import { getBuildBanner } from 'build/buildBanner'; import { getBuildBanner } from 'build/buildBanner';
@ -52,7 +52,7 @@ const context = await esbuild.context({
], ],
}), }),
esbuildSvelte({ esbuildSvelte({
compilerOptions: { css: 'injected', dev: true, sveltePath: 'svelte' }, compilerOptions: { css: 'injected', dev: true },
preprocess: sveltePreprocess(), preprocess: sveltePreprocess(),
filterWarnings: warning => { filterWarnings: warning => {
// we don't want warnings from node modules that we can do nothing about // we don't want warnings from node modules that we can do nothing about

View file

@ -6,7 +6,7 @@ import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from 'utils/
async function runPreconditions(): Promise<void> { async function runPreconditions(): Promise<void> {
// run preconditions // run preconditions
await $seq( await $seq(
[`bun run format`, `bun run lint:fix`, `bun run test`], [`bun run format`, `bun run test`],
(cmd: string) => { (cmd: string) => {
throw new UserError(`precondition "${cmd}" failed`); throw new UserError(`precondition "${cmd}" failed`);
}, },

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

@ -1,7 +1,7 @@
{ {
"id": "obsidian-media-db-plugin", "id": "obsidian-media-db-plugin",
"name": "Media DB", "name": "Media DB",
"version": "0.7.2", "version": "0.8.0",
"minAppVersion": "1.5.0", "minAppVersion": "1.5.0",
"description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.",
"author": "Moritz Jung", "author": "Moritz Jung",

View file

@ -1,7 +1,7 @@
{ {
"id": "obsidian-media-db-plugin", "id": "obsidian-media-db-plugin",
"name": "Media DB", "name": "Media DB",
"version": "0.7.2", "version": "0.8.0",
"minAppVersion": "1.5.0", "minAppVersion": "1.5.0",
"description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.",
"author": "Moritz Jung", "author": "Moritz Jung",

View file

@ -1,6 +1,6 @@
{ {
"name": "obsidian-media-db-plugin", "name": "obsidian-media-db-plugin",
"version": "0.7.2", "version": "0.8.0",
"description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -14,8 +14,8 @@
"lint": "eslint --max-warnings=0 src/**", "lint": "eslint --max-warnings=0 src/**",
"lint:fix": "eslint --max-warnings=0 --fix src/**", "lint:fix": "eslint --max-warnings=0 --fix src/**",
"svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"", "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": "bun run format:check && bun run tsc && bun run test",
"check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run test", "check:fix": "bun run format && bun run tsc && bun run test",
"release": "bun run automation/release.ts", "release": "bun run automation/release.ts",
"stats": "bun run automation/stats.ts" "stats": "bun run automation/stats.ts"
}, },
@ -25,27 +25,24 @@
"devDependencies": { "devDependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@lemons_dev/parsinom": "^0.0.12", "@lemons_dev/parsinom": "^0.0.12",
"@happy-dom/global-registrator": "^14.3.6", "@happy-dom/global-registrator": "^14.12.3",
"@tsconfig/svelte": "^5.0.3", "@types/bun": "^1.1.16",
"@types/bun": "^1.0.10", "builtin-modules": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^7.3.1", "esbuild": "^0.24.2",
"@typescript-eslint/parser": "^7.3.1", "esbuild-plugin-copy-watch": "^2.3.1",
"builtin-modules": "^3.3.0", "esbuild-svelte": "^0.8.2",
"esbuild": "^0.20.2", "eslint": "^9.18.0",
"esbuild-plugin-copy-watch": "^2.1.0", "eslint-plugin-import": "^2.31.0",
"esbuild-svelte": "^0.8.0",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-isaacscript": "^3.12.2",
"eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-only-warn": "^1.1.0",
"obsidian": "latest", "obsidian": "latest",
"prettier": "^3.2.5", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.2.2", "prettier-plugin-svelte": "^3.3.3",
"string-argv": "^0.3.2", "string-argv": "^0.3.2",
"svelte": "^4.2.12", "svelte": "^5.17.5",
"svelte-check": "^3.6.8", "svelte-check": "^4.1.4",
"svelte-preprocess": "^5.1.3", "svelte-preprocess": "^6.0.3",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"typescript": "^5.4.3" "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
} }
} }

View file

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

View file

@ -1,13 +1,13 @@
import { MediaTypeModel } from '../models/MediaTypeModel'; import type MediaDbPlugin from '../main';
import { MediaType } from '../utils/MediaType'; import type { MediaTypeModel } from '../models/MediaTypeModel';
import MediaDbPlugin from '../main'; import type { MediaType } from '../utils/MediaType';
export abstract class APIModel { export abstract class APIModel {
apiName: string; apiName!: string;
apiUrl: string; apiUrl!: string;
apiDescription: string; apiDescription!: string;
types: MediaType[]; types!: MediaType[];
plugin: MediaDbPlugin; plugin!: MediaDbPlugin;
/** /**
* This function should query the api and return a list of matches. The matches should be caped at 20. * 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 { 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 { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class BoardGameGeekAPI extends APIModel { export class BoardGameGeekAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -38,8 +38,8 @@ export class BoardGameGeekAPI extends APIModel {
const ret: MediaTypeModel[] = []; const ret: MediaTypeModel[] = [];
for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) {
const id = boardgame.attributes.getNamedItem('objectid')!.value; const id = boardgame.attributes.getNamedItem('objectid')?.value;
const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')!.textContent!; const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined;
const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; const year = boardgame.querySelector('yearpublished')?.textContent ?? '';
ret.push( ret.push(
@ -49,7 +49,7 @@ export class BoardGameGeekAPI extends APIModel {
title, title,
englishTitle: title, englishTitle: title,
year, year,
} as BoardGameModel), }),
); );
} }
@ -72,21 +72,29 @@ export class BoardGameGeekAPI extends APIModel {
const response = new window.DOMParser().parseFromString(data, 'text/xml'); const response = new window.DOMParser().parseFromString(data, 'text/xml');
// console.debug(response); // console.debug(response);
const boardgame = response.querySelector('boardgame')!; const boardgame = response.querySelector('boardgame');
const title = boardgame.querySelector('name[primary=true]')!.textContent!; 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 year = boardgame.querySelector('yearpublished')?.textContent ?? '';
const image = boardgame.querySelector('image')?.textContent ?? undefined; const image = boardgame.querySelector('image')?.textContent ?? undefined;
const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); 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 complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0');
const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0');
const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0');
const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; 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({ return new BoardGameModel({
title: title, title: title ?? undefined,
englishTitle: title, englishTitle: title ?? undefined,
year: year === '0' ? '' : year, year: year === '0' ? '' : year,
dataSource: this.apiName, dataSource: this.apiName,
url: `https://boardgamegeek.com/boardgame/${id}`, url: `https://boardgamegeek.com/boardgame/${id}`,
@ -107,6 +115,6 @@ export class BoardGameGeekAPI extends APIModel {
played: false, played: false,
personalRating: 0, personalRating: 0,
}, },
} as BoardGameModel); });
} }
} }

View file

@ -0,0 +1,109 @@
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;
apiDateFormat: string = 'YYYY-MM-DD';
constructor(plugin: MediaDbPlugin) {
super();
this.plugin = plugin;
this.apiName = 'GiantBombAPI';
this.apiDescription = 'A free API for games.';
this.apiUrl = 'https://www.giantbomb.com/api';
this.types = [MediaType.Game];
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
if (!this.plugin.settings.GiantBombKey) {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `${this.apiUrl}/games?api_key=${this.plugin.settings.GiantBombKey}&filter=name:${encodeURIComponent(title)}&format=json`;
const fetchData = await requestUrl({
url: searchUrl,
});
// console.debug(fetchData);
if (fetchData.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (fetchData.status === 429) {
throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`);
}
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const data = await fetchData.json;
// console.debug(data);
const ret: MediaTypeModel[] = [];
for (const result of data.results) {
ret.push(
new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: new Date(result.original_release_date).getFullYear().toString(),
dataSource: this.apiName,
id: result.guid,
}),
);
}
return ret;
}
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
if (!this.plugin.settings.GiantBombKey) {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `${this.apiUrl}/game/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.GiantBombKey}&format=json`;
const fetchData = await requestUrl({
url: searchUrl,
});
console.debug(fetchData);
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const data = await fetchData.json;
// console.debug(data);
const result = data.results;
return new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: new Date(result.original_release_date).getFullYear().toString(),
dataSource: this.apiName,
url: result.site_detail_url,
id: result.guid,
developers: result.developers?.map((x: any) => x.name) ?? [],
publishers: result.publishers?.map((x: any) => x.name) ?? [],
genres: result.genres?.map((x: any) => x.name) ?? [],
onlineRating: 0,
image: result.image?.super_url ?? '',
released: true,
releaseDate: result.original_release_date ?? 'unknown',
userData: {
played: false,
personalRating: 0,
},
});
}
}

View file

@ -1,9 +1,9 @@
import { APIModel } from '../APIModel'; import type MediaDbPlugin from '../../main';
import { MediaTypeModel } from '../../models/MediaTypeModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MovieModel } from '../../models/MovieModel'; import { MovieModel } from '../../models/MovieModel';
import MediaDbPlugin from '../../main';
import { SeriesModel } from '../../models/SeriesModel'; import { SeriesModel } from '../../models/SeriesModel';
import { MediaType } from '../../utils/MediaType'; import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class MALAPI extends APIModel { export class MALAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -52,7 +52,7 @@ export class MALAPI extends APIModel {
year: result.year ?? result.aired?.prop?.from?.year ?? '', year: result.year ?? result.aired?.prop?.from?.year ?? '',
dataSource: this.apiName, dataSource: this.apiName,
id: result.mal_id, id: result.mal_id,
} as MovieModel), }),
); );
} }
if (type === 'movie' || type === 'special') { if (type === 'movie' || type === 'special') {
@ -64,7 +64,7 @@ export class MALAPI extends APIModel {
year: result.year ?? result.aired?.prop?.from?.year ?? '', year: result.year ?? result.aired?.prop?.from?.year ?? '',
dataSource: this.apiName, dataSource: this.apiName,
id: result.mal_id, id: result.mal_id,
} as MovieModel), }),
); );
} else if (type === 'series' || type === 'ova') { } else if (type === 'series' || type === 'ova') {
ret.push( ret.push(
@ -75,7 +75,7 @@ export class MALAPI extends APIModel {
year: result.year ?? result.aired?.prop?.from?.year ?? '', year: result.year ?? result.aired?.prop?.from?.year ?? '',
dataSource: this.apiName, dataSource: this.apiName,
id: result.mal_id, id: result.mal_id,
} as SeriesModel), }),
); );
} }
} }
@ -127,7 +127,7 @@ export class MALAPI extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, personalRating: 0,
}, },
} as MovieModel); });
} }
if (type === 'movie' || type === 'special') { if (type === 'movie' || type === 'special') {
@ -159,7 +159,7 @@ export class MALAPI extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, personalRating: 0,
}, },
} as MovieModel); });
} else if (type === 'series' || type === 'ova') { } else if (type === 'series' || type === 'ova') {
return new SeriesModel({ return new SeriesModel({
subType: type, subType: type,
@ -190,9 +190,9 @@ export class MALAPI extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, 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 type MediaDbPlugin from '../../main';
import { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { MangaModel } from '../../models/MangaModel'; import { MangaModel } from '../../models/MangaModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType'; import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class MALAPIManga extends APIModel { export class MALAPIManga extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -73,7 +73,7 @@ export class MALAPIManga extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, personalRating: 0,
}, },
} as MangaModel), }),
); );
} }
@ -123,6 +123,6 @@ export class MALAPIManga extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, personalRating: 0,
}, },
} as MangaModel); });
} }
} }

View file

@ -1,9 +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 { 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 { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class MobyGamesAPI extends APIModel { export class MobyGamesAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -22,7 +23,7 @@ export class MobyGamesAPI extends APIModel {
console.log(`MDB | api "${this.apiName}" queried by Title`); console.log(`MDB | api "${this.apiName}" queried by Title`);
if (!this.plugin.settings.MobyGamesKey) { if (!this.plugin.settings.MobyGamesKey) {
throw Error(`MDB | API key for ${this.apiName} missing.`); throw new Error(`MDB | API key for ${this.apiName} missing.`);
} }
const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`; const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`;
@ -104,6 +105,6 @@ export class MobyGamesAPI extends APIModel {
personalRating: 0, 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 { requestUrl } from 'obsidian';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MusicReleaseModel } from '../../models/MusicReleaseModel'; import { MusicReleaseModel } from '../../models/MusicReleaseModel';
import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils';
import { MediaType } from '../../utils/MediaType'; import { MediaType } from '../../utils/MediaType';
import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils';
import { APIModel } from '../APIModel';
export class MusicBrainzAPI extends APIModel { export class MusicBrainzAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -55,7 +55,7 @@ export class MusicBrainzAPI extends APIModel {
artists: result['artist-credit'].map((a: any) => a.name), artists: result['artist-credit'].map((a: any) => a.name),
subType: result['primary-type'], subType: result['primary-type'],
} as MusicReleaseModel), }),
); );
} }
@ -97,6 +97,6 @@ export class MusicBrainzAPI extends APIModel {
userData: { userData: {
personalRating: 0, personalRating: 0,
}, },
} as MusicReleaseModel); });
} }
} }

View file

@ -1,10 +1,11 @@
import { APIModel } from '../APIModel'; import { Notice } from 'obsidian';
import { MediaTypeModel } from '../../models/MediaTypeModel'; import type MediaDbPlugin from '../../main';
import { MovieModel } from '../../models/MovieModel';
import MediaDbPlugin from '../../main';
import { SeriesModel } from '../../models/SeriesModel';
import { GameModel } from '../../models/GameModel'; 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 { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class OMDbAPI extends APIModel { export class OMDbAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -29,7 +30,7 @@ export class OMDbAPI extends APIModel {
console.log(`MDB | api "${this.apiName}" queried by Title`); console.log(`MDB | api "${this.apiName}" queried by Title`);
if (!this.plugin.settings.OMDbKey) { if (!this.plugin.settings.OMDbKey) {
throw Error(`MDB | API key for ${this.apiName} missing.`); throw new Error(`MDB | API key for ${this.apiName} missing.`);
} }
const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`; const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`;
@ -73,7 +74,7 @@ export class OMDbAPI extends APIModel {
year: result.Year, year: result.Year,
dataSource: this.apiName, dataSource: this.apiName,
id: result.imdbID, id: result.imdbID,
} as MovieModel), }),
); );
} else if (type === 'series') { } else if (type === 'series') {
ret.push( ret.push(
@ -84,7 +85,7 @@ export class OMDbAPI extends APIModel {
year: result.Year, year: result.Year,
dataSource: this.apiName, dataSource: this.apiName,
id: result.imdbID, id: result.imdbID,
} as SeriesModel), }),
); );
} else if (type === 'game') { } else if (type === 'game') {
ret.push( ret.push(
@ -95,7 +96,7 @@ export class OMDbAPI extends APIModel {
year: result.Year, year: result.Year,
dataSource: this.apiName, dataSource: this.apiName,
id: result.imdbID, id: result.imdbID,
} as GameModel), }),
); );
} }
} }
@ -161,7 +162,7 @@ export class OMDbAPI extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, personalRating: 0,
}, },
} as MovieModel); });
} else if (type === 'series') { } else if (type === 'series') {
return new SeriesModel({ return new SeriesModel({
type: type, type: type,
@ -193,7 +194,7 @@ export class OMDbAPI extends APIModel {
lastWatched: '', lastWatched: '',
personalRating: 0, personalRating: 0,
}, },
} as SeriesModel); });
} else if (type === 'game') { } else if (type === 'game') {
return new GameModel({ return new GameModel({
type: type, type: type,
@ -217,9 +218,9 @@ export class OMDbAPI extends APIModel {
played: false, played: false,
personalRating: 0, 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 { BookModel } from 'src/models/BookModel';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType'; import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class OpenLibraryAPI extends APIModel { export class OpenLibraryAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -42,7 +42,7 @@ export class OpenLibraryAPI extends APIModel {
dataSource: this.apiName, dataSource: this.apiName,
id: result.key, id: result.key,
author: result.author_name ?? 'unknown', author: result.author_name ?? 'unknown',
} as BookModel), }),
); );
} }
@ -87,6 +87,6 @@ export class OpenLibraryAPI extends APIModel {
lastRead: '', lastRead: '',
personalRating: 0, 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 { 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 { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class SteamAPI extends APIModel { export class SteamAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -49,7 +49,7 @@ export class SteamAPI extends APIModel {
year: '', year: '',
dataSource: this.apiName, dataSource: this.apiName,
id: result.appid, id: result.appid,
} as GameModel), }),
); );
} }
@ -94,19 +94,19 @@ export class SteamAPI extends APIModel {
url: `https://store.steampowered.com/app/${result.steam_appid}`, url: `https://store.steampowered.com/app/${result.steam_appid}`,
id: result.steam_appid, id: result.steam_appid,
developers: result['developers'], developers: result.developers,
publishers: result['publishers'], publishers: result.publishers,
genres: result.genres?.map((x: any) => x.description) ?? [], genres: result.genres?.map((x: any) => x.description) ?? [],
onlineRating: Number.parseFloat(result.metacritic?.score ?? 0), onlineRating: Number.parseFloat(result.metacritic?.score ?? 0),
image: result.header_image ?? '', image: result.header_image ?? '',
released: !result.release_date?.comming_soon, released: !result.release_date?.coming_soon,
releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat) ?? 'unknown', releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat) ?? 'unknown',
userData: { userData: {
played: false, played: false,
personalRating: 0, personalRating: 0,
}, },
} as GameModel); });
} }
} }

View file

@ -1,8 +1,8 @@
import { APIModel } from '../APIModel'; import type MediaDbPlugin from '../../main';
import { MediaTypeModel } from '../../models/MediaTypeModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import { WikiModel } from '../../models/WikiModel'; import { WikiModel } from '../../models/WikiModel';
import { MediaType } from '../../utils/MediaType'; import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
export class WikipediaAPI extends APIModel { export class WikipediaAPI extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -42,7 +42,7 @@ export class WikipediaAPI extends APIModel {
year: '', year: '',
dataSource: this.apiName, dataSource: this.apiName,
id: result.pageid, id: result.pageid,
} as WikiModel), }),
); );
} }
@ -73,10 +73,10 @@ export class WikipediaAPI extends APIModel {
id: result.pageid, id: result.pageid,
wikiUrl: result.fullurl, 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, length: result.length,
userData: {}, userData: {},
} as WikiModel); });
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import { App, ButtonComponent, Modal, Setting } from 'obsidian'; import type { App, ButtonComponent } from 'obsidian';
import { SelectModalElement } from './SelectModalElement'; import { Modal, Setting } from 'obsidian';
import { mod } from '../utils/Utils'; import { mod } from '../utils/Utils';
import { SelectModalElement } from './SelectModalElement';
export abstract class SelectModal<T> extends Modal { export abstract class SelectModal<T> extends Modal {
allowMultiSelect: boolean; allowMultiSelect: boolean;
@ -142,7 +143,7 @@ export abstract class SelectModal<T> extends Modal {
} }
// nothing is highlighted // nothing is highlighted
this.selectModalElements.last().setHighlighted(true); this.selectModalElements.last()?.setHighlighted(true);
} }
highlightDown(): void { highlightDown(): void {
@ -154,20 +155,20 @@ export abstract class SelectModal<T> extends Modal {
} }
// nothing is highlighted // nothing is highlighted
this.selectModalElements.first().setHighlighted(true); this.selectModalElements.first()?.setHighlighted(true);
} }
private getNextSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> { private getNextSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id + 1; let nextId = selectModalElement.id + 1;
nextId = mod(nextId, this.selectModalElements.length); 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> { private getPreviousSelectModalElement(selectModalElement: SelectModalElement<T>): SelectModalElement<T> {
let nextId = selectModalElement.id - 1; let nextId = selectModalElement.id - 1;
nextId = mod(nextId, this.selectModalElements.length); 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> { export class SelectModalElement<T> {
selectModal: SelectModal<T>; selectModal: SelectModal<T>;
@ -35,6 +35,8 @@ export class SelectModalElement<T> {
this.element.on('mouseleave', '#' + this.getHTMLId(), () => { this.element.on('mouseleave', '#' + this.getHTMLId(), () => {
this.setHighlighted(false); this.setHighlighted(false);
}); });
this.highlighted = false;
} }
getHTMLId(): string { getHTMLId(): string {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +1,29 @@
import { MediaTypeModel } from './MediaTypeModel';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaType } from '../utils/MediaType'; import { MediaType } from '../utils/MediaType';
import type { ModelToData } from '../utils/Utils';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type MusicReleaseData = ModelToData<MusicReleaseModel>;
export class MusicReleaseModel extends MediaTypeModel { 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[]; genres: string[];
artists: string[]; artists: string[];
image: string;
rating: number; rating: number;
userData: { userData: {
personalRating: number; personalRating: number;
}; };
constructor(obj: any = {}) { constructor(obj: MusicReleaseData) {
super(); super();
this.genres = undefined; this.genres = [];
this.artists = undefined; this.artists = [];
this.image = undefined; this.image = '';
this.rating = undefined; this.rating = 0;
this.userData = { this.userData = {
personalRating: undefined, personalRating: 0,
}; };
migrateObject(this, obj, this); migrateObject(this, obj, this);

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { PropertyMappingOption } from './PropertyMapping'; import type MediaDbPlugin from '../main';
import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import MediaDbPlugin from '../main'; import { PropertyMappingOption } from './PropertyMapping';
export class PropertyMapper { export class PropertyMapper {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -66,7 +66,7 @@ export class PropertyMapper {
return obj; 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> = {}; 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 { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils';
import { MediaType } from '../utils/MediaType';
export enum PropertyMappingOption { export enum PropertyMappingOption {
Default = 'default', Default = 'default',

View file

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

View file

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

View file

@ -1,17 +1,19 @@
import { App, Notice, PluginSettingTab, Setting } from 'obsidian'; import type { App } from 'obsidian';
import { Notice, PluginSettingTab, Setting } from 'obsidian';
import MediaDbPlugin from '../main'; import { mount } from 'svelte';
import { FolderSuggest } from './suggesters/FolderSuggest'; import type MediaDbPlugin from '../main';
import { FileSuggest } from './suggesters/FileSuggest'; import type { MediaTypeModel } from '../models/MediaTypeModel';
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte';
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping';
import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import { MediaTypeModel } from '../models/MediaTypeModel';
import { fragWithHTML } from '../utils/Utils'; 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 { export interface MediaDbPluginSettings {
OMDbKey: string; OMDbKey: string;
MobyGamesKey: string; MobyGamesKey: string;
GiantBombKey: string;
sfwFilter: boolean; sfwFilter: boolean;
templates: boolean; templates: boolean;
customDateFormat: string; customDateFormat: string;
@ -78,6 +80,7 @@ export interface MediaDbPluginSettings {
const DEFAULT_SETTINGS: MediaDbPluginSettings = { const DEFAULT_SETTINGS: MediaDbPluginSettings = {
OMDbKey: '', OMDbKey: '',
MobyGamesKey: '', MobyGamesKey: '',
GiantBombKey: '',
sfwFilter: true, sfwFilter: true,
templates: true, templates: true,
customDateFormat: 'L', customDateFormat: 'L',
@ -203,6 +206,18 @@ export class MediaDbSettingTab extends PluginSettingTab {
}); });
}); });
new Setting(containerEl)
.setName('Giant Bomb Key')
.setDesc('API key for "www.giantbomb.com".')
.addText(cb => {
cb.setPlaceholder('API key')
.setValue(this.plugin.settings.GiantBombKey)
.onChange(data => {
this.plugin.settings.GiantBombKey = data;
void this.plugin.saveSettings();
});
});
new Setting(containerEl) new Setting(containerEl)
.setName('SFW filter') .setName('SFW filter')
.setDesc('Only shows SFW results for APIs that offer filtering.') .setDesc('Only shows SFW results for APIs that offer filtering.')
@ -240,7 +255,10 @@ export class MediaDbSettingTab extends PluginSettingTab {
.onChange(data => { .onChange(data => {
const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat;
this.plugin.settings.customDateFormat = newDateFormat; 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(); void this.plugin.saveSettings();
}); });
}); });
@ -675,7 +693,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
Don't forget to save your changes using the save button for each individual category. Don't forget to save your changes using the save button for each individual category.
</p>`; </p>`;
new PropertyMappingModelsComponent({ mount(PropertyMappingModelsComponent, {
target: this.containerEl, target: this.containerEl,
props: { props: {
models: this.plugin.settings.propertyMappingModels.map(x => x.copy()), 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 { TextInputSuggest } from './Suggest';
import { TAbstractFile, TFile } from 'obsidian';
export class FileSuggest extends TextInputSuggest<TFile> { export class FileSuggest extends TextInputSuggest<TFile> {
getSuggestions(inputStr: string): 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 // 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'; import { TextInputSuggest } from './Suggest';
export class FolderSuggest extends TextInputSuggest<TFolder> { 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 // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { App, ISuggestOwner, Scope } from 'obsidian'; import type { Instance as PopperInstance } from '@popperjs/core';
import { createPopper, 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'; import { wrapAround } from 'src/utils/Utils';
export class Suggest<T> { export class Suggest<T> {
private owner: ISuggestOwner<T>; private owner: ISuggestOwner<T>;
private values: T[]; private values: T[];
private suggestions: HTMLDivElement[]; private suggestions: HTMLElement[];
private selectedItem: number; private selectedItem: number;
private containerEl: HTMLElement; private containerEl: HTMLElement;
constructor(owner: ISuggestOwner<T>, containerEl: HTMLElement, scope: Scope) { constructor(owner: ISuggestOwner<T>, containerEl: HTMLElement, scope: Scope) {
this.owner = owner; this.owner = owner;
this.containerEl = containerEl; this.containerEl = containerEl;
this.values = [];
this.suggestions = [];
this.selectedItem = 0;
containerEl.on('click', '.suggestion-item', this.onSuggestionClick.bind(this)); containerEl.on('click', '.suggestion-item', (e, el) => this.onSuggestionClick(e, el));
containerEl.on('mousemove', '.suggestion-item', this.onSuggestionMouseover.bind(this)); containerEl.on('mousemove', '.suggestion-item', (e, el) => this.onSuggestionMouseover(e, el));
scope.register([], 'ArrowUp', event => { scope.register([], 'ArrowUp', event => {
if (!event.isComposing) { if (!event.isComposing) {
this.setSelectedItem(this.selectedItem - 1, true); this.setSelectedItem(this.selectedItem - 1, true);
return false; return false;
} }
return undefined;
}); });
scope.register([], 'ArrowDown', event => { scope.register([], 'ArrowDown', event => {
@ -30,6 +36,7 @@ export class Suggest<T> {
this.setSelectedItem(this.selectedItem + 1, true); this.setSelectedItem(this.selectedItem + 1, true);
return false; return false;
} }
return undefined;
}); });
scope.register([], 'Enter', event => { scope.register([], 'Enter', event => {
@ -37,10 +44,11 @@ export class Suggest<T> {
this.useSelectedItem(event); this.useSelectedItem(event);
return false; return false;
} }
return undefined;
}); });
} }
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { onSuggestionClick(event: MouseEvent, el: HTMLElement): void {
event.preventDefault(); event.preventDefault();
const item = this.suggestions.indexOf(el); const item = this.suggestions.indexOf(el);
@ -48,7 +56,7 @@ export class Suggest<T> {
this.useSelectedItem(event); this.useSelectedItem(event);
} }
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { onSuggestionMouseover(_event: MouseEvent, el: HTMLElement): void {
const item = this.suggestions.indexOf(el); const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false); this.setSelectedItem(item, false);
} }
@ -95,7 +103,7 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
protected app: App; protected app: App;
protected inputEl: HTMLInputElement; protected inputEl: HTMLInputElement;
private popper: PopperInstance; private popper?: PopperInstance;
private scope: Scope; private scope: Scope;
private suggestEl: HTMLElement; private suggestEl: HTMLElement;
private suggest: Suggest<T>; private suggest: Suggest<T>;
@ -126,13 +134,13 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
if (suggestions.length > 0) { if (suggestions.length > 0) {
this.suggest.setSuggestions(suggestions); this.suggest.setSuggestions(suggestions);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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 { open(container: HTMLElement, inputEl: HTMLElement): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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); container.appendChild(this.suggestEl);
this.popper = createPopper(inputEl, this.suggestEl, { this.popper = createPopper(inputEl, this.suggestEl, {
@ -162,7 +170,7 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
close(): void { close(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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.suggest.setSuggestions([]);
this.popper?.destroy(); this.popper?.destroy();

View file

@ -1,16 +1,17 @@
import { MediaDbPluginSettings } from '../settings/Settings'; import type { App, TAbstractFile, TFile } from 'obsidian';
import { MediaType } from './MediaType'; import { TFolder } from 'obsidian';
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 { BoardGameModel } from '../models/BoardGameModel'; import { BoardGameModel } from '../models/BoardGameModel';
import { BookModel } from '../models/BookModel'; 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 { WikiModel } from '../models/WikiModel';
import type { MediaDbPluginSettings } from '../settings/Settings';
import { MediaType } from './MediaType';
import { replaceTags } from './Utils';
export const MEDIA_TYPES: MediaType[] = [ export const MEDIA_TYPES: MediaType[] = [
MediaType.Movie, MediaType.Movie,
@ -28,7 +29,11 @@ export class MediaTypeManager {
mediaTemplateMap: Map<MediaType, string>; mediaTemplateMap: Map<MediaType, string>;
mediaFolderMap: 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 { updateTemplates(settings: MediaDbPluginSettings): void {
this.mediaFileNameTemplateMap = new Map<MediaType, string>(); this.mediaFileNameTemplateMap = new Map<MediaType, string>();
@ -66,7 +71,7 @@ export class MediaTypeManager {
getFileName(mediaTypeModel: MediaTypeModel): string { 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 // 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> { async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise<string> {
@ -76,7 +81,7 @@ export class MediaTypeManager {
return ''; 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. // 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 // 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))) { if (!(await app.vault.adapter.exists(folderPath))) {
await app.vault.createFolder(folderPath); await app.vault.createFolder(folderPath);
} }
const folder: TAbstractFile = app.vault.getAbstractFileByPath(folderPath); const folder = app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) { if (!(folder instanceof TFolder)) {
throw Error(`Expected ${folder} to be instance of TFolder`); throw Error(`Expected ${folder} to be instance of TFolder`);
@ -141,6 +146,6 @@ export class MediaTypeManager {
return new BookModel(obj); 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 { MediaDbAdvancedSearchModal } from '../modals/MediaDbAdvancedSearchModal';
import { MediaDbIdSearchModal } from '../modals/MediaDbIdSearchModal'; 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 { 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 { export enum ModalResultCode {
SUCCESS = 'SUCCESS', SUCCESS = 'SUCCESS',
@ -15,60 +15,59 @@ export enum ModalResultCode {
ERROR = 'ERROR', 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. * Object containing the data {@link ModalHelper.createSearchModal} returns.
* On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}. * On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}.
* On {@link ModalResultCode.ERROR} this contains a reference to that error. * On {@link ModalResultCode.ERROR} this contains a reference to that error.
*/ */
export interface SearchModalResult { export type SearchModalResult = ModalResult<SearchModalData>;
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR;
data?: SearchModalData;
error?: Error;
}
/** /**
* Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns. * Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns.
* On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}. * On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}.
* On {@link ModalResultCode.ERROR} this contains a reference to that error. * On {@link ModalResultCode.ERROR} this contains a reference to that error.
*/ */
export interface AdvancedSearchModalResult { export type AdvancedSearchModalResult = ModalResult<AdvancedSearchModalData>;
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR;
data?: AdvancedSearchModalData;
error?: Error;
}
/** /**
* Object containing the data {@link ModalHelper.createIdSearchModal} returns. * Object containing the data {@link ModalHelper.createIdSearchModal} returns.
* On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}. * On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}.
* On {@link ModalResultCode.ERROR} this contains a reference to that error. * On {@link ModalResultCode.ERROR} this contains a reference to that error.
*/ */
export interface IdSearchModalResult { export type IdSearchModalResult = ModalResult<IdSearchModalData>;
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR;
data?: IdSearchModalData;
error?: Error;
}
/** /**
* Object containing the data {@link ModalHelper.createSelectModal} returns. * Object containing the data {@link ModalHelper.createSelectModal} returns.
* On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}. * On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}.
* On {@link ModalResultCode.ERROR} this contains a reference to that error. * On {@link ModalResultCode.ERROR} this contains a reference to that error.
*/ */
export interface SelectModalResult { export type SelectModalResult = SkippableModalResult<SelectModalData>;
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.SKIP | ModalResultCode.ERROR;
data?: SelectModalData;
error?: Error;
}
/** /**
* Object containing the data {@link ModalHelper.createPreviewModal} returns. * Object containing the data {@link ModalHelper.createPreviewModal} returns.
* On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}. * On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}.
* On {@link ModalResultCode.ERROR} this contains a reference to that error. * On {@link ModalResultCode.ERROR} this contains a reference to that error.
*/ */
export interface PreviewModalResult { export type PreviewModalResult = ModalResult<PreviewModalData>;
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR;
data?: PreviewModalData;
error?: Error;
}
/** /**
* The data the search modal returns. * 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 * @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. * @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); const { searchModalResult, searchModal } = await this.createSearchModal(searchModalOptions);
console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`); console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`);
@ -271,7 +273,7 @@ export class ModalHelper {
return callbackRes; return callbackRes;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
new Notice(e.toString()); new Notice(`${e}`);
searchModal.close(); searchModal.close();
return undefined; return undefined;
} }
@ -314,7 +316,7 @@ export class ModalHelper {
async openAdvancedSearchModal( async openAdvancedSearchModal(
advancedSearchModalOptions: AdvancedSearchModalOptions, advancedSearchModalOptions: AdvancedSearchModalOptions,
submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise<MediaTypeModel[]>, submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise<MediaTypeModel[]>,
): Promise<MediaTypeModel[]> { ): Promise<MediaTypeModel[] | undefined> {
const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions); const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions);
console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`); console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`);
@ -337,7 +339,7 @@ export class ModalHelper {
return callbackRes; return callbackRes;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
new Notice(e.toString()); new Notice(`${e}`);
advancedSearchModal.close(); advancedSearchModal.close();
return undefined; return undefined;
} }
@ -377,8 +379,8 @@ export class ModalHelper {
*/ */
async openIdSearchModal( async openIdSearchModal(
idSearchModalOptions: IdSearchModalOptions, idSearchModalOptions: IdSearchModalOptions,
submitCallback: (idSearchModalData: IdSearchModalData) => Promise<MediaTypeModel>, submitCallback: (idSearchModalData: IdSearchModalData) => Promise<MediaTypeModel | undefined>,
): Promise<MediaTypeModel> { ): Promise<MediaTypeModel | undefined> {
const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions); const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions);
console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`); console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`);
@ -396,12 +398,12 @@ export class ModalHelper {
} }
try { try {
const callbackRes: MediaTypeModel = await submitCallback(idSearchModalResult.data); const callbackRes = await submitCallback(idSearchModalResult.data);
idSearchModal.close(); idSearchModal.close();
return callbackRes; return callbackRes;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
new Notice(e.toString()); new Notice(`${e}`);
idSearchModal.close(); idSearchModal.close();
return undefined; 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 * @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. * @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); const { selectModalResult, selectModal } = await this.createSelectModal(selectModalOptions);
console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`); console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`);
@ -468,7 +473,7 @@ export class ModalHelper {
return callbackRes; return callbackRes;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
new Notice(e.toString()); new Notice(`${e}`);
selectModal.close(); selectModal.close();
return; return;
} }
@ -500,12 +505,12 @@ export class ModalHelper {
console.warn(previewModalResult.error); console.warn(previewModalResult.error);
new Notice(previewModalResult.error.toString()); new Notice(previewModalResult.error.toString());
previewModal.close(); previewModal.close();
return undefined; return true;
} }
if (previewModalResult.code === ModalResultCode.CLOSE) { if (previewModalResult.code === ModalResultCode.CLOSE) {
// modal is already being closed // modal is already being closed
return undefined; return true;
} }
try { try {
@ -514,9 +519,9 @@ export class ModalHelper {
return callbackRes; return callbackRes;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
new Notice(e.toString()); new Notice(`${e}`);
previewModal.close(); previewModal.close();
return; return true;
} }
} }
} }

View file

@ -1,5 +1,5 @@
import { MediaTypeModel } from '../models/MediaTypeModel'; import type { TFile, TFolder, App } from 'obsidian';
import { TFile, TFolder, App } from 'obsidian'; import type { MediaTypeModel } from '../models/MediaTypeModel';
export const pluginName: string = 'obsidian-media-db-plugin'; export const pluginName: string = 'obsidian-media-db-plugin';
export const contactEmail: string = 'm.projects.code@gmail.com'; export const contactEmail: string = 'm.projects.code@gmail.com';
@ -15,7 +15,7 @@ export function wrapAround(value: number, size: number): number {
} }
export function containsOnlyLettersAndUnderscores(str: string): boolean { export function containsOnlyLettersAndUnderscores(str: string): boolean {
return /^[a-zA-Z_]+$/.test(str); return /^[\p{Letter}\p{M}_]+$/u.test(str);
} }
export function replaceIllegalFileNameCharactersInString(string: string): string { export function replaceIllegalFileNameCharactersInString(string: string): string {
@ -82,7 +82,7 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin
return '{{ INVALID TEMPLATE TAG }}'; return '{{ INVALID TEMPLATE TAG }}';
} }
function traverseMetaData(path: Array<string>, mediaTypeModel: MediaTypeModel): any { function traverseMetaData(path: string[], mediaTypeModel: MediaTypeModel): any {
let o: any = mediaTypeModel; let o: any = mediaTypeModel;
for (const part of path) { 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> { export async function useTemplaterPluginInFile(app: App, file: TFile): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const templater = (app as any).plugins.plugins['templater-obsidian']; 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); 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": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"module": "ESNext", "module": "ESNext",
"target": "ES6", "target": "ESNext",
"allowJs": true, "allowJs": true,
"checkJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"strict": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"sourceMap": true,
"lib": ["DOM", "ESNext"], "lib": ["DOM", "ESNext"],
"types": ["svelte"],
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": ["src/**/*.ts", "tests/**/*.ts"] "include": ["src/**/*.ts", "tests/**/*.ts"]

View file

@ -2,5 +2,6 @@
"0.1.7": "0.14.0", "0.1.7": "0.14.0",
"0.7.0": "1.5.0", "0.7.0": "1.5.0",
"0.7.1": "1.5.0", "0.7.1": "1.5.0",
"0.7.2": "1.5.0" "0.7.2": "1.5.0",
"0.8.0": "1.5.0"
} }