mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
Migrated app.js and components.js to TypeScript. Extracted inline scripts from base.html to cardHover.ts and cardImages.ts modules for better maintainability and code reuse.
153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
/**
|
|
* Card Image URL Builders & Retry Logic
|
|
*
|
|
* Utilities for constructing card image URLs and handling image load failures
|
|
* with automatic fallback to different image sizes.
|
|
*
|
|
* Features:
|
|
* - Build card image URLs with face (front/back) support
|
|
* - Build Scryfall image URLs with version control
|
|
* - Automatic retry on image load failure (different sizes)
|
|
* - Cache-busting support for failed loads
|
|
* - HTMX swap integration for dynamic content
|
|
*
|
|
* NOTE: This module exposes functions globally on window for browser compatibility
|
|
*/
|
|
|
|
interface ImageRetryState {
|
|
vi: number; // Current version index
|
|
nocache: number; // Cache-busting flag (0 or 1)
|
|
versions: string[]; // Image versions to try ['small', 'normal', 'large']
|
|
}
|
|
|
|
const IMG_FLAG = '__cardImgRetry';
|
|
|
|
/**
|
|
* Normalize card name by removing synergy suffixes
|
|
*/
|
|
function normalizeCardName(raw: string): string {
|
|
if (!raw) return raw;
|
|
const normalize = (window as any).__normalizeCardName || ((name: string) => {
|
|
if (!name) return name;
|
|
const m = /(.*?)(\s*-\s*Synergy\s*\(.*\))$/i.exec(name);
|
|
if (m) return m[1].trim();
|
|
return name;
|
|
});
|
|
return normalize(raw);
|
|
}
|
|
|
|
/**
|
|
* Build card image URL with face support (front/back)
|
|
* @param name - Card name
|
|
* @param version - Image version ('small', 'normal', 'large')
|
|
* @param nocache - Add cache-busting timestamp
|
|
* @param face - Card face ('front' or 'back')
|
|
*/
|
|
function buildCardUrl(name: string, version?: string, nocache?: boolean, face?: string): string {
|
|
name = normalizeCardName(name);
|
|
const q = encodeURIComponent(name || '');
|
|
let url = '/api/images/' + (version || 'normal') + '/' + q;
|
|
if (face === 'back') url += '?face=back';
|
|
if (nocache) url += (face === 'back' ? '&' : '?') + 't=' + Date.now();
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Build Scryfall image URL
|
|
* @param name - Card name
|
|
* @param version - Image version ('small', 'normal', 'large')
|
|
* @param nocache - Add cache-busting timestamp
|
|
*/
|
|
function buildScryfallImageUrl(name: string, version?: string, nocache?: boolean): string {
|
|
name = normalizeCardName(name);
|
|
const q = encodeURIComponent(name || '');
|
|
let url = '/api/images/' + (version || 'normal') + '/' + q;
|
|
if (nocache) url += '?t=' + Date.now();
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Bind error handler to an image element for automatic retry with fallback versions
|
|
* @param img - Image element with data-card-name attribute
|
|
* @param versions - Array of image versions to try in order
|
|
*/
|
|
function bindCardImageRetry(img: HTMLImageElement, versions?: string[]): void {
|
|
try {
|
|
if (!img || (img as any)[IMG_FLAG]) return;
|
|
const name = img.getAttribute('data-card-name') || '';
|
|
if (!name) return;
|
|
|
|
// Default versions: normal -> large
|
|
const versionList = versions && versions.length ? versions.slice() : ['normal', 'large'];
|
|
(img as any)[IMG_FLAG] = {
|
|
vi: 0,
|
|
nocache: 0,
|
|
versions: versionList
|
|
} as ImageRetryState;
|
|
|
|
img.addEventListener('error', function() {
|
|
const st = (img as any)[IMG_FLAG] as ImageRetryState;
|
|
if (!st) return;
|
|
|
|
// Try next version
|
|
if (st.vi < st.versions.length - 1) {
|
|
st.vi += 1;
|
|
img.src = buildScryfallImageUrl(name, st.versions[st.vi], false);
|
|
}
|
|
// Try cache-busting current version
|
|
else if (!st.nocache) {
|
|
st.nocache = 1;
|
|
img.src = buildScryfallImageUrl(name, st.versions[st.vi], true);
|
|
}
|
|
});
|
|
|
|
// If initial load already failed before binding, try next immediately
|
|
if (img.complete && img.naturalWidth === 0) {
|
|
const st = (img as any)[IMG_FLAG] as ImageRetryState;
|
|
const current = img.src || '';
|
|
const first = buildScryfallImageUrl(name, st.versions[0], false);
|
|
|
|
// Check if current src matches first version
|
|
if (current.indexOf(encodeURIComponent(name)) !== -1 &&
|
|
current.indexOf('version=' + st.versions[0]) !== -1) {
|
|
st.vi = Math.min(1, st.versions.length - 1);
|
|
img.src = buildScryfallImageUrl(name, st.versions[st.vi], false);
|
|
} else {
|
|
// Re-trigger current request (may succeed if transient error)
|
|
img.src = current;
|
|
}
|
|
}
|
|
} catch (_) {
|
|
// Silently fail - image retry is a nice-to-have feature
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bind retry handlers to all card images in the document
|
|
*/
|
|
function bindAllCardImageRetries(): void {
|
|
document.querySelectorAll('img[data-card-name]').forEach((img) => {
|
|
// Use thumbnail fallbacks for card-thumb, otherwise preview fallbacks
|
|
const versions = (img.classList && img.classList.contains('card-thumb'))
|
|
? ['small', 'normal', 'large']
|
|
: ['normal', 'large'];
|
|
bindCardImageRetry(img as HTMLImageElement, versions);
|
|
});
|
|
}
|
|
|
|
// Expose globally for browser usage
|
|
(window as any).__initCardImages = function initCardImages(): void {
|
|
// Expose retry binding globally for dynamic content
|
|
(window as any).bindAllCardImageRetries = bindAllCardImageRetries;
|
|
|
|
// Initial bind
|
|
bindAllCardImageRetries();
|
|
|
|
// Re-bind after HTMX swaps
|
|
document.addEventListener('htmx:afterSwap', bindAllCardImageRetries);
|
|
};
|
|
|
|
// Auto-initialize on load
|
|
if (typeof window !== 'undefined') {
|
|
(window as any).__initCardImages();
|
|
}
|