1(function initShowsPage() { 2 const grid = document.getElementById("showsGrid"); 3 const countEl = document.getElementById("showsCount"); 4 const searchInput = document.getElementById("showsSearch"); 5 if (!grid) return; 6 let allShows = []; 7 8 function crunchyrollUrl(show) { 9 if (show.url) return show.url; 10 const slug = String(show.search || show.title || "") 11 .trim() 12 .replace(/\s+/g, "-"); 13 return `https://www.crunchyroll.com/search?q=${encodeURIComponent(slug)}`; 14 } 15 16 function escapeHtml(value) { 17 return String(value) 18 .replace(/&/g, "&") 19 .replace(/</g, "<") 20 .replace(/>/g, ">"); 21 } 22 23 function groupByLetter(shows) { 24 const groups = new Map(); 25 for (const show of shows) { 26 const title = String(show.title || show.search || "Untitled").trim(); 27 const first = title.charAt(0).toUpperCase(); 28 const letter = /[A-Z]/.test(first) ? first : "#"; 29 const list = groups.get(letter) ?? []; 30 list.push(show); 31 groups.set(letter, list); 32 } 33 return Array.from(groups.entries()).sort((a, b) => { 34 if (a[0] === "#") return 1; 35 if (b[0] === "#") return -1; 36 return a[0].localeCompare(b[0]); 37 }); 38 } 39
40 function renderShows(shows, query = "") { 41 if (countEl) { 42 const base = `${shows.length} show${shows.length === 1 ? "" : "s"} with Japanese subtitles`; 43 countEl.textContent = query ? `${base} (filtered by "${query}")` : base; 44 } 45 46 if (shows.length === 0) { 47 grid.innerHTML = ` 48 <div class="shows-empty"> 49 No shows found${query ? ` for "${escapeHtml(query)}"` : ""}. 50 </div> 51 `; 52 return; 53 } 54 55 const groups = groupByLetter(shows); 56 grid.innerHTML = groups 57 .map(([letter, entries]) => { 58 const cards = entries 59 .map((show) => { 60 const title = show.title || show.search || "Untitled"; 61 const href = crunchyrollUrl(show); 62 return ` 63 <a class="show-card" href="${href}" target="_blank" rel="noopener noreferrer"> 64 <span class="show-card__body"> 65 <span class="show-card__title">${escapeHtml(title)}</span> 66 <span class="show-card__meta">Open on Crunchyroll</span> 67 </span> 68 </a> 69 `; 70 }) 71 .join(""); 72 return ` 73 <section class="shows-group"> 74 <h2 class="shows-group__title">${letter}</h2> 75 <div class="shows-group__grid">${cards}</div> 76 </section> 77 `; 78 }) 79 .join(""); 80 } 81 82 function applyFilter() { 83 const query = String(searchInput?.value ?? "").trim().toLowerCase(); 84 if (!query) { 85 renderShows(allShows); 86 return; 87 } 88 const filtered = allShows.filter((show) => 89 `${show.title || ""} ${show.search || ""}`.toLowerCase().includes(query), 90 ); 91 renderShows(filtered, query); 92 } 93 94 fetch("data/available-shows.json") 95 .then((res) => { 96 if (!res.ok) throw new Error("Failed to load shows"); 97 return res.json(); 98 }) 99 .then((shows) => { 100 if (!Array.isArray(shows)) throw new Error("Invalid shows data"); 101 shows.sort((a, b) => String(a.title || "").localeCompare(String(b.title || ""))); 102 allShows = shows; 103 renderShows(allShows); 104 searchInput?.addEventListener("input", applyFilter); 105 }) 106 .catch(() => { 107 if (countEl) countEl.textContent = "Could not load supported shows."; 108 grid.innerHTML = ""; 109 }); 110})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.