mirror of
https://github.com/Cametendo/devil-fruit-encyclopedia.git
synced 2026-09-13 15:59:48 +02:00
Merge pull request #5 from Cametendo/codex/add-images-for-characters-and-devil-fruits
Load proper Fandom thumbnails for character and fruit portraits
This commit is contained in:
@@ -786,3 +786,35 @@ p { margin: 0 0 1em; color: var(--text-muted); }
|
||||
.fruit-detail-header { padding: 1.25rem; }
|
||||
.filter-meta { margin-left: 0; width: 100%; }
|
||||
}
|
||||
|
||||
/* ---------- Proper wiki images ---------- */
|
||||
.proper-image {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.proper-image__fallback,
|
||||
.proper-image__img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.proper-image__img {
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.proper-image--fruit .proper-image__img {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
padding: 8%;
|
||||
background: radial-gradient(circle at 50% 45%, rgba(255,255,255,0.08), #0b1220 70%);
|
||||
}
|
||||
|
||||
.proper-image.is-loaded .proper-image__fallback { opacity: 0; }
|
||||
|
||||
@@ -82,4 +82,6 @@
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
||||
|
||||
hydrateProperImages(root);
|
||||
})();
|
||||
|
||||
@@ -79,4 +79,6 @@
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
||||
|
||||
hydrateProperImages(root);
|
||||
})();
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
renderCardGrid(grid, results, fruitCardHTML, 'No Devil Fruits match your search.');
|
||||
resultCount.textContent = `${results.length} of ${FRUITS.length} Devil Fruits`;
|
||||
hydrateProperImages(grid);
|
||||
}
|
||||
|
||||
chipRow.addEventListener('click', (e) => {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
renderCardGrid(grid, results, characterCardHTML, 'No characters match your search. Try a different name or affiliation.');
|
||||
resultCount.textContent = `${results.length} of ${CHARACTERS.length} characters`;
|
||||
hydrateProperImages(grid);
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', applyFilters);
|
||||
|
||||
104
js/portraits.js
104
js/portraits.js
@@ -74,7 +74,26 @@ function iconKeyForCharacter(c) {
|
||||
return 'star';
|
||||
}
|
||||
|
||||
function characterPortraitSVG(c) {
|
||||
function characterImageTitle(c) {
|
||||
return c.imageTitle || c.name;
|
||||
}
|
||||
|
||||
function fruitImageTitle(f) {
|
||||
if (f.imageTitle) return f.imageTitle;
|
||||
if (f.id === 'gomu-gomu-no-mi') return 'Hito Hito no Mi, Model: Nika';
|
||||
return f.name;
|
||||
}
|
||||
|
||||
function properImageHTML(kind, title, label, fallback) {
|
||||
return `
|
||||
<span class="proper-image proper-image--${kind}" data-image-title="${escapeHtml(title)}" data-image-kind="${kind}">
|
||||
<span class="proper-image__fallback">${fallback}</span>
|
||||
<img class="proper-image__img" alt="${escapeHtml(label)}" loading="lazy" referrerpolicy="no-referrer" hidden>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function characterPortraitFallbackSVG(c) {
|
||||
const accent = accentFor(c);
|
||||
const hue2 = (hashHue(c.id + 'x') + 45) % 360;
|
||||
const icon = ROLE_ICONS[iconKeyForCharacter(c)] || ROLE_ICONS.star;
|
||||
@@ -104,7 +123,7 @@ function characterPortraitSVG(c) {
|
||||
|
||||
const CATEGORY_HEX = { Paramecia: '#a06cd5', Zoan: '#4c9a5a', Logia: '#3f9bd0' };
|
||||
|
||||
function fruitPortraitSVG(f) {
|
||||
function fruitPortraitFallbackSVG(f) {
|
||||
const color = CATEGORY_HEX[f.category] || '#e3b04b';
|
||||
const glow = f.subtype && /mythical|special/i.test(f.subtype);
|
||||
const uid = f.id.replace(/[^a-z0-9]/g, '');
|
||||
@@ -126,3 +145,84 @@ function fruitPortraitSVG(f) {
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function characterPortraitSVG(c) {
|
||||
return properImageHTML('character', characterImageTitle(c), `${c.name} portrait`, characterPortraitFallbackSVG(c));
|
||||
}
|
||||
|
||||
function fruitPortraitSVG(f) {
|
||||
return properImageHTML('fruit', fruitImageTitle(f), `${f.name} fruit`, fruitPortraitFallbackSVG(f));
|
||||
}
|
||||
|
||||
const IMAGE_CACHE_KEY = 'opwikiProperImages:v1';
|
||||
const IMAGE_API_ENDPOINT = 'https://onepiece.fandom.com/api.php';
|
||||
let properImageCache = null;
|
||||
|
||||
function getProperImageCache() {
|
||||
if (properImageCache) return properImageCache;
|
||||
try {
|
||||
properImageCache = JSON.parse(localStorage.getItem(IMAGE_CACHE_KEY) || '{}');
|
||||
} catch (_) {
|
||||
properImageCache = {};
|
||||
}
|
||||
return properImageCache;
|
||||
}
|
||||
|
||||
function saveProperImageCache() {
|
||||
try {
|
||||
localStorage.setItem(IMAGE_CACHE_KEY, JSON.stringify(getProperImageCache()));
|
||||
} catch (_) {
|
||||
// Non-critical: images still load for the current page if storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function wikiThumbnailUrl(title, size = 700) {
|
||||
const params = new URLSearchParams({
|
||||
action: 'query',
|
||||
prop: 'pageimages',
|
||||
format: 'json',
|
||||
piprop: 'thumbnail',
|
||||
pithumbsize: String(size),
|
||||
redirects: '1',
|
||||
origin: '*',
|
||||
titles: title,
|
||||
});
|
||||
return `${IMAGE_API_ENDPOINT}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function resolveProperImage(title) {
|
||||
const cache = getProperImageCache();
|
||||
if (Object.prototype.hasOwnProperty.call(cache, title)) return cache[title];
|
||||
|
||||
const response = await fetch(wikiThumbnailUrl(title));
|
||||
if (!response.ok) throw new Error(`Image lookup failed for ${title}`);
|
||||
const data = await response.json();
|
||||
const page = Object.values(data.query?.pages || {})[0];
|
||||
const source = page?.thumbnail?.source || '';
|
||||
cache[title] = source;
|
||||
saveProperImageCache();
|
||||
return source;
|
||||
}
|
||||
|
||||
function hydrateProperImages(root = document) {
|
||||
const holders = [...root.querySelectorAll('.proper-image[data-image-title]')];
|
||||
holders.forEach(async (holder) => {
|
||||
const img = holder.querySelector('.proper-image__img');
|
||||
if (!img || holder.dataset.loaded) return;
|
||||
holder.dataset.loaded = 'pending';
|
||||
|
||||
try {
|
||||
const source = await resolveProperImage(holder.dataset.imageTitle);
|
||||
if (!source) throw new Error('No thumbnail returned');
|
||||
img.addEventListener('load', () => {
|
||||
holder.classList.add('is-loaded');
|
||||
img.hidden = false;
|
||||
}, { once: true });
|
||||
img.src = source;
|
||||
holder.dataset.loaded = 'true';
|
||||
} catch (_) {
|
||||
holder.dataset.loaded = 'fallback';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user