another cleanup pass

This commit is contained in:
Moritz Jung 2026-05-21 20:14:11 +02:00
parent 898b495f52
commit ad568b5c9f
8 changed files with 125 additions and 124 deletions

View file

@ -22,7 +22,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
super(plugin.app); super(plugin.app);
this.plugin = plugin; this.plugin = plugin;
this.selectedApis = []; this.selectedApis = [...(advancedSearchModalOptions.preselectedAPIs ?? [])];
this.title = advancedSearchModalOptions.modalTitle ?? ''; this.title = advancedSearchModalOptions.modalTitle ?? '';
this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; this.query = advancedSearchModalOptions.prefilledSearchString ?? '';
this.isBusy = false; this.isBusy = false;
@ -101,7 +101,9 @@ export class MediaDbAdvancedSearchModal extends Modal {
apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName));
apiToggleComponent.onChange(value => { apiToggleComponent.onChange(value => {
if (value) { if (value) {
if (!this.selectedApis.includes(api.apiName)) {
this.selectedApis.push(api.apiName); this.selectedApis.push(api.apiName);
}
} else { } else {
this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); this.selectedApis = this.selectedApis.filter(x => x !== api.apiName);
} }

View file

@ -23,8 +23,8 @@ export class MediaDbIdSearchModal extends Modal {
this.plugin = plugin; this.plugin = plugin;
this.title = idSearchModalOptions.modalTitle ?? ''; this.title = idSearchModalOptions.modalTitle ?? '';
this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName; this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0]?.apiName ?? '';
this.query = ''; this.query = idSearchModalOptions.prefilledSearchString ?? '';
this.isBusy = false; this.isBusy = false;
} }
@ -74,6 +74,7 @@ export class MediaDbIdSearchModal extends Modal {
const searchComponent = new TextComponent(contentEl); const searchComponent = new TextComponent(contentEl);
searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder); searchComponent.setPlaceholder(placeholder);
searchComponent.setValue(this.query);
searchComponent.onChange(value => (this.query = value)); searchComponent.onChange(value => (this.query = value));
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
@ -93,6 +94,7 @@ export class MediaDbIdSearchModal extends Modal {
for (const api of this.plugin.apiManager.apis) { for (const api of this.plugin.apiManager.apis) {
apiSelectorComponent.addOption(api.apiName, api.apiName); apiSelectorComponent.addOption(api.apiName, api.apiName);
} }
apiSelectorComponent.setValue(this.selectedApi);
apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); contentEl.createDiv({ cls: 'media-db-plugin-spacer' });

View file

@ -109,12 +109,13 @@ export class MediaDbSearchModal extends Modal {
if (value) { if (value) {
if (currentToggle && currentToggle !== apiToggleComponent) { if (currentToggle && currentToggle !== apiToggleComponent) {
currentToggle.setValue(false); currentToggle.setValue(false);
this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType);
} }
currentToggle = apiToggleComponent; currentToggle = apiToggleComponent;
this.selectedTypes.push(mediaType); this.selectedTypes = [mediaType];
} else { } else {
if (currentToggle === apiToggleComponent) {
currentToggle = undefined; currentToggle = undefined;
}
this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType);
} }
}); });

View file

@ -62,5 +62,6 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
onClose(): void { onClose(): void {
this.closeCallback?.(); this.closeCallback?.();
super.onClose();
} }
} }

View file

@ -4,6 +4,7 @@ import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
export interface SeasonSelectModalElement { export interface SeasonSelectModalElement {
season_number: number; season_number: number;
name: string; name: string;
episode_count?: number;
air_date?: string; air_date?: string;
poster_path?: string; poster_path?: string;
} }
@ -33,7 +34,6 @@ export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalEleme
submit(): void { submit(): void {
const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value); const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value);
this.submitCallback?.(selected); this.submitCallback?.(selected);
this.close();
} }
skip(): void { skip(): void {
@ -47,4 +47,9 @@ export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalEleme
setCloseCb(cb: (err?: Error) => void): void { setCloseCb(cb: (err?: Error) => void): void {
this.closeCallback = cb; this.closeCallback = cb;
} }
onClose(): void {
this.closeCallback?.();
super.onClose();
}
} }

View file

@ -81,6 +81,7 @@ export abstract class SelectModal<T> extends Modal {
onOpen(): void { onOpen(): void {
const { contentEl, titleEl } = this; const { contentEl, titleEl } = this;
this.selectModalElements = [];
titleEl.createEl('h2', { text: this.title }); titleEl.createEl('h2', { text: this.title });
contentEl.addClass('media-db-plugin-select-modal'); contentEl.addClass('media-db-plugin-select-modal');
contentEl.createEl('p', { text: this.description }); contentEl.createEl('p', { text: this.description });
@ -125,6 +126,11 @@ export abstract class SelectModal<T> extends Modal {
}); });
} }
onClose(): void {
this.selectModalElements = [];
this.contentEl.empty();
}
activateHighlighted(): void { activateHighlighted(): void {
for (const selectModalElement of this.selectModalElements) { for (const selectModalElement of this.selectModalElements) {
if (selectModalElement.isHighlighted()) { if (selectModalElement.isHighlighted()) {

View file

@ -2,7 +2,6 @@ import { MarkdownView, Notice } from 'obsidian';
import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI';
import type MediaDbPlugin from 'packages/obsidian/src/main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal';
import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
@ -288,22 +287,16 @@ export class MediaDbEntryHelper {
} }
private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise<SeasonSelectModalElement[] | undefined> { private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise<SeasonSelectModalElement[] | undefined> {
const modal = new MediaDbSeasonSelectModal( return await this.plugin.modalHelper.promptSeasonSelectModal({
this.plugin, seasons: allSeasons.map(season => ({
allSeasons.map(season => ({
season_number: season.seasonNumber, season_number: season.seasonNumber,
name: season.seasonTitle || season.title, name: season.seasonTitle || season.title,
episode_count: season.episodes || 0, episode_count: season.episodes || 0,
air_date: season.year, air_date: season.year,
poster_path: season.image, poster_path: season.image,
})), })),
true, multiSelect: true,
seriesTitle, seriesName: seriesTitle,
);
return await new Promise(resolve => {
modal.setSubmitCb(resolve);
modal.open();
}); });
} }

View file

@ -4,6 +4,8 @@ import { MediaDbIdSearchModal } from 'packages/obsidian/src/modals/MediaDbIdSear
import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal'; import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal';
import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal';
import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal';
import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal';
import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError';
@ -66,6 +68,12 @@ export interface PreviewModalOptions {
elements?: MediaTypeModel[]; elements?: MediaTypeModel[];
} }
export interface SeasonSelectModalOptions {
seasons?: SeasonSelectModalElement[];
multiSelect?: boolean;
seriesName?: string;
}
export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = {
modalTitle: 'Media DB Search', modalTitle: 'Media DB Search',
preselectedTypes: [], preselectedTypes: [],
@ -110,6 +118,14 @@ export interface ModalLifecycle {
close(): void; close(): void;
} }
interface CloseAwareModal extends ModalLifecycle {
setCloseCb(closeCallback: (err?: Error) => void): void;
}
interface SubmitModal<TData> extends CloseAwareModal {
setSubmitCb(submitCallback: (res: TData) => void): void;
}
export interface ModalSession<T, TModal extends ModalLifecycle> { export interface ModalSession<T, TModal extends ModalLifecycle> {
modalResult: Outcome<T, MDBError>; modalResult: Outcome<T, MDBError>;
modal: TModal; modal: TModal;
@ -134,11 +150,11 @@ export class ModalHelper {
let resolved = false; let resolved = false;
const modalResult = await new Promise<Outcome<TData, MDBError>>(resolve => { const modalResult = await new Promise<Outcome<TData, MDBError>>(resolve => {
const resolveSession = (result: Outcome<TData, MDBError>): void => { const resolveSession = (result: Outcome<TData, MDBError>): void => {
if (!resolved) {
if (result.status === OutcomeStatus.Cancelled && !closeRequested) { if (result.status === OutcomeStatus.Cancelled && !closeRequested) {
cancelledByUser = true; cancelledByUser = true;
} }
if (!resolved) {
resolved = true; resolved = true;
resolve(result); resolve(result);
} }
@ -159,6 +175,40 @@ export class ModalHelper {
}; };
} }
private createCloseOutcome(err: Error | undefined, message: string): Outcome<never, MDBError> {
if (err) {
return failure(toMdbError(err, { kind: MDBErrorKind.Modal, message }));
}
return cancelled();
}
private async openSubmitModalSession<TData, TModal extends SubmitModal<TData>>(
createModal: () => TModal,
closeErrorMessage: string,
wireExtraHandlers?: (modal: TModal, resolve: (result: Outcome<TData, MDBError>) => void) => void,
): Promise<ModalSession<TData, TModal>> {
return await this.openModalCore<TData, TModal>(createModal, (modal, resolve) => {
modal.setSubmitCb(res => resolve(success(res)));
modal.setCloseCb(err => resolve(this.createCloseOutcome(err, closeErrorMessage)));
wireExtraHandlers?.(modal, resolve);
});
}
private async resolveModalSessionOutcome<TData, TModal extends ModalLifecycle>(
sessionPromise: Promise<ModalSession<TData, TModal>>,
closeOn: readonly OutcomeStatus[] = [OutcomeStatus.Ok],
): Promise<Outcome<TData, MDBError>> {
const session = await sessionPromise;
const { modalResult } = session;
if (closeOn.includes(modalResult.status)) {
session.close();
}
return modalResult;
}
private async resolveOutcome<T>(outcomePromise: Promise<Outcome<T, MDBError>>): Promise<T | undefined> { private async resolveOutcome<T>(outcomePromise: Promise<Outcome<T, MDBError>>): Promise<T | undefined> {
const outcome = await outcomePromise; const outcome = await outcomePromise;
@ -200,30 +250,13 @@ export class ModalHelper {
} }
async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise<Outcome<SearchModalData, MDBError>> { async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise<Outcome<SearchModalData, MDBError>> {
const session = await this.createSearchModalSession(searchModalOptions); return await this.resolveModalSessionOutcome(this.createSearchModalSession(searchModalOptions));
const { modalResult } = session;
if (modalResult.status === OutcomeStatus.Ok) {
session.close();
}
return modalResult;
} }
async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise<ModalSession<SearchModalData, MediaDbSearchModal>> { async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise<ModalSession<SearchModalData, MediaDbSearchModal>> {
return await this.openModalCore<SearchModalData, MediaDbSearchModal>( return await this.openSubmitModalSession<SearchModalData, MediaDbSearchModal>(
() => new MediaDbSearchModal(this.plugin, searchModalOptions), () => new MediaDbSearchModal(this.plugin, searchModalOptions),
(modal, resolve) => { 'Search modal closed with an error',
modal.setSubmitCb(res => resolve(success(res)));
modal.setCloseCb(err => {
if (err) {
resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Search modal closed with an error' })));
return;
}
resolve(cancelled());
});
},
); );
} }
@ -232,30 +265,13 @@ export class ModalHelper {
} }
async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise<Outcome<AdvancedSearchModalData, MDBError>> { async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise<Outcome<AdvancedSearchModalData, MDBError>> {
const session = await this.createAdvancedSearchModalSession(advancedSearchModalOptions); return await this.resolveModalSessionOutcome(this.createAdvancedSearchModalSession(advancedSearchModalOptions));
const { modalResult } = session;
if (modalResult.status === OutcomeStatus.Ok) {
session.close();
}
return modalResult;
} }
async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise<ModalSession<AdvancedSearchModalData, MediaDbAdvancedSearchModal>> { async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise<ModalSession<AdvancedSearchModalData, MediaDbAdvancedSearchModal>> {
return await this.openModalCore<AdvancedSearchModalData, MediaDbAdvancedSearchModal>( return await this.openSubmitModalSession<AdvancedSearchModalData, MediaDbAdvancedSearchModal>(
() => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions),
(modal, resolve) => { 'Advanced search modal closed with an error',
modal.setSubmitCb(res => resolve(success(res)));
modal.setCloseCb(err => {
if (err) {
resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Advanced search modal closed with an error' })));
return;
}
resolve(cancelled());
});
},
); );
} }
@ -264,30 +280,13 @@ export class ModalHelper {
} }
async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise<Outcome<IdSearchModalData, MDBError>> { async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise<Outcome<IdSearchModalData, MDBError>> {
const session = await this.createIdSearchModalSession(idSearchModalOptions); return await this.resolveModalSessionOutcome(this.createIdSearchModalSession(idSearchModalOptions));
const { modalResult } = session;
if (modalResult.status === OutcomeStatus.Ok) {
session.close();
}
return modalResult;
} }
async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise<ModalSession<IdSearchModalData, MediaDbIdSearchModal>> { async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise<ModalSession<IdSearchModalData, MediaDbIdSearchModal>> {
return await this.openModalCore<IdSearchModalData, MediaDbIdSearchModal>( return await this.openSubmitModalSession<IdSearchModalData, MediaDbIdSearchModal>(
() => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions),
(modal, resolve) => { 'Id search modal closed with an error',
modal.setSubmitCb(res => resolve(success(res)));
modal.setCloseCb(err => {
if (err) {
resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Id search modal closed with an error' })));
return;
}
resolve(cancelled());
});
},
); );
} }
@ -296,28 +295,16 @@ export class ModalHelper {
} }
async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise<Outcome<SelectModalData, MDBError>> { async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise<Outcome<SelectModalData, MDBError>> {
const session = await this.openModalCore<SelectModalData, MediaDbSearchResultModal>( return await this.resolveModalSessionOutcome(
this.openSubmitModalSession<SelectModalData, MediaDbSearchResultModal>(
() => new MediaDbSearchResultModal(this.plugin, selectModalOptions), () => new MediaDbSearchResultModal(this.plugin, selectModalOptions),
'Select modal closed with an error',
(modal, resolve) => { (modal, resolve) => {
modal.setSubmitCb(res => resolve(success(res)));
modal.setSkipCallback(() => resolve(skipped())); modal.setSkipCallback(() => resolve(skipped()));
modal.setCloseCb(err => {
if (err) {
resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Select modal closed with an error' })));
return;
}
resolve(cancelled());
});
}, },
),
[OutcomeStatus.Ok, OutcomeStatus.Skipped],
); );
const { modalResult } = session;
if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) {
session.close();
}
return modalResult;
} }
async promptSelectModal(selectModalOptions: SelectModalOptions): Promise<SelectModalData | undefined> { async promptSelectModal(selectModalOptions: SelectModalOptions): Promise<SelectModalData | undefined> {
@ -325,30 +312,34 @@ export class ModalHelper {
} }
async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise<Outcome<PreviewModalData, MDBError>> { async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise<Outcome<PreviewModalData, MDBError>> {
const session = await this.openModalCore<PreviewModalData, MediaDbPreviewModal>( return await this.resolveModalSessionOutcome(
this.openSubmitModalSession<PreviewModalData, MediaDbPreviewModal>(
() => new MediaDbPreviewModal(this.plugin, previewModalOptions), () => new MediaDbPreviewModal(this.plugin, previewModalOptions),
(modal, resolve) => { 'Preview modal closed with an error',
modal.setSubmitCb(res => resolve(success(res))); ),
modal.setCloseCb(err => {
if (err) {
resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Preview modal closed with an error' })));
return;
}
resolve(cancelled());
});
},
); );
const { modalResult } = session;
if (modalResult.status === OutcomeStatus.Ok) {
session.close();
}
return modalResult;
} }
async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise<PreviewModalData | undefined> { async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise<PreviewModalData | undefined> {
return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions)); return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions));
} }
async createSeasonSelectModalOutcome(seasonSelectModalOptions: SeasonSelectModalOptions): Promise<Outcome<SeasonSelectModalElement[], MDBError>> {
return await this.resolveModalSessionOutcome(
this.openSubmitModalSession<SeasonSelectModalElement[], MediaDbSeasonSelectModal>(
() =>
new MediaDbSeasonSelectModal(
this.plugin,
seasonSelectModalOptions.seasons ?? [],
seasonSelectModalOptions.multiSelect ?? true,
seasonSelectModalOptions.seriesName,
),
'Season select modal closed with an error',
),
);
}
async promptSeasonSelectModal(seasonSelectModalOptions: SeasonSelectModalOptions): Promise<SeasonSelectModalElement[] | undefined> {
return await this.resolveOutcome(this.createSeasonSelectModalOutcome(seasonSelectModalOptions));
}
} }