Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <style>
- body {
- background: burlywood;
- }
- .region_list {
- display: flex;
- flex-wrap: wrap;
- font-size: 2em;
- width: 40%;
- margin: auto;
- row-gap: 1.25em;
- column-gap: 1.25em;
- text-align: center;
- }
- .region_flag {
- width: 2em;
- color: black;
- text-decoration: none;
- }
- .region_flag:hover {
- color: beige;
- }
- .region_lang_selector {
- position: absolute;
- display: flex;
- border-radius: 0.5em;
- background-color: beige;
- padding: 0.25em 1em;
- border: 1px solid black;
- font-size: 0.75em;
- justify-content: space-between;
- column-gap: 0.5em;
- /* Translate to center of parent element (width 2em) */
- transform: translateX(-50%) translateX(1em) translateY(-0.25em);
- transition: opacity 0.1s ease-out,
- visibility 0.1s ease-out;
- }
- .title_card {
- display: flex;
- flex-direction: column;
- align-items: center;
- /* Adjust position of first item */
- margin-top: -0.75em;
- }
- .title_card_contents {
- width: 50%;
- background: beige;
- border: 2px solid brown;
- border-radius: 5px;
- margin: 0.25em;
- padding: 0.5em;
- transition: width 0.3s ease-out;
- }
- .title_card_contents:hover {
- background: #f5f5bf;
- }
- </style>
- </head>
- <body>
- <div id="navigation">
- <div style="margin: auto; width: 50%; display: flex; justify-content: space-between; align-items: center">
- <a id="content-prev" style="font-size: 2em" href="#">← 前 50</a>
- <a style="font-size: 2em" href="#regions">地区选择</a>
- <a style="font-size: 2em" href="#contents?content-type=directory&content-start=0">专题分类</a>
- <a style="font-size: 2em" href="#contents?content-type=title&content-start=0">所有软件</a>
- <a style="font-size: 2em" href="#contents?content-type=movie&content-start=0">所有视频</a>
- <a id="content-next" style="font-size: 2em" href="#">后 50 →</a>
- </div>
- <div style="width: 50%; text-align: center; margin: auto">
- <img id="directory-banner" />
- <span id="directory-description" style="display: block; color: maroon"></span>
- </div>
- </div>
- <p id="root"></p>
- <script>
- function withRequestedResource(url, fn) {
- fetch(url).then(response => {
- if (!response.ok) {
- console.log(`Error while trying to load "${url}": ${response.status}`);
- return;
- }
- response.text().then(text => fn(text));
- });
- }
- function map_url(url) {
- let name = url;
- let filename = url.substring(url.lastIndexOf("/") + 1);
- if (name.startsWith("https://kanzashi-movie-ctr.cdn.nintendo.net/m/")) {
- // Video server: Replace "moflex" extension with "mp4"
- filename = filename.substring(0, filename.lastIndexOf(".moflex")) + ".mp4";
- return "kanzashi-movie/" + filename;
- } else if (name.startsWith("https://kanzashi-ctr.cdn.nintendo.net/i/") ||
- name.startsWith("https://kanzashi-wup.cdn.nintendo.net/i/")) {
- // Image serer
- return "kanzashi/" + filename;
- } else {
- console.log(`Unknown resource URL "${url}"`);
- return "UNKNOWN";
- }
- };
- // Search arguments from client-side route
- let search_args = new URLSearchParams();
- // Search results displayed on the current page
- let search_results = [];
- let previous_route = null;
- window.onhashchange = reload;
- reload();
- function populateRegionList(containerNode) {
- const regions = [
- "AD", "AE", "AG", "AI", "AL", "AN", "AR", "AT", "AU", "AW", "AZ", "BA",
- "BB", "BE", "BG", "BM", "BO", "BR", "BS", "BW", "BZ", "CA", "CH", "CL",
- "CN", "CO", "CR", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "EC", "EE",
- "ER", "ES", "FI", "FR", "GB", "GD", "GF", "GG", "GI", "GP", "GR", "GT",
- "GY", "HK", "HN", "HR", "HT", "HU", "IE", "IL", "IM", "IN", "IS", "IT",
- "JE", "JM", "JP", "KN", "KR", "KY", "LC", "LI", "LS", "LT", "LU", "LV",
- "MC", "ME", "MK", "ML", "MQ", "MR", "MS", "MT", "MX", "MY", "MZ", "NA",
- "NE", "NI", "NL", "NO", "NZ", "PA", "PE", "PL", "PT", "PY", "RO", "RS",
- "RU", "SA", "SD", "SE", "SG", "SI", "SK", "SM", "SO", "SR", "SV", "SZ",
- "TC", "TD", "TR", "TT", "TW", "US", "UY", "VA", "VC", "VE", "VG", "VI",
- "ZA", "ZM", "ZW",
- ];
- Promise.all(regions.map(r => fetch(`samurai/${r}/languages`))).then(responses => {
- // Wait for all responses, removing any requests that failed
- Promise.all(responses.map(xml => (xml.ok ? xml.text() : Promise.resolve(null)))).then(all_xmls => all_xmls.forEach((xml, index) => {
- if (!xml) {
- return;
- }
- let region = regions[index];
- let node = document.createElement("span");
- node.className = "region_flag";
- node.textContent = String.fromCodePoint(...region.split('').map(ch => ch.codePointAt(0) - 65 + 0x1F1E6)) + ` ${region}`;
- let languages = document.createElement("div")
- languages.className = "region_lang_selector";
- {
- let parser = new DOMParser();
- let xmlDoc = parser.parseFromString(xml, "text/xml");
- for (lang of Array.from(xmlDoc.querySelectorAll(`languages > language`))) {
- let lang_element = document.createElement("a");
- lang_element.textContent = lang.getElementsByTagName("name")[0].textContent;
- lang_element.href = `#contents?region=${region}&language=${lang.getElementsByTagName("iso_code")[0].textContent}&content-type=title&content-start=0`;
- languages.appendChild(lang_element);
- }
- }
- languages.style.visibility = "hidden";
- languages.style.opacity = 0;
- node.appendChild(languages)
- node.onclick = () => {
- for (active_langs of document.getElementsByClassName("region_lang_selector")) {
- active_langs.style.opacity = 0;
- active_langs.style.visibility = "hidden"
- }
- languages.style.visibility = "visible"
- languages.style.opacity = 1;
- }
- containerNode.appendChild(node);
- }));
- });
- }
- function populateContentList(args, containerNode, titles) {
- if (args.content_type == "directory") {
- populateDirectoryList(args, containerNode, titles);
- return;
- }
- for (let title_index = args.content_start; title_index < Math.min(titles.length, args.content_start + args.content_length); ++title_index) {
- let content_type = titles[title_index].nodeName;
- const node = document.createElement("div");
- node.className = "title_card_contents";
- let title = titles[title_index];
- const contentId = title.getAttribute("id");
- {
- let headerNode = document.createElement("div")
- headerNode.style["display"] = "flex"
- headerNode.style["align-items"] = "start"
- let icon = title.getElementsByTagName("icon_url")[0];
- if (!icon) {
- // Some movies have a thumbnail_url instead
- icon = title.getElementsByTagName("thumbnail_url")[0];
- }
- if (icon) {
- headerNode.innerHTML = `<img src="` + map_url(icon.textContent) + `" />`;
- }
- headerNode.innerHTML += `<span style="margin-left: 0.5em; font-size: 1.25em">` + title.getElementsByTagName("name")[0].textContent.replace("\n", "<br>").replace("<br><br>", "<br>") + `</span>`;
- // Render stars
- // TODO: Support fractional star display
- const score = title.querySelector(`${content_type} > star_rating_info > score`)?.textContent
- if (score || content_type != "directory") {
- let starHtml = `<span class="num-stars-label" style="margin-left: auto; color: gray">${score ? "" : "(无投票数据)"}</span><span>`;
- for (let i = 0; i < 5; ++i) {
- starHtml += `<span style="color: orange">` + (i < (+score + 0.25) ? "★" : "☆") + `</span>`;
- }
- headerNode.innerHTML += starHtml + `</span>`;
- }
- node.appendChild(headerNode);
- }
- let ratingInfo = document.createElement("div");
- ratingInfo.style["text-align"] = "right";
- const ratingSystem = title.getElementsByTagName("rating_system")[0];
- if (ratingSystem) {
- const rating = title.getElementsByTagName("rating")[0];
- ratingInfo.innerHTML = ratingSystem.getElementsByTagName("name")[0].textContent;
- ratingInfo.innerHTML += `: ` + rating.getElementsByTagName("name")[0].textContent;
- } else if (content_type != "directory") {
- ratingInfo.innerHTML = "(无分级信息)";
- }
- node.appendChild(ratingInfo);
- node.onclick = () => {
- // Unregister on-click handler
- // TODO: Implement collapsing on re-click
- node.onclick = () => {}
- // TODO: Load detailed content page and extract description
- let child = document.createElement("div");
- // TODO: Insert the banner at the top instead
- // child.innerHTML = `<img src="` + map_url(title.getElementsByTagName("banner_url")[0].textContent) + `" />`;
- let secondaryHeader = document.createElement("div");
- secondaryHeader.style["display"] = "flex"
- secondaryHeader.style["align-items"] = "center"
- let thumbnail = document.createElement("img");
- thumbnail.style["padding-right"] = "0.25em";
- secondaryHeader.appendChild(thumbnail);
- let ternaryHeader = document.createElement("div");
- {
- let priceText = document.createElement("p");
- priceText.className = "price-label";
- ternaryHeader.appendChild(priceText);
- }
- {
- let platformText = document.createElement("p");
- platformText.className = "platform-label";
- ternaryHeader.appendChild(platformText);
- }
- {
- let releaseText = document.createElement("p");
- releaseText.className = "releasedate-label";
- ternaryHeader.appendChild(releaseText);
- }
- secondaryHeader.appendChild(ternaryHeader);
- {
- let rating = document.createElement("img");
- rating.className = "rating-img";
- rating.style["margin-left"] = "auto";
- secondaryHeader.appendChild(rating);
- }
- child.appendChild(secondaryHeader);
- withRequestedResource(`samurai/${args.region}/${args.language}/${content_type}/${contentId}`, response => {
- let xml = parser.parseFromString(response, "text/xml");
- let thumbnail_urls = Array.from(xml.querySelectorAll(`${content_type} > thumbnails > thumbnail`),
- tag => map_url(tag.getAttribute("url")));
- // TODO: Replace the header icon instead
- if (thumbnail_urls.length > 0) {
- thumbnail.src = thumbnail_urls[0];
- let index = 0;
- if (thumbnail_urls.length > 1) {
- setInterval(function() {
- child.getElementsByTagName("img")[0].src = thumbnail_urls[index];
- index = (index + 1) % thumbnail_urls.length;
- }, 1000);
- }
- }
- if (xml.getElementsByTagName("description")[0]) {
- child.innerHTML += `<p>` + xml.getElementsByTagName("description")[0].textContent + `</p>`;
- child.innerHTML += `<hr>`;
- }
- // Match both in standalone video documents and embedded video documents
- let videos = Array.from(xml.querySelectorAll(`movie > files > file > movie_url`),
- url => {
- return `<video controls="controls" preload="auto" src="${map_url(url.textContent)}" style="margin-bottom: 240"></video>`;
- }).join("");
- let data = Array.from(xml.querySelectorAll(`${content_type} > screenshots > screenshot`),
- node => {
- let upper = node.querySelector("image_url[type=upper]");
- let lower = node.querySelector("image_url[type=lower]");
- if (lower || upper) {
- return `<div><img src=${map_url(upper.textContent)} /><br><img src=${map_url(lower.textContent)} style="margin-left: 40"/></div>`
- } else {
- // Wii U uses a single image_url tag, but actually show the thumbnail_url here instead since the full screenshot is too large
- // TODO: Pop-up the full screenshot on click
- let image = map_url(node.querySelector("thumbnail_url").textContent);
- return `<div><img src=${image} style="margin-left: 40"/></div>`
- }
- }).join("");
- let mediaHTML = `<div style="overflow:scroll"><div style="display: flex; gap: 0.5em">`;
- mediaHTML += videos;
- mediaHTML += data + `</div></div>`;
- child.innerHTML += mediaHTML;
- let platformTag = xml.querySelector("platform > name");
- if (platformTag) {
- child.getElementsByClassName("platform-label")[0].textContent = platformTag.textContent;
- }
- let release_date_on_eshop = xml.getElementsByTagName("release_date_on_eshop")[0];
- if (release_date_on_eshop) {
- child.getElementsByClassName("releasedate-label")[0].textContent = "发布日: " + release_date_on_eshop.textContent;
- }
- child.getElementsByClassName("rating-img")[0].src = map_url(xml.querySelector(`${content_type} > rating_info > rating > icons > icon[type=small]`).getAttribute("url"));
- let numScoreVotes = xml.querySelector(`${content_type} > star_rating_info > votes`)?.textContent;
- if (numScoreVotes) {
- node.getElementsByClassName("num-stars-label")[0].textContent = "(" + numScoreVotes + ")";
- }
- });
- if (content_type == "title") {
- withRequestedResource(encodeURIComponent(`ninja/${args.region}/${args.language}/titles/online_prices%3Ftitle%5B%5D%3D${contentId}`), response => {
- let xml = parser.parseFromString(response,"text/xml");
- child.getElementsByClassName("price-label")[0].textContent = xml.getElementsByTagName("amount")[0].textContent;
- });
- }
- node.style["width"] = "80%"
- node.appendChild(child);
- }
- containerNode.appendChild(node);
- }
- }
- function populateDirectoryList(args, containerNode, titles) {
- let content_type = args.content_type;
- for (let title_index = args.content_start; title_index < Math.min(titles.length, args.content_start + args.content_length); ++title_index) {
- const node = document.createElement("a");
- node.href= `#directory/${titles[title_index].getAttribute("id")}?content-type=%2A&content-start=0`;
- node.className = "title_card_contents";
- let title = titles[title_index];
- const contentId = title.getAttribute("id");
- {
- let headerNode = document.createElement("div")
- headerNode.style["display"] = "flex"
- headerNode.style["align-items"] = "start"
- let icon = title.getElementsByTagName("icon_url")[0];
- headerNode.innerHTML = `<img src="` + map_url(icon.textContent) + `" />`;
- headerNode.innerHTML += `<span style="margin-left: 0.5em; font-size: 1.25em">` + title.getElementsByTagName("name")[0].textContent.replace("\n", "<br>").replace("<br><br>", "<br>") + `</span>`;
- node.appendChild(headerNode);
- }
- containerNode.appendChild(node);
- }
- }
- function refreshNavigationBar(route, args) {
- {
- let prev_button = document.getElementById("content-prev");
- prev_button.style["pointer-events"] = (args.content_start == 0) ? "none" : "";
- prev_button.style.visibility = (args.content_start == 0) ? "hidden" : "visible";
- document.getElementById("content-prev").href = `#${route}?content-start=${Math.max(0, args.content_start - args.content_length)}`;
- let next_button = document.getElementById("content-next");
- next_button.style["pointer-events"] = (args.content_start + args.content_length >= search_results.length) ? "none" : "";
- next_button.style.visibility = (args.content_start + args.content_length >= search_results.length) ? "hidden" : "visible";
- next_button.href = `#${route}?content-start=${args.content_start + args.content_length}`;
- }
- document.getElementById("navigation").style.visibility = (route == "regions") ? "hidden" : "";
- }
- function reload() {
- let query_loc = window.location.hash.indexOf('?');
- if (query_loc == -1) {
- query_loc = window.location.hash.length;
- }
- let route = window.location.hash.substring(1, query_loc);
- let route_changed = previous_route != route;
- previous_route = route;
- let query_string = window.location.hash.substring(query_loc);
- if (route == "") {
- route = "regions";
- }
- let previous_content_type = search_args.get("content-type") ?? "title";
- for ([key, value] of new URLSearchParams(query_string)) {
- search_args.set(key, value);
- }
- let args = {
- region: search_args.get("region") ?? "US",
- language: search_args.get("language") ?? "en",
- content_start: parseInt(search_args.get("content-start") ?? 0),
- content_length: parseInt(search_args.get("content-length") ?? 50),
- content_type: search_args.get("content-type") ?? "*",
- };
- search_args.set("content-type", args.content_type);
- // Serialize arguments back to window url
- window.history.replaceState(null, null, window.location.pathname + "#" + route + "?" + search_args.toString());
- const containerNode = document.createElement("div");
- refreshNavigationBar(route, args);
- if (route_changed) {
- document.getElementById("directory-banner").src = "";
- document.getElementById("directory-description").textContent = "";
- }
- if (route == "regions") {
- containerNode.className = "region_list";
- populateRegionList(containerNode);
- } else if (route == "contents") {
- containerNode.className = "title_card";
- if (route_changed || args.content_type != previous_content_type) {
- let xhr = new XMLHttpRequest();
- xhr.open('GET', `samurai/${args.region}/${args.language}/${args.content_type == "directory" ? "directories" : "contents"}`);
- xhr.onreadystatechange = () => {
- if (xhr.readyState !== XMLHttpRequest.DONE) {
- return;
- }
- parser = new DOMParser();
- let xmlDoc = parser.parseFromString(xhr.responseText, "text/xml");
- // TODO: Support filters like "demo_available=true"
- search_results = xmlDoc.querySelectorAll(args.content_type != "directory" ? `contents > content > ${args.content_type}` : `directories > directory` );
- populateContentList(args, containerNode, search_results);
- refreshNavigationBar(route, args);
- };
- xhr.send();
- } else {
- populateContentList(args, containerNode, search_results);
- }
- } else if (route.startsWith("directory/")) {
- containerNode.className = "title_card";
- if (route_changed || args.content_type != previous_content_type) {
- let xhr = new XMLHttpRequest();
- xhr.open('GET', `samurai/${args.region}/${args.language}/${route}`);
- xhr.onreadystatechange = () => {
- if (xhr.readyState !== XMLHttpRequest.DONE) {
- return;
- }
- parser = new DOMParser();
- let xmlDoc = parser.parseFromString(xhr.responseText, "text/xml");
- search_results = xmlDoc.querySelectorAll(args.content_type != "directory" ? `contents > content > ${args.content_type}` : `directories > directory` );
- populateContentList(args, containerNode, search_results);
- {
- let directoryBanner = xmlDoc.querySelector("directory > banner_url");
- document.getElementById("directory-banner").src = directoryBanner ? map_url(directoryBanner.textContent) : "";
- let directoryDesc = xmlDoc.querySelector("directory > description");
- document.getElementById("directory-description").textContent = directoryDesc.textContent;
- }
- refreshNavigationBar(route, args);
- };
- xhr.send();
- } else {
- populateContentList(args, containerNode, search_results);
- }
- }
- document.getElementById("root").innerHTML = "";
- document.getElementById("root").appendChild(containerNode);
- }
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement