Created
June 20, 2026 10:24
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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