add obsidian eslint plugin, solve major issues
This commit is contained in:
parent
84214a1c3c
commit
498e2611ae
105 changed files with 5702 additions and 5308 deletions
24
packages/obsidian/src/settings/Icon.tsx
Normal file
24
packages/obsidian/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>
|
||||
);
|
||||
}
|
||||
108
packages/obsidian/src/settings/PropertyMapper.ts
Normal file
108
packages/obsidian/src/settings/PropertyMapper.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
|
||||
|
||||
export class PropertyMapper {
|
||||
plugin: MediaDbPlugin;
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object using the conversion rules for its type.
|
||||
* Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
convertObject(obj: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!Object.hasOwn(obj, 'type')) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (!MEDIA_TYPES.includes(obj.type as MediaType)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type);
|
||||
if (!propertyMappingModel) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const propertyMappings = propertyMappingModel.properties;
|
||||
const propertyMappingByProperty = new Map(propertyMappings.map(mapping => [mapping.property, mapping]));
|
||||
|
||||
const newObj: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const propertyMapping = propertyMappingByProperty.get(key);
|
||||
|
||||
if (!propertyMapping) {
|
||||
newObj[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
let finalValue = value;
|
||||
if (propertyMapping.wikilink) {
|
||||
if (typeof value === 'string') {
|
||||
finalValue = `[[${value}]]`;
|
||||
} else if (Array.isArray(value)) {
|
||||
finalValue = (value as unknown[]).map((v: unknown) => (typeof v === 'string' ? `[[${v}]]` : v));
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyMapping.mapping === PropertyMappingOption.Map) {
|
||||
newObj[propertyMapping.newProperty] = finalValue;
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Default) {
|
||||
newObj[key] = finalValue;
|
||||
}
|
||||
}
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object back using the conversion rules for its type.
|
||||
* Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
convertObjectBack(obj: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!Object.hasOwn(obj, 'type')) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj.type === 'manga') {
|
||||
obj.type = 'comicManga';
|
||||
Logger.debug(`MDB | updated metadata type`, obj.type);
|
||||
}
|
||||
if (!MEDIA_TYPES.includes(obj.type as MediaType)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type);
|
||||
const propertyMappings = propertyMappingModel?.properties ?? [];
|
||||
const propertyMappingByOriginal = new Map(propertyMappings.map(mapping => [mapping.property, mapping]));
|
||||
const propertyMappingByMapped = new Map(propertyMappings.map(mapping => [mapping.newProperty, mapping]));
|
||||
|
||||
const originalObj: Record<string, unknown> = { ...obj };
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const normalProperty = propertyMappingByOriginal.get(key);
|
||||
if (normalProperty) {
|
||||
originalObj[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappedProperty = propertyMappingByMapped.get(key);
|
||||
if (mappedProperty) {
|
||||
originalObj[mappedProperty.property] = value;
|
||||
delete originalObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return originalObj;
|
||||
}
|
||||
}
|
||||
253
packages/obsidian/src/settings/PropertyMapping.ts
Normal file
253
packages/obsidian/src/settings/PropertyMapping.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from 'packages/obsidian/src/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',
|
||||
Remove = 'remove',
|
||||
}
|
||||
|
||||
export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove];
|
||||
|
||||
export class PropertyMappingModel {
|
||||
type: MediaType;
|
||||
properties: PropertyMapping[];
|
||||
|
||||
constructor(type: MediaType, properties?: PropertyMapping[]) {
|
||||
this.type = type;
|
||||
this.properties = properties ?? [];
|
||||
}
|
||||
|
||||
validate(): { res: boolean; err?: Error } {
|
||||
Logger.debug(`MDB | validated property mappings for ${this.type}`);
|
||||
|
||||
// check properties
|
||||
for (const property of this.properties) {
|
||||
const propertyValidation = property.validate();
|
||||
if (!propertyValidation.res) {
|
||||
return {
|
||||
res: false,
|
||||
err: propertyValidation.err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// check for name collisions
|
||||
for (const property of this.getMappedProperties()) {
|
||||
const propertiesWithSameTarget = this.getMappedProperties().filter(x => x.newProperty === property.newProperty);
|
||||
if (propertiesWithSameTarget.length === 0) {
|
||||
// if we get there, then something in this code is wrong
|
||||
} else if (propertiesWithSameTarget.length === 1) {
|
||||
// all good
|
||||
} else {
|
||||
// two or more properties are mapped to the same property
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingNameConflictError(
|
||||
`Multiple remapped properties (${propertiesWithSameTarget.map(x => x.toString()).toString()}) may not share the same name.`,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
// remapped properties may not have the same name as any original property
|
||||
for (const property of this.getMappedProperties()) {
|
||||
const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property);
|
||||
if (propertiesWithSameTarget.length === 0) {
|
||||
// all good
|
||||
} else {
|
||||
// a mapped property shares the same name with an original property
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingNameConflictError(`Remapped property (${property}) may not share it's new name with an existing property.`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
res: true,
|
||||
};
|
||||
}
|
||||
|
||||
getMappedProperties(): PropertyMapping[] {
|
||||
return this.properties.filter(x => x.mapping === PropertyMappingOption.Map);
|
||||
}
|
||||
|
||||
copy(): PropertyMappingModel {
|
||||
const copy = new PropertyMappingModel(this.type);
|
||||
for (const property of this.properties) {
|
||||
const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked, property.wikilink);
|
||||
copy.properties.push(propertyCopy);
|
||||
}
|
||||
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 {
|
||||
property: string;
|
||||
newProperty: string;
|
||||
locked: boolean;
|
||||
mapping: PropertyMappingOption;
|
||||
wikilink: boolean;
|
||||
|
||||
constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean, wikilink?: boolean) {
|
||||
this.property = property;
|
||||
this.newProperty = newProperty;
|
||||
this.mapping = mapping;
|
||||
this.locked = locked ?? false;
|
||||
this.wikilink = wikilink ?? false;
|
||||
}
|
||||
|
||||
validate(): { res: boolean; err?: Error } {
|
||||
// locked property may only be default
|
||||
if (this.locked) {
|
||||
if (this.mapping === PropertyMappingOption.Remove) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be removed.`),
|
||||
};
|
||||
}
|
||||
if (this.mapping === PropertyMappingOption.Map) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be remapped.`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mapping === PropertyMappingOption.Default) {
|
||||
return { res: true };
|
||||
}
|
||||
if (this.mapping === PropertyMappingOption.Remove) {
|
||||
return { res: true };
|
||||
}
|
||||
|
||||
if (!this.property || !containsOnlyLettersAndUnderscores(this.property)) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": property may not be empty and may only contain letters and underscores.`),
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.newProperty || !containsOnlyLettersAndUnderscores(this.newProperty)) {
|
||||
return {
|
||||
res: false,
|
||||
err: new PropertyMappingValidationError(
|
||||
`Error in property mapping "${this.toString()}": new property may not be empty and may only contain letters and underscores.`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
res: true,
|
||||
};
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.mapping === PropertyMappingOption.Default) {
|
||||
return this.property;
|
||||
} else if (this.mapping === PropertyMappingOption.Map) {
|
||||
return `${this.property} -> ${this.newProperty}`;
|
||||
} else if (this.mapping === PropertyMappingOption.Remove) {
|
||||
return `remove ${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);
|
||||
}
|
||||
}
|
||||
138
packages/obsidian/src/settings/PropertyMappingModelComponent.tsx
Normal file
138
packages/obsidian/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">N/A</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
861
packages/obsidian/src/settings/Settings.ts
Normal file
861
packages/obsidian/src/settings/Settings.ts
Normal file
|
|
@ -0,0 +1,861 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Notice, PluginSettingTab, SecretComponent, SettingGroup } from 'obsidian';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import PropertyMappingModelsComponent from 'packages/obsidian/src/settings/PropertyMappingModelsComponent';
|
||||
import { FileSuggest } from 'packages/obsidian/src/settings/suggesters/FileSuggest';
|
||||
import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderSuggest';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
|
||||
import { unCamelCase } from 'packages/obsidian/src/utils/Utils';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
function createDateFormatDescription(preview: string): DocumentFragment {
|
||||
return createFragment(frag => {
|
||||
const container = frag.createDiv();
|
||||
container.appendText('Your custom date format. Use ');
|
||||
container.createEl('em', { text: "'YYYY-MM-DD'" });
|
||||
container.appendText(' for example.');
|
||||
container.createEl('br');
|
||||
container.appendText('For more syntax, refer to ');
|
||||
container.createEl('a', {
|
||||
href: 'https://momentjs.com/docs/#/displaying/format/',
|
||||
text: 'format reference',
|
||||
});
|
||||
container.appendText('.');
|
||||
container.createEl('br');
|
||||
container.appendText('Your current syntax looks like this: ');
|
||||
container.createEl('em', { text: preview });
|
||||
});
|
||||
}
|
||||
|
||||
function createPropertyMappingsDescription(): DocumentFragment {
|
||||
return createFragment(frag => {
|
||||
const container = frag.createDiv();
|
||||
container.createEl('p', {
|
||||
text: 'Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.',
|
||||
});
|
||||
container.createEl('p', {
|
||||
text: 'You can choose to keep the original name, rename the property, or remove it entirely.',
|
||||
});
|
||||
const paragraph = container.createEl('p');
|
||||
paragraph.createEl('strong', {
|
||||
text: 'Remember to save your changes using the save button for each individual category.',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
export interface MediaDbPluginSettings {
|
||||
OMDbKeyId: string;
|
||||
TMDBKeyId: string;
|
||||
MobyGamesKeyId: string;
|
||||
GiantBombKeyId: string;
|
||||
IGDBClientId: string;
|
||||
IGDBClientSecret: string;
|
||||
RAWGAPIKeyId: string;
|
||||
ComicVineKeyId: string;
|
||||
BoardgameGeekKeyId: string;
|
||||
|
||||
sfwFilter: boolean;
|
||||
templates: boolean;
|
||||
customDateFormat: string;
|
||||
openNoteInNewTab: boolean;
|
||||
useDefaultFrontMatter: boolean;
|
||||
enableTemplaterIntegration: boolean;
|
||||
imageDownload: boolean;
|
||||
imageFolder: string;
|
||||
|
||||
BoardgameGeekAPI_disabledMediaTypes: MediaType[];
|
||||
ComicVineAPI_disabledMediaTypes: MediaType[];
|
||||
GiantBombAPI_disabledMediaTypes: MediaType[];
|
||||
IGDBAPI_disabledMediaTypes: MediaType[];
|
||||
RAWGAPI_disabledMediaTypes: MediaType[];
|
||||
MALAPI_disabledMediaTypes: MediaType[];
|
||||
MALAPIManga_disabledMediaTypes: MediaType[];
|
||||
MobyGamesAPI_disabledMediaTypes: MediaType[];
|
||||
MusicBrainzAPI_disabledMediaTypes: MediaType[];
|
||||
OMDbAPI_disabledMediaTypes: MediaType[];
|
||||
OpenLibraryAPI_disabledMediaTypes: MediaType[];
|
||||
SteamAPI_disabledMediaTypes: MediaType[];
|
||||
TMDBMovieAPI_disabledMediaTypes: MediaType[];
|
||||
TMDBSeasonAPI_disabledMediaTypes: MediaType[];
|
||||
TMDBSeriesAPI_disabledMediaTypes: MediaType[];
|
||||
VNDBAPI_disabledMediaTypes: MediaType[];
|
||||
WikipediaAPI_disabledMediaTypes: MediaType[];
|
||||
|
||||
movieTemplate: string;
|
||||
seriesTemplate: string;
|
||||
seasonTemplate: string;
|
||||
mangaTemplate: string;
|
||||
gameTemplate: string;
|
||||
wikiTemplate: string;
|
||||
musicReleaseTemplate: string;
|
||||
boardgameTemplate: string;
|
||||
bookTemplate: string;
|
||||
|
||||
movieFileNameTemplate: string;
|
||||
seriesFileNameTemplate: string;
|
||||
seasonFileNameTemplate: string;
|
||||
mangaFileNameTemplate: string;
|
||||
gameFileNameTemplate: string;
|
||||
wikiFileNameTemplate: string;
|
||||
musicReleaseFileNameTemplate: string;
|
||||
boardgameFileNameTemplate: string;
|
||||
bookFileNameTemplate: string;
|
||||
|
||||
movieFolder: string;
|
||||
seriesFolder: string;
|
||||
seasonFolder: string;
|
||||
mangaFolder: string;
|
||||
gameFolder: string;
|
||||
wikiFolder: string;
|
||||
musicReleaseFolder: string;
|
||||
boardgameFolder: string;
|
||||
bookFolder: string;
|
||||
|
||||
propertyMappingModels: PropertyMappingModelData[];
|
||||
|
||||
// DEPRECATED: Use propertyMappingModels instead
|
||||
moviePropertyConversionRules: string;
|
||||
seriesPropertyConversionRules: string;
|
||||
seasonPropertyConversionRules: string;
|
||||
mangaPropertyConversionRules: string;
|
||||
gamePropertyConversionRules: string;
|
||||
wikiPropertyConversionRules: string;
|
||||
musicReleasePropertyConversionRules: string;
|
||||
boardgamePropertyConversionRules: string;
|
||||
bookPropertyConversionRules: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to get/set settings for a specific media type.
|
||||
*/
|
||||
class MediaTypeMappedSettings {
|
||||
mediaType: MediaType;
|
||||
|
||||
constructor(mediaType: MediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
getTemplate(settings: MediaDbPluginSettings): string {
|
||||
switch (this.mediaType) {
|
||||
case MediaType.Movie:
|
||||
return settings.movieTemplate;
|
||||
case MediaType.Series:
|
||||
return settings.seriesTemplate;
|
||||
case MediaType.Season:
|
||||
return settings.seasonTemplate;
|
||||
case MediaType.ComicManga:
|
||||
return settings.mangaTemplate;
|
||||
case MediaType.Game:
|
||||
return settings.gameTemplate;
|
||||
case MediaType.Wiki:
|
||||
return settings.wikiTemplate;
|
||||
case MediaType.MusicRelease:
|
||||
return settings.musicReleaseTemplate;
|
||||
case MediaType.BoardGame:
|
||||
return settings.boardgameTemplate;
|
||||
case MediaType.Book:
|
||||
return settings.bookTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
setTemplate(settings: MediaDbPluginSettings, template: string): void {
|
||||
switch (this.mediaType) {
|
||||
case MediaType.Movie:
|
||||
settings.movieTemplate = template;
|
||||
break;
|
||||
case MediaType.Series:
|
||||
settings.seriesTemplate = template;
|
||||
break;
|
||||
case MediaType.Season:
|
||||
settings.seasonTemplate = template;
|
||||
break;
|
||||
case MediaType.ComicManga:
|
||||
settings.mangaTemplate = template;
|
||||
break;
|
||||
case MediaType.Game:
|
||||
settings.gameTemplate = template;
|
||||
break;
|
||||
case MediaType.Wiki:
|
||||
settings.wikiTemplate = template;
|
||||
break;
|
||||
case MediaType.MusicRelease:
|
||||
settings.musicReleaseTemplate = template;
|
||||
break;
|
||||
case MediaType.BoardGame:
|
||||
settings.boardgameTemplate = template;
|
||||
break;
|
||||
case MediaType.Book:
|
||||
settings.bookTemplate = template;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getFileNameTemplate(settings: MediaDbPluginSettings): string {
|
||||
switch (this.mediaType) {
|
||||
case MediaType.Movie:
|
||||
return settings.movieFileNameTemplate;
|
||||
case MediaType.Series:
|
||||
return settings.seriesFileNameTemplate;
|
||||
case MediaType.Season:
|
||||
return settings.seasonFileNameTemplate;
|
||||
case MediaType.ComicManga:
|
||||
return settings.mangaFileNameTemplate;
|
||||
case MediaType.Game:
|
||||
return settings.gameFileNameTemplate;
|
||||
case MediaType.Wiki:
|
||||
return settings.wikiFileNameTemplate;
|
||||
case MediaType.MusicRelease:
|
||||
return settings.musicReleaseFileNameTemplate;
|
||||
case MediaType.BoardGame:
|
||||
return settings.boardgameFileNameTemplate;
|
||||
case MediaType.Book:
|
||||
return settings.bookFileNameTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
setFileNameTemplate(settings: MediaDbPluginSettings, template: string): void {
|
||||
switch (this.mediaType) {
|
||||
case MediaType.Movie:
|
||||
settings.movieFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.Series:
|
||||
settings.seriesFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.Season:
|
||||
settings.seasonFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.ComicManga:
|
||||
settings.mangaFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.Game:
|
||||
settings.gameFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.Wiki:
|
||||
settings.wikiFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.MusicRelease:
|
||||
settings.musicReleaseFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.BoardGame:
|
||||
settings.boardgameFileNameTemplate = template;
|
||||
break;
|
||||
case MediaType.Book:
|
||||
settings.bookFileNameTemplate = template;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getFolder(settings: MediaDbPluginSettings): string {
|
||||
switch (this.mediaType) {
|
||||
case MediaType.Movie:
|
||||
return settings.movieFolder;
|
||||
case MediaType.Series:
|
||||
return settings.seriesFolder;
|
||||
case MediaType.Season:
|
||||
return settings.seasonFolder;
|
||||
case MediaType.ComicManga:
|
||||
return settings.mangaFolder;
|
||||
case MediaType.Game:
|
||||
return settings.gameFolder;
|
||||
case MediaType.Wiki:
|
||||
return settings.wikiFolder;
|
||||
case MediaType.MusicRelease:
|
||||
return settings.musicReleaseFolder;
|
||||
case MediaType.BoardGame:
|
||||
return settings.boardgameFolder;
|
||||
case MediaType.Book:
|
||||
return settings.bookFolder;
|
||||
}
|
||||
}
|
||||
|
||||
setFolder(settings: MediaDbPluginSettings, folder: string): void {
|
||||
switch (this.mediaType) {
|
||||
case MediaType.Movie:
|
||||
settings.movieFolder = folder;
|
||||
break;
|
||||
case MediaType.Series:
|
||||
settings.seriesFolder = folder;
|
||||
break;
|
||||
case MediaType.Season:
|
||||
settings.seasonFolder = folder;
|
||||
break;
|
||||
case MediaType.ComicManga:
|
||||
settings.mangaFolder = folder;
|
||||
break;
|
||||
case MediaType.Game:
|
||||
settings.gameFolder = folder;
|
||||
break;
|
||||
case MediaType.Wiki:
|
||||
settings.wikiFolder = folder;
|
||||
break;
|
||||
case MediaType.MusicRelease:
|
||||
settings.musicReleaseFolder = folder;
|
||||
break;
|
||||
case MediaType.BoardGame:
|
||||
settings.boardgameFolder = folder;
|
||||
break;
|
||||
case MediaType.Book:
|
||||
settings.bookFolder = folder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Defaults
|
||||
const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
||||
OMDbKeyId: '',
|
||||
TMDBKeyId: '',
|
||||
MobyGamesKeyId: '',
|
||||
GiantBombKeyId: '',
|
||||
IGDBClientId: '',
|
||||
IGDBClientSecret: '',
|
||||
RAWGAPIKeyId: '',
|
||||
ComicVineKeyId: '',
|
||||
BoardgameGeekKeyId: '',
|
||||
|
||||
sfwFilter: true,
|
||||
templates: true,
|
||||
customDateFormat: 'L',
|
||||
openNoteInNewTab: true,
|
||||
useDefaultFrontMatter: true,
|
||||
enableTemplaterIntegration: false,
|
||||
imageDownload: false,
|
||||
imageFolder: 'Media DB/images',
|
||||
|
||||
BoardgameGeekAPI_disabledMediaTypes: [],
|
||||
ComicVineAPI_disabledMediaTypes: [],
|
||||
GiantBombAPI_disabledMediaTypes: [],
|
||||
IGDBAPI_disabledMediaTypes: [],
|
||||
RAWGAPI_disabledMediaTypes: [],
|
||||
MALAPI_disabledMediaTypes: [],
|
||||
MALAPIManga_disabledMediaTypes: [],
|
||||
MobyGamesAPI_disabledMediaTypes: [],
|
||||
MusicBrainzAPI_disabledMediaTypes: [],
|
||||
OMDbAPI_disabledMediaTypes: [],
|
||||
OpenLibraryAPI_disabledMediaTypes: [],
|
||||
SteamAPI_disabledMediaTypes: [],
|
||||
TMDBMovieAPI_disabledMediaTypes: [],
|
||||
TMDBSeasonAPI_disabledMediaTypes: [],
|
||||
TMDBSeriesAPI_disabledMediaTypes: [],
|
||||
VNDBAPI_disabledMediaTypes: [],
|
||||
WikipediaAPI_disabledMediaTypes: [],
|
||||
|
||||
movieTemplate: '',
|
||||
seriesTemplate: '',
|
||||
seasonTemplate: '',
|
||||
mangaTemplate: '',
|
||||
gameTemplate: '',
|
||||
wikiTemplate: '',
|
||||
musicReleaseTemplate: '',
|
||||
boardgameTemplate: '',
|
||||
bookTemplate: '',
|
||||
|
||||
movieFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
seriesFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
seasonFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
mangaFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
gameFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
wikiFileNameTemplate: '{{ title }}',
|
||||
musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})',
|
||||
boardgameFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
bookFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
|
||||
movieFolder: 'Media DB/movies',
|
||||
seriesFolder: 'Media DB/series',
|
||||
seasonFolder: 'Media DB/series',
|
||||
mangaFolder: 'Media DB/comics',
|
||||
gameFolder: 'Media DB/games',
|
||||
wikiFolder: 'Media DB/wiki',
|
||||
musicReleaseFolder: 'Media DB/music',
|
||||
boardgameFolder: 'Media DB/boardgames',
|
||||
bookFolder: 'Media DB/books',
|
||||
|
||||
propertyMappingModels: [],
|
||||
|
||||
// DEPRECATED
|
||||
moviePropertyConversionRules: '',
|
||||
seriesPropertyConversionRules: '',
|
||||
seasonPropertyConversionRules: '',
|
||||
mangaPropertyConversionRules: '',
|
||||
gamePropertyConversionRules: '',
|
||||
wikiPropertyConversionRules: '',
|
||||
musicReleasePropertyConversionRules: '',
|
||||
boardgamePropertyConversionRules: '',
|
||||
bookPropertyConversionRules: '',
|
||||
};
|
||||
|
||||
export const lockedPropertyMappings: string[] = ['type', 'id', 'dataSource'];
|
||||
|
||||
export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings {
|
||||
const defaultSettings = DEFAULT_SETTINGS;
|
||||
|
||||
// construct property mapping defaults
|
||||
const propertyMappingModels: PropertyMappingModelData[] = [];
|
||||
for (const mediaType of MEDIA_TYPES) {
|
||||
const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType);
|
||||
const metadataObj = model.toMetaDataObject();
|
||||
|
||||
const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType);
|
||||
|
||||
for (const key of Object.keys(metadataObj)) {
|
||||
propertyMappingModel.properties.push(
|
||||
new PropertyMapping(
|
||||
key,
|
||||
'',
|
||||
PropertyMappingOption.Default,
|
||||
lockedPropertyMappings.includes(key),
|
||||
false, // wikilink default
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to plain data for serialization
|
||||
propertyMappingModels.push(propertyMappingModel.toJSON());
|
||||
}
|
||||
|
||||
defaultSettings.propertyMappingModels = propertyMappingModels;
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
// MARK: Settings Tab
|
||||
export class MediaDbSettingTab extends PluginSettingTab {
|
||||
plugin: MediaDbPlugin;
|
||||
|
||||
constructor(app: App, plugin: MediaDbPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt));
|
||||
|
||||
// MARK: General settings
|
||||
const generalGroup = new SettingGroup(containerEl);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('SFW filter')
|
||||
.setDesc('Only shows SFW results for APIs that offer filtering.')
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.sfwFilter).onChange(data => {
|
||||
this.plugin.settings.sfwFilter = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Resolve {{ tags }} in templates')
|
||||
.setDesc('Whether to resolve {{ tags }} in templates. The spaces inside the curly braces are important.')
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.templates).onChange(data => {
|
||||
this.plugin.settings.templates = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Date format')
|
||||
.setDesc(createDateFormatDescription(this.plugin.dateFormatter.getPreview()))
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat)
|
||||
.setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat)
|
||||
.onChange(data => {
|
||||
const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat;
|
||||
this.plugin.settings.customDateFormat = newDateFormat;
|
||||
const previewEl = activeDocument.getElementById('media-db-dateformat-preview');
|
||||
if (previewEl) {
|
||||
previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview
|
||||
}
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Open note in new tab')
|
||||
.setDesc('Open the newly created note in a new tab.')
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.openNoteInNewTab).onChange(data => {
|
||||
this.plugin.settings.openNoteInNewTab = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Use default front matter')
|
||||
.setDesc('Whether to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.')
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => {
|
||||
this.plugin.settings.useDefaultFrontMatter = data;
|
||||
void this.plugin.saveSettings();
|
||||
// Redraw settings to display/remove the property mappings
|
||||
this.display();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Enable Templater integration')
|
||||
.setDesc(
|
||||
'Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.',
|
||||
)
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => {
|
||||
this.plugin.settings.enableTemplaterIntegration = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Download images')
|
||||
.setDesc('Downloads images for new notes in the folder below')
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.imageDownload).onChange(data => {
|
||||
this.plugin.settings.imageDownload = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
generalGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Image folder')
|
||||
.setDesc('Where downloaded images should be stored.')
|
||||
.addSearch(cb => {
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.imageFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.imageFolder)
|
||||
.setValue(this.plugin.settings.imageFolder)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.imageFolder = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// MARK: API keys
|
||||
const apiKeyGroup = new SettingGroup(containerEl);
|
||||
apiKeyGroup.setHeading('API Keys');
|
||||
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('OMDb API key')
|
||||
.setDesc('API key for "www.omdbapi.com".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.OMDbKeyId).onChange(data => {
|
||||
this.plugin.settings.OMDbKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('TMDB API key')
|
||||
.setDesc('API Read Access Token for "https://www.themoviedb.org".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.TMDBKeyId).onChange(data => {
|
||||
this.plugin.settings.TMDBKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Moby Games key')
|
||||
.setDesc('API key for "www.mobygames.com".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.MobyGamesKeyId).onChange(data => {
|
||||
this.plugin.settings.MobyGamesKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Giant Bomb Key')
|
||||
.setDesc('API key for "www.giantbomb.com".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.GiantBombKeyId).onChange(data => {
|
||||
this.plugin.settings.GiantBombKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('IGDB Client ID')
|
||||
.setDesc('Client ID for IGDB API (Required for Twitch OAuth).')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.IGDBClientId).onChange(data => {
|
||||
this.plugin.settings.IGDBClientId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('IGDB Client Secret')
|
||||
.setDesc('Client Secret for IGDB API.')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.IGDBClientSecret).onChange(data => {
|
||||
this.plugin.settings.IGDBClientSecret = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('RAWG API Key')
|
||||
.setDesc('API key for "rawg.io".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.RAWGAPIKeyId).onChange(data => {
|
||||
this.plugin.settings.RAWGAPIKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Comic Vine Key')
|
||||
.setDesc('API key for "www.comicvine.gamespot.com".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.ComicVineKeyId).onChange(data => {
|
||||
this.plugin.settings.ComicVineKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName('Boardgame Geek Key')
|
||||
.setDesc('API key for "www.boardgamegeek.com".')
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.BoardgameGeekKeyId).onChange(data => {
|
||||
this.plugin.settings.BoardgameGeekKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
|
||||
// MARK: Media type settings
|
||||
|
||||
// Create a map to store APIs for each media type
|
||||
const mediaTypeApiMap = new Map<MediaType, string[]>();
|
||||
|
||||
// Populate the map with APIs for each media type dynamically
|
||||
for (const api of this.plugin.apiManager.apis) {
|
||||
for (const mediaType of api.types) {
|
||||
if (!mediaTypeApiMap.has(mediaType)) {
|
||||
mediaTypeApiMap.set(mediaType, []);
|
||||
}
|
||||
mediaTypeApiMap.get(mediaType)!.push(api.apiName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const mediaTypeSetting of mediaTypeSettings) {
|
||||
const mediaTypeGroup = new SettingGroup(containerEl);
|
||||
const mediaType = mediaTypeSetting.mediaType;
|
||||
const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType);
|
||||
const mediaTypeNameLower = mediaTypeName.toLowerCase();
|
||||
|
||||
mediaTypeGroup.setHeading(`${mediaTypeName} settings`);
|
||||
|
||||
// Folder
|
||||
mediaTypeGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName(`Import Folder`)
|
||||
.setDesc(`Where newly imported ${mediaTypeNameLower} should be placed.`)
|
||||
.addSearch(cb => {
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
mediaTypeSetting.setFolder(this.plugin.settings, folder.path);
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS))
|
||||
.setValue(mediaTypeSetting.getFolder(this.plugin.settings))
|
||||
.onChange(data => {
|
||||
mediaTypeSetting.setFolder(this.plugin.settings, data);
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Template
|
||||
mediaTypeGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName(`Template`)
|
||||
.setDesc(`Template file to be used when creating a new note for a ${mediaTypeNameLower}.`)
|
||||
.addSearch(cb => {
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
mediaTypeSetting.setTemplate(this.plugin.settings, file.path);
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(`Example: ${mediaTypeNameLower}Template.md`)
|
||||
.setValue(mediaTypeSetting.getTemplate(this.plugin.settings))
|
||||
.onChange(data => {
|
||||
mediaTypeSetting.setTemplate(this.plugin.settings, data);
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// File name template
|
||||
mediaTypeGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName(`File name template`)
|
||||
.setDesc(`Template for the file name used when creating a new note for a ${mediaTypeNameLower}.`)
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder(`Example: ${mediaTypeSetting.getFileNameTemplate(DEFAULT_SETTINGS)}`)
|
||||
.setValue(mediaTypeSetting.getFileNameTemplate(this.plugin.settings))
|
||||
.onChange(data => {
|
||||
mediaTypeSetting.setFileNameTemplate(this.plugin.settings, data);
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// APIs
|
||||
const apis = mediaTypeApiMap.get(mediaType) ?? [];
|
||||
if (apis.length > 1) {
|
||||
for (const apiName of apis) {
|
||||
const api = this.plugin.apiManager.apis.find(api => api.apiName === apiName);
|
||||
if (api) {
|
||||
const disabledMediaTypes = api.getDisabledMediaTypes();
|
||||
|
||||
mediaTypeGroup.addSetting(
|
||||
setting =>
|
||||
void setting
|
||||
.setName(apiName)
|
||||
.setDesc(`Use ${apiName} API for ${unCamelCase(mediaType)}.`)
|
||||
.addToggle(cb => {
|
||||
cb.setValue(!disabledMediaTypes.includes(mediaType)).onChange(data => {
|
||||
if (data) {
|
||||
const index = disabledMediaTypes.indexOf(mediaType);
|
||||
if (index != -1) {
|
||||
disabledMediaTypes.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
disabledMediaTypes.push(mediaType);
|
||||
}
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Property mappings
|
||||
|
||||
if (this.plugin.settings.useDefaultFrontMatter) {
|
||||
const mappingGroup = new SettingGroup(containerEl);
|
||||
mappingGroup.setHeading('Property mappings');
|
||||
mappingGroup.addSetting(setting => {
|
||||
setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
new Notice(`MDB: Property mappings for ${model.type} saved successfully.`);
|
||||
void this.plugin.saveSettings();
|
||||
},
|
||||
}),
|
||||
setting.descEl,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/obsidian/src/settings/suggesters/FileSuggest.ts
Normal file
17
packages/obsidian/src/settings/suggesters/FileSuggest.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { AbstractInputSuggest, TFile } from 'obsidian';
|
||||
|
||||
export class FileSuggest extends AbstractInputSuggest<TFile> {
|
||||
protected getSuggestions(query: string): TFile[] | Promise<TFile[]> {
|
||||
const lowerCaseInputStr = query.toLowerCase();
|
||||
|
||||
// we do two filters because otherwise TS type inference does convert the array to TFile[]
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(file => file instanceof TFile)
|
||||
.filter(file => file.path.toLowerCase().includes(lowerCaseInputStr));
|
||||
}
|
||||
|
||||
renderSuggestion(value: TFile, el: HTMLElement): void {
|
||||
el.setText(value.path);
|
||||
}
|
||||
}
|
||||
17
packages/obsidian/src/settings/suggesters/FolderSuggest.ts
Normal file
17
packages/obsidian/src/settings/suggesters/FolderSuggest.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { AbstractInputSuggest, TFolder } from 'obsidian';
|
||||
|
||||
export class FolderSuggest extends AbstractInputSuggest<TFolder> {
|
||||
protected getSuggestions(query: string): TFolder[] | Promise<TFolder[]> {
|
||||
const lowerCaseInputStr = query.toLowerCase();
|
||||
|
||||
// we do two filters because otherwise TS type inference does convert the array to TFolder[]
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(file => file instanceof TFolder)
|
||||
.filter(file => file.path.toLowerCase().includes(lowerCaseInputStr));
|
||||
}
|
||||
|
||||
renderSuggestion(value: TFolder, el: HTMLElement): void {
|
||||
el.setText(value.path);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue