Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3988bf4
Add support for images in media search
ltctceplrm Feb 16, 2026
e4a8e2a
Added images to season search selection
ltctceplrm Feb 16, 2026
47a78d5
Ran prettier
ltctceplrm Feb 16, 2026
68e781e
Normalize css between files
ltctceplrm Feb 16, 2026
f8939d2
Update styles.css
ltctceplrm Feb 16, 2026
c78f7f9
Different rate limit for MALAPI and MALAPIManga
ltctceplrm Feb 16, 2026
33a5c9c
Refactor thumbnails to js instead of hardcoded css
ltctceplrm Apr 7, 2026
2ba460d
Ran prettier
ltctceplrm Apr 7, 2026
3fd393f
Merge branch 'master' into image2
ltctceplrm Apr 7, 2026
0bc0c88
Revert "Merge branch 'master' into image2"
ltctceplrm Jun 7, 2026
fe5fdb9
migrate to secret storage and other improvements
mProjectsCode Apr 6, 2026
d0118e5
fix merge
mProjectsCode Apr 6, 2026
b41402b
add unrelease changelog section
mProjectsCode Apr 6, 2026
e4cefe3
[auto] bump version to `0.8.0-canary.20260406T152025`
mProjectsCode Apr 6, 2026
c6be7de
fix scripts
mProjectsCode Apr 6, 2026
f55e488
[auto] bump version to `0.8.0-canary.20260406T152358`
mProjectsCode Apr 6, 2026
3721e14
fix scripts 2
mProjectsCode Apr 6, 2026
260cd83
[auto] bump version to `0.8.0-canary.20260406T152718`
mProjectsCode Apr 6, 2026
ec33974
add obsidian eslint plugin, solve major issues
mProjectsCode May 6, 2026
e058bcb
cleanup of new result code
mProjectsCode May 6, 2026
1405a05
more changes to modal flow, fixes some issues I hope
mProjectsCode May 21, 2026
1bf7b47
another cleanup pass
mProjectsCode May 21, 2026
1f31acd
add repo config
mProjectsCode Jun 2, 2026
2683ea7
update deps
mProjectsCode Jun 2, 2026
254f7af
fix config
mProjectsCode Jun 2, 2026
74e11d5
fix errors
mProjectsCode Jun 2, 2026
ad2bba4
[auto] bump version to `0.8.0-canary.20260602T115558`
mProjectsCode Jun 2, 2026
f3ac8ac
remove solid; rework season search
mProjectsCode Jun 3, 2026
27b320e
add check pr workflow
mProjectsCode Jun 3, 2026
2e0efba
Merge branch 'master' into image2
ltctceplrm Jun 7, 2026
213268d
Fix casing + ran prettier
ltctceplrm Jun 7, 2026
176bf25
Fixed error when image url can't be downloaded
ltctceplrm Jun 8, 2026
7e32ea4
Improve rate limit delay with auto-fetcher
ltctceplrm Jun 8, 2026
01945b0
Move css to styles.css (WIP)
ltctceplrm Jun 8, 2026
417e2ed
normalized css classes + added missing return type on function
ltctceplrm Jun 8, 2026
6a4913a
Replace emoji with setIcon
ltctceplrm Jun 8, 2026
1cfeca5
Several small improvements
ltctceplrm Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/checkPR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:

- name: Run Checks
run: |
bun run check
bun run check
2 changes: 2 additions & 0 deletions packages/obsidian/src/api/apis/TMDBSeasonAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class TMDBSeasonAPI extends APIModel {
dataSource: this.apiName,
id: result.id?.toString() ?? '',
seasonCount: totalSeasons,
image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '',
});
}),
);
Expand Down Expand Up @@ -242,6 +243,7 @@ export class TMDBSeasonAPI extends APIModel {
id: `${tvId}/season/${seasonNumber}`,
seasonTitle: season.name ?? titleText,
seasonNumber: seasonNumber,
image: season.poster_path ?? '',
}),
);
}
Expand Down
111 changes: 106 additions & 5 deletions packages/obsidian/src/modals/MediaDbSearchResultModal.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type MediaDbPlugin from 'packages/obsidian/src/main';
import { MediaItemComponent } from 'packages/obsidian/src/modals/MediaItemComponent';
import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper';

interface RenderedMediaItem extends MediaTypeModel {
titleEl?: HTMLElement;
summaryEl?: HTMLElement;
}

export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
plugin: MediaDbPlugin;

Expand All @@ -15,6 +21,8 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
skipCallback?: () => void;
submitButtonText: string;

private autoFetchIndex: number;
Comment thread
ltctceplrm marked this conversation as resolved.

constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) {
selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions);
super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect);
Expand All @@ -25,6 +33,7 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
this.submitButtonText = selectModalOptions.submitButtonText ?? 'Ok';
this.busy = false;
this.sendCallback = false;
this.autoFetchIndex = 0;
}

setSubmitCb(submitCallback: (res: SelectModalData) => void): void {
Expand All @@ -39,14 +48,106 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
this.skipCallback = skipCallback;
}

// Renders each suggestion item.
/**
* Returns the rate limit delay based on API source. MAL APIs allow max 3 per second, but that still triggers rate limits, so using 750ms delay.
* @param dataSource The API source name (e.g., 'MALAPI', 'MALAPIManga')
* @returns The delay in milliseconds (750ms for MAL, 200ms for others)
*/
private getDelayForApi(dataSource: string): number {
const isMalApi = dataSource === 'MALAPI' || dataSource === 'MALAPIManga';
return isMalApi ? 750 : 200;
}

/**
* Renders a media suggestion item in the modal.
* Creates the MediaItemComponent and auto-fetches detailed info if the item lacks image or year.
* @param item The media type model to render
* @param el The HTMLElement to render into
*/
renderElement(item: MediaTypeModel, el: HTMLElement): void {
el.createDiv({ text: this.plugin.mediaTypeManager.getFileName(item) });
el.createEl('small', { text: `${item.getSummary()}\n` });
el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` });
const mediaComponent = new MediaItemComponent(el, {
imageUrl: this.getImageUrl(item),
imageAlt: item.title,
onImageError: (): void => {
console.debug('MDB | Image failed to load for', item.id);
},
onImageLoad: (): void => {
console.debug('MDB | Image loaded for', item.id);
},
renderContent: (contentEl: HTMLElement): void => {
const titleEl = contentEl.createDiv({
text: this.plugin.mediaTypeManager.getFileName(item),
cls: 'media-db-plugin-select-title',
});
const summaryEl = contentEl.createEl('small', { text: `${item.getSummary()}\n` });
contentEl.createEl('small', {
text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`,
});

const renderedItem: RenderedMediaItem = item;
renderedItem.titleEl = titleEl;
renderedItem.summaryEl = summaryEl;
},
});

this.autoFetchDetails(item, mediaComponent);
}

private getImageUrl(item: MediaTypeModel): string | undefined {
if (item.image && item.image !== 'NSFW') {
if (!String(item.image).includes('null')) {
return item.image;
}
}
return item.image;
}

private autoFetchDetails(item: MediaTypeModel, mediaComponent: MediaItemComponent): void {
const needsFetch = !item.image || !item.year;
if (!needsFetch) return;

const apiDelay = this.getDelayForApi(item.dataSource);
const index = this.autoFetchIndex++;
const delayMs = index * apiDelay;

console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms', `(${apiDelay}ms per request)`);

window.setTimeout(async () => {
if (item.image && item.year) return;
console.debug('MDB | auto-fetching detail for', item.dataSource, item.id);
try {
console.debug('MDB | fetching detailed info for', item.dataSource, item.id);
const detailedResult = await this.plugin.apiManager.queryDetailedInfo(item);
if (!detailedResult.ok) {
console.warn('MDB | failed to fetch detailed info', detailedResult.error);
return;
}

const detailed = detailedResult.value;
console.debug('MDB | detailed fetch result', detailed?.dataSource, detailed?.id, detailed?.image, detailed?.year);

if (detailed?.image && !item.image) {
item.image = detailed.image;
mediaComponent.updateImage(item.image);
}

if (!item.year && detailed?.year) {
item.year = detailed.year;

const renderedItem: RenderedMediaItem = item;
if (renderedItem.titleEl) {
renderedItem.titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item);
}
if (renderedItem.summaryEl) {
renderedItem.summaryEl.textContent = `${item.getSummary()}\n`;
}
}
} catch (e) {
console.warn('MDB | Failed to fetch detail', e);
}
}, delayMs);
}

// Perform action on the selected suggestion.
submit(): void {
if (!this.busy) {
this.busy = true;
Expand Down
15 changes: 11 additions & 4 deletions packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type MediaDbPlugin from 'packages/obsidian/src/main';
import { MediaItemComponent } from 'packages/obsidian/src/modals/MediaItemComponent';
import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';

export interface SeasonSelectModalElement {
Expand All @@ -25,10 +26,16 @@ export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalEleme
}

renderElement(season: SeasonSelectModalElement, el: HTMLElement): void {
el.createDiv({ text: `${season.name}` });
if (season.air_date) {
el.createEl('small', { text: `Air date: ${season.air_date}` });
}
new MediaItemComponent(el, {
imageUrl: season.poster_path ? (season.poster_path.startsWith('http') ? season.poster_path : `https://image.tmdb.org/t/p/w780${season.poster_path}`) : undefined,
imageAlt: season.name,
renderContent: (contentEl: HTMLElement): void => {
contentEl.createDiv({ text: `${season.name}` });
if (season.air_date) {
contentEl.createEl('small', { text: `Air date: ${season.air_date}` });
}
},
});
}

submit(): void {
Expand Down
94 changes: 94 additions & 0 deletions packages/obsidian/src/modals/MediaItemComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { setIcon } from 'obsidian';

export interface MediaItemComponentOptions {
imageUrl?: string;
imageAlt?: string;
onImageError?: () => void;
onImageLoad?: () => void;
renderContent: (contentEl: HTMLElement) => void;
}

export class MediaItemComponent {
private container: HTMLElement;
private thumbEl!: HTMLElement;
private contentEl!: HTMLElement;
private imgEl: HTMLImageElement | undefined;
private options: MediaItemComponentOptions;

constructor(container: HTMLElement, options: MediaItemComponentOptions) {
this.container = container;
this.options = options;

this.setup();
}

private setup(): void {
// Set container layout
this.container.addClass('media-db-plugin-select-media-item-component');

// Create thumbnail
this.thumbEl = this.container.createDiv({ cls: 'media-db-plugin-select-media-item-thumb' });

// Create content area
this.contentEl = this.container.createDiv({ cls: 'media-db-plugin-select-media-item-content' });

// Render custom content
this.options.renderContent(this.contentEl);

// Setup image if provided
if (this.options.imageUrl) {
this.loadImage(this.options.imageUrl);
} else {
this.showPlaceholder();
}
}

private loadImage(url: string): void {
if (!this.imgEl) {
this.imgEl = activeDocument.createElement('img');
this.imgEl.loading = 'lazy';
this.imgEl.alt = this.options.imageAlt ?? 'Media item';
this.imgEl.className = 'media-db-plugin-select-media-item-image';

this.imgEl.onerror = (): void => {
this.showPlaceholder();
this.options.onImageError?.();
};

this.imgEl.onload = (): void => {
this.options.onImageLoad?.();
};

this.thumbEl.empty();
this.thumbEl.appendChild(this.imgEl);
}

this.imgEl.src = url;
}

private showPlaceholder(): void {
this.thumbEl.empty();
const iconEl = this.thumbEl.createDiv('icon');
setIcon(iconEl, 'image');
iconEl.className = 'media-db-plugin-select-media-item-placeholder';
}

public updateImage(url: string | undefined): void {
if (url && url !== 'NSFW') {
if (!String(url).includes('null')) {
this.loadImage(url);
} else {
this.showPlaceholder();
}
} else if (url === 'NSFW') {
this.thumbEl.empty();
this.thumbEl.createEl('span', { text: 'NSFW', cls: 'media-db-plugin-select-media-item-placeholder' });
} else {
this.showPlaceholder();
}
}

public getContentElement(): HTMLElement {
return this.contentEl;
}
}
39 changes: 39 additions & 0 deletions packages/obsidian/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,45 @@ small.media-db-plugin-list-text {
font-size: 16px;
}

.media-db-plugin-select-media-item-component {
display: flex;
gap: 8px;
align-items: flex-start;
}

.media-db-plugin-select-media-item-thumb {
width: 48px;
height: 72px;
flex: 0 0 48px;
overflow: hidden;
background: var(--background-modifier-hover);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}

.media-db-plugin-select-media-item-content {
flex: 1;
min-width: 0;
}

.media-db-plugin-select-media-item-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

.media-db-plugin-select-media-item-placeholder {
font-size: 24px;
color: var(--text-muted);
}

.media-db-plugin-select-title {
font-weight: 600;
}

.media-db-plugin-select-element-selected {
border-left: 5px solid var(--interactive-accent) !important;
background: var(--background-secondary-alt);
Expand Down
3 changes: 2 additions & 1 deletion packages/obsidian/src/utils/MediaDbFileHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ export class MediaDbFileHelper {

if (!imageResult.ok) {
Logger.warn('MDB | Failed to download image:', imageResult.error);
return imageResult;
delete mediaTypeModel.image;
return ok(undefined);
}
}

Expand Down