bye svelte; welcome solid
This commit is contained in:
parent
97cf350a32
commit
2fe6f6fc37
21 changed files with 686 additions and 569 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
github: ['mProjectsCode']
|
||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
bun install
|
bun install
|
||||||
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 dist/main.js dist/manifest.json dist/styles.css ${{ env.PLUGIN_NAME }}
|
||||||
zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }}
|
zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }}
|
||||||
ls
|
ls
|
||||||
|
|
||||||
|
|
@ -49,6 +49,6 @@ jobs:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip
|
${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip
|
||||||
main.js
|
dist/main.js
|
||||||
manifest.json
|
dist/manifest.json
|
||||||
styles.css
|
dist/styles.css
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -37,4 +37,4 @@ exampleVault/.obsidian/plugins/obsidian-media-db-plugin/*
|
||||||
|
|
||||||
exampleVault/Media DB/*
|
exampleVault/Media DB/*
|
||||||
meta.txt
|
meta.txt
|
||||||
main.js
|
dist/
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import manifest from '../../manifest.json' assert { type: 'json' };
|
import manifest from '../../manifest.json' with { type: 'json' };
|
||||||
|
|
||||||
export function getBuildBanner(buildType: string, getVersion: (version: string) => string) {
|
export function getBuildBanner(buildType: string, getVersion: (version: string) => string) {
|
||||||
return `/*
|
return `/*
|
||||||
|
|
@ -9,30 +9,16 @@ By: ${manifest.author} (${manifest.authorUrl})
|
||||||
Time: ${new Date().toUTCString()}
|
Time: ${new Date().toUTCString()}
|
||||||
Version: ${getVersion(manifest.version)}
|
Version: ${getVersion(manifest.version)}
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
THIS IS A GENERATED/BUNDLED FILE
|
||||||
if you want to view the source, please visit the github repository of this plugin
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) ${new Date().getFullYear()} ${manifest.author}
|
Copyright (c) ${new Date().getFullYear()} ${manifest.author}
|
||||||
|
-------------------------------------------
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
*/
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -7,7 +7,7 @@ import * as plugin_import from 'eslint-plugin-import';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.svelte', 'src/api/schemas/'],
|
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css', 'src/api/schemas/'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['src/**/*.ts'],
|
files: ['src/**/*.ts'],
|
||||||
|
|
|
||||||
31
package.json
31
package.json
|
|
@ -4,18 +4,17 @@
|
||||||
"description": "A plugin that can query multiple APIs for movies, series, anime, manga, books, comics, games, music and wiki articles, and import them into your vault.",
|
"description": "A plugin that can query multiple APIs for movies, series, anime, manga, books, comics, games, music and wiki articles, and import them into your vault.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run automation/build/esbuild.dev.config.ts",
|
"dev": "vite build --watch --mode development",
|
||||||
"build": "bun run tsc && bun run automation/build/esbuild.config.ts",
|
"build": "bun run tsc && vite build --mode production",
|
||||||
"tsc": "tsc -noEmit -skipLibCheck",
|
"tsc": "tsc -noEmit -skipLibCheck",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:log": "LOG_TESTS=true bun test",
|
"test:log": "LOG_TESTS=true bun test",
|
||||||
"format": "prettier --write --plugin prettier-plugin-svelte .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check --plugin prettier-plugin-svelte .",
|
"format:check": "prettier --check .",
|
||||||
"lint": "eslint --max-warnings=0 src/**",
|
"lint": "eslint --max-warnings=0 --no-warn-ignored src/**",
|
||||||
"lint:fix": "eslint --max-warnings=0 --fix src/**",
|
"lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**",
|
||||||
"svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"",
|
"check": "bun run format:check && bun run tsc && bun run lint",
|
||||||
"check": "bun run format:check && bun run tsc && bun run lint && bun run svelte-check",
|
"check:fix": "bun run format && bun run tsc && bun run lint:fix",
|
||||||
"check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run svelte-check",
|
|
||||||
"release": "bun run automation/release.ts",
|
"release": "bun run automation/release.ts",
|
||||||
"stats": "bun run automation/stats.ts"
|
"stats": "bun run automation/stats.ts"
|
||||||
},
|
},
|
||||||
|
|
@ -28,9 +27,6 @@
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@types/bun": "^1.3.7",
|
"@types/bun": "^1.3.7",
|
||||||
"builtin-modules": "^5.0.0",
|
"builtin-modules": "^5.0.0",
|
||||||
"esbuild": "^0.25.12",
|
|
||||||
"esbuild-plugin-copy-watch": "^2.3.1",
|
|
||||||
"esbuild-svelte": "^0.9.4",
|
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-only-warn": "^1.1.0",
|
"eslint-plugin-only-warn": "^1.1.0",
|
||||||
|
|
@ -39,13 +35,14 @@
|
||||||
"openapi-fetch": "^0.14.1",
|
"openapi-fetch": "^0.14.1",
|
||||||
"openapi-typescript": "^7.10.1",
|
"openapi-typescript": "^7.10.1",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-svelte": "^3.4.1",
|
"solid-js": "^1.9.3",
|
||||||
"string-argv": "^0.3.2",
|
"string-argv": "^0.3.2",
|
||||||
"svelte": "^5.48.5",
|
|
||||||
"svelte-check": "^4.3.5",
|
|
||||||
"svelte-preprocess": "^6.0.3",
|
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.54.0"
|
"typescript-eslint": "^8.54.0",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vite-plugin-banner": "^0.8.1",
|
||||||
|
"vite-plugin-solid": "^2.11.0",
|
||||||
|
"vite-plugin-static-copy": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/main.ts
36
src/main.ts
|
|
@ -23,7 +23,7 @@ import type {SeasonSelectModalElement} from './modals/MediaDbSeasonSelectModal';
|
||||||
import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal';
|
import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal';
|
||||||
import type { MediaTypeModel } from './models/MediaTypeModel';
|
import type { MediaTypeModel } from './models/MediaTypeModel';
|
||||||
import { PropertyMapper } from './settings/PropertyMapper';
|
import { PropertyMapper } from './settings/PropertyMapper';
|
||||||
import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping';
|
import { PropertyMappingModel } from './settings/PropertyMapping';
|
||||||
import type { MediaDbPluginSettings } from './settings/Settings';
|
import type { MediaDbPluginSettings } from './settings/Settings';
|
||||||
import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings';
|
import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings';
|
||||||
import { BulkImportHelper } from './utils/BulkImportHelper';
|
import { BulkImportHelper } from './utils/BulkImportHelper';
|
||||||
|
|
@ -33,6 +33,7 @@ import type { SearchModalOptions } from './utils/ModalHelper';
|
||||||
import { ModalHelper } from './utils/ModalHelper';
|
import { ModalHelper } from './utils/ModalHelper';
|
||||||
import type { CreateNoteOptions } from './utils/Utils';
|
import type { CreateNoteOptions } from './utils/Utils';
|
||||||
import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils';
|
import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils';
|
||||||
|
import 'src/styles.css';
|
||||||
|
|
||||||
export type Metadata = Record<string, unknown>;
|
export type Metadata = Record<string, unknown>;
|
||||||
|
|
||||||
|
|
@ -652,39 +653,18 @@ export default class MediaDbPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(): Promise<void> {
|
async loadSettings(): Promise<void> {
|
||||||
// console.log(DEFAULT_SETTINGS);
|
|
||||||
const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings;
|
const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings;
|
||||||
const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this);
|
const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this);
|
||||||
const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings);
|
const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings);
|
||||||
|
|
||||||
// migrate the settings loaded from the disk to match the structure of the default settings
|
// Migrate property mappings using the dedicated migration method
|
||||||
const newPropertyMappings: PropertyMappingModel[] = [];
|
const migratedModels = PropertyMappingModel.migrateModels(
|
||||||
for (const defaultPropertyMappingModel of defaultSettings.propertyMappingModels) {
|
loadedSettings.propertyMappingModels || [],
|
||||||
const newPropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type);
|
defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)),
|
||||||
if (newPropertyMappingModel === undefined) {
|
|
||||||
// if the propertyMappingModel exists in the default settings but not the loaded settings, add it
|
|
||||||
newPropertyMappings.push(defaultPropertyMappingModel);
|
|
||||||
} else {
|
|
||||||
// if the propertyMappingModel also exists in the loaded settings, add it from there
|
|
||||||
const newProperties: PropertyMapping[] = [];
|
|
||||||
|
|
||||||
for (const defaultProperty of defaultPropertyMappingModel.properties) {
|
|
||||||
const newProperty = newPropertyMappingModel.properties.find(x => x.property === defaultProperty.property);
|
|
||||||
if (newProperty === undefined) {
|
|
||||||
// default property is an instance
|
|
||||||
newProperties.push(defaultProperty);
|
|
||||||
} else {
|
|
||||||
// newProperty is just an object and take locked status from default property
|
|
||||||
newProperties.push(
|
|
||||||
new PropertyMapping(newProperty.property, newProperty.newProperty, newProperty.mapping, defaultProperty.locked, newProperty.wikilink ?? false),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newPropertyMappings.push(new PropertyMappingModel(newPropertyMappingModel.type, newProperties));
|
// Store as plain data for serialization
|
||||||
}
|
loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON());
|
||||||
}
|
|
||||||
loadedSettings.propertyMappingModels = newPropertyMappings;
|
|
||||||
|
|
||||||
this.settings = loadedSettings;
|
this.settings = loadedSettings;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<!--adapted from @joethei's code: https://github.com/joethei/obsidian-rss/blob/master/src/view/IconComponent.svelte-->
|
|
||||||
<!--adapted from @javalent's code: https://discord.com/channels/686053708261228577/840286264964022302/902949764209987654-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { setIcon } from 'obsidian';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
iconName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { iconName = '' }: Props = $props();
|
|
||||||
|
|
||||||
let iconEl: HTMLElement | undefined = $state();
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
setIcon(iconEl!, iconName);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if iconName.length > 0}
|
|
||||||
<div class="icon-wrapper">
|
|
||||||
<div bind:this={iconEl} class="icon"></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon-wrapper {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
position: absolute;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
top: calc(50% - 10px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
24
src/settings/Icon.tsx
Normal file
24
src/settings/Icon.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { onMount, Show } from 'solid-js';
|
||||||
|
import { setIcon } from 'obsidian';
|
||||||
|
|
||||||
|
interface IconProps {
|
||||||
|
iconName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Icon(props: IconProps) {
|
||||||
|
let iconEl: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (iconEl) {
|
||||||
|
setIcon(iconEl, props.iconName || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={(props.iconName || '').length > 0}>
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<div ref={iconEl} class="icon"></div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -27,8 +27,12 @@ export class PropertyMapper {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type);
|
||||||
const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties;
|
if (!propertyMappingModel) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyMappings = propertyMappingModel.properties;
|
||||||
|
|
||||||
const newObj: Record<string, unknown> = {};
|
const newObj: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
|
@ -80,7 +84,8 @@ export class PropertyMapper {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type)?.properties ?? [];
|
const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type);
|
||||||
|
const propertyMappings = propertyMappingModel?.properties ?? [];
|
||||||
|
|
||||||
const originalObj: Record<string, unknown> = {};
|
const originalObj: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
import type { MediaType } from '../utils/MediaType';
|
import type { MediaType } from '../utils/MediaType';
|
||||||
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils';
|
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils';
|
||||||
|
|
||||||
|
// Plain object interfaces for serialization
|
||||||
|
export interface PropertyMappingData {
|
||||||
|
property: string;
|
||||||
|
newProperty: string;
|
||||||
|
mapping: PropertyMappingOption;
|
||||||
|
locked?: boolean;
|
||||||
|
wikilink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertyMappingModelData {
|
||||||
|
type: MediaType;
|
||||||
|
properties: PropertyMappingData[];
|
||||||
|
}
|
||||||
|
|
||||||
export enum PropertyMappingOption {
|
export enum PropertyMappingOption {
|
||||||
Default = 'default',
|
Default = 'default',
|
||||||
Map = 'remap',
|
Map = 'remap',
|
||||||
|
|
@ -80,6 +94,72 @@ export class PropertyMappingModel {
|
||||||
}
|
}
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialization - returns a plain object that can be JSON.stringify'd
|
||||||
|
toJSON(): PropertyMappingModelData {
|
||||||
|
return {
|
||||||
|
type: this.type,
|
||||||
|
properties: this.properties.map(p => p.toJSON()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialization - creates a PropertyMappingModel from a plain object
|
||||||
|
static fromJSON(json: PropertyMappingModelData): PropertyMappingModel {
|
||||||
|
return new PropertyMappingModel(
|
||||||
|
json.type,
|
||||||
|
json.properties.map(p => PropertyMapping.fromJSON(p)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates loaded settings to match the structure of default settings.
|
||||||
|
* - Adds new properties from defaults that don't exist in loaded settings
|
||||||
|
* - Preserves user customizations from loaded settings
|
||||||
|
* - Updates locked status from defaults
|
||||||
|
*
|
||||||
|
* @param loadedModels - Models loaded from disk (may be outdated)
|
||||||
|
* @param defaultModels - Current default models (source of truth for structure)
|
||||||
|
* @returns Migrated models with correct structure and preserved user settings
|
||||||
|
*/
|
||||||
|
static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] {
|
||||||
|
const migratedModels: PropertyMappingModel[] = [];
|
||||||
|
|
||||||
|
for (const defaultModel of defaultModels) {
|
||||||
|
const loadedModel = loadedModels.find(m => m.type === defaultModel.type);
|
||||||
|
|
||||||
|
if (!loadedModel) {
|
||||||
|
// New model type - use default
|
||||||
|
migratedModels.push(defaultModel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate properties
|
||||||
|
const migratedProperties: PropertyMapping[] = [];
|
||||||
|
for (const defaultProperty of defaultModel.properties) {
|
||||||
|
const loadedProperty = loadedModel.properties.find(p => p.property === defaultProperty.property);
|
||||||
|
|
||||||
|
if (!loadedProperty) {
|
||||||
|
// New property - use default
|
||||||
|
migratedProperties.push(defaultProperty);
|
||||||
|
} else {
|
||||||
|
// Existing property - merge: take locked from default, customizations from loaded
|
||||||
|
migratedProperties.push(
|
||||||
|
new PropertyMapping(
|
||||||
|
loadedProperty.property,
|
||||||
|
loadedProperty.newProperty,
|
||||||
|
loadedProperty.mapping,
|
||||||
|
defaultProperty.locked, // locked status from default
|
||||||
|
loadedProperty.wikilink ?? false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migratedModels.push(new PropertyMappingModel(defaultModel.type, migratedProperties));
|
||||||
|
}
|
||||||
|
|
||||||
|
return migratedModels;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PropertyMapping {
|
export class PropertyMapping {
|
||||||
|
|
@ -153,4 +233,20 @@ export class PropertyMapping {
|
||||||
|
|
||||||
return this.property;
|
return this.property;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialization - returns a plain object
|
||||||
|
toJSON(): PropertyMappingData {
|
||||||
|
return {
|
||||||
|
property: this.property,
|
||||||
|
newProperty: this.newProperty,
|
||||||
|
mapping: this.mapping,
|
||||||
|
locked: this.locked,
|
||||||
|
wikilink: this.wikilink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialization - creates a PropertyMapping from a plain object
|
||||||
|
static fromJSON(json: PropertyMappingData): PropertyMapping {
|
||||||
|
return new PropertyMapping(json.property, json.newProperty, json.mapping, json.locked, json.wikilink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions } from './PropertyMapping';
|
|
||||||
import { capitalizeFirstLetter } from '../utils/Utils';
|
|
||||||
import Icon from './Icon.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
model: PropertyMappingModel;
|
|
||||||
save: (model: PropertyMappingModel) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { model, save }: Props = $props();
|
|
||||||
|
|
||||||
let unsavedChanges = $state(false);
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
|
|
||||||
let validationResult: { res: boolean; err?: Error } | undefined = $derived(model.validate());
|
|
||||||
|
|
||||||
function onModelUpdate() {
|
|
||||||
unsavedChanges = true;
|
|
||||||
model = model.copy();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="media-db-plugin-property-mappings-model-container">
|
|
||||||
<div class="media-db-plugin-property-mappings-model-header">
|
|
||||||
<div class="setting-item-name">{capitalizeFirstLetter(model.type)}</div>
|
|
||||||
|
|
||||||
<div class="media-db-plugin-property-mappings-model-actions">
|
|
||||||
{#if unsavedChanges}
|
|
||||||
<div class="media-db-plugin-property-mapping-unsaved-changes">Unsaved changes</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="media-db-plugin-property-mappings-save-button {validationResult?.res ? 'mod-cta' : 'mod-muted'}"
|
|
||||||
onclick={() => {
|
|
||||||
if (model.validate().res) {
|
|
||||||
save(model);
|
|
||||||
unsavedChanges = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !validationResult?.res}
|
|
||||||
<div class="media-db-plugin-property-mapping-validation">
|
|
||||||
{validationResult?.err?.message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="media-db-plugin-property-mappings-table-container">
|
|
||||||
<table class="media-db-plugin-property-mappings-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="col-property">Property</th>
|
|
||||||
<th class="col-mapping">Mapping</th>
|
|
||||||
<th class="col-new-name">New name</th>
|
|
||||||
<th class="col-wikilink">Wikilink</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each model.properties as property}
|
|
||||||
<tr>
|
|
||||||
<td class="col-property">
|
|
||||||
<code>{property.property}</code>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{#if property.locked}
|
|
||||||
<td class="col-locked" colspan="3">
|
|
||||||
<div class="media-db-plugin-property-binding-text">property cannot be remapped</div>
|
|
||||||
</td>
|
|
||||||
{:else}
|
|
||||||
<td class="col-mapping">
|
|
||||||
<select class="dropdown" bind:value={property.mapping} onchange={() => onModelUpdate()}>
|
|
||||||
{#each propertyMappingOptions as remappingOption}
|
|
||||||
<option value={remappingOption}>
|
|
||||||
{remappingOption}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="col-new-name">
|
|
||||||
{#if property.mapping === PropertyMappingOption.Map}
|
|
||||||
<div class="media-db-plugin-property-mapping-to">
|
|
||||||
<Icon iconName="arrow-right" />
|
|
||||||
<input
|
|
||||||
class="media-db-plugin-property-mapping-input"
|
|
||||||
type="text"
|
|
||||||
spellcheck="false"
|
|
||||||
bind:value={property.newProperty}
|
|
||||||
onchange={() => onModelUpdate()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<span class="media-db-plugin-property-mapping-to-disabled">—</span>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="col-wikilink">
|
|
||||||
<label class="media-db-plugin-property-mapping-wikilink-label" title="Convert value to wikilink ([[value]])">
|
|
||||||
<input type="checkbox" bind:checked={property.wikilink} onchange={() => onModelUpdate()} />
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Container */
|
|
||||||
.media-db-plugin-property-mappings-model-container {
|
|
||||||
margin-bottom: var(--size-4-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header and actions */
|
|
||||||
.media-db-plugin-property-mappings-model-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--size-4-4);
|
|
||||||
gap: var(--size-4-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-model-header .setting-item-name {
|
|
||||||
font-weight: var(--font-semibold);
|
|
||||||
font-size: var(--font-ui-medium);
|
|
||||||
color: var(--text-normal);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-model-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--size-4-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mapping-unsaved-changes {
|
|
||||||
color: var(--text-warning);
|
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-save-button {
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-save-button.mod-muted {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validation error */
|
|
||||||
.media-db-plugin-property-mapping-validation {
|
|
||||||
color: var(--text-error);
|
|
||||||
background: rgba(var(--color-red-rgb), 0.1);
|
|
||||||
padding: var(--size-4-3) var(--size-4-4);
|
|
||||||
margin-bottom: var(--size-4-4);
|
|
||||||
border-left: 3px solid var(--text-error);
|
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
line-height: 1.5;
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table container */
|
|
||||||
.media-db-plugin-property-mappings-table-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table header */
|
|
||||||
.media-db-plugin-property-mappings-table thead {
|
|
||||||
border-bottom: 1px solid var(--background-modifier-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-table th {
|
|
||||||
padding: var(--size-4-2) var(--size-4-3);
|
|
||||||
padding-left: 0;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: var(--font-semibold);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-ui-smaller);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table body */
|
|
||||||
.media-db-plugin-property-mappings-table tbody tr {
|
|
||||||
transition: background-color 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-table td {
|
|
||||||
padding: var(--size-4-3) var(--size-4-3) var(--size-4-3) 0;
|
|
||||||
border-bottom: 1px solid var(--background-modifier-border-hover);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mappings-table tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Column widths */
|
|
||||||
.col-property {
|
|
||||||
width: 25%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-mapping {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-new-name {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-wikilink {
|
|
||||||
width: 15%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-locked {
|
|
||||||
text-align: center;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Property name styling */
|
|
||||||
.media-db-plugin-property-mappings-table code {
|
|
||||||
padding: var(--size-4-1) var(--size-4-2);
|
|
||||||
margin: 0;
|
|
||||||
background: var(--code-background);
|
|
||||||
color: var(--code-normal);
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
font-size: var(--font-ui-smaller);
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Locked property text */
|
|
||||||
.media-db-plugin-property-binding-text {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown select - use Obsidian defaults */
|
|
||||||
.media-db-plugin-property-mappings-table select.dropdown {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remap input */
|
|
||||||
.media-db-plugin-property-mapping-to {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--size-4-2);
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mapping-input {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mapping-to-disabled {
|
|
||||||
color: var(--text-faint);
|
|
||||||
font-size: var(--font-ui-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wikilink checkbox */
|
|
||||||
.media-db-plugin-property-mapping-wikilink-label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--size-4-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-property-mapping-wikilink-label input[type='checkbox'] {
|
|
||||||
cursor: pointer;
|
|
||||||
width: var(--checkbox-size);
|
|
||||||
height: var(--checkbox-size);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
138
src/settings/PropertyMappingModelComponent.tsx
Normal file
138
src/settings/PropertyMappingModelComponent.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { createSignal, createMemo, For, Show } from 'solid-js';
|
||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping';
|
||||||
|
import { capitalizeFirstLetter } from '../utils/Utils';
|
||||||
|
import Icon from './Icon';
|
||||||
|
|
||||||
|
interface PropertyMappingModelComponentProps {
|
||||||
|
model: PropertyMappingModelData;
|
||||||
|
save: (model: PropertyMappingModelData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) {
|
||||||
|
const [unsavedChanges, setUnsavedChanges] = createSignal(false);
|
||||||
|
|
||||||
|
// Create a store from the model's plain data
|
||||||
|
const [modelData, setModelData] = createStore(props.model);
|
||||||
|
|
||||||
|
// Derive the validation result reactively
|
||||||
|
const validationResult = createMemo(() => {
|
||||||
|
const model = PropertyMappingModel.fromJSON(modelData);
|
||||||
|
return model.validate();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onModelUpdate = () => {
|
||||||
|
setUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const model = PropertyMappingModel.fromJSON(modelData);
|
||||||
|
if (model.validate().res) {
|
||||||
|
props.save(model);
|
||||||
|
setUnsavedChanges(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="media-db-plugin-property-mappings-model-container">
|
||||||
|
<div class="media-db-plugin-property-mappings-model-header">
|
||||||
|
<div class="setting-item-name">{capitalizeFirstLetter(modelData.type)}</div>
|
||||||
|
|
||||||
|
<div class="media-db-plugin-property-mappings-model-actions">
|
||||||
|
<Show when={unsavedChanges()}>
|
||||||
|
<div class="media-db-plugin-property-mapping-unsaved-changes">Unsaved changes</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<button class={`media-db-plugin-property-mappings-save-button ${validationResult().res ? 'mod-cta' : 'mod-muted'}`} onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!validationResult().res}>
|
||||||
|
<div class="media-db-plugin-property-mapping-validation">{validationResult().err?.message}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="media-db-plugin-property-mappings-table-container">
|
||||||
|
<table class="media-db-plugin-property-mappings-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-property">Property</th>
|
||||||
|
<th class="col-mapping">Mapping</th>
|
||||||
|
<th class="col-new-name">New name</th>
|
||||||
|
<th class="col-wikilink">Wikilink</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={modelData.properties}>
|
||||||
|
{(property, index) => (
|
||||||
|
<tr>
|
||||||
|
<td class="col-property">
|
||||||
|
<code>{property.property}</code>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={!property.locked}
|
||||||
|
fallback={
|
||||||
|
<td class="col-locked" colspan={3}>
|
||||||
|
<div class="media-db-plugin-property-binding-text">property cannot be remapped</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<td class="col-mapping">
|
||||||
|
<select
|
||||||
|
class="dropdown"
|
||||||
|
value={property.mapping}
|
||||||
|
onChange={e => {
|
||||||
|
setModelData('properties', index(), 'mapping', e.currentTarget.value as PropertyMappingOption);
|
||||||
|
setModelData('properties', index(), 'newProperty', '');
|
||||||
|
onModelUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={propertyMappingOptions}>{remappingOption => <option value={remappingOption}>{remappingOption}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="col-new-name">
|
||||||
|
<Show
|
||||||
|
when={property.mapping === PropertyMappingOption.Map}
|
||||||
|
fallback={<span class="media-db-plugin-property-mapping-to-disabled">—</span>}
|
||||||
|
>
|
||||||
|
<div class="media-db-plugin-property-mapping-to">
|
||||||
|
<Icon iconName="arrow-right" />
|
||||||
|
<input
|
||||||
|
class="media-db-plugin-property-mapping-input"
|
||||||
|
type="text"
|
||||||
|
spellcheck={false}
|
||||||
|
value={property.newProperty}
|
||||||
|
onInput={e => {
|
||||||
|
setModelData('properties', index(), 'newProperty', e.currentTarget.value);
|
||||||
|
onModelUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="col-wikilink">
|
||||||
|
<label class="media-db-plugin-property-mapping-wikilink-label" title="Convert value to wikilink ([[value]])">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={property.wikilink}
|
||||||
|
onChange={e => {
|
||||||
|
setModelData('properties', index(), 'wikilink', e.currentTarget.checked);
|
||||||
|
onModelUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</Show>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { PropertyMappingModel } from './PropertyMapping';
|
|
||||||
import PropertyMappingModelComponent from './PropertyMappingModelComponent.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
models?: PropertyMappingModel[];
|
|
||||||
save: (model: PropertyMappingModel) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { models = [], save }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="setting-item" style="display: flex; gap: 10px; flex-direction: column; align-items: stretch;">
|
|
||||||
{#each models as model}
|
|
||||||
<PropertyMappingModelComponent {model} {save}></PropertyMappingModelComponent>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<pre>{JSON.stringify(models, null, 4)}</pre>
|
|
||||||
-->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
16
src/settings/PropertyMappingModelsComponent.tsx
Normal file
16
src/settings/PropertyMappingModelsComponent.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { For } from 'solid-js';
|
||||||
|
import { type PropertyMappingModelData } from './PropertyMapping';
|
||||||
|
import PropertyMappingModelComponent from './PropertyMappingModelComponent';
|
||||||
|
|
||||||
|
interface PropertyMappingModelsComponentProps {
|
||||||
|
models?: PropertyMappingModelData[];
|
||||||
|
save: (model: PropertyMappingModelData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PropertyMappingModelsComponent(props: PropertyMappingModelsComponentProps) {
|
||||||
|
return (
|
||||||
|
<div class="setting-item" style={{ display: 'flex', gap: '10px', 'flex-direction': 'column', 'align-items': 'stretch' }}>
|
||||||
|
<For each={props.models || []}>{model => <PropertyMappingModelComponent model={model} save={props.save} />}</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import type { App } from 'obsidian';
|
import type { App } from 'obsidian';
|
||||||
import { Notice, PluginSettingTab, SettingGroup } from 'obsidian';
|
import { Notice, PluginSettingTab, SettingGroup } from 'obsidian';
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
import { MediaType } from 'src/utils/MediaType';
|
import { MediaType } from 'src/utils/MediaType';
|
||||||
import { mount } from 'svelte';
|
|
||||||
import type MediaDbPlugin from '../main';
|
import type MediaDbPlugin from '../main';
|
||||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||||
import { fragWithHTML, unCamelCase } from '../utils/Utils';
|
import { fragWithHTML, unCamelCase } from '../utils/Utils';
|
||||||
|
import type { PropertyMappingModelData } from './PropertyMapping';
|
||||||
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping';
|
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping';
|
||||||
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte';
|
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent';
|
||||||
import { FileSuggest } from './suggesters/FileSuggest';
|
import { FileSuggest } from './suggesters/FileSuggest';
|
||||||
import { FolderSuggest } from './suggesters/FolderSuggest';
|
import { FolderSuggest } from './suggesters/FolderSuggest';
|
||||||
|
|
||||||
|
|
@ -74,7 +75,7 @@ export interface MediaDbPluginSettings {
|
||||||
boardgameFolder: string;
|
boardgameFolder: string;
|
||||||
bookFolder: string;
|
bookFolder: string;
|
||||||
|
|
||||||
propertyMappingModels: PropertyMappingModel[];
|
propertyMappingModels: PropertyMappingModelData[];
|
||||||
|
|
||||||
// DEPRECATED: Use propertyMappingModels instead
|
// DEPRECATED: Use propertyMappingModels instead
|
||||||
moviePropertyConversionRules: string;
|
moviePropertyConversionRules: string;
|
||||||
|
|
@ -347,12 +348,10 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings
|
||||||
const defaultSettings = DEFAULT_SETTINGS;
|
const defaultSettings = DEFAULT_SETTINGS;
|
||||||
|
|
||||||
// construct property mapping defaults
|
// construct property mapping defaults
|
||||||
const propertyMappingModels: PropertyMappingModel[] = [];
|
const propertyMappingModels: PropertyMappingModelData[] = [];
|
||||||
for (const mediaType of MEDIA_TYPES) {
|
for (const mediaType of MEDIA_TYPES) {
|
||||||
const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType);
|
const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType);
|
||||||
const metadataObj = model.toMetaDataObject();
|
const metadataObj = model.toMetaDataObject();
|
||||||
// console.log(metadataObj);
|
|
||||||
// console.log(model);
|
|
||||||
|
|
||||||
const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType);
|
const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType);
|
||||||
|
|
||||||
|
|
@ -368,43 +367,14 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
propertyMappingModels.push(propertyMappingModel);
|
// Convert to plain data for serialization
|
||||||
}
|
propertyMappingModels.push(propertyMappingModel.toJSON());
|
||||||
|
|
||||||
// MIGRATION: Ensure all property mappings have wikilink defined (for settings loaded from disk)
|
|
||||||
if (defaultSettings.propertyMappingModels && Array.isArray(defaultSettings.propertyMappingModels)) {
|
|
||||||
for (const model of defaultSettings.propertyMappingModels) {
|
|
||||||
if (model.properties && Array.isArray(model.properties)) {
|
|
||||||
for (const prop of model.properties) {
|
|
||||||
if (typeof prop.wikilink === 'undefined') {
|
|
||||||
prop.wikilink = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultSettings.propertyMappingModels = propertyMappingModels;
|
defaultSettings.propertyMappingModels = propertyMappingModels;
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures all property mappings in loaded settings have the wikilink property defined.
|
|
||||||
*/
|
|
||||||
export function ensureWikilinkOnPropertyMappings(settings: MediaDbPluginSettings): void {
|
|
||||||
if (settings.propertyMappingModels && Array.isArray(settings.propertyMappingModels)) {
|
|
||||||
for (const model of settings.propertyMappingModels) {
|
|
||||||
if (model.properties && Array.isArray(model.properties)) {
|
|
||||||
for (const prop of model.properties) {
|
|
||||||
if (typeof prop.wikilink === 'undefined') {
|
|
||||||
prop.wikilink = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Settings Tab
|
// MARK: Settings Tab
|
||||||
export class MediaDbSettingTab extends PluginSettingTab {
|
export class MediaDbSettingTab extends PluginSettingTab {
|
||||||
plugin: MediaDbPlugin;
|
plugin: MediaDbPlugin;
|
||||||
|
|
@ -787,27 +757,23 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
mount(PropertyMappingModelsComponent, {
|
render(
|
||||||
target: setting.descEl,
|
() =>
|
||||||
props: {
|
PropertyMappingModelsComponent({
|
||||||
models: this.plugin.settings.propertyMappingModels.map(x => x.copy()),
|
models: structuredClone(this.plugin.settings.propertyMappingModels),
|
||||||
save: (model: PropertyMappingModel): void => {
|
save: (model: PropertyMappingModelData): void => {
|
||||||
const propertyMappingModels: PropertyMappingModel[] = [];
|
// Update the matching model in settings (stored as plain data)
|
||||||
|
const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type);
|
||||||
for (const model2 of this.plugin.settings.propertyMappingModels) {
|
if (index !== -1) {
|
||||||
if (model2.type === model.type) {
|
this.plugin.settings.propertyMappingModels[index] = model;
|
||||||
propertyMappingModels.push(model);
|
|
||||||
} else {
|
|
||||||
propertyMappingModels.push(model2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.plugin.settings.propertyMappingModels = propertyMappingModels;
|
|
||||||
new Notice(`MDB: Property mappings for ${model.type} saved successfully.`);
|
new Notice(`MDB: Property mappings for ${model.type} saved successfully.`);
|
||||||
void this.plugin.saveSettings();
|
void this.plugin.saveSettings();
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
});
|
setting.descEl,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
259
src/styles.css
Normal file
259
src/styles.css
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
.media-db-plugin-list-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-list-toggle {
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-list-text-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-list-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
small.media-db-plugin-list-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-select-modal {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-select-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-select-element {
|
||||||
|
cursor: pointer;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px 0 5px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-select-element-selected {
|
||||||
|
border-left: 5px solid var(--interactive-accent) !important;
|
||||||
|
background: var(--background-secondary-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-select-element-hover {
|
||||||
|
background: var(--background-secondary-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-preview-modal {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-preview-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-spacer {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-button:focus {
|
||||||
|
/*outline: 1px solid white;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-preview {
|
||||||
|
border-radius: var(--modal-radius);
|
||||||
|
border: var(--modal-border-width) solid var(--modal-border-color);
|
||||||
|
padding: var(--size-4-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Component Styles */
|
||||||
|
.icon-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Property Mapping Component Styles */
|
||||||
|
.media-db-plugin-property-mappings-model-container {
|
||||||
|
margin-bottom: var(--size-4-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-model-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--size-4-4);
|
||||||
|
gap: var(--size-4-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-model-header .setting-item-name {
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
color: var(--text-normal);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-model-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-4-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-unsaved-changes {
|
||||||
|
color: var(--text-warning);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-save-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-save-button.mod-muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-validation {
|
||||||
|
color: var(--text-error);
|
||||||
|
background: rgba(var(--color-red-rgb), 0.1);
|
||||||
|
padding: var(--size-4-3) var(--size-4-4);
|
||||||
|
margin-bottom: var(--size-4-4);
|
||||||
|
border-left: 3px solid var(--text-error);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table thead {
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table th {
|
||||||
|
padding: var(--size-4-2) var(--size-4-3);
|
||||||
|
padding-left: 0;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table tbody tr {
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table td {
|
||||||
|
padding: var(--size-4-3) var(--size-4-3) var(--size-4-3) 0;
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border-hover);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-property {
|
||||||
|
width: 25%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-mapping {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-new-name {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-wikilink {
|
||||||
|
width: 15%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-locked {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table code {
|
||||||
|
padding: var(--size-4-1) var(--size-4-2);
|
||||||
|
margin: 0;
|
||||||
|
background: var(--code-background);
|
||||||
|
color: var(--code-normal);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-binding-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mappings-table select.dropdown {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-to {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-4-2);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-to-disabled {
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-wikilink-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--size-4-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-db-plugin-property-mapping-wikilink-label input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
width: var(--checkbox-size);
|
||||||
|
height: var(--checkbox-size);
|
||||||
|
}
|
||||||
75
styles.css
75
styles.css
|
|
@ -1,75 +0,0 @@
|
||||||
.media-db-plugin-list-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-content: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-list-toggle {
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-list-text-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-list-text {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
small.media-db-plugin-list-text {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-select-modal {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-select-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 5px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-select-element {
|
|
||||||
cursor: pointer;
|
|
||||||
border-left: 5px solid transparent;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 5px 0 5px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-select-element-selected {
|
|
||||||
border-left: 5px solid var(--interactive-accent) !important;
|
|
||||||
background: var(--background-secondary-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-select-element-hover {
|
|
||||||
background: var(--background-secondary-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-preview-modal {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-preview-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-spacer {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-button:focus {
|
|
||||||
/*outline: 1px solid white;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-db-plugin-preview {
|
|
||||||
border-radius: var(--modal-radius);
|
|
||||||
border: var(--modal-border-width) solid var(--modal-border-color);
|
|
||||||
padding: var(--size-4-4);
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,10 @@
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"lib": ["DOM", "ESNext"],
|
"lib": ["DOM", "ESNext"],
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"types": ["vite/client"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
80
vite.config.ts
Normal file
80
vite.config.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import solidPlugin from 'vite-plugin-solid';
|
||||||
|
import builtins from 'builtin-modules';
|
||||||
|
import { getBuildBanner } from './automation/build/buildBanner';
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
|
import banner from 'vite-plugin-banner';
|
||||||
|
import manifest from './manifest.json' with { type: 'json' };
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const entryFile = 'src/main.ts';
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const prod = mode === 'production';
|
||||||
|
const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}`;
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
solidPlugin(),
|
||||||
|
banner({
|
||||||
|
outDir: outDir,
|
||||||
|
content: getBuildBanner(prod ? 'Release Build' : 'Dev Build', version => version),
|
||||||
|
}),
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: 'manifest.json',
|
||||||
|
dest: outDir,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
src: path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, entryFile),
|
||||||
|
name: 'main',
|
||||||
|
fileName: () => 'main.js',
|
||||||
|
formats: ['cjs'],
|
||||||
|
},
|
||||||
|
minify: prod,
|
||||||
|
sourcemap: prod ? false : 'inline',
|
||||||
|
cssCodeSplit: false,
|
||||||
|
emptyOutDir: false,
|
||||||
|
outDir: '',
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: path.resolve(__dirname, entryFile),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
dir: outDir,
|
||||||
|
entryFileNames: 'main.js',
|
||||||
|
assetFileNames: 'styles.css',
|
||||||
|
},
|
||||||
|
external: [
|
||||||
|
'obsidian',
|
||||||
|
'electron',
|
||||||
|
'@codemirror/autocomplete',
|
||||||
|
'@codemirror/collab',
|
||||||
|
'@codemirror/commands',
|
||||||
|
'@codemirror/language',
|
||||||
|
'@codemirror/lint',
|
||||||
|
'@codemirror/search',
|
||||||
|
'@codemirror/state',
|
||||||
|
'@codemirror/view',
|
||||||
|
'@lezer/common',
|
||||||
|
'@lezer/highlight',
|
||||||
|
'@lezer/lr',
|
||||||
|
...builtins,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue