From ad568b5c9f041dd96eac768794e3dc9a69b1d4fb Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 21 May 2026 20:14:11 +0200 Subject: [PATCH] another cleanup pass --- .../src/modals/MediaDbAdvancedSearchModal.ts | 6 +- .../src/modals/MediaDbIdSearchModal.ts | 6 +- .../obsidian/src/modals/MediaDbSearchModal.ts | 7 +- .../src/modals/MediaDbSearchResultModal.ts | 1 + .../src/modals/MediaDbSeasonSelectModal.ts | 7 +- packages/obsidian/src/modals/SelectModal.ts | 6 + .../obsidian/src/utils/MediaDbEntryHelper.ts | 15 +- packages/obsidian/src/utils/ModalHelper.ts | 201 +++++++++--------- 8 files changed, 125 insertions(+), 124 deletions(-) diff --git a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index 3bedff7..2282376 100644 --- a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -22,7 +22,7 @@ export class MediaDbAdvancedSearchModal extends Modal { super(plugin.app); this.plugin = plugin; - this.selectedApis = []; + this.selectedApis = [...(advancedSearchModalOptions.preselectedAPIs ?? [])]; this.title = advancedSearchModalOptions.modalTitle ?? ''; this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; this.isBusy = false; @@ -101,7 +101,9 @@ export class MediaDbAdvancedSearchModal extends Modal { apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); apiToggleComponent.onChange(value => { if (value) { - this.selectedApis.push(api.apiName); + if (!this.selectedApis.includes(api.apiName)) { + this.selectedApis.push(api.apiName); + } } else { this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); } diff --git a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 1427d1e..48a5192 100644 --- a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -23,8 +23,8 @@ export class MediaDbIdSearchModal extends Modal { this.plugin = plugin; this.title = idSearchModalOptions.modalTitle ?? ''; - this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName; - this.query = ''; + this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0]?.apiName ?? ''; + this.query = idSearchModalOptions.prefilledSearchString ?? ''; this.isBusy = false; } @@ -74,6 +74,7 @@ export class MediaDbIdSearchModal extends Modal { const searchComponent = new TextComponent(contentEl); searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); + searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); 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) { apiSelectorComponent.addOption(api.apiName, api.apiName); } + apiSelectorComponent.setValue(this.selectedApi); apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); diff --git a/packages/obsidian/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts index 03398cb..248da59 100644 --- a/packages/obsidian/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -109,12 +109,13 @@ export class MediaDbSearchModal extends Modal { if (value) { if (currentToggle && currentToggle !== apiToggleComponent) { currentToggle.setValue(false); - this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } currentToggle = apiToggleComponent; - this.selectedTypes.push(mediaType); + this.selectedTypes = [mediaType]; } else { - currentToggle = undefined; + if (currentToggle === apiToggleComponent) { + currentToggle = undefined; + } this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } }); diff --git a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts index e513663..f6f372f 100644 --- a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -62,5 +62,6 @@ export class MediaDbSearchResultModal extends SelectModal { onClose(): void { this.closeCallback?.(); + super.onClose(); } } diff --git a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index 6508c63..5bf902d 100644 --- a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -4,6 +4,7 @@ import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; export interface SeasonSelectModalElement { season_number: number; name: string; + episode_count?: number; air_date?: string; poster_path?: string; } @@ -33,7 +34,6 @@ export class MediaDbSeasonSelectModal extends SelectModal x.isActive()).map(x => x.value); this.submitCallback?.(selected); - this.close(); } skip(): void { @@ -47,4 +47,9 @@ export class MediaDbSeasonSelectModal extends SelectModal void): void { this.closeCallback = cb; } + + onClose(): void { + this.closeCallback?.(); + super.onClose(); + } } diff --git a/packages/obsidian/src/modals/SelectModal.ts b/packages/obsidian/src/modals/SelectModal.ts index 8d9ef3e..d6c142f 100644 --- a/packages/obsidian/src/modals/SelectModal.ts +++ b/packages/obsidian/src/modals/SelectModal.ts @@ -81,6 +81,7 @@ export abstract class SelectModal extends Modal { onOpen(): void { const { contentEl, titleEl } = this; + this.selectModalElements = []; titleEl.createEl('h2', { text: this.title }); contentEl.addClass('media-db-plugin-select-modal'); contentEl.createEl('p', { text: this.description }); @@ -125,6 +126,11 @@ export abstract class SelectModal extends Modal { }); } + onClose(): void { + this.selectModalElements = []; + this.contentEl.empty(); + } + activateHighlighted(): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.isHighlighted()) { diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 7714161..b5c4f7e 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -2,7 +2,6 @@ import { MarkdownView, Notice } from 'obsidian'; import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; import type MediaDbPlugin from 'packages/obsidian/src/main'; 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 { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; @@ -288,22 +287,16 @@ export class MediaDbEntryHelper { } private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { - const modal = new MediaDbSeasonSelectModal( - this.plugin, - allSeasons.map(season => ({ + return await this.plugin.modalHelper.promptSeasonSelectModal({ + seasons: allSeasons.map(season => ({ season_number: season.seasonNumber, name: season.seasonTitle || season.title, episode_count: season.episodes || 0, air_date: season.year, poster_path: season.image, })), - true, - seriesTitle, - ); - - return await new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); + multiSelect: true, + seriesName: seriesTitle, }); } diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index b014a8d..3df0ee7 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -4,6 +4,8 @@ import { MediaDbIdSearchModal } from 'packages/obsidian/src/modals/MediaDbIdSear import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal'; import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; 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 { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; @@ -66,6 +68,12 @@ export interface PreviewModalOptions { elements?: MediaTypeModel[]; } +export interface SeasonSelectModalOptions { + seasons?: SeasonSelectModalElement[]; + multiSelect?: boolean; + seriesName?: string; +} + export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { modalTitle: 'Media DB Search', preselectedTypes: [], @@ -110,6 +118,14 @@ export interface ModalLifecycle { close(): void; } +interface CloseAwareModal extends ModalLifecycle { + setCloseCb(closeCallback: (err?: Error) => void): void; +} + +interface SubmitModal extends CloseAwareModal { + setSubmitCb(submitCallback: (res: TData) => void): void; +} + export interface ModalSession { modalResult: Outcome; modal: TModal; @@ -134,11 +150,11 @@ export class ModalHelper { let resolved = false; const modalResult = await new Promise>(resolve => { const resolveSession = (result: Outcome): void => { - if (result.status === OutcomeStatus.Cancelled && !closeRequested) { - cancelledByUser = true; - } - if (!resolved) { + if (result.status === OutcomeStatus.Cancelled && !closeRequested) { + cancelledByUser = true; + } + resolved = true; resolve(result); } @@ -159,6 +175,40 @@ export class ModalHelper { }; } + private createCloseOutcome(err: Error | undefined, message: string): Outcome { + if (err) { + return failure(toMdbError(err, { kind: MDBErrorKind.Modal, message })); + } + + return cancelled(); + } + + private async openSubmitModalSession>( + createModal: () => TModal, + closeErrorMessage: string, + wireExtraHandlers?: (modal: TModal, resolve: (result: Outcome) => void) => void, + ): Promise> { + return await this.openModalCore(createModal, (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => resolve(this.createCloseOutcome(err, closeErrorMessage))); + wireExtraHandlers?.(modal, resolve); + }); + } + + private async resolveModalSessionOutcome( + sessionPromise: Promise>, + closeOn: readonly OutcomeStatus[] = [OutcomeStatus.Ok], + ): Promise> { + const session = await sessionPromise; + const { modalResult } = session; + + if (closeOn.includes(modalResult.status)) { + session.close(); + } + + return modalResult; + } + private async resolveOutcome(outcomePromise: Promise>): Promise { const outcome = await outcomePromise; @@ -200,30 +250,13 @@ export class ModalHelper { } async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { - const session = await this.createSearchModalSession(searchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createSearchModalSession(searchModalOptions)); } async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbSearchModal(this.plugin, searchModalOptions), - (modal, resolve) => { - 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()); - }); - }, + 'Search modal closed with an error', ); } @@ -232,30 +265,13 @@ export class ModalHelper { } async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - const session = await this.createAdvancedSearchModalSession(advancedSearchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createAdvancedSearchModalSession(advancedSearchModalOptions)); } async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), - (modal, resolve) => { - 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()); - }); - }, + 'Advanced search modal closed with an error', ); } @@ -264,30 +280,13 @@ export class ModalHelper { } async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { - const session = await this.createIdSearchModalSession(idSearchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createIdSearchModalSession(idSearchModalOptions)); } async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), - (modal, resolve) => { - 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()); - }); - }, + 'Id search modal closed with an error', ); } @@ -296,28 +295,16 @@ export class ModalHelper { } async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { - const session = await this.openModalCore( - () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - 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()); - }); - }, + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), + 'Select modal closed with an error', + (modal, resolve) => { + modal.setSkipCallback(() => resolve(skipped())); + }, + ), + [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 { @@ -325,30 +312,34 @@ export class ModalHelper { } async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { - const session = await this.openModalCore( - () => new MediaDbPreviewModal(this.plugin, previewModalOptions), - (modal, resolve) => { - 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()); - }); - }, + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => new MediaDbPreviewModal(this.plugin, previewModalOptions), + 'Preview modal closed with an error', + ), ); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; } async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise { return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions)); } + + async createSeasonSelectModalOutcome(seasonSelectModalOptions: SeasonSelectModalOptions): Promise> { + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => + new MediaDbSeasonSelectModal( + this.plugin, + seasonSelectModalOptions.seasons ?? [], + seasonSelectModalOptions.multiSelect ?? true, + seasonSelectModalOptions.seriesName, + ), + 'Season select modal closed with an error', + ), + ); + } + + async promptSeasonSelectModal(seasonSelectModalOptions: SeasonSelectModalOptions): Promise { + return await this.resolveOutcome(this.createSeasonSelectModalOutcome(seasonSelectModalOptions)); + } }