Skip to content

Instantly share code, notes, and snippets.

@lemonadeforlife
Created June 20, 2026 10:24
Show Gist options
  • Select an option

  • Save lemonadeforlife/b914a9a1f106cb5ae4ec0ea04ec8bbc8 to your computer and use it in GitHub Desktop.

Select an option

Save lemonadeforlife/b914a9a1f106cb5ae4ec0ea04ec8bbc8 to your computer and use it in GitHub Desktop.
Converts MyAnimeList's table list view into MAL's native seasonal-anime card markup
// ==UserScript==
// @name MAL Seasonal View
// @namespace mal-list-to-card
// @version 3.0
// @description Converts MyAnimeList's table list view into MAL's native seasonal-anime card markup
// @author lemonadeforlife
// @match https://myanimelist.net/anime.php?cat=anime*
// @grant none
// ==/UserScript==
(function () {
'use strict';
function escapeHtml(str) {
return String(str == null ? '' : str).replace(/[&<>"']/g, function (c) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
});
}
function upgradeImageUrl(url) {
if (!url) return url;
url = url.split('?')[0];
url = url.replace(/\/r\/\d+x\d+\//, '/');
url = url.replace(/(?<!l)(\.(jpg|jpeg|png|webp))$/i, 'l$1');
return url;
}
function parseYear(dateStr) {
const m = dateStr.match(/^(\d{2})-(\d{2})-(\d{2})$/);
if (!m) return '';
const yy = parseInt(m[3], 10);
return yy > 50 ? 1900 + yy : 2000 + yy;
}
function formatMembers(raw) {
const num = parseInt(String(raw).replace(/[^\d]/g, ''), 10);
if (Number.isNaN(num)) return raw;
if (num >= 1e6) return (num / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
if (num >= 1e3) return Math.round(num / 1e3) + 'K';
return String(num);
}
function parseRow(tr) {
const tds = tr.querySelectorAll(':scope > td');
if (tds.length < 6) return null;
if (!tds[0].querySelector('.picSurround')) return null;
const imgEl = tds[0].querySelector('img');
const rawImg = imgEl ? (imgEl.dataset.src || imgEl.src) : '';
const img = upgradeImageUrl(rawImg);
const titleLink = tr.querySelector('a.hoverinfo_trigger.fw-b');
const href = titleLink ? titleLink.href : '#';
const romaji = titleLink ? titleLink.textContent.trim() : '';
const altDiv = tr.querySelector('div[id^="anime"][title]');
const english = altDiv ? altDiv.getAttribute('title').trim() : '';
const synopsisDiv = tr.querySelector('div.pt4');
let synopsis = '';
if (synopsisDiv) {
const clone = synopsisDiv.cloneNode(true);
const readMore = clone.querySelector('a');
if (readMore) readMore.remove();
synopsis = clone.textContent.trim();
}
const statusLink = tr.querySelector('a.Lightbox_AddEdit');
let type, eps, score, date, members;
if (tds.length >= 7) {
type = tds[2].textContent.trim();
eps = tds[3].textContent.trim();
score = tds[4].textContent.trim();
date = tds[5].textContent.trim();
members = tds[6].textContent.trim();
} else {
type = tds[2].textContent.trim();
eps = tds[3].textContent.trim();
score = tds[4].textContent.trim();
date = '';
members = tds[5].textContent.trim();
}
const hidden = tr.style.display === 'none';
return { img, href, romaji, english, synopsis, statusLink, type, eps, score, date, members, hidden };
}
function buildSeasonalCard(data) {
const card = document.createElement('div');
card.className = 'js-anime-category-producer seasonal-anime js-seasonal-anime js-anime-type-all js-anime-type-1 mal-card-from-list';
if (data.hidden) card.style.display = 'none';
const title = data.english || data.romaji;
const year = parseYear(data.date);
const scoreInt = parseInt(data.score, 10) || 0;
const membersFmt = formatMembers(data.members);
card.innerHTML = `
<div>
<div class="title"><div class="title-text">
<h3 class="h3_anime_subtitle">${escapeHtml(data.english || data.romaji)}</h3>
<h2 class="h2_anime_title"><a href="${data.href}" class="link-title">${escapeHtml(data.romaji)}</a></h2>
<h3 class="h3_anime_subtitle">${escapeHtml(data.english || data.romaji)}</h3>
</div></div>
<div class="prodsrc">
<div class="info">
<span class="item">${escapeHtml(data.type)}${year ? ', ' + year : ''}</span>
<span class="item"><span>${escapeHtml(data.eps)} eps</span></span>
</div>
</div>
</div>
<div class="image">
<a href="${data.href}"><img width="167" alt="${escapeHtml(title)}" src="${data.img}"></a>
<a href="${data.href}" class="link-image">${escapeHtml(title)}</a>
</div>
<div class="synopsis js-synopsis">
<p class="preline">${escapeHtml(data.synopsis)}</p>
</div>
<div class="information">
<div class="information-item scormem">
<div class="scormem-container">
<div class="scormem-item score score-label score-${scoreInt}" title="Score">
<i class="fa-regular fa-star mr4"></i>${escapeHtml(data.score)}
</div>
<div class="scormem-item member" title="Members">
<i class="fa-solid fa-user mr4"></i>${escapeHtml(membersFmt)}
</div>
</div>
</div>
<div class="information-item buttons mal-btn-slot"></div>
</div>
`;
const prodsrcEl = card.querySelector('.prodsrc');
if (prodsrcEl) {
const prodsrcStyles = {
'background-color': '#181818',
'color': '#a3a3a3',
'display': 'flex',
'flex-direction': 'row',
'font-size': '18px',
'height': '22px',
'justify-content': 'center',
'line-height': '22px',
'overflow': 'hidden',
'padding': '1px 6px',
'position': 'relative',
'text-align': 'center'
};
Object.entries(prodsrcStyles).forEach(([prop, val]) => {
prodsrcEl.style.setProperty(prop, val, 'important');
});
}
if (data.statusLink) {
const btn = data.statusLink.cloneNode(true);
const span = btn.querySelector('span') || btn;
const isAdd = span.textContent.trim().toLowerCase() === 'add';
if (isAdd) {
span.textContent = 'Add To List';
btn.style.backgroundColor = '#4f74c8';
btn.style.color = '#fff';
}
btn.style.borderRadius = '3px';
btn.style.display = 'block';
btn.style.fontSize = '11px';
btn.style.height = '19px';
btn.style.lineHeight = '21px';
btn.style.width = '68px';
card.querySelector('.mal-btn-slot').appendChild(btn);
}
return card;
}
function convert() {
const containers = document.querySelectorAll('div.js-block-list.list');
containers.forEach(container => {
const table = container.querySelector('table');
if (!table || container.dataset.malCardified) return;
const rows = table.querySelectorAll('tbody > tr');
const cards = [];
rows.forEach(tr => {
const data = parseRow(tr);
if (data && (data.romaji || data.english) && data.img) cards.push(buildSeasonalCard(data));
});
if (!cards.length) return;
const grid = document.createElement('div');
grid.className = 'seasonal-anime-list js-seasonal-anime-list';
cards.forEach(c => grid.appendChild(c));
table.style.display = 'none';
container.dataset.malCardified = 'true';
container.appendChild(grid);
const toggleBtn = document.createElement('button');
toggleBtn.textContent = 'Switch to list view';
toggleBtn.style.margin = '10px 0';
let isCard = true;
toggleBtn.addEventListener('click', () => {
isCard = !isCard;
grid.style.display = isCard ? '' : 'none';
table.style.display = isCard ? 'none' : '';
toggleBtn.textContent = isCard ? 'Switch to list view' : 'Switch to card view';
});
container.insertBefore(toggleBtn, container.firstChild);
});
}
const run = () => setTimeout(convert, 300);
if (document.readyState === 'complete' || document.readyState === 'interactive') {
run();
} else {
document.addEventListener('DOMContentLoaded', run);
}
const observer = new MutationObserver(() => convert());
observer.observe(document.body, { childList: true, subtree: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment