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

View file

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

View file

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

View file

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

View file

@ -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<SeasonSelectModalEleme
submit(): void {
const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value);
this.submitCallback?.(selected);
this.close();
}
skip(): void {
@ -47,4 +47,9 @@ export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalEleme
setCloseCb(cb: (err?: Error) => void): void {
this.closeCallback = cb;
}
onClose(): void {
this.closeCallback?.();
super.onClose();
}
}

View file

@ -81,6 +81,7 @@ export abstract class SelectModal<T> 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<T> extends Modal {
});
}
onClose(): void {
this.selectModalElements = [];
this.contentEl.empty();
}
activateHighlighted(): void {
for (const selectModalElement of this.selectModalElements) {
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 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<SeasonSelectModalElement[] | undefined> {
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,
});
}

View file

@ -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<TData> extends CloseAwareModal {
setSubmitCb(submitCallback: (res: TData) => void): void;
}
export interface ModalSession<T, TModal extends ModalLifecycle> {
modalResult: Outcome<T, MDBError>;
modal: TModal;
@ -134,11 +150,11 @@ export class ModalHelper {
let resolved = false;
const modalResult = await new Promise<Outcome<TData, MDBError>>(resolve => {
const resolveSession = (result: Outcome<TData, MDBError>): 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<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> {
const outcome = await outcomePromise;
@ -200,30 +250,13 @@ export class ModalHelper {
}
async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise<Outcome<SearchModalData, MDBError>> {
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<ModalSession<SearchModalData, MediaDbSearchModal>> {
return await this.openModalCore<SearchModalData, MediaDbSearchModal>(
return await this.openSubmitModalSession<SearchModalData, MediaDbSearchModal>(
() => 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<Outcome<AdvancedSearchModalData, MDBError>> {
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<ModalSession<AdvancedSearchModalData, MediaDbAdvancedSearchModal>> {
return await this.openModalCore<AdvancedSearchModalData, MediaDbAdvancedSearchModal>(
return await this.openSubmitModalSession<AdvancedSearchModalData, MediaDbAdvancedSearchModal>(
() => 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<Outcome<IdSearchModalData, MDBError>> {
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<ModalSession<IdSearchModalData, MediaDbIdSearchModal>> {
return await this.openModalCore<IdSearchModalData, MediaDbIdSearchModal>(
return await this.openSubmitModalSession<IdSearchModalData, MediaDbIdSearchModal>(
() => 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<Outcome<SelectModalData, MDBError>> {
const session = await this.openModalCore<SelectModalData, MediaDbSearchResultModal>(
() => 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<SelectModalData, MediaDbSearchResultModal>(
() => 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<SelectModalData | undefined> {
@ -325,30 +312,34 @@ export class ModalHelper {
}
async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise<Outcome<PreviewModalData, MDBError>> {
const session = await this.openModalCore<PreviewModalData, MediaDbPreviewModal>(
() => 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<PreviewModalData, MediaDbPreviewModal>(
() => 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<PreviewModalData | undefined> {
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));
}
}