Advertisement
Guest User

Untitled

a guest
Mar 27th, 2023
366
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 20.51 KB | Source Code | 0 0
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5. <style>
  6. body {
  7.   background: burlywood;
  8. }
  9. .region_list {
  10.   display: flex;
  11.   flex-wrap: wrap;
  12.   font-size: 2em;
  13.   width: 40%;
  14.   margin: auto;
  15.   row-gap: 1.25em;
  16.   column-gap: 1.25em;
  17.   text-align: center;
  18. }
  19. .region_flag {
  20.   width: 2em;
  21.   color: black;
  22.   text-decoration: none;
  23. }
  24. .region_flag:hover {
  25.   color: beige;
  26. }
  27. .region_lang_selector {
  28.   position: absolute;
  29.   display: flex;
  30.   border-radius: 0.5em;
  31.   background-color: beige;
  32.   padding: 0.25em 1em;
  33.   border: 1px solid black;
  34.  
  35.   font-size: 0.75em;
  36.   justify-content: space-between;
  37.   column-gap: 0.5em;
  38.  
  39.   /* Translate to center of parent element (width 2em) */
  40.   transform: translateX(-50%) translateX(1em) translateY(-0.25em);
  41.  
  42.   transition: opacity 0.1s ease-out,
  43.               visibility 0.1s ease-out;
  44. }
  45.  
  46. .title_card {
  47.   display: flex;
  48.   flex-direction: column;
  49.   align-items: center;
  50.  
  51.   /* Adjust position of first item */
  52.   margin-top: -0.75em;
  53. }
  54. .title_card_contents {
  55.   width: 50%;
  56.   background: beige;
  57.   border: 2px solid brown;
  58.   border-radius: 5px;
  59.   margin: 0.25em;
  60.   padding: 0.5em;
  61.   transition: width 0.3s ease-out;
  62. }
  63. .title_card_contents:hover {
  64.   background: #f5f5bf;
  65. }
  66. </style>
  67. </head>
  68. <body>
  69.  
  70. <div id="navigation">
  71.   <div style="margin: auto; width: 50%; display: flex; justify-content: space-between; align-items: center">
  72.     <a id="content-prev" style="font-size: 2em" href="#">← 前 50</a>
  73.     <a style="font-size: 2em" href="#regions">地区选择</a>
  74.     <a style="font-size: 2em" href="#contents?content-type=directory&content-start=0">专题分类</a>
  75.     <a style="font-size: 2em" href="#contents?content-type=title&content-start=0">所有软件</a>
  76.     <a style="font-size: 2em" href="#contents?content-type=movie&content-start=0">所有视频</a>
  77.     <a id="content-next" style="font-size: 2em" href="#">后 50 →</a>
  78.   </div>
  79.   <div style="width: 50%; text-align: center; margin: auto">
  80.     <img id="directory-banner" />
  81.     <span id="directory-description" style="display: block; color: maroon"></span>
  82.   </div>
  83. </div>
  84. <p id="root"></p>
  85.  
  86. <script>
  87.   function withRequestedResource(url, fn) {
  88.     fetch(url).then(response => {
  89.       if (!response.ok) {
  90.         console.log(`Error while trying to load "${url}": ${response.status}`);
  91.         return;
  92.       }
  93.       response.text().then(text => fn(text));
  94.     });
  95.   }
  96.  
  97.   function map_url(url) {
  98.     let name = url;
  99.     let filename = url.substring(url.lastIndexOf("/") + 1);
  100.     if (name.startsWith("https://kanzashi-movie-ctr.cdn.nintendo.net/m/")) {
  101.       // Video server: Replace "moflex" extension with "mp4"
  102.       filename = filename.substring(0, filename.lastIndexOf(".moflex")) + ".mp4";
  103.       return "kanzashi-movie/" + filename;
  104.     } else if (name.startsWith("https://kanzashi-ctr.cdn.nintendo.net/i/") ||
  105.                name.startsWith("https://kanzashi-wup.cdn.nintendo.net/i/")) {
  106.       // Image serer
  107.       return "kanzashi/" + filename;
  108.     } else {
  109.       console.log(`Unknown resource URL "${url}"`);
  110.       return "UNKNOWN";
  111.     }
  112.   };
  113.  
  114.   // Search arguments from client-side route
  115.   let search_args = new URLSearchParams();
  116.  
  117.   // Search results displayed on the current page
  118.   let search_results = [];
  119.  
  120.   let previous_route = null;
  121.  
  122.   window.onhashchange = reload;
  123.   reload();
  124.  
  125.   function populateRegionList(containerNode) {
  126.     const regions = [
  127.         "AD", "AE", "AG", "AI", "AL", "AN", "AR", "AT", "AU", "AW", "AZ", "BA",
  128.         "BB", "BE", "BG", "BM", "BO", "BR", "BS", "BW", "BZ", "CA", "CH", "CL",
  129.         "CN", "CO", "CR", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "EC", "EE",
  130.         "ER", "ES", "FI", "FR", "GB", "GD", "GF", "GG", "GI", "GP", "GR", "GT",
  131.         "GY", "HK", "HN", "HR", "HT", "HU", "IE", "IL", "IM", "IN", "IS", "IT",
  132.         "JE", "JM", "JP", "KN", "KR", "KY", "LC", "LI", "LS", "LT", "LU", "LV",
  133.         "MC", "ME", "MK", "ML", "MQ", "MR", "MS", "MT", "MX", "MY", "MZ", "NA",
  134.         "NE", "NI", "NL", "NO", "NZ", "PA", "PE", "PL", "PT", "PY", "RO", "RS",
  135.         "RU", "SA", "SD", "SE", "SG", "SI", "SK", "SM", "SO", "SR", "SV", "SZ",
  136.         "TC", "TD", "TR", "TT", "TW", "US", "UY", "VA", "VC", "VE", "VG", "VI",
  137.         "ZA", "ZM", "ZW",
  138.     ];
  139.  
  140.     Promise.all(regions.map(r => fetch(`samurai/${r}/languages`))).then(responses => {
  141.       // Wait for all responses, removing any requests that failed
  142.       Promise.all(responses.map(xml => (xml.ok ? xml.text() : Promise.resolve(null)))).then(all_xmls => all_xmls.forEach((xml, index) => {
  143.         if (!xml) {
  144.           return;
  145.         }
  146.  
  147.         let region = regions[index];
  148.         let node = document.createElement("span");
  149.         node.className = "region_flag";
  150.         node.textContent = String.fromCodePoint(...region.split('').map(ch => ch.codePointAt(0) - 65 + 0x1F1E6)) + ` ${region}`;
  151.  
  152.         let languages = document.createElement("div")
  153.         languages.className = "region_lang_selector";
  154.         {
  155.             let parser = new DOMParser();
  156.             let xmlDoc = parser.parseFromString(xml, "text/xml");
  157.             for (lang of Array.from(xmlDoc.querySelectorAll(`languages > language`))) {
  158.               let lang_element = document.createElement("a");
  159.               lang_element.textContent = lang.getElementsByTagName("name")[0].textContent;
  160.               lang_element.href = `#contents?region=${region}&language=${lang.getElementsByTagName("iso_code")[0].textContent}&content-type=title&content-start=0`;
  161.               languages.appendChild(lang_element);
  162.             }
  163.         }
  164.         languages.style.visibility = "hidden";
  165.         languages.style.opacity = 0;
  166.         node.appendChild(languages)
  167.         node.onclick = () => {
  168.           for (active_langs of document.getElementsByClassName("region_lang_selector")) {
  169.             active_langs.style.opacity = 0;
  170.             active_langs.style.visibility = "hidden"
  171.           }
  172.           languages.style.visibility = "visible"
  173.           languages.style.opacity = 1;
  174.         }
  175.         containerNode.appendChild(node);
  176.       }));
  177.     });
  178.   }
  179.  
  180.   function populateContentList(args, containerNode, titles) {
  181.     if (args.content_type == "directory") {
  182.       populateDirectoryList(args, containerNode, titles);
  183.       return;
  184.     }
  185.  
  186.     for (let title_index = args.content_start; title_index < Math.min(titles.length, args.content_start + args.content_length); ++title_index) {
  187.      let content_type = titles[title_index].nodeName;
  188.  
  189.      const node = document.createElement("div");
  190.      node.className = "title_card_contents";
  191.      let title = titles[title_index];
  192.      const contentId = title.getAttribute("id");
  193.      {
  194.        let headerNode = document.createElement("div")
  195.        headerNode.style["display"] = "flex"
  196.        headerNode.style["align-items"] = "start"
  197.  
  198.        let icon = title.getElementsByTagName("icon_url")[0];
  199.        if (!icon) {
  200.          // Some movies have a thumbnail_url instead
  201.          icon = title.getElementsByTagName("thumbnail_url")[0];
  202.        }
  203.        if (icon) {
  204.          headerNode.innerHTML = `<img src="` + map_url(icon.textContent) + `" />`;
  205.         }
  206.         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>`;
  207.  
  208.         // Render stars
  209.         // TODO: Support fractional star display
  210.         const score = title.querySelector(`${content_type} > star_rating_info > score`)?.textContent
  211.         if (score || content_type != "directory") {
  212.           let starHtml = `<span class="num-stars-label" style="margin-left: auto; color: gray">${score ? "" : "(无投票数据)"}</span><span>`;
  213.           for (let i = 0; i < 5; ++i) {
  214.            starHtml += `<span style="color: orange">` + (i < (+score + 0.25) ? "★" : "☆") + `</span>`;
  215.           }
  216.           headerNode.innerHTML += starHtml + `</span>`;
  217.         }
  218.  
  219.         node.appendChild(headerNode);
  220.       }
  221.  
  222.       let ratingInfo = document.createElement("div");
  223.       ratingInfo.style["text-align"] = "right";
  224.       const ratingSystem = title.getElementsByTagName("rating_system")[0];
  225.       if (ratingSystem) {
  226.         const rating = title.getElementsByTagName("rating")[0];
  227.         ratingInfo.innerHTML = ratingSystem.getElementsByTagName("name")[0].textContent;
  228.         ratingInfo.innerHTML += `: ` + rating.getElementsByTagName("name")[0].textContent;
  229.       } else if (content_type != "directory") {
  230.         ratingInfo.innerHTML = "(无分级信息)";
  231.       }
  232.       node.appendChild(ratingInfo);
  233.  
  234.       node.onclick = () => {
  235.         // Unregister on-click handler
  236.         // TODO: Implement collapsing on re-click
  237.         node.onclick = () => {}
  238.  
  239.         // TODO: Load detailed content page and extract description
  240.         let child = document.createElement("div");
  241. // TODO: Insert the banner at the top instead
  242. //        child.innerHTML = `<img src="` + map_url(title.getElementsByTagName("banner_url")[0].textContent) + `" />`;
  243.  
  244.         let secondaryHeader = document.createElement("div");
  245.         secondaryHeader.style["display"] = "flex"
  246.         secondaryHeader.style["align-items"] = "center"
  247.  
  248.         let thumbnail = document.createElement("img");
  249.         thumbnail.style["padding-right"] = "0.25em";
  250.         secondaryHeader.appendChild(thumbnail);
  251.  
  252.         let ternaryHeader = document.createElement("div");
  253.         {
  254.           let priceText = document.createElement("p");
  255.           priceText.className = "price-label";
  256.           ternaryHeader.appendChild(priceText);
  257.         }
  258.  
  259.         {
  260.           let platformText = document.createElement("p");
  261.           platformText.className = "platform-label";
  262.           ternaryHeader.appendChild(platformText);
  263.         }
  264.  
  265.         {
  266.           let releaseText = document.createElement("p");
  267.           releaseText.className = "releasedate-label";
  268.           ternaryHeader.appendChild(releaseText);
  269.         }
  270.         secondaryHeader.appendChild(ternaryHeader);
  271.  
  272.         {
  273.           let rating = document.createElement("img");
  274.           rating.className = "rating-img";
  275.           rating.style["margin-left"] = "auto";
  276.           secondaryHeader.appendChild(rating);
  277.         }
  278.  
  279.         child.appendChild(secondaryHeader);
  280.  
  281.         withRequestedResource(`samurai/${args.region}/${args.language}/${content_type}/${contentId}`, response => {
  282.             let xml = parser.parseFromString(response, "text/xml");
  283.  
  284.             let thumbnail_urls = Array.from(xml.querySelectorAll(`${content_type} > thumbnails > thumbnail`),
  285.                                                                  tag => map_url(tag.getAttribute("url")));
  286.  
  287. // TODO: Replace the header icon instead
  288.             if (thumbnail_urls.length > 0) {
  289.               thumbnail.src = thumbnail_urls[0];
  290.               let index = 0;
  291.               if (thumbnail_urls.length > 1) {
  292.                 setInterval(function() {
  293.                     child.getElementsByTagName("img")[0].src = thumbnail_urls[index];
  294.                     index = (index + 1) % thumbnail_urls.length;
  295.                 }, 1000);
  296.               }
  297.             }
  298.  
  299.             if (xml.getElementsByTagName("description")[0]) {
  300.               child.innerHTML += `<p>` + xml.getElementsByTagName("description")[0].textContent + `</p>`;
  301.               child.innerHTML += `<hr>`;
  302.             }
  303.  
  304.             // Match both in standalone video documents and embedded video documents
  305.             let videos = Array.from(xml.querySelectorAll(`movie > files > file > movie_url`),
  306.                 url => {
  307.                     return `<video controls="controls" preload="auto" src="${map_url(url.textContent)}" style="margin-bottom: 240"></video>`;
  308.                 }).join("");
  309.  
  310.             let data = Array.from(xml.querySelectorAll(`${content_type} > screenshots > screenshot`),
  311.                 node => {
  312.                     let upper = node.querySelector("image_url[type=upper]");
  313.                     let lower = node.querySelector("image_url[type=lower]");
  314.                     if (lower || upper) {
  315.                       return `<div><img src=${map_url(upper.textContent)} /><br><img src=${map_url(lower.textContent)} style="margin-left: 40"/></div>`
  316.                     } else {
  317.                       // Wii U uses a single image_url tag, but actually show the thumbnail_url here instead since the full screenshot is too large
  318.                       // TODO: Pop-up the full screenshot on click
  319.                       let image = map_url(node.querySelector("thumbnail_url").textContent);
  320.                       return `<div><img src=${image} style="margin-left: 40"/></div>`
  321.                     }
  322.                 }).join("");
  323.             let mediaHTML = `<div style="overflow:scroll"><div style="display: flex; gap: 0.5em">`;
  324.             mediaHTML += videos;
  325.             mediaHTML += data + `</div></div>`;
  326.             child.innerHTML += mediaHTML;
  327.  
  328.             let platformTag = xml.querySelector("platform > name");
  329.             if (platformTag) {
  330.               child.getElementsByClassName("platform-label")[0].textContent = platformTag.textContent;
  331.             }
  332.  
  333.             let release_date_on_eshop = xml.getElementsByTagName("release_date_on_eshop")[0];
  334.             if (release_date_on_eshop) {
  335.               child.getElementsByClassName("releasedate-label")[0].textContent = "发布日: " + release_date_on_eshop.textContent;
  336.             }
  337.             child.getElementsByClassName("rating-img")[0].src = map_url(xml.querySelector(`${content_type} > rating_info > rating > icons > icon[type=small]`).getAttribute("url"));
  338.             let numScoreVotes = xml.querySelector(`${content_type} > star_rating_info > votes`)?.textContent;
  339.             if (numScoreVotes) {
  340.               node.getElementsByClassName("num-stars-label")[0].textContent =  "(" + numScoreVotes + ")";
  341.             }
  342.         });
  343.  
  344.         if (content_type == "title") {
  345.           withRequestedResource(encodeURIComponent(`ninja/${args.region}/${args.language}/titles/online_prices%3Ftitle%5B%5D%3D${contentId}`), response => {
  346.             let xml = parser.parseFromString(response,"text/xml");
  347.             child.getElementsByClassName("price-label")[0].textContent = xml.getElementsByTagName("amount")[0].textContent;
  348.           });
  349.         }
  350.  
  351.         node.style["width"] = "80%"
  352.  
  353.         node.appendChild(child);
  354.       }
  355.  
  356.       containerNode.appendChild(node);
  357.     }
  358.   }
  359.  
  360.   function populateDirectoryList(args, containerNode, titles) {
  361.     let content_type = args.content_type;
  362.  
  363.     for (let title_index = args.content_start; title_index < Math.min(titles.length, args.content_start + args.content_length); ++title_index) {
  364.      const node = document.createElement("a");
  365.      node.href= `#directory/${titles[title_index].getAttribute("id")}?content-type=%2A&content-start=0`;
  366.      node.className = "title_card_contents";
  367.      let title = titles[title_index];
  368.      const contentId = title.getAttribute("id");
  369.      {
  370.        let headerNode = document.createElement("div")
  371.        headerNode.style["display"] = "flex"
  372.        headerNode.style["align-items"] = "start"
  373.  
  374.        let icon = title.getElementsByTagName("icon_url")[0];
  375.        headerNode.innerHTML = `<img src="` + map_url(icon.textContent) + `" />`;
  376.         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>`;
  377.  
  378.         node.appendChild(headerNode);
  379.       }
  380.  
  381.       containerNode.appendChild(node);
  382.     }
  383.   }
  384.  
  385.   function refreshNavigationBar(route, args) {
  386.     {
  387.       let prev_button = document.getElementById("content-prev");
  388.       prev_button.style["pointer-events"] = (args.content_start == 0) ? "none" : "";
  389.       prev_button.style.visibility = (args.content_start == 0) ? "hidden" : "visible";
  390.       document.getElementById("content-prev").href = `#${route}?content-start=${Math.max(0, args.content_start - args.content_length)}`;
  391.  
  392.       let next_button = document.getElementById("content-next");
  393.       next_button.style["pointer-events"] = (args.content_start + args.content_length >= search_results.length) ? "none" : "";
  394.       next_button.style.visibility = (args.content_start + args.content_length >= search_results.length) ? "hidden" : "visible";
  395.       next_button.href = `#${route}?content-start=${args.content_start + args.content_length}`;
  396.     }
  397.  
  398.     document.getElementById("navigation").style.visibility = (route == "regions") ? "hidden" : "";
  399.   }
  400.  
  401.   function reload() {
  402.     let query_loc = window.location.hash.indexOf('?');
  403.     if (query_loc == -1) {
  404.       query_loc = window.location.hash.length;
  405.     }
  406.     let route = window.location.hash.substring(1, query_loc);
  407.     let route_changed = previous_route != route;
  408.     previous_route = route;
  409.     let query_string = window.location.hash.substring(query_loc);
  410.     if (route == "") {
  411.       route = "regions";
  412.     }
  413.  
  414.     let previous_content_type = search_args.get("content-type") ?? "title";
  415.     for ([key, value] of new URLSearchParams(query_string)) {
  416.       search_args.set(key, value);
  417.     }
  418.     let args = {
  419.       region: search_args.get("region") ?? "US",
  420.       language: search_args.get("language") ?? "en",
  421.       content_start: parseInt(search_args.get("content-start") ?? 0),
  422.       content_length: parseInt(search_args.get("content-length") ?? 50),
  423.       content_type: search_args.get("content-type") ?? "*",
  424.     };
  425.     search_args.set("content-type", args.content_type);
  426.  
  427.     // Serialize arguments back to window url
  428.     window.history.replaceState(null, null, window.location.pathname + "#" + route + "?" + search_args.toString());
  429.  
  430.     const containerNode = document.createElement("div");
  431.  
  432.     refreshNavigationBar(route, args);
  433.  
  434.     if (route_changed) {
  435.       document.getElementById("directory-banner").src = "";
  436.       document.getElementById("directory-description").textContent = "";
  437.     }
  438.  
  439.     if (route == "regions") {
  440.       containerNode.className = "region_list";
  441.       populateRegionList(containerNode);
  442.     } else if (route == "contents") {
  443.       containerNode.className = "title_card";
  444.  
  445.       if (route_changed || args.content_type != previous_content_type) {
  446.         let xhr = new XMLHttpRequest();
  447.         xhr.open('GET', `samurai/${args.region}/${args.language}/${args.content_type == "directory" ? "directories" : "contents"}`);
  448.         xhr.onreadystatechange = () => {
  449.           if (xhr.readyState !== XMLHttpRequest.DONE) {
  450.             return;
  451.           }
  452.           parser = new DOMParser();
  453.           let xmlDoc = parser.parseFromString(xhr.responseText, "text/xml");
  454.           // TODO: Support filters like "demo_available=true"
  455.           search_results = xmlDoc.querySelectorAll(args.content_type != "directory" ? `contents > content > ${args.content_type}` : `directories > directory` );
  456.           populateContentList(args, containerNode, search_results);
  457.  
  458.           refreshNavigationBar(route, args);
  459.         };
  460.         xhr.send();
  461.       } else {
  462.         populateContentList(args, containerNode, search_results);
  463.       }
  464.     } else if (route.startsWith("directory/")) {
  465.       containerNode.className = "title_card";
  466.  
  467.       if (route_changed || args.content_type != previous_content_type) {
  468.         let xhr = new XMLHttpRequest();
  469.         xhr.open('GET', `samurai/${args.region}/${args.language}/${route}`);
  470.         xhr.onreadystatechange = () => {
  471.           if (xhr.readyState !== XMLHttpRequest.DONE) {
  472.             return;
  473.           }
  474.           parser = new DOMParser();
  475.           let xmlDoc = parser.parseFromString(xhr.responseText, "text/xml");
  476.           search_results = xmlDoc.querySelectorAll(args.content_type != "directory" ? `contents > content > ${args.content_type}` : `directories > directory` );
  477.           populateContentList(args, containerNode, search_results);
  478.  
  479.           {
  480.             let directoryBanner = xmlDoc.querySelector("directory > banner_url");
  481.             document.getElementById("directory-banner").src = directoryBanner ? map_url(directoryBanner.textContent) : "";
  482.  
  483.             let directoryDesc = xmlDoc.querySelector("directory > description");
  484.             document.getElementById("directory-description").textContent = directoryDesc.textContent;
  485.           }
  486.  
  487.           refreshNavigationBar(route, args);
  488.         };
  489.         xhr.send();
  490.       } else {
  491.         populateContentList(args, containerNode, search_results);
  492.       }
  493.     }
  494.  
  495.     document.getElementById("root").innerHTML = "";
  496.     document.getElementById("root").appendChild(containerNode);
  497.   }
  498. </script>
  499.  
  500. </body>
  501. </html>
  502.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement