diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4968f5b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: ['mProjectsCode'] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84fe8d9..2594994 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: bun install bun run build 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 }} ls @@ -49,6 +49,6 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} files: | ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip - main.js - manifest.json - styles.css + dist/main.js + dist/manifest.json + dist/styles.css diff --git a/.gitignore b/.gitignore index 2c1c4e4..80d9bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ exampleVault/.obsidian/plugins/obsidian-media-db-plugin/* exampleVault/Media DB/* meta.txt -main.js \ No newline at end of file +dist/ \ No newline at end of file diff --git a/automation/build/buildBanner.ts b/automation/build/buildBanner.ts index b8a62cf..849aec3 100644 --- a/automation/build/buildBanner.ts +++ b/automation/build/buildBanner.ts @@ -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) { return `/* @@ -9,30 +9,16 @@ By: ${manifest.author} (${manifest.authorUrl}) Time: ${new Date().toUTCString()} 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 ------------------------------------------- -MIT License - 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 -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: +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. -The above copyright notice and this permission notice shall be included in all -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. +You should have received a copy of the GNU General Public License along with this program. If not, see . */ `; } diff --git a/bun.lockb b/bun.lockb index f7a1bfc..419cad8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs index 8d66d47..c634b74 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,7 +7,7 @@ import * as plugin_import from 'eslint-plugin-import'; 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'], diff --git a/package.json b/package.json index 2fbc05c..92d08d1 100644 --- a/package.json +++ b/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.", "main": "main.js", "scripts": { - "dev": "bun run automation/build/esbuild.dev.config.ts", - "build": "bun run tsc && bun run automation/build/esbuild.config.ts", + "dev": "vite build --watch --mode development", + "build": "bun run tsc && vite build --mode production", "tsc": "tsc -noEmit -skipLibCheck", "test": "bun test", "test:log": "LOG_TESTS=true bun test", - "format": "prettier --write --plugin prettier-plugin-svelte .", - "format:check": "prettier --check --plugin prettier-plugin-svelte .", - "lint": "eslint --max-warnings=0 src/**", - "lint:fix": "eslint --max-warnings=0 --fix src/**", - "svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"", - "check": "bun run format:check && bun run tsc && bun run lint && bun run svelte-check", - "check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run svelte-check", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint --max-warnings=0 --no-warn-ignored src/**", + "lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**", + "check": "bun run format:check && bun run tsc && bun run lint", + "check:fix": "bun run format && bun run tsc && bun run lint:fix", "release": "bun run automation/release.ts", "stats": "bun run automation/stats.ts" }, @@ -28,9 +27,6 @@ "@popperjs/core": "^2.11.8", "@types/bun": "^1.3.7", "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-plugin-import": "^2.32.0", "eslint-plugin-only-warn": "^1.1.0", @@ -39,13 +35,14 @@ "openapi-fetch": "^0.14.1", "openapi-typescript": "^7.10.1", "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.4.1", + "solid-js": "^1.9.3", "string-argv": "^0.3.2", - "svelte": "^5.48.5", - "svelte-check": "^4.3.5", - "svelte-preprocess": "^6.0.3", "tslib": "^2.8.1", "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" } } diff --git a/src/main.ts b/src/main.ts index 78d2173..ce08a21 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,11 +19,11 @@ import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; import { VNDBAPI } from './api/apis/VNDBAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; -import type {SeasonSelectModalElement} from './modals/MediaDbSeasonSelectModal'; -import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; +import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from './models/MediaTypeModel'; import { PropertyMapper } from './settings/PropertyMapper'; -import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping'; +import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings'; import { BulkImportHelper } from './utils/BulkImportHelper'; @@ -33,6 +33,7 @@ import type { SearchModalOptions } from './utils/ModalHelper'; import { ModalHelper } from './utils/ModalHelper'; import type { CreateNoteOptions } from './utils/Utils'; import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; +import 'src/styles.css'; export type Metadata = Record; @@ -652,39 +653,18 @@ export default class MediaDbPlugin extends Plugin { } async loadSettings(): Promise { - // console.log(DEFAULT_SETTINGS); const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); - // migrate the settings loaded from the disk to match the structure of the default settings - const newPropertyMappings: PropertyMappingModel[] = []; - for (const defaultPropertyMappingModel of defaultSettings.propertyMappingModels) { - const newPropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type); - if (newPropertyMappingModel === undefined) { - // if the propertyMappingModel exists in the default settings but not the loaded settings, add it - newPropertyMappings.push(defaultPropertyMappingModel); - } else { - // if the propertyMappingModel also exists in the loaded settings, add it from there - const newProperties: PropertyMapping[] = []; + // Migrate property mappings using the dedicated migration method + const migratedModels = PropertyMappingModel.migrateModels( + loadedSettings.propertyMappingModels || [], + defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), + ); - 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)); - } - } - loadedSettings.propertyMappingModels = newPropertyMappings; + // Store as plain data for serialization + loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); this.settings = loadedSettings; } diff --git a/src/settings/Icon.svelte b/src/settings/Icon.svelte deleted file mode 100644 index a73d4a4..0000000 --- a/src/settings/Icon.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - - -{#if iconName.length > 0} -
-
-
-{/if} - - diff --git a/src/settings/Icon.tsx b/src/settings/Icon.tsx new file mode 100644 index 0000000..9723197 --- /dev/null +++ b/src/settings/Icon.tsx @@ -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 ( + 0}> +
+
+
+
+ ); +} diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 9ca5e0c..bf08d32 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -27,8 +27,12 @@ export class PropertyMapper { return obj; } - // @ts-ignore - 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); + if (!propertyMappingModel) { + return obj; + } + + const propertyMappings = propertyMappingModel.properties; const newObj: Record = {}; @@ -80,7 +84,8 @@ export class PropertyMapper { 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 = {}; diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index de4adaa..82d044e 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -1,6 +1,20 @@ import type { MediaType } from '../utils/MediaType'; 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 { Default = 'default', Map = 'remap', @@ -80,6 +94,72 @@ export class PropertyMappingModel { } 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 { @@ -153,4 +233,20 @@ export class PropertyMapping { 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); + } } diff --git a/src/settings/PropertyMappingModelComponent.svelte b/src/settings/PropertyMappingModelComponent.svelte deleted file mode 100644 index 064eec5..0000000 --- a/src/settings/PropertyMappingModelComponent.svelte +++ /dev/null @@ -1,296 +0,0 @@ - - -
-
-
{capitalizeFirstLetter(model.type)}
- -
- {#if unsavedChanges} -
Unsaved changes
- {/if} - - -
-
- - {#if !validationResult?.res} -
- {validationResult?.err?.message} -
- {/if} - -
- - - - - - - - - - - {#each model.properties as property} - - - - {#if property.locked} - - {:else} - - - - - - {/if} - - {/each} - -
PropertyMappingNew nameWikilink
- {property.property} - -
property cannot be remapped
-
- - - {#if property.mapping === PropertyMappingOption.Map} -
- - onModelUpdate()} - /> -
- {:else} - - {/if} -
-
-
- - diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx new file mode 100644 index 0000000..d9a9017 --- /dev/null +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -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 ( +
+
+
{capitalizeFirstLetter(modelData.type)}
+ +
+ +
Unsaved changes
+
+ + +
+
+ + +
{validationResult().err?.message}
+
+ +
+ + + + + + + + + + + + {(property, index) => ( + + + + +
property cannot be remapped
+ + } + > +
+ + + + + + + )} + + +
PropertyMappingNew nameWikilink
+ {property.property} + + + + —} + > +
+ + { + setModelData('properties', index(), 'newProperty', e.currentTarget.value); + onModelUpdate(); + }} + /> +
+
+
+
+
+ ); +} diff --git a/src/settings/PropertyMappingModelsComponent.svelte b/src/settings/PropertyMappingModelsComponent.svelte deleted file mode 100644 index edfd197..0000000 --- a/src/settings/PropertyMappingModelsComponent.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
- {#each models as model} - - {/each} - - -
- - diff --git a/src/settings/PropertyMappingModelsComponent.tsx b/src/settings/PropertyMappingModelsComponent.tsx new file mode 100644 index 0000000..35274ff --- /dev/null +++ b/src/settings/PropertyMappingModelsComponent.tsx @@ -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 ( +
+ {model => } +
+ ); +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index e82ef8b..4c24635 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,13 +1,14 @@ import type { App } from 'obsidian'; import { Notice, PluginSettingTab, SettingGroup } from 'obsidian'; +import { render } from 'solid-js/web'; import { MediaType } from 'src/utils/MediaType'; -import { mount } from 'svelte'; import type MediaDbPlugin from '../main'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { fragWithHTML, unCamelCase } from '../utils/Utils'; +import type { PropertyMappingModelData } from './PropertyMapping'; import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; -import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte'; +import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; import { FileSuggest } from './suggesters/FileSuggest'; import { FolderSuggest } from './suggesters/FolderSuggest'; @@ -74,7 +75,7 @@ export interface MediaDbPluginSettings { boardgameFolder: string; bookFolder: string; - propertyMappingModels: PropertyMappingModel[]; + propertyMappingModels: PropertyMappingModelData[]; // DEPRECATED: Use propertyMappingModels instead moviePropertyConversionRules: string; @@ -347,12 +348,10 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings const defaultSettings = DEFAULT_SETTINGS; // construct property mapping defaults - const propertyMappingModels: PropertyMappingModel[] = []; + const propertyMappingModels: PropertyMappingModelData[] = []; for (const mediaType of MEDIA_TYPES) { const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType); const metadataObj = model.toMetaDataObject(); - // console.log(metadataObj); - // console.log(model); const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType); @@ -368,43 +367,14 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings ); } - propertyMappingModels.push(propertyMappingModel); - } - - // 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; - } - } - } - } + // Convert to plain data for serialization + propertyMappingModels.push(propertyMappingModel.toJSON()); } defaultSettings.propertyMappingModels = propertyMappingModels; 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 export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; @@ -787,27 +757,23 @@ export class MediaDbSettingTab extends PluginSettingTab { ), ); - mount(PropertyMappingModelsComponent, { - target: setting.descEl, - props: { - models: this.plugin.settings.propertyMappingModels.map(x => x.copy()), - save: (model: PropertyMappingModel): void => { - const propertyMappingModels: PropertyMappingModel[] = []; - - for (const model2 of this.plugin.settings.propertyMappingModels) { - if (model2.type === model.type) { - propertyMappingModels.push(model); - } else { - propertyMappingModels.push(model2); + render( + () => + PropertyMappingModelsComponent({ + models: structuredClone(this.plugin.settings.propertyMappingModels), + save: (model: PropertyMappingModelData): void => { + // Update the matching model in settings (stored as plain data) + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; } - } - this.plugin.settings.propertyMappingModels = propertyMappingModels; - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); - void this.plugin.saveSettings(); - }, - }, - }); + new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + void this.plugin.saveSettings(); + }, + }), + setting.descEl, + ); }); } } diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..b204d1d --- /dev/null +++ b/src/styles.css @@ -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); +} diff --git a/styles.css b/styles.css deleted file mode 100644 index b7351e4..0000000 --- a/styles.css +++ /dev/null @@ -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); -} diff --git a/tsconfig.json b/tsconfig.json index 9825095..c6eb672 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,10 @@ "moduleDetection": "force", "sourceMap": true, "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"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ffa6b85 --- /dev/null +++ b/vite.config.ts @@ -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, + ], + }, + }, + }; +});