bye svelte; welcome solid

This commit is contained in:
Moritz Jung 2026-01-29 12:19:27 +01:00
parent 97cf350a32
commit 2fe6f6fc37
21 changed files with 686 additions and 569 deletions

View file

@ -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<string, unknown>;
@ -652,39 +653,18 @@ export default class MediaDbPlugin extends Plugin {
}
async loadSettings(): Promise<void> {
// 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;
}

View file

@ -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
View 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>
);
}

View file

@ -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<string, unknown> = {};
@ -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<string, unknown> = {};

View file

@ -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);
}
}

View file

@ -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>

View 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>
);
}

View file

@ -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>

View 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>
);
}

View file

@ -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,
);
});
}
}

259
src/styles.css Normal file
View 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);
}