remove solid; rework season search
This commit is contained in:
parent
ee985419e0
commit
21ad8c59d0
17 changed files with 407 additions and 357 deletions
|
|
@ -1,9 +1,18 @@
|
|||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
|
||||
import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { Result } from 'packages/obsidian/src/utils/result';
|
||||
|
||||
export interface SeasonListAPIModel extends APIModel {
|
||||
getSeasonsForSeries(seriesId: string): Promise<Result<SeasonModel[], MDBError>>;
|
||||
}
|
||||
|
||||
export function isSeasonListAPIModel(api: APIModel | undefined): api is SeasonListAPIModel {
|
||||
return typeof api?.getSeasonsForSeries === 'function';
|
||||
}
|
||||
|
||||
export abstract class APIModel {
|
||||
apiName!: string;
|
||||
apiUrl!: string;
|
||||
|
|
@ -22,6 +31,12 @@ export abstract class APIModel {
|
|||
|
||||
abstract getDisabledMediaTypes(): MediaType[];
|
||||
|
||||
getSeasonsForSeries?(seriesId: string): Promise<Result<SeasonModel[], MDBError>>;
|
||||
|
||||
getSeasonApiNameForSeries(_series: MediaTypeModel): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
hasType(type: MediaType): boolean {
|
||||
const disabledMediaTypes = this.getDisabledMediaTypes();
|
||||
return this.types.includes(type) && !disabledMediaTypes.includes(type);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel';
|
|||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
|
||||
import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
|
||||
import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError';
|
||||
|
|
@ -153,14 +154,13 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
}
|
||||
}
|
||||
|
||||
return new SeasonModel({
|
||||
return new SeasonSearchResultModel({
|
||||
title: `${result.name ?? result.original_name ?? ''}`,
|
||||
englishTitle: result.name ?? result.original_name ?? '',
|
||||
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
id: result.id?.toString() ?? '',
|
||||
seasonTitle: result.name ?? result.original_name ?? '',
|
||||
seasonNumber: totalSeasons,
|
||||
seasonCount: totalSeasons,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -252,4 +252,8 @@ export class TMDBSeriesAPI extends APIModel {
|
|||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes;
|
||||
}
|
||||
|
||||
getSeasonApiNameForSeries(_series: MediaTypeModel): string {
|
||||
return 'TMDBSeasonAPI';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
packages/obsidian/src/models/SeasonSearchResultModel.ts
Normal file
30
packages/obsidian/src/models/SeasonSearchResultModel.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type SeasonSearchResultData = ModelToData<SeasonSearchResultModel>;
|
||||
|
||||
export class SeasonSearchResultModel extends MediaTypeModel {
|
||||
seasonCount: number;
|
||||
|
||||
constructor(obj: SeasonSearchResultData) {
|
||||
super();
|
||||
this.seasonCount = 0;
|
||||
|
||||
migrateObject(this, obj, this);
|
||||
this.type = this.getMediaType();
|
||||
}
|
||||
|
||||
getTags(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
getMediaType(): MediaType {
|
||||
return MediaType.Season;
|
||||
}
|
||||
|
||||
getSummary(): string {
|
||||
return `${this.seasonCount} ${this.seasonCount === 1 ? 'season' : 'seasons'}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
159
packages/obsidian/src/settings/PropertyMappingModelComponent.ts
Normal file
159
packages/obsidian/src/settings/PropertyMappingModelComponent.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { Component, setIcon } from 'obsidian';
|
||||
import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { capitalizeFirstLetter } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
interface PropertyMappingModelComponentProps {
|
||||
model: PropertyMappingModelData;
|
||||
save: (model: PropertyMappingModelData) => void;
|
||||
}
|
||||
|
||||
export default class PropertyMappingModelComponent extends Component {
|
||||
private readonly containerEl: HTMLElement;
|
||||
private readonly save: (model: PropertyMappingModelData) => void;
|
||||
private modelData: PropertyMappingModelData;
|
||||
private unsavedChanges = false;
|
||||
|
||||
constructor(containerEl: HTMLElement, props: PropertyMappingModelComponentProps) {
|
||||
super();
|
||||
this.containerEl = containerEl;
|
||||
this.modelData = props.model;
|
||||
this.save = props.save;
|
||||
}
|
||||
|
||||
override onload(): void {
|
||||
this.render();
|
||||
}
|
||||
|
||||
override onunload(): void {
|
||||
this.containerEl.empty();
|
||||
}
|
||||
|
||||
private get validationResult(): { res: boolean; err?: Error } {
|
||||
const model = PropertyMappingModel.fromJSON(this.modelData);
|
||||
return model.validate();
|
||||
}
|
||||
|
||||
private handleSave(): void {
|
||||
const model = PropertyMappingModel.fromJSON(this.modelData);
|
||||
if (!model.validate().res) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.save(model.toJSON());
|
||||
this.unsavedChanges = false;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onModelUpdate(): void {
|
||||
this.unsavedChanges = true;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
this.containerEl.empty();
|
||||
|
||||
const validationResult = this.validationResult;
|
||||
const rootEl = this.containerEl.createDiv('media-db-plugin-property-mappings-model-container');
|
||||
const headerEl = rootEl.createDiv('media-db-plugin-property-mappings-model-header');
|
||||
headerEl.createDiv({ cls: 'setting-item-name', text: capitalizeFirstLetter(this.modelData.type) });
|
||||
|
||||
const actionsEl = headerEl.createDiv('media-db-plugin-property-mappings-model-actions');
|
||||
if (this.unsavedChanges) {
|
||||
actionsEl.createDiv({ cls: 'media-db-plugin-property-mapping-unsaved-changes', text: 'Unsaved changes' });
|
||||
}
|
||||
|
||||
const saveButtonEl = actionsEl.createEl('button', {
|
||||
cls: `media-db-plugin-property-mappings-save-button ${validationResult.res ? 'mod-cta' : 'mod-muted'}`,
|
||||
text: 'Save',
|
||||
});
|
||||
saveButtonEl.addEventListener('click', () => this.handleSave());
|
||||
|
||||
if (!validationResult.res) {
|
||||
rootEl.createDiv({
|
||||
cls: 'media-db-plugin-property-mapping-validation',
|
||||
text: validationResult.err?.message ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
const tableContainerEl = rootEl.createDiv('media-db-plugin-property-mappings-table-container');
|
||||
const tableEl = tableContainerEl.createEl('table', 'media-db-plugin-property-mappings-table');
|
||||
const tableHeadEl = tableEl.createEl('thead');
|
||||
const headerRowEl = tableHeadEl.createEl('tr');
|
||||
headerRowEl.createEl('th', { cls: 'col-property', text: 'Property' });
|
||||
headerRowEl.createEl('th', { cls: 'col-mapping', text: 'Mapping' });
|
||||
headerRowEl.createEl('th', { cls: 'col-new-name', text: 'New name' });
|
||||
headerRowEl.createEl('th', { cls: 'col-wikilink', text: 'Wikilink' });
|
||||
|
||||
const tableBodyEl = tableEl.createEl('tbody');
|
||||
for (const [index, property] of this.modelData.properties.entries()) {
|
||||
const rowEl = tableBodyEl.createEl('tr');
|
||||
const propertyCellEl = rowEl.createEl('td', 'col-property');
|
||||
propertyCellEl.createEl('code', { text: property.property });
|
||||
|
||||
if (property.locked) {
|
||||
const lockedCellEl = rowEl.createEl('td', 'col-locked');
|
||||
lockedCellEl.colSpan = 3;
|
||||
lockedCellEl.createDiv({
|
||||
cls: 'media-db-plugin-property-binding-text',
|
||||
text: 'property cannot be remapped',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappingCellEl = rowEl.createEl('td', 'col-mapping');
|
||||
const selectEl = mappingCellEl.createEl('select', 'dropdown');
|
||||
for (const remappingOption of propertyMappingOptions) {
|
||||
selectEl.createEl('option', {
|
||||
attr: { value: remappingOption },
|
||||
text: remappingOption,
|
||||
});
|
||||
}
|
||||
selectEl.value = propertyMappingOptions.includes(property.mapping) ? property.mapping : PropertyMappingOption.Default;
|
||||
selectEl.addEventListener('change', event => {
|
||||
const targetEl = event.currentTarget as HTMLSelectElement;
|
||||
this.modelData.properties[index].mapping = targetEl.value as PropertyMappingOption;
|
||||
this.modelData.properties[index].newProperty = '';
|
||||
this.onModelUpdate();
|
||||
});
|
||||
|
||||
const newNameCellEl = rowEl.createEl('td', 'col-new-name');
|
||||
if (property.mapping === PropertyMappingOption.Map) {
|
||||
const mappingToEl = newNameCellEl.createDiv('media-db-plugin-property-mapping-to');
|
||||
const iconWrapperEl = mappingToEl.createDiv('icon-wrapper');
|
||||
const iconEl = iconWrapperEl.createDiv('icon');
|
||||
setIcon(iconEl, 'arrow-right');
|
||||
|
||||
const inputEl = mappingToEl.createEl('input', {
|
||||
cls: 'media-db-plugin-property-mapping-input',
|
||||
type: 'text',
|
||||
});
|
||||
inputEl.spellcheck = false;
|
||||
inputEl.value = property.newProperty;
|
||||
inputEl.addEventListener('input', event => {
|
||||
const targetEl = event.currentTarget as HTMLInputElement;
|
||||
this.modelData.properties[index].newProperty = targetEl.value;
|
||||
this.onModelUpdate();
|
||||
});
|
||||
} else {
|
||||
newNameCellEl.createSpan({
|
||||
cls: 'media-db-plugin-property-mapping-to-disabled',
|
||||
text: 'N/A',
|
||||
});
|
||||
}
|
||||
|
||||
const wikilinkCellEl = rowEl.createEl('td', 'col-wikilink');
|
||||
const wikilinkLabelEl = wikilinkCellEl.createEl('label', {
|
||||
cls: 'media-db-plugin-property-mapping-wikilink-label',
|
||||
attr: { title: 'Convert value to wikilink ([[value]])' },
|
||||
});
|
||||
const wikilinkInputEl = wikilinkLabelEl.createEl('input', { type: 'checkbox' });
|
||||
wikilinkInputEl.checked = property.wikilink ?? false;
|
||||
wikilinkInputEl.addEventListener('change', event => {
|
||||
const targetEl = event.currentTarget as HTMLInputElement;
|
||||
this.modelData.properties[index].wikilink = targetEl.checked;
|
||||
this.onModelUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
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,40 @@
|
|||
import { Component } from 'obsidian';
|
||||
import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import PropertyMappingModelComponent from 'packages/obsidian/src/settings/PropertyMappingModelComponent';
|
||||
|
||||
interface PropertyMappingModelsComponentProps {
|
||||
models?: PropertyMappingModelData[];
|
||||
save: (model: PropertyMappingModelData) => void;
|
||||
}
|
||||
|
||||
export default class PropertyMappingModelsComponent extends Component {
|
||||
private readonly containerEl: HTMLElement;
|
||||
private readonly models: PropertyMappingModelData[];
|
||||
private readonly save: (model: PropertyMappingModelData) => void;
|
||||
|
||||
constructor(containerEl: HTMLElement, props: PropertyMappingModelsComponentProps) {
|
||||
super();
|
||||
this.containerEl = containerEl;
|
||||
this.models = props.models ?? [];
|
||||
this.save = props.save;
|
||||
}
|
||||
|
||||
override onload(): void {
|
||||
this.containerEl.empty();
|
||||
const rootEl = this.containerEl.createDiv('setting-item media-db-plugin-property-mappings-models-container');
|
||||
|
||||
for (const model of this.models) {
|
||||
const modelContainerEl = rootEl.createDiv();
|
||||
this.addChild(
|
||||
new PropertyMappingModelComponent(modelContainerEl, {
|
||||
model,
|
||||
save: this.save,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
override onunload(): void {
|
||||
this.containerEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderS
|
|||
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 => {
|
||||
|
|
@ -425,14 +424,25 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings
|
|||
// MARK: Settings Tab
|
||||
export class MediaDbSettingTab extends PluginSettingTab {
|
||||
plugin: MediaDbPlugin;
|
||||
private propertyMappingModelsComponent?: PropertyMappingModelsComponent;
|
||||
|
||||
constructor(app: App, plugin: MediaDbPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
override hide(): void {
|
||||
this.propertyMappingModelsComponent?.unload();
|
||||
this.propertyMappingModelsComponent = undefined;
|
||||
super.hide();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
if (this.propertyMappingModelsComponent) {
|
||||
this.propertyMappingModelsComponent.unload();
|
||||
this.propertyMappingModelsComponent = undefined;
|
||||
}
|
||||
containerEl.empty();
|
||||
|
||||
const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt));
|
||||
|
|
@ -837,24 +847,21 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
mappingGroup.setHeading('Property mappings');
|
||||
mappingGroup.addSetting(setting => {
|
||||
setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription());
|
||||
const propertyMappingsEl = setting.descEl.createDiv();
|
||||
this.propertyMappingModelsComponent = new PropertyMappingModelsComponent(propertyMappingsEl, {
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
new Notice(`MDB: Property mappings for ${model.type} saved successfully.`);
|
||||
void this.plugin.saveSettings();
|
||||
},
|
||||
});
|
||||
this.propertyMappingModelsComponent.load();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,13 @@ small.media-db-plugin-list-text {
|
|||
}
|
||||
|
||||
/* Property Mapping Component Styles */
|
||||
.media-db-plugin-property-mappings-models-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mappings-model-container {
|
||||
margin-bottom: var(--size-4-8);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { TFolder } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal';
|
||||
import { MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { OutcomeStatus } from 'packages/obsidian/src/utils/result';
|
||||
import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils';
|
||||
|
|
@ -26,7 +26,7 @@ export class BulkImportHelper {
|
|||
|
||||
async import(folder: TFolder): Promise<void> {
|
||||
const erroredFiles: BulkImportError[] = [];
|
||||
let canceled: boolean = false;
|
||||
let canceled = false;
|
||||
|
||||
const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{
|
||||
selectedAPI: string;
|
||||
|
|
@ -87,7 +87,11 @@ export class BulkImportHelper {
|
|||
}
|
||||
|
||||
if (modelResult.value) {
|
||||
await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined);
|
||||
const createResult = await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined);
|
||||
if (!createResult.ok) {
|
||||
return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -127,8 +131,25 @@ export class BulkImportHelper {
|
|||
return { filePath: file.path, error: `no search results selected` };
|
||||
}
|
||||
|
||||
const seasonSelectionResult = await this.plugin.entryHelper.handleSeasonSearchSelections(selectModalResult.data.selected, appendContent ? file : undefined);
|
||||
if (seasonSelectionResult.handled) {
|
||||
if (!seasonSelectionResult.created) {
|
||||
return { filePath: file.path, error: 'season selection canceled or no season note was created' };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected);
|
||||
await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
|
||||
if (detailedResults.length === 0) {
|
||||
return { filePath: file.path, error: 'failed to load details for selected search results' };
|
||||
}
|
||||
|
||||
const createResult = await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
|
||||
if (!createResult.ok) {
|
||||
return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import type { TFile } from 'obsidian';
|
||||
import { MarkdownView, Notice } from 'obsidian';
|
||||
import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI';
|
||||
import type { SeasonListAPIModel } from 'packages/obsidian/src/api/APIModel';
|
||||
import { isSeasonListAPIModel } from 'packages/obsidian/src/api/APIModel';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
|
||||
import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel';
|
||||
import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
|
||||
import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
|
|
@ -112,6 +115,7 @@ export class MediaDbEntryHelper {
|
|||
types.length === 1 && types[0] === MediaType.Season
|
||||
? await this.plugin.modalHelper.promptSelectModal({
|
||||
elements: filteredSearchResults,
|
||||
multiSelect: false,
|
||||
description: 'Select one search result to proceed.',
|
||||
submitButtonText: 'Ok',
|
||||
})
|
||||
|
|
@ -121,7 +125,11 @@ export class MediaDbEntryHelper {
|
|||
return;
|
||||
}
|
||||
|
||||
const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected);
|
||||
if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectResults = await this.queryDetails(selectResultsData.selected);
|
||||
|
||||
if (selectResults.length === 0) {
|
||||
return;
|
||||
|
|
@ -167,6 +175,10 @@ export class MediaDbEntryHelper {
|
|||
return;
|
||||
}
|
||||
|
||||
if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectResults = await this.queryDetails(selectResultsData.selected);
|
||||
if (selectResults.length < 1) {
|
||||
return;
|
||||
|
|
@ -239,31 +251,62 @@ export class MediaDbEntryHelper {
|
|||
return detailModels;
|
||||
}
|
||||
|
||||
private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> {
|
||||
if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') {
|
||||
const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title);
|
||||
return { handled: true, seasonsCreated: created };
|
||||
private async handleSeasonWorkflow(types: MediaType[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> {
|
||||
if (!types.includes(MediaType.Series) || !types.includes(MediaType.Season)) {
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
if (types.includes('series') && selectResults.some(result => result.dataSource === 'TMDBSeriesAPI')) {
|
||||
const seriesResults = selectResults.filter(result => result.dataSource === 'TMDBSeriesAPI');
|
||||
if (seriesResults.length === 1 && types.includes('season')) {
|
||||
const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title);
|
||||
return { handled: true, seasonsCreated: created };
|
||||
}
|
||||
const seriesResults = selectResults.filter(result => result.getMediaType() === MediaType.Series);
|
||||
if (seriesResults.length !== 1) {
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
const seriesResult = seriesResults[0];
|
||||
const sourceApi = this.plugin.apiManager.getApiByName(seriesResult.dataSource);
|
||||
const seasonApiName = sourceApi?.getSeasonApiNameForSeries(seriesResult);
|
||||
if (seasonApiName) {
|
||||
const created = await this.showSeasonSelectAndCreate(seriesResult.id, seriesResult.title, undefined, seasonApiName);
|
||||
return { handled: true, seasonsCreated: created };
|
||||
}
|
||||
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise<boolean> {
|
||||
const tmdbSeasonAPI = this.plugin.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI | undefined;
|
||||
if (!tmdbSeasonAPI) {
|
||||
new Notice('TMDBSeasonAPI not available.');
|
||||
private isSeasonSearchResult(model: MediaTypeModel): model is SeasonSearchResultModel {
|
||||
return model instanceof SeasonSearchResultModel || (model.getMediaType() === MediaType.Season && typeof (model as { seasonCount?: unknown }).seasonCount === 'number');
|
||||
}
|
||||
|
||||
async handleSeasonSearchSelections(selectedResults: MediaTypeModel[], attachFile?: TFile): Promise<{ handled: boolean; created: boolean }> {
|
||||
const seasonSearchResults = selectedResults.filter(result => this.isSeasonSearchResult(result));
|
||||
if (seasonSearchResults.length === 0) {
|
||||
return { handled: false, created: false };
|
||||
}
|
||||
|
||||
if (selectedResults.length !== 1) {
|
||||
new Notice('Select exactly one season search result before choosing seasons.');
|
||||
return { handled: true, created: false };
|
||||
}
|
||||
|
||||
const selectedResult = seasonSearchResults[0];
|
||||
return {
|
||||
handled: true,
|
||||
created: await this.showSeasonSelectAndCreate(selectedResult.id, selectedResult.englishTitle || selectedResult.title, attachFile, selectedResult.dataSource),
|
||||
};
|
||||
}
|
||||
|
||||
private getSeasonListApi(apiName: string): SeasonListAPIModel | undefined {
|
||||
const api = this.plugin.apiManager.getApiByName(apiName);
|
||||
return isSeasonListAPIModel(api) ? api : undefined;
|
||||
}
|
||||
|
||||
private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string, attachFile: TFile | undefined, seasonApiName: string): Promise<boolean> {
|
||||
const seasonAPI = this.getSeasonListApi(seasonApiName);
|
||||
if (!seasonAPI) {
|
||||
new Notice(`${seasonApiName} does not support season selection.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId);
|
||||
const allSeasonsResult = await seasonAPI.getSeasonsForSeries(seriesId);
|
||||
if (!allSeasonsResult.ok) {
|
||||
this.reportMdbError(allSeasonsResult.error);
|
||||
new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`);
|
||||
|
|
@ -281,8 +324,13 @@ export class MediaDbEntryHelper {
|
|||
return false;
|
||||
}
|
||||
|
||||
await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI);
|
||||
new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`);
|
||||
const createdCount = await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, seasonAPI, attachFile);
|
||||
if (createdCount === 0) {
|
||||
new Notice('No season entries were created.');
|
||||
return false;
|
||||
}
|
||||
|
||||
new Notice(`Successfully created ${createdCount} season ${createdCount === 1 ? 'entry' : 'entries'}.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -300,21 +348,36 @@ export class MediaDbEntryHelper {
|
|||
});
|
||||
}
|
||||
|
||||
private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise<void> {
|
||||
await Promise.all(
|
||||
private async createNotesForSelectedSeasons(
|
||||
selectedSeasons: SeasonSelectModalElement[],
|
||||
allSeasons: SeasonModel[],
|
||||
seasonAPI: SeasonListAPIModel,
|
||||
attachFile?: TFile,
|
||||
): Promise<number> {
|
||||
const results = await Promise.all(
|
||||
selectedSeasons.map(async selectedSeason => {
|
||||
const seasonModel = allSeasons.find(season => season.seasonNumber === selectedSeason.season_number);
|
||||
if (seasonModel) {
|
||||
const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id);
|
||||
const fullMetadataResult = await seasonAPI.getById(seasonModel.id);
|
||||
if (!fullMetadataResult.ok) {
|
||||
this.reportMdbError(fullMetadataResult.error);
|
||||
new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]);
|
||||
const createResult = await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value], attachFile);
|
||||
if (!createResult.ok) {
|
||||
this.reportMdbError(createResult.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
return results.filter(created => created).length;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue